GrapesJS / mjml

Newsletter Builder with MJML components in GrapesJS
http://grapesjs.com/demo-mjml.html
BSD 3-Clause "New" or "Revised" License
638 stars 228 forks source link

Can't use grapesjs-mjml inside a react app. #223

Closed jakeFeldman closed 3 years ago

jakeFeldman commented 3 years ago

Hello and thanks for grapesjs!

I'm running into an issue using the MJML plugin with grapesjs inside a react app.

The canvas loads but I'm not able to drop any blocks into the canvas area. Not using the the mjml plugin the canvas loads and allows for blocks to be dropped into the canvas area.

I get the following warning when using grapesjs-mjml plugin:

Invalid target position: Target collection not found, Target is not droppable, accepts [undefined], Component not draggable, acceptable by [undefined]

Here is my setup, maybe I'm missing something simple.

"grapesjs": "^0.16.30",
"grapesjs-mjml": "^0.3.4",
import 'grapesjs/dist/css/grapes.min.css';
import grapesjs from 'grapesjs';
import grapesjsMJML from 'grapesjs-mjml';
import React, { useEffect } from 'react';

export const Editor = () => {
    useEffect(() => {
        const editor = grapesjs.init({
            container: '#email-editor',
            fromElement: true,
            avoidInlineStyle: false,
            panels: { defaults: [] },
            plugins: [grapesjsMJML],
            pluginsOpts: {
                [grapesjsMJML]: {}
            },
            height: '300px',
            blockManager: {
                appendTo: '#email-editor-blocks',
            }
        });
    }, []);
    return (
        <div>
            <div id="email-editor" />
            <div id="email-editor-blocks" />
        </div>
    );
};
DRoet commented 3 years ago

Hmm interesting, I've had problems in the past using VueJS (https://github.com/artf/grapesjs-mjml/issues/155) but I haven't seen this error before.

Does the block manager get rendered correctly? also could you try the unminified build aswell and see if that work:

import grapesjsMJML from 'grapesjs-mjml/dist/grapesjs-mjml'
jakeFeldman commented 3 years ago

Hey @DRoet thanks for the quick reply. The block manager gets rendered properly for my setup (i.e. no panel but attaching to a custom div)

Using the existing setup with the unminified build I get a new warning/error. The outcome is the same but seems to narrow it down more.

Invalid target position: Component not draggable, acceptable by [[data-gjs-type=mj-body], [data-gjs-type=mj-wrapper]]

I put together a repo https://github.com/jakeFeldman/grapesjs-mjml-demo for testing if that helps. I'll try to pull the repo down and see if I can isolate whats happening.

DRoet commented 3 years ago

Could you check on the latest release (0.4.0)? we now use the official mjml-browser build which should offer better web compatibility.

The import grapesjsMJML from 'grapesjs-mjml/dist/grapesjs-mjml' workaround is also no longer needed (when it comes to VueJS)

jakeFeldman commented 3 years ago

Hello @DRoet thanks for following up. I updated to version 0.4.0 and I still received the same error/warning.

If I use the default blocks panel I get this warning

Invalid target position: Component not draggable, acceptable by [[data-gjs-type=mj-body], [data-gjs-type=mj-wrapper]]

If I use my custom blocks panel I get the original error.

Invalid target position: Target collection not found, Target is not droppable, accepts [undefined], Component not draggable, acceptable by [undefined]
edward-vo commented 3 years ago

Try adding

<mjml>
  <mj-body>
    <mj-section>
    </mj-section>
  </mj-body>
</mjml>

It can be done thru editor.addComponents or fromElement

jakeFeldman commented 3 years ago

Thanks @edward-vo. I'll give it a try and see if that resolves my issue!

buzztnt commented 3 years ago

@jakeFeldman - did this resolve the issue ?

jakeFeldman commented 3 years ago

@buzztnt @edward-vo Adding the suggested mjml tags using editor.addComponents did allow me to add to the editor. We can close this issue!

Drew-Daniels commented 2 years ago

I'm seeing the same issue in our setup now. The previous set up worked prior to upgrading grapesjs from 0.18.4 to 0.19.5, but no longer works. I am getting this warning when I attempt to drag any block item onto the canvas:

{
    "errors": [
        "Target collection not found",
        "Component not draggable, acceptable by [[data-gjs-type=mj-body], [data-gjs-type=mj-wrapper]]"
    ],
    "model": {
        "tagName": "mj-section",
        "type": "mj-section"
    },
    "context": "sorter",
    "level": "warning"
}

defaults.js:

import grapesJSMJML from 'grapesjs-mjml';

const defaults = {
  container: '#gjs',
  height: '600px',
  width: 'auto',
  fromElement: true,

  plugins: [grapesJSMJML],
  pluginsOpts: {
    [grapesJSMJML]: {
      cmdBtnMoveLabel: 'Move',
      cmdBtnUndoLabel: 'Undo',
      cmdBtnRedoLabel: 'Redo',
      cmdBtnDesktopLabel: 'Desktop',
      cmdBtnTabletLabel: 'Tablet',
      cmdBtnMobileLabel: 'Mobile',

      expTplBtnTitle: 'View Code',
      fullScrBtnTitle: 'FullScreen',
      swichtVwBtnTitle: 'View Components',
      defaultTemplate: '', // Default template in case the canvas is empty
      categoryLabel: 'Basic',
    },
  },
  storageManager: {
    id: '', // 'gjs-',             // Prefix identifier that will be used on parameters
    type: 'remote', // Type of the storage
    autosave: true, // Store data automatically
    autoload: true, // Autoload stored data on init
    stepsBeforeSave: 1, // If autosave enabled, indicates how many changes are necessary before store method is triggered
  },
  colorPicker: {
    appendTo: '#picker-container',
  },
  blockManager: {
    appendTo: '#mjml-editor-blocks',
  },
  deviceManager: {
    devices: [
      {
        name: 'Desktop',
        width: '', // default size
      },
      {
        name: 'Mobile',
        width: '320px', // this value will be used on canvas width
        widthMedia: '480px', // this value will be used in CSS @media
      },
    ],
  },
  traitManager: {
    appendTo: '.styles-container',
  },
  styleManager: {
    appendTo: '.styles-container',
    sectors: [
      {
        name: 'Dimension',
        open: false,
        // Use built-in properties
        buildProps: ['width', 'min-height', 'padding'],
        // Use `properties` to define/override single property
        properties: [
          {
            // Type of the input,
            // options: integer | radio | select | color | slider | file | composite | stack
            type: 'integer',
            name: 'The width', // Label for the property
            property: 'width', // CSS property (if buildProps contains it will be extended)
            units: ['px', '%'], // Units, available only for 'integer' types
            defaults: 'auto', // Default value
            min: 0, // Min value, available only for 'integer' types
          },
        ],
      },
    ],
  },
  panels: {
    defaults: [
      {
        id: 'layers',
        el: '.panel__right',
        // Make the panel resizable
        resizable: {
          maxDim: 350,
          minDim: 200,
          tc: 0, // Top handler
          cl: 1, // Left handler
          cr: 0, // Right handler
          bc: 0, // Bottom handler
          // Being a flex child we need to change `flex-basis` property
          // instead of the `width` (default)
          keyWidth: 'flex-basis',
        },
      },
      {
        id: 'panel-devices',
        el: '.panel__devices',
        buttons: [
          {
            id: 'device-desktop',
            className: 'fa fa-desktop',
            command: 'set-device-desktop',
            active: true,
            togglable: false,
          },
          {
            id: 'device-mobile',
            className: 'fa fa-mobile',
            command: 'set-device-mobile',
            togglable: false,
          },
        ],
      },
      {
        id: 'panel-options',
        el: '#panel__options',
        buttons: [
          {
            id: 'undo',
            className: 'fa fa-undo',
            command: 'undo',
          },
          {
            id: 'redo',
            className: 'fa fa-repeat',
            command: 'redo',
          },
          {
            id: 'mjml-import',
            className: 'fa fa-upload',
            command: 'mjml-import',
          },
        ],
      },
      {
        id: 'basic-actions',
        el: '.panel__basic-actions',
        buttons: [
          {
            id: 'visibility',
            active: true, // active by default
            className: 'fa fa-square-o',
            command: 'sw-visibility', // Built-in command
          },
          {
            id: 'preview',
            className: 'fa fa-eye',
            command: 'preview',
            context: 'preview',
            attributes: { title: 'Preview' },
          },
          {
            id: 'export',
            className: 'fa fa-download',
            command: 'export-template',
            context: 'export-template', // For grouping context of buttons from the same panel
          },
          {
            id: 'show-json',
            className: 'fa fa-code',
            context: 'show-json',
            command(editor) {
              editor.Modal.setTitle('Components JSON')
                .setContent(
                  `<textarea style="width:100%; height: 450px;">
                ${JSON.stringify(editor.getComponents())}
              </textarea>`,
                )
                .open();
            },
          },
        ],
      },
    ],
  },
};

export default defaults;

Ged.js (i.e., grapesJS Editor)

import GrapesJS from 'grapesjs';
import ReactDOMServer from 'react-dom/server';
import React from 'react';
import defaultsDeep from 'lodash/defaultsDeep';
import defaults from './defaults';

/* eslint-disable */
class Ged {
  constructor(id, { variables, linkVariables }) {
    const accessToken = localStorage.getItem('<localStorageKeyHere>');
    const config = defaultsDeep(
      {
        storageManager: {
          options:
          {
            remote:
            {
              headers: {
                Authorization: `Bearer ${accessToken}`,
              },
              urlStore: `${REACT_APP_API_URL}/api/email_templates/${id}/builder_store`,
              urlLoad: `${REACT_APP_API_URL}/api/email_templates/${id}/builder_load`,
              contentTypeJson: true,
              credentials: 'same-origin',
              onStore: (data, editor) => {
                const pagesHtml = editor.Pages.getAll().map(page => {
                  const component = page.getMainComponent();
                  return {
                    html: editor.getHtml({ component }),
                    css: editor.getCss({ component }),
                  }
                });
                return {
                  ...data, ...pagesHtml[0]
                };
              },
              onLoad: result => {
                return {...result, pages : [{ component: result.html }]};
              },
            },
          }
        },
        richTextEditor: {
          actions: this.getActions(variables),
        },
      },
      defaults,
    );
    if (Ged.editor) {
      Ged.editor.destroy();
    }
    Ged.editor = GrapesJS.init(config);
    Ged.editor.Components.addType('wrapper', {
      model: {
        defaults: {
          tagName: 'div', // replace body to div
        },
        // And/or skip wrapper in the HTML output
        toHTML: function(opts) {
          return this.getInnerHTML(opts);
        }
      }
    });
    this.removeDevicePanel();
    this.addDeviceCommands();
    this.updateImage();
    this.addVariables(linkVariables);
    this.addBlocks();
    this.removeBlocks();
  }

  removeBlocks() {
    const { editor } = Ged;
    editor.BlockManager.remove('mj-hero');
    editor.BlockManager.remove('mj-wrapper');
  }

  addBlocks() {
    const { editor } = Ged;
    editor.BlockManager.add('a', {
      label: 'Link',
      content: '<a class="link">Select to change the URL</a>',
      attributes: { class: 'fa fa-link' },
    });
    editor.DomComponents.addType('link', {
      model: {
        defaults: {
          draggable: '[data-gjs-type=mj-text]',
        },
      },
    });
    editor.DomComponents.addType('mj-text', {
      model: {
        defaults: {
          droppable: ['a'],
        },
      },
    });
  }

  updateImage() {
    const comps = Ged.editor.DomComponents;
    const originalImage = comps.getType('mj-image');
    comps.addType('mj-image', {
      model: {
        defaults: {
          traits: ['href', 'rel', 'alt', 'title', 'src'],
          void: false,
        },
      },
    });
  }

  getActions(variables) {
    const { editor } = Ged;
    const selectDropdown = (
      <select className="gjs-field">
        <option value="" disabled selected>
          Variables
        </option>
        {variables.map((option) => (
          <option key={option.key} value={`{{ ${option.key} }}`}>
            {option.display_name}
          </option>
        ))}
      </select>
    );
    const actions = [
      'bold',
      'italic',
      'underline',
      'strikethrough',
      'link',
      {
        name: 'vars',
        icon: ReactDOMServer.renderToString(selectDropdown),
        // Bind the 'result' on 'change' listener
        event: 'change',
        result: (rte, action) => rte.insertHTML(action.btn.firstChild.value),
        // Reset the select on change
        update: (rte, action) => {
          action.btn.firstChild.value = '';
        },
      },
    ];

    return actions;
  }

  removeDevicePanel() {
    const { editor } = Ged;
    editor.Panels.removePanel('devices-c');
  }

  addDeviceCommands() {
    const { editor } = Ged;

    editor.Commands.add('set-device-desktop', {
      run: (editor) => editor.setDevice('Desktop'),
    });

    editor.Commands.add('set-device-mobile', {
      run: (editor) => editor.setDevice('Mobile'),
    });
  }

  addVariables(variables) {
    const { editor } = Ged;

    const buttonOptions = variables.map((option) => ({
      value: `{{ ${option.key}}}`,
      name: option.display_name,
    }));

    editor.DomComponents.addType('mj-button', {
      model: {
        defaults: {
          traits: [
            {
              type: 'select',
              label: 'Link',
              name: 'href',
              options: buttonOptions,
            },
          ],
        },
      },
    });
  }
}

export default Ged;

Builder.js:

import React from 'react';
import isEmpty from 'lodash/isEmpty';
import 'grapesjs/dist/css/grapes.min.css';
import './geditor.less';
import Ged from './Ged';

const { useEffect, useState } = React;

// slim example from https://github.com/thanhtunguet/grapesjs-react/issues/12
function Builder(props) {
  const { id, variables, linkVariables } = props;
  const [editor, setEditor] = useState(null);
  useEffect(() => {
    if (!editor && !isEmpty(variables) && !isEmpty(linkVariables)) {
      const newEditor = new Ged(id, { variables, linkVariables });
      setEditor(newEditor);
    }
  }, []);

  return (
    <div id="mjml-editor">
      <div className="mjml-editor-row" style={{ height: '100%' }}>
        <div id="mjml-editor-layers" className="mjml-editor-column" style={{ flexBasis: '200px' }}>
          <div id="panel__options" />
          <br />
          <br />
          <div id="mjml-editor-blocks" />
        </div>
        <div className="mjml-editor-column mjml-editor-clm">
          <div className="panel__top">
            <div className="panel__basic-actions" />
            <div className="panel__devices" />
            <div className="panel__switcher" />
          </div>
          <div className="editor-row">
            <div className="editor-canvas">
              <div id="gjs" style={{ overflow: 'hidden', height: '100%' }}>
                <mjml>
                  <mj-body>
                    <mj-section></mj-section>
                  </mj-body>
                </mjml>
              </div>
              <div id="picker-container" className="gjs-editor-cont" />
            </div>
            <div className="panel__right">
              <div className="layers-container" />
              <div className="styles-container" style={{ overflow: 'auto' }} />
              <div className="traits-container" />
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

export default Builder;

What I have tried:

Previously:

Builder.js:

<div id="gjs" style={{ overflow: 'hidden', height: '100%' }}>
  <mjml>
    <mj-body>
      <mj-section></mj-section>
    </mj-body>
  </mjml>
</div>

Ged.js:

...
Ged.editor = GrapesJS.init(config);
Ged.editor.Components.addType('wrapper', {
  model: {
    defaults: {
      tagName: 'div', // replace body to div
    },
    // And/or skip wrapper in the HTML output
    toHTML: function(opts) {
      return this.getInnerHTML(opts);
    }
  }
});
...

Currently:

Builder.js:

...
<div id="gjs" style={{ overflow: 'hidden', height: '100%' }} />
...

Ged.js:

...
Ged.editor = GrapesJS.init(config);
Ged.editor.Components.addType('wrapper', {
  model: {
    defaults: {
      tagName: 'div', // replace body to div
    },
    // And/or skip wrapper in the HTML output
    toHTML: function(opts) {
      return this.getInnerHTML(opts);
    }
  }
});
Ged.editor.addComponents('<mjml><mj-body><mj-section></mj-section></mj-body></mjml>')
...
Drew-Daniels commented 2 years ago

Something I'm noticing is that:

image

AbdelrhmanAmin commented 1 year ago

Here is how ChatGPT 4 helped me:

useEffect(() => {
  if (editorContainerRef.current !== null) {
    const editor = grapesjs.init({
      container: editorContainerRef.current,
      plugins: [grapesjsMjml],
      pluginsOpts: {
        [grapesjsMjml]: {},
      },
      storageManager: false,
    });

    // Set the initial content of the canvas
    const defaultContent = `
      <mjml>
        <mj-body>
        </mj-body>
      </mjml>`;
    editor.setComponents(defaultContent);

    return () => {
      editor.destroy();
    };
  }
}, [editorContainerRef]);
devarsalanali commented 1 year ago

// Set the initial content of the canvas const defaultContent = `

`; editor.setComponents(defaultContent);

This helped me too. Thank you so much

LEEDKCT commented 10 months ago

But if we remove storagManager: false, the blocks are not adding. Why?