Skip to content

Instantly share code, notes, and snippets.

@markbao
Created July 15, 2016 22:33
Show Gist options
  • Save markbao/01d64217bdc823b1da8f4c38a67ae400 to your computer and use it in GitHub Desktop.
Save markbao/01d64217bdc823b1da8f4c38a67ae400 to your computer and use it in GitHub Desktop.
Integrating Slate into a Rails app

Integrating Slate into a Rails app

Want to use the excellent rich text editor Slate in a Rails app? Beware – since you'll be integrating React with your Rails app, it'll turn out pretty complicated and you'll have to do things (e.g. start server, managing dependencies, etc.) a bit differently than before. Let's start.

1. Install react_on_rails

react_on_rails is pretty much the best option for integrating React into Rails. Install it like so:

  1. Add this to your gemfile:

    gem "react_on_rails", "~> 5"
  2. Commit to git.

  3. Generate the example.

    rails generate react_on_rails:install
  4. Bundle and npm install.

    bundle && npm install
  5. Start the Rails server, now using a new command:

    npm start

2. Add a new app to the Webpack bundles

Open client/app/bundles and create a new app. We'll call it BetterEditor here. BetterEditor will be an enclosure for the Slate editor.

From your app's root directory:

mkdir client/app/bundles/BetterEditor
mkdir client/app/bundles/BetterEditor/containers
mkdir client/app/bundles/BetterEditor/startup

3. Set up the app JS

We need to set up the startup script to attach BetterEditor to react_on_rails. Create this file for BetterEditorApp (which encloses BetterEditor) in the React app's startup folder:

client/app/bundles/BetterEditor/startup/BetterEditorApp.jsx

import React from 'react';
import ReactOnRails from 'react-on-rails';

import BetterEditor from '../containers/BetterEditor';

const BetterEditorApp = (props) => (
  <BetterEditor {...props} />
);

// This is how react_on_rails can see the BetterEditorApp in the browser.
ReactOnRails.register({ BetterEditorApp });

Finally, let's write the app's JS to set up Slate. Create this file in the React app's containers folder:

client/app/bundles/BetterEditor/containers/BetterEditor.jsx

See file below.

4. Hook into Rails

In the view you want to see the editor, inject the React component:

<%= react_component("BetterEditorApp", props: {html: '<b>testing</b>'}, prerender: false) %>

You'll have to do more customization, but that's the bulk of it!

Integrating Slate into a Rails app

Want to use the excellent rich text editor Slate in a Rails app? Beware – since you'll be integrating React with your Rails app, it'll turn out pretty complicated and you'll have to do things (e.g. start server, managing dependencies, etc.) a bit differently than before. Let's start.

1. Install react_on_rails

react_on_rails is pretty much the best option for integrating React into Rails. Install it like so:

  1. Add this to your gemfile:

    gem "react_on_rails", "~> 5"
  2. Commit to git.

  3. Generate the example.

    rails generate react_on_rails:install
  4. Bundle and npm install.

    bundle && npm install
  5. Start the Rails server, now using a new command:

    npm start

2. Add a new app to the Webpack bundles

Open client/app/bundles and create a new app. We'll call it BetterEditor here. BetterEditor will be an enclosure for the Slate editor.

From your app's root directory:

mkdir client/app/bundles/BetterEditor
mkdir client/app/bundles/BetterEditor/containers
mkdir client/app/bundles/BetterEditor/startup

3. Set up the app JS

We need to set up the startup script to attach BetterEditor to react_on_rails. Create this file for BetterEditorApp (which encloses BetterEditor) in the React app's startup folder:

client/app/bundles/BetterEditor/startup/BetterEditorApp.jsx

import React from 'react';
import ReactOnRails from 'react-on-rails';

import BetterEditor from '../containers/BetterEditor';

const BetterEditorApp = (props) => (
  <BetterEditor {...props} />
);

// This is how react_on_rails can see the BetterEditorApp in the browser.
ReactOnRails.register({ BetterEditorApp });

Finally, let's write the app's JS to set up Slate. Create this file in the React app's containers folder:

client/app/bundles/BetterEditor/containers/BetterEditor.jsx

See file below.

4. Hook into Rails

In the view you want to see the editor, inject the React component:

<%= react_component("BetterEditorApp", props: {html: '<b>testing</b>'}, prerender: false) %>

