Create Editor Components 

Hint

GrapesJS component description is available in the GrapesJS documentation, as well as the way of creating component types.

To simplify adding new types of component there, component type builders were created that include all needed data of a new component type. Under the hood, they implement the same actions that are propose in the GrapesJS documentation but make component type declaration more structured and convenient.

Creating Type In Single Module 

To create your own component type, first create a descendant of BaseTypeBuilder:

src/{YourBundleName}/Resources/js/app/grapesjs/types/some-type.js 
import BaseType from 'orocms/js/app/grapesjs/types/base-type';

const SomeType = BaseType.extend({
    button: {
        label: 'Button label',
        category: 'Basic',
        attributes: {
            'class': 'fa fa-hand-pointer-o'
        }
    },

    modelProps: {
        defaults: {
            classes: ['component-class', 'some-class'],
            tagName: 'span'
        },

        // Optional: Define some functionality after model was created
        // Example: Add default child components
        init() {
            const components = this.get('components');

            if (!components.length) {
                components.add([{
                    type: 'textnode',
                    content: 'Inner content'
                }]);
            }
        }

        someMethod() {
            // Some custom logic
        },

        // Optional: Define the parent type name that will be extended in the model
        extend: 'parent-type',

        // Optional: Following GrapesJS documentation could define some methods which going to extend for type model
        extendFn: ['someMethod']
    },

    viewProps: {
        onRender() {
            // Some custom logic
        },

        // Optional: Defining parent type name, it type going to extend for view
        extendView: 'parent-type',

        // Optional: Following GrapesJS documentation could define some methods which going to extend for type view
        extendFnView: ['someMethod']
    },

    editorEvents: {
        'component:create': 'onCreate',
        'prevent component:selected': 'onSelect'
    },

    commands: {
        'my-command': () => {
            // Some custom logic
        }
    },

    usedTags: ['div', 'span'],

    constructor: function SomeType(options) {
        SomeType.__super__.constructor.call(this, options);
    },

    onInit() {
        this.editor.runCommand('my-command');
    },

    onCreate(model) {
        if (this.isOwnModel(model)) {
            // Some custom logic
        }
    },

    onSelect(model) {
        if (this.isOwnModel(model)) {
            // Some custom logic
        }
    },

    isComponent(el) {
        return el.nodeType === Node.ELEMENT_NODE && el.tagName === 'SPAN' && el.classList.contains('some-class');
    }
}, {
    // Define static property with type name
    type: 'some-type'
});

export default SomeType;

Creating Type With Function Constructors 

You can define the model and the view type as a constructor function, as this gives you more freedom when implementing the required logic. You can provide a more advance method to extend existing types in the editor. You can also separate large components into different modules, as illustrated in the example below.

Example

Create a type model module called some-type-model.js. It is similar to other Backbone components but should be created in a special function with BaseTypeModel and other options in the function attributes.

src/{YourBundleName}/Resources/js/app/grapesjs/types/some-type/some-type-model.js 
export default (BaseTypeModel, {editor}) => {
    const SomeTypeModel = BaseTypeModel.extend({
        constructor: function SomeTypeModel(...args) {
            // Note: Constructor should always return super
            return SomeTypeModel.__super__.constructor.apply(this, args);
        },

        init() {
            const components = this.get('components');

            if (!components.length) {
                components.add([{
                    type: 'textnode',
                    content: 'Inner content'
                }]);
            }
        },

        getAttributes(opts = {}) {
            const attributes = SomeTypeModel.__super__.getAttributes.call(this, opts);

            attributes['new-attr'] = 'Attribute value';

            return attributes;
        }
    });

    Object.defineProperty(SomeTypeModel.prototype, 'defaults', {
        value: {
            ...SomeTypeModel.prototype.defaults,
            classes: ['component-class', 'some-class'],
            tagName: 'span'
        }
    });

    return SomeTypeModel;
};

Create a type view module some-type-view.js the same way you create a type model.

src/{YourBundleName}/Resources/js/app/grapesjs/types/some-type/some-type-view.js 
export default (BaseTypeView, {editor}) => {
    const SomeTypeView = BaseTypeView.extend({
        constructor: function SomeTypeView(...args) {
            // Notice: Constructor should always returns super
            return SomeTypeView.__super__.constructor.apply(this, args);
        },

        onRender() {
            // Do something after rendered
        }
    });

    return SomeTypeView;
};

Create a component of type some-type. It is similar to Creating Type In Single Module but the model and the view are placed in separate modules. Import them into the header of main module and set in properties ModelType and ViewType.

src/{YourBundleName}/Resources/js/app/grapesjs/types/some-type/index.js 
import BaseType from 'orocms/js/app/grapesjs/types/base-type';
import ModelType from './some-type-model.js';
import ViewType from './some-type-view.js';

const SomeType = BaseType.extend({
    button: {
        label: 'Button label',
        category: 'Basic',
        attributes: {
            'class': 'fa fa-hand-pointer-o'
        }
    },

    ModelType,

    ViewType,

    editorEvents: {
        'component:create': 'onCreate',
        'prevent component:selected': 'onSelect'
    },

    commands: {
        'my-command': () => {
            // Some custom logic
        }
    },

    usedTags: ['div', 'span'],

    constructor: function SomeType(options) {
        SomeType.__super__.constructor.call(this, options);
    },

    onInit() {
        this.editor.runCommand('my-command');
    },

    onCreate() {
        // Some custom logic
    },

    onSelect() {
        // Some custom logic
    },

    isComponent(el) {
        return el.nodeType === Node.ELEMENT_NODE && el.tagName === 'SPAN' && el.classList.contains('some-class');
    }
}, {
    // Define static property with type name
    type: 'some-type'
});

export default SomeType;

Properties 

parentType

String

The name of the component type that used as a parent. If it is not determined, use the default type

editor

Object

An instance of GrapesJS WYSIWYG

button

Object

Data to register a panel button (if required for the new component type)

label

String

The button label

category

String

Place the button to the category container

attributes

Object

An object of the attributes such as class name or data attribute

TypeModel

Function

Defining model as the constructor function

TypeView

Function

Defining view as the constructor function

modelProps

Object

Methods and props used to extend the WYSIWYG component model. Prop defaults is merged with the default model attributes.

viewProps

Object

Methods and props used to extend the WYSIWYG component view

commands

Object

The key is the command name and the value is the command callback

editorEvents

Object

The key is the event name and the value is the name of the builder method

template

HTML

Set the component template; if the template is not set, the button will use its own component type as content.

Static Properties 

type

String

Required

Define the component type name

priority

Number

Optional

Define the priority order to apply the component type. It is important when creating a type from a parent type

Methods 

Name

Return

Options

Description

getTypeModelOptions

Object

Return object with properties from wrapper component for provide in the type model instance.

getTypeViewOptions

Object

Return object with properties from wrapper component for provide in the type model instance

getButtonTemplateData

Object

Return data for the button template

isOwnModel

Boolean

TypeModel

Detect is it model instance of for current type

onInit

Object

Hook after wrapper component initialized

getType

String

Get component type name

isComponent

Object|String|Boolean

DOMNode

Identify the the component type

Component Type Registration 

For registering new type need create plugin or use exists plugin. Plugins loading dynamical, required add to jsmodules.yml dynamic path

Hint

Pay attention type should registered before editor initialized.

Register the created type builder with the component manager:

src/{YourBundleName}/Resources/js/app/grapesjs/plugins/foo-plugin.js 
import GrapesJS from 'grapesjs';
import ComponentManager from 'orocms/js/app/grapesjs/plugins/components/component-manager';
import SomeTypeBuilder from '{yourbundlename}/js/app/grapesjs/types/some-type';

export default GrapesJS.plugins.add('foo-plugin', () => {
    ComponentManager.registerComponentTypes({
        'some': {
            Constructor: SomeTypeBuilder
        }
    });
});

Use Symfony Form Type Extension to add the new plugin to the WYSIWYG field. For more information about Symfony forms, see form type extension.

src/{YourBundleName}/Form/Extension/FooWysiwygTypeExtension.php 
<?php

namespace App\Form\Extension;

use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FileType;

class FooWysiwygTypeExtension extends AbstractTypeExtension
{
    public function finishView(FormView $view, FormInterface $form, array $options): void
    {
        $pageComponentOptions = json_decode($view->vars['attr']['data-page-component-options'] ?? '', true);
        $pageComponentOptions['builderPlugins']['foo-plugin']['jsmodule'] = '{yourbundlename}/js/app/grapesjs/plugins/foo-plugin';
        $view->vars['attr']['data-page-component-options'] = json_encode($pageComponentOptions);
    }

    /**
     * Returns an array of extended types.
     */
    public static function getExtendedTypes(): iterable
    {
        return [WYSIWYGType::class];
    }
}

Add the path to the file to dynamic imports in jsmodules.yml. If jsmodules.yml does not exist, you need to create it. For more information about jsmodules.yml, see JS Modules.

src/{YourBundleName}/Resources/config/jsmodules.yml 
dynamic-imports:
    foo_app:
        - {yourbundlename}/js/app/grapesjs/plugins/foo-plugin