Add ActionText with Trix rich text editor to React
Rails 6 introduced Action Text for rich text content and editing. To integrate Action Text with React applications, you need to use Basecamp's Trix editor. This post covers how to set up Action Text with Trix in a React frontend.
What is Action Text?
Action Text brings rich text content and editing to Rails. It includes the Trix editor and handles everything needed for rich text content, including attachments, images, and formatting.
Backend Setup
1. Install Action Text
rails action_text:install
rails db:migrate
2. Configure Model
Add Action Text to your model:
# app/models/post.rb
class Post < ApplicationRecord
has_rich_text :content
end
3. API Endpoint
Create an API endpoint to handle Action Text:
# app/controllers/api/posts_controller.rb
class Api::PostsController < ApplicationController
def create
post = Post.new(post_params)
if post.save
render json: {
id: post.id,
content: post.content.to_s,
content_trix: post.content.to_trix_html
}
else
render json: { errors: post.errors }, status: :unprocessable_entity
end
end
private
def post_params
params.require(:post).permit(:title, :content)
end
end
Frontend Setup
1. Install Trix
Install Trix via npm:
npm install trix
# or
yarn add trix
2. Import Trix Styles
Import Trix CSS in your React app:
// src/index.js or App.js
import 'trix/dist/trix.css'
3. Create Trix Component
Create a React component for Trix:
// src/components/TrixEditor.js
import React, { useEffect, useRef } from 'react'
import Trix from 'trix'
function TrixEditor({ value, onChange, ...props }) {
const trixRef = useRef(null)
const inputRef = useRef(null)
useEffect(() => {
const editor = trixRef.current?.editor
if (editor && value !== editor.getDocument().toString()) {
editor.loadHTML(value || '')
}
}, [value])
useEffect(() => {
const trixElement = trixRef.current
const handleChange = () => {
const html = trixElement?.value || ''
onChange?.(html)
}
trixElement?.addEventListener('trix-change', handleChange)
return () => {
trixElement?.removeEventListener('trix-change', handleChange)
}
}, [onChange])
return (
<div>
<input
ref={inputRef}
type="hidden"
id="trix-input"
value={value || ''}
/>
<trix-editor
ref={trixRef}
input="trix-input"
{...props}
/>
</div>
)
}
export default TrixEditor
Using the Component
Use TrixEditor in your forms:
// src/components/PostForm.js
import React, { useState } from 'react'
import TrixEditor from './TrixEditor'
function PostForm() {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const handleSubmit = async (e) => {
e.preventDefault()
const response = await fetch('/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
post: {
title,
content,
},
}),
})
const data = await response.json()
console.log('Post created:', data)
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Post title"
required
/>
<TrixEditor
value={content}
onChange={setContent}
placeholder="Write your post..."
/>
<button type="submit">Create Post</button>
</form>
)
}
export default PostForm
Handling Direct Uploads
For file attachments, configure direct uploads:
Backend: Direct Upload Endpoint
# app/controllers/api/direct_uploads_controller.rb
class Api::DirectUploadsController < ApplicationController
def create
blob = ActiveStorage::Blob.create_before_direct_upload!(
filename: params[:filename],
byte_size: params[:byte_size],
content_type: params[:content_type],
checksum: params[:checksum]
)
render json: {
signed_id: blob.signed_id,
url: blob.service_url_for_direct_upload,
headers: blob.service_headers_for_direct_upload
}
end
end
Frontend: Configure Trix for Direct Upload
// src/components/TrixEditor.js
import React, { useEffect, useRef } from 'react'
import Trix from 'trix'
function TrixEditor({ value, onChange, ...props }) {
const trixRef = useRef(null)
const inputRef = useRef(null)
useEffect(() => {
const trixElement = trixRef.current
const handleAttachmentAdd = async (event) => {
const attachment = event.attachment
const file = attachment.file
if (file) {
// Get direct upload URL
const formData = new FormData()
formData.append('filename', file.name)
formData.append('byte_size', file.size)
formData.append('content_type', file.type)
formData.append('checksum', await calculateChecksum(file))
const response = await fetch('/api/direct_uploads', {
method: 'POST',
body: formData,
})
const { url, headers, signed_id } = await response.json()
// Upload file directly
await fetch(url, {
method: 'PUT',
headers: JSON.parse(headers),
body: file,
})
// Set attachment attributes
attachment.setAttributes({
url: url,
sgid: signed_id,
})
}
}
trixElement?.addEventListener('trix-attachment-add', handleAttachmentAdd)
return () => {
trixElement?.removeEventListener('trix-attachment-add', handleAttachmentAdd)
}
}, [])
// ... rest of component
}
async function calculateChecksum(file) {
const buffer = await file.arrayBuffer()
const hashBuffer = await crypto.subtle.digest('MD5', buffer)
const hashArray = Array.from(new Uint8Array(hashBuffer))
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
}
export default TrixEditor
Rendering Action Text Content
To render Action Text content in React:
// src/components/PostContent.js
import React from 'react'
function PostContent({ content }) {
return (
<div
dangerouslySetInnerHTML={{ __html: content }}
className="trix-content"
/>
)
}
export default PostContent
Styling Trix Content
Add styles for rendered Trix content:
/* src/styles/trix-content.css */
.trix-content {
/* Match Trix editor styles */
}
.trix-content img {
max-width: 100%;
height: auto;
}
.trix-content .attachment {
display: inline-block;
position: relative;
max-width: 100%;
margin: 0.5em 0;
}
Pre-populating Editor
To pre-populate the editor with existing content:
// When editing an existing post
const [content, setContent] = useState(post.content_trix || '')
<TrixEditor
value={content}
onChange={setContent}
/>
Common Issues and Solutions
Issue: Trix not loading
Make sure Trix CSS is imported and the component is properly initialized.
Issue: Attachments not working
Configure direct uploads and ensure CORS is set up correctly for your storage service.
Issue: Content not saving
Ensure you're sending the HTML content, not just the text content.
Best Practices
- Sanitize content: Sanitize HTML content before rendering
- Handle errors: Provide error handling for uploads
- Loading states: Show loading indicators during uploads
- File size limits: Enforce file size limits
- Content validation: Validate content on both client and server
Conclusion
Integrating Action Text with Trix in React requires some setup, but provides a powerful rich text editing solution. By configuring direct uploads and properly handling Trix events, you can create a seamless rich text editing experience in your React application.