├── README.md ├── package.json ├── public └── index.html └── src ├── Listing └── index.js ├── base-react-component.js ├── index.js ├── mui-components └── index.js └── react-components.js /README.md: -------------------------------------------------------------------------------- 1 | # grapesjs-react-components-by-artf 2 | 3 | Forked from https://codesandbox.io/s/grapesjs-react-components-n6sff 4 | 5 | Related issue: https://github.com/artf/grapesjs/issues/1970 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grapesjs-react-components", 3 | "version": "1.0.0", 4 | "description": "Use React components in GrapesJS", 5 | "keywords": [ 6 | "react", 7 | "grapesjs" 8 | ], 9 | "main": "src/index.js", 10 | "dependencies": { 11 | "@material-ui/core": "4.12.3", 12 | "grapesjs": "0.17.22", 13 | "grapesjs-blocks-basic": "0.1.8", 14 | "react": "17.0.2", 15 | "react-dom": "17.0.2" 16 | }, 17 | "devDependencies": { 18 | "typescript": "3.3.3" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test --env=jsdom", 24 | "eject": "react-scripts eject" 25 | }, 26 | "browserslist": [ 27 | ">0.2%", 28 | "not dead", 29 | "not ie <= 11", 30 | "not op_mini all" 31 | ] 32 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Listing/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Listing extends React.Component { 4 | constructor(props) { 5 | super(); 6 | this.state = { 7 | counter: 0 8 | }; 9 | } 10 | 11 | updateCounter() { 12 | const { counter } = this.state; 13 | this.setState({ 14 | counter: counter + 1 15 | }); 16 | } 17 | 18 | componentDidMount() { 19 | setInterval(this.updateCounter.bind(this), 1000); 20 | } 21 | 22 | render() { 23 | const { props, state } = this; 24 | const { mlsid, children } = props; 25 | const { counter } = state; 26 | 27 | return ( 28 |
29 |
Mlsid: {mlsid}
30 |
{children}
31 |
Counter: {counter}
32 |
33 | ); 34 | } 35 | } 36 | 37 | export default Listing; 38 | -------------------------------------------------------------------------------- /src/base-react-component.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | import React from 'react'; 3 | 4 | export default (editor) => { 5 | const domc = editor.Components; 6 | const defType = domc.getType('default'); 7 | const defModel = defType.model; 8 | const wrpChld = 'data-chld'; 9 | 10 | // Main React component 11 | domc.addType('react-component', { 12 | model: { 13 | toHTML(opts = {}) { 14 | return defModel.prototype.toHTML.call(this, { 15 | ...opts, 16 | tag: this.get('type') 17 | }); 18 | } 19 | }, 20 | view: { 21 | tagName: 'div', 22 | 23 | init() { 24 | const { model } = this; 25 | this.listenTo(model, 'change:attributes', this.render); 26 | this.listenTo(model.components(), 'add remove reset', this.__upRender); 27 | }, 28 | 29 | getChildrenContainer() { 30 | const { childrenContainer } = this; 31 | if (childrenContainer) return childrenContainer; 32 | 33 | this.childrenContainer = document.createElement('childc'); 34 | 35 | return this.childrenContainer; 36 | }, 37 | 38 | /** 39 | * We need this container to understand if the React component is able 40 | * to render children 41 | */ 42 | createReactChildWrap() { 43 | return React.createElement('span', { [wrpChld]: true }); 44 | }, 45 | 46 | createReactEl(cmp, props) { 47 | return React.createElement(cmp, props, this.createReactChildWrap()); 48 | }, 49 | 50 | mountReact(cmp, el) { 51 | ReactDOM.render(cmp, el); 52 | }, 53 | 54 | render() { 55 | const { model, el } = this; 56 | this.updateAttributes(); 57 | this.renderChildren(); 58 | const reactEl = this.createReactEl(model.get('component'), { 59 | ...model.get('attributes') 60 | }); 61 | this.mountReact(reactEl, el); 62 | const chld = el.querySelector(`span[${wrpChld}]`); 63 | 64 | // If the container is found, the react component is able to render children 65 | if (chld) { 66 | const chldCont = this.getChildrenContainer(); 67 | while (chldCont.firstChild) { 68 | chld.appendChild(chldCont.firstChild); 69 | } 70 | } 71 | 72 | return this; 73 | }, 74 | 75 | __upRender() { 76 | clearTimeout(this._upr); 77 | this._upr = setTimeout(() => this.render()); 78 | } 79 | } 80 | }); 81 | }; 82 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import grapesjs from 'grapesjs'; 2 | import Basics from 'grapesjs-blocks-basic'; 3 | 4 | import BaseReactComponent from './base-react-component'; 5 | import ReactComponents from './react-components'; 6 | import MuiComponents from './mui-components'; 7 | 8 | const editor = grapesjs.init({ 9 | container: '#gjs', 10 | height: '100%', 11 | storageManager: false, 12 | noticeOnUnload: false, 13 | plugins: [Basics, BaseReactComponent, ReactComponents, MuiComponents] 14 | }); 15 | 16 | editor.setComponents(` 17 |
18 | 19 | Foo 20 | 21 | 22 |
23 | Bar 24 |
25 | 26 | Click Me 27 | 28 | 29 |
30 | 31 | 32 | Click Me 33 | 34 | 35 | 36 |
37 | `); 38 | -------------------------------------------------------------------------------- /src/mui-components/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { create } from 'jss'; 3 | import { StylesProvider, jssPreset } from '@material-ui/styles'; 4 | import { Button, Slider, SnackbarContent } from '@material-ui/core'; 5 | 6 | export default (editor) => { 7 | const { Blocks, Components } = editor; 8 | const sheetsManager = new Map(); 9 | 10 | // Helper for MUI components 11 | const addCmp = ({ type, component, props }) => { 12 | Components.addType(type, { 13 | extend: 'react-component', 14 | model: { 15 | defaults: { 16 | ...props, 17 | component 18 | } 19 | }, 20 | view: { 21 | /** 22 | * We need this in order to render MUI styles in the canvas 23 | */ 24 | createReactEl(cmp, props) { 25 | const cmpMain = React.createElement( 26 | cmp, 27 | props, 28 | this.createReactChildWrap() 29 | ); 30 | return React.createElement( 31 | StylesProvider, 32 | { 33 | sheetsManager, 34 | jss: create({ 35 | plugins: [...jssPreset().plugins], 36 | insertionPoint: this.em.get('Canvas').getDocument().head 37 | }) 38 | }, 39 | cmpMain 40 | ); 41 | } 42 | }, 43 | isComponent: (el) => el.tagName === type.toUpperCase() 44 | }); 45 | 46 | Blocks.add(type, { 47 | label: type, 48 | category: 'MUI', 49 | content: { type } 50 | }); 51 | }; 52 | 53 | addCmp({ 54 | type: 'MuiButton', 55 | component: Button, 56 | props: { 57 | attributes: { 58 | color: 'primary', 59 | variant: 'contained' 60 | }, 61 | components: 'Click me', 62 | traits: [ 63 | { 64 | type: 'select', 65 | label: 'Variant', 66 | name: 'variant', 67 | options: [ 68 | { value: 'contained', name: 'Contained' }, 69 | { value: 'outlined', name: 'Outlined' } 70 | ] 71 | }, 72 | 73 | { 74 | type: 'checkbox', 75 | label: 'Disabled', 76 | name: 'disabled' 77 | }, 78 | { 79 | type: 'select', 80 | label: 'Color', 81 | name: 'color', 82 | options: [ 83 | { value: 'primary', name: 'Primary' }, 84 | { value: 'secondary', name: 'Secondary' } 85 | ] 86 | } 87 | ] 88 | } 89 | }); 90 | 91 | addCmp({ 92 | type: 'Slider', 93 | component: Slider, 94 | props: { 95 | stylable: false, 96 | editable: true, 97 | void: true, 98 | attributes: { 99 | min: 0, 100 | max: 100 101 | }, 102 | traits: [ 103 | { 104 | type: 'number', 105 | label: 'Min', 106 | name: 'min' 107 | }, 108 | { 109 | type: 'number', 110 | label: 'Max', 111 | name: 'max' 112 | } 113 | ] 114 | } 115 | }); 116 | 117 | addCmp({ 118 | type: 'Snackbar', 119 | component: (props) => 120 | React.createElement(SnackbarContent, { 121 | ...props, 122 | message: props.children 123 | }), 124 | props: { 125 | stylable: false, 126 | editable: true, 127 | traits: [] 128 | } 129 | }); 130 | }; 131 | -------------------------------------------------------------------------------- /src/react-components.js: -------------------------------------------------------------------------------- 1 | import Listing from './Listing'; 2 | 3 | export default (editor) => { 4 | editor.Components.addType('Listing', { 5 | extend: 'react-component', 6 | model: { 7 | defaults: { 8 | component: Listing, 9 | stylable: true, 10 | resizable: true, 11 | editable: true, 12 | draggable: true, 13 | droppable: true, 14 | attributes: { 15 | mlsid: 'Default MLSID', 16 | editable: true 17 | }, 18 | traits: [ 19 | { 20 | type: 'number', 21 | label: 'MLS ID', 22 | name: 'mlsid' 23 | } 24 | ] 25 | } 26 | }, 27 | isComponent: (el) => el.tagName === 'LISTING' 28 | }); 29 | 30 | editor.BlockManager.add('listing', { 31 | label: "
Listing
", 32 | category: 'React Components', 33 | content: 'Foo' 34 | }); 35 | }; 36 | --------------------------------------------------------------------------------