You'll have to do more customization, but that's the bulk of it!

import React, { PropTypes } from 'react';
import { Editor, Html, Raw, Utils } from 'slate'
import keycode from 'keycode'
// Includes code from Slate examples.
// https://github.com/ianstormtaylor/slate/blob/master/examples/paste-html/index.js
/**
* Define a set of node renderers.
*
* @type {Object}
*/
const NODES = {
'bulleted-list': props => <ul {...props.attributes}>{props.children}</ul>,
'code': props => <pre><code {...props.attributes}>{props.children}</code></pre>,
'heading-one': props => <h1 {...props.attributes}>{props.children}</h1>,
'heading-two': props => <h2 {...props.attributes}>{props.children}</h2>,
'heading-three': props => <h3 {...props.attributes}>{props.children}</h3>,
'heading-four': props => <h4 {...props.attributes}>{props.children}</h4>,
'heading-five': props => <h5 {...props.attributes}>{props.children}</h5>,
'heading-six': props => <h6 {...props.attributes}>{props.children}</h6>,
'list-item': props => <li {...props.attributes}>{props.children}</li>,
'numbered-list': props => <ol {...props.attributes}>{props.children}</ol>,
'block-quote': props => <blockquote {...props.attributes}>{props.children}</blockquote>,
'link': (props) => {
const { data } = props.node
const href = data.get('href')
return <a href={href} {...props.attributes}>{props.children}</a>
}
}
/**
* Define a set of mark renderers.
*
* @type {Object}
*/
const MARKS = {
bold: {
fontWeight: 'bold'
},
code: {
fontFamily: 'monospace',
backgroundColor: '#eee',
padding: '3px',
borderRadius: '4px'
},
italic: {
fontStyle: 'italic'
},
underlined: {
textDecoration: 'underline'
}
}
/**
* Tags to blocks.
*
* @type {Object}
*/
const BLOCK_TAGS = {
p: 'paragraph',
li: 'list-item',
ul: 'bulleted-list',
ol: 'numbered-list',
blockquote: 'block-quote',
pre: 'code',
h1: 'heading-one',
h2: 'heading-two',
h3: 'heading-three',
h4: 'heading-four',
h5: 'heading-five',
h6: 'heading-six'
}
/**
* Tags to marks.
*
* @type {Object}
*/
const MARK_TAGS = {
b: 'bold',
strong: 'bold',
em: 'italic',
i: 'italic',
u: 'underline',
s: 'strikethrough',
code: 'code'
}
/**
* Serializer rules.
*
* @type {Array}
*/
const RULES = [
{
deserialize(el, next) {
const block = BLOCK_TAGS[el.tagName]
if (!block) return
return {
kind: 'block',
type: block,
nodes: next(el.children)
}
}
},
{
deserialize(el, next) {
const mark = MARK_TAGS[el.tagName]
if (!mark) return
return {
kind: 'mark',
type: mark,
nodes: next(el.children)
}
}
},
{
// Special case for code blocks, which need to grab the nested children.
deserialize(el, next) {
if (el.tagName != 'pre') return
const code = el.children[0]
const children = code && code.tagName == 'code'
? code.children
: el.children
return {
kind: 'block',
type: 'code',
nodes: next(children)
}
}
},
{
// Special case for links, to grab their href.
deserialize(el, next) {
if (el.tagName != 'a') return
return {
kind: 'inline',
type: 'link',
nodes: next(el.children),
data: {
href: el.attribs.href
}
}
}
}
]
/**
* Create a new HTML serializer with `RULES`.
*
* @type {Html}
*/
const serializer = new Html(RULES)
export default class BetterEditor extends React.Component {
constructor(props, context) {
super(props, context);
// How to set initial state in ES6 class syntax
// https://facebook.github.io/react/docs/reusable-components.html#es6-classes
if ('html' in this.props) {
this.state = { state: serializer.deserialize(this.props.html) }
} else {
this.state = { state: serializer.deserialize('') }
}
}
// /**
// * Deserialize the raw initial state.
// *
// * @type {Object}
// */
//
// state = {
// state: initialState
// };
/**
* Check if the current selection has a mark with `type` in it.
*
* @param {String} type
* @return {Boolean}
*/
hasMark = (type) => {
const { state } = this.state
return state.marks.some(mark => mark.type == type)
}
/**
* Check if the any of the currently selected blocks are of `type`.
*
* @param {String} type
* @return {Boolean}
*/
hasBlock = (type) => {
const { state } = this.state
return state.blocks.some(node => node.type == type)
}
/**
* On key down, if it's a formatting command toggle a mark.
*
* @param {Event} e
* @param {State} state
* @return {State}
*/
onKeyDown = (e, state) => {
if (!Utils.Key.isCommand(e)) return
const key = keycode(e.which)
let mark
switch (key) {
case 'b':
mark = 'bold'
break
case 'i':
mark = 'italic'
break
case 'u':
mark = 'underlined'
break
case '`':
mark = 'code'
break
default:
return
}
state = state
.transform()
[this.hasMark(mark) ? 'unmark' : 'mark'](mark)
.apply()
e.preventDefault()
return state
}
/**
* When a mark button is clicked, toggle the current mark.
*
* @param {Event} e
* @param {String} type
*/
onClickMark = (e, type) => {
e.preventDefault()
const isActive = this.hasMark(type)
let { state } = this.state
state = state
.transform()
[isActive ? 'unmark' : 'mark'](type)
.apply()
this.setState({ state })
}
/**
* When a block button is clicked, toggle the block type.
*
* @param {Event} e
* @param {String} type
*/
onClickBlock = (e, type) => {
e.preventDefault()
const isActive = this.hasBlock(type)
let { state } = this.state
state = state
.transform()
.setBlock(isActive ? 'paragraph' : type)
.apply()
this.setState({ state })
}
/**
* On change, save the new state.
*
* @param {State} state
*/
onChange = (state) => {
this.setState({ state })
}
/**
* Render the toolbar.
*
* @return {Element}
*/
renderToolbar = () => {
return (
<div className="menu toolbar-menu">
{this.renderMarkButton('bold', 'format_bold')}
{this.renderMarkButton('italic', 'format_italic')}
{this.renderMarkButton('underlined', 'format_underlined')}
{this.renderMarkButton('code', 'code')}
{this.renderBlockButton('heading-one', 'looks_one')}
{this.renderBlockButton('heading-two', 'looks_two')}
{this.renderBlockButton('block-quote', 'format_quote')}
{this.renderBlockButton('numbered-list', 'format_list_numbered')}
{this.renderBlockButton('bulleted-list', 'format_list_bulleted')}
</div>
)
}
/**
* Render a mark-toggling toolbar button.
*
* @param {String} type
* @param {String} icon
* @return {Element}
*/
renderMarkButton = (type, icon) => {
const isActive = this.hasMark(type)
const onMouseDown = e => this.onClickMark(e, type)
return (
<span className="button" onMouseDown={onMouseDown} data-active={isActive}>
<span className="material-icons">{icon}</span>
</span>
)
}
/**
* Render a block-toggling toolbar button.
*
* @param {String} type
* @param {String} icon
* @return {Element}
*/
renderBlockButton = (type, icon) => {
const isActive = this.hasBlock(type)
const onMouseDown = e => this.onClickBlock(e, type)
return (
<span className="button" onMouseDown={onMouseDown} data-active={isActive}>
<span className="material-icons">{icon}</span>
</span>
)
}
/**
* Render the Slate editor.
*
* @return {Element}
*/
renderEditor = () => {
return (
<div className="editor">
<Editor
placeholder={'Type here...'}
state={this.state.state}
renderNode={this.renderNode}
renderMark={this.renderMark}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
onPaste={this.onPaste}
/>
</div>
)
}
/**
* On paste, deserialize the HTML and then insert the fragment.
*
* @param {Event} e
* @param {Object} paste
* @param {State} state
*/
onPaste = (e, paste, state) => {
if (paste.type != 'html') return
const { html } = paste
const { document } = serializer.deserialize(html)
return state
.transform()
.insertFragment(document)
.apply()
}
/**
* Render.
*
* @return {Component}
*/
render = () => {
return (
<div className="bettereditor">
{this.renderToolbar()}
{this.renderEditor()}
</div>
)
}
/**
* Return a node renderer for a Slate `node`.
*
* @param {Node} node
* @return {Component or Void}
*/
renderNode = (node) => {
return NODES[node.type]
}
/**
* Return a mark renderer for a Slate `mark`.
*
* @param {Mark} mark
* @return {Object or Void}
*/
renderMark = (mark) => {
return MARKS[mark.type]
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment