Skip to content

Commit

Permalink
add controlled component functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
Mattias Wikstrom committed Feb 19, 2018
1 parent 95582da commit 6cf4a24
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 29 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Expand Up @@ -2,4 +2,5 @@ node_modules
lib
.DS_Store
yarn.lock
storybook-static
storybook-static
*.tgz
25 changes: 25 additions & 0 deletions README.md
Expand Up @@ -127,6 +127,31 @@ Here is a full list of the events available:
* `onVisualAid`
</details>

## Using as a controlled component

If you want to use the component as a [controlled component](https://reactjs.org/docs/forms.html#controlled-components) you should use the `onEditorChange` instead of the `onChange` event, like this:

```js
class MyComponent extends React.Component {
constructor(props) {
super(props);

this.state = { content: '' };
this.handleEditorChange = this.handleEditorChange.bind(this);
}

handleEditorChange(content) {
this.setState({ content });
}

render() {
return (
<Editor value={this.state.content} onEditorChange={this.handleEditorChange} />
)
}
}
```

## Loading TinyMCE
### Auto-loading from TinyMCE Cloud
The `Editor` component needs TinyMCE to be globally available to work, but to make it as easy as possible it will automatically load [TinyMCE Cloud](https://www.tinymce.com/docs/get-started-cloud/) if it can't find TinyMCE available when the component is mounting. To get rid of the `This domain is not registered...` warning, sign up for the cloud and enter the api key like this:
Expand Down
9 changes: 6 additions & 3 deletions src/Utils.ts
Expand Up @@ -6,16 +6,19 @@
*
*/

import { EditorPropTypes } from './components/EditorPropTypes';
import { eventPropTypes } from './components/EditorPropTypes';

const isValidKey = (keys: string[]) => (key: string) => keys.indexOf(key) !== -1;

// tslint:disable-next-line:ban-types
export const isFunction = (x: any): x is Function => typeof x === 'function';

export const bindHandlers = (props: any, editor: any): void => {
Object.keys(props)
.filter(isValidKey(Object.keys(EditorPropTypes)))
.filter(isValidKey(Object.keys(eventPropTypes)))
.forEach((key: string) => {
const handler = props[key];
if (typeof handler === 'function') {
if (isFunction(handler)) {
editor.on(key.substring(2), (e: any) => handler(e, editor));
}
});
Expand Down
53 changes: 39 additions & 14 deletions src/components/Editor.tsx
Expand Up @@ -6,19 +6,20 @@
*
*/

import { ValidationMap } from 'prop-types';
import * as React from 'react';
import { IEvents } from '../Events';
import { EventHandler, IEvents } from '../Events';
import * as ScriptLoader from '../ScriptLoader';
import { getTinymce } from '../TinyMCE';
import { bindHandlers, isTextarea, mergePlugins, uuid } from '../Utils';
import { bindHandlers, isFunction, isTextarea, mergePlugins, uuid } from '../Utils';
import { EditorPropTypes, IEditorPropTypes } from './EditorPropTypes';

export interface IProps {
apiKey: string;
id: string;
inline: boolean;
initialValue: string;
onEditorChange: EventHandler<any>;
value: string;
init: Record<string, any>;
tagName: string;
cloudChannel: string;
Expand All @@ -32,9 +33,10 @@ const scriptState = ScriptLoader.create();
export class Editor extends React.Component<IAllProps> {
public static propTypes: IEditorPropTypes = EditorPropTypes;
private element: Element | null = null;
private id: string;
private editor: any;
private inline: boolean;
private id?: string;
private editor?: Record<any, any>;
private inline?: boolean;
private currentContent?: string | null;

public componentWillMount() {
this.id = this.id || this.props.id || uuid('tiny-react');
Expand All @@ -54,15 +56,26 @@ export class Editor extends React.Component<IAllProps> {
}

public componentWillUnmount() {
this.cleanUp();
getTinymce().remove(this.editor);
}

public componentWillReceiveProps(nextProps: Partial<IProps>) {
if (this.editor && this.editor.initialized) {
this.currentContent = this.currentContent || this.editor.getContent();

if (typeof nextProps.value === 'string' && nextProps.value !== this.props.value && nextProps.value !== this.currentContent) {
this.editor.setContent(nextProps.value);
}
}
}

public render() {
return this.inline ? this.renderInline() : this.renderIframe();
}

private initialise = () => {
const initialValue = typeof this.props.initialValue === 'string' ? this.props.initialValue : '';
const initialValue =
typeof this.props.value === 'string' ? this.props.value : typeof this.props.initialValue === 'string' ? this.props.initialValue : '';
const finalInit = {
...this.props.init,
target: this.element,
Expand All @@ -71,8 +84,9 @@ export class Editor extends React.Component<IAllProps> {
toolbar: this.props.toolbar || (this.props.init && this.props.init.toolbar),
setup: (editor: any) => {
this.editor = editor;
editor.on('init', () => editor.setContent(initialValue));
bindHandlers(this.props, editor);
editor.on('init', () => {
this.initEditor(editor, initialValue);
});

if (this.props.init && typeof this.props.init.setup === 'function') {
this.props.init.setup(editor);
Expand All @@ -87,6 +101,21 @@ export class Editor extends React.Component<IAllProps> {
getTinymce().init(finalInit);
};

private initEditor(editor: any, initialValue: string) {
editor.setContent(initialValue);

if (isFunction(this.props.onEditorChange)) {
editor.on('change keyup setcontent', (e: any) => {
this.currentContent = editor.getContent();
if (isFunction(this.props.onEditorChange)) {
this.props.onEditorChange(this.currentContent);
}
});
}

bindHandlers(this.props, editor);
}

private renderInline() {
const { tagName = 'div' } = this.props;

Expand All @@ -99,8 +128,4 @@ export class Editor extends React.Component<IAllProps> {
private renderIframe() {
return <textarea ref={(elm) => (this.element = elm)} style={{ visibility: 'hidden' }} id={this.id} />;
}

private cleanUp() {
getTinymce().remove(this.editor);
}
}
26 changes: 16 additions & 10 deletions src/components/EditorPropTypes.ts
Expand Up @@ -14,16 +14,7 @@ export type CopyProps<T> = { [P in keyof T]: PropTypes.Requireable<any> };

export interface IEditorPropTypes extends CopyProps<IEvents>, CopyProps<IProps> {}

export const EditorPropTypes: IEditorPropTypes = {
apiKey: PropTypes.string,
id: PropTypes.string,
inline: PropTypes.bool,
init: PropTypes.object,
initialValue: PropTypes.string,
tagName: PropTypes.string,
cloudChannel: PropTypes.oneOf(['stable', 'dev', 'testing']),
plugins: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
toolbar: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
export const eventPropTypes = {
onActivate: PropTypes.func,
onAddUndo: PropTypes.func,
onBeforeAddUndo: PropTypes.func,
Expand Down Expand Up @@ -88,3 +79,18 @@ export const EditorPropTypes: IEditorPropTypes = {
onUndo: PropTypes.func,
onVisualAid: PropTypes.func
};

export const EditorPropTypes: IEditorPropTypes = {
apiKey: PropTypes.string,
id: PropTypes.string,
inline: PropTypes.bool,
init: PropTypes.object,
initialValue: PropTypes.string,
onEditorChange: PropTypes.func,
value: PropTypes.string,
tagName: PropTypes.string,
cloudChannel: PropTypes.oneOf(['stable', 'dev', 'testing']),
plugins: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
toolbar: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
...eventPropTypes
};
49 changes: 48 additions & 1 deletion stories/index.tsx
Expand Up @@ -11,18 +11,65 @@ setDefaults({
propTables: false
});

class StateFulEditor extends React.Component<any, { data: string }> {
constructor(props) {
super(props);
this.state = {
data: '<p>hello</p>'
};
}

public handleChange(data: string) {
this.setState({ data });
}

public componentDidMount() {
setTimeout(() => {
this.setState({ data: '<p>new content2</p>' });
}, 100);
setTimeout(() => {
this.setState({ data: '<p>new content3</p>' });
}, 1000);
setTimeout(() => {
this.setState({ data: '<p>new content1</p>' });
}, 0);
}

public render() {
const textareaStyle = { width: '100%', height: '200px', fontSize: '1em' };
return (
<div>
<Editor {...this.props} value={this.state.data} onEditorChange={(e) => this.handleChange(e)} />
<textarea style={textareaStyle} value={this.state.data} onChange={(e) => this.handleChange(e.target.value)} />
</div>
);
}
}

storiesOf('Editor', module)
.add(
'Controlled input editor',
withInfo({
text: 'Simple iframe editor with some initial html value. Logs editor content on change event.'
})(() => <StateFulEditor plugins="table" />)
)
.add(
'Iframe editor',
withInfo({
text: 'Simple iframe editor with some initial html value. Logs editor content on change event.'
})(() => <Editor initialValue={content} onChange={(event, editor) => console.log(editor.getContent())} plugins="link table" />)
)
.add(
'Inline init editor',
withInfo({
text: 'Simple inline editor with some initial html value. Logs editor content on change event.'
})(() => <Editor init={{ inline: true, plugins: 'link table wordcount', toolbar: 'bold link table' }} />)
)
.add(
'Inline editor',
withInfo({
text: 'Simple inline editor with some initial html value. Logs editor content on change event.'
})(() => <Editor inline initialValue={content} onChange={(event, editor) => console.log(editor.getContent())} />)
})(() => <Editor inline plugins="link table wordcount" toolbar="bold link table" />)
)
.add(
'Inlite editor',
Expand Down
2 changes: 2 additions & 0 deletions tsconfig.json
Expand Up @@ -7,6 +7,8 @@
"moduleResolution": "node",
"noImplicitAny": true,
"declaration": true,
"strict": true,
"noUnusedLocals": true,
"outDir": "./lib",
"preserveConstEnums": true,
"strictNullChecks": true,
Expand Down

0 comments on commit 6cf4a24

Please sign in to comment.