In the first step the HTML string is parsed and trasformed to what is called **Component Definition**, so the result of the input would be:
@ -76,6 +81,8 @@ You can notice the result is similar to what is generally called a **Virtual DOM
The meaning of properties like `tagName`, `attributes` and `components` are quite obvious, but what about `type`?! This particular property specifies the actual **Component** of our **Component Definition** (you check the list of default components [below](#built-in-components)) and if it's omitted, the default one will be used `type: 'default'`.
At this point, a good question would be, how the editor assignes those types by starting from a simple HTML string? This step is identified as **Component Recognition** and it's explained in detail in the next paragraph.
### Component Recognition and Component Type Stack
As we said before, when you pass an HTML string as a component to the editor, that string is parsed and compiled to the [Component Definition](#component-definition) with a new `type` property. To understand what `type` should be assigned, for each parsed HTML Element, the editor iterates over all the defined components, called **Component Type Stack**, and checks via `isComponent` method (we will see it later) if that component type is appropriate for that element. The Component Type Stack is just a simple array of component types but what is matter is the order of those types. Any new added custom **Component Type** (we'll see later how to create them) goes on top of the Component Type Stack and each element returned from the parser iterates the stack from top to bottom (the last element of the stack is the `default` one), the iteration stops once one of the component returns a truthy value from the `isComponent` method.
@ -87,6 +94,7 @@ If you're importing big chunks of HTML code you might want to improve the perfor
:::
### Component instance
Once the **Component Definition** is ready and the type is assigned, the [Component](api/component) instance can be created (known also as the **Model**). Let's step back to our previous example with the HTML string, the result of the `append` method is an array of added components.
@ -139,8 +147,6 @@ So, the **Component instance** is responable for the **final data** (eg. HTML, J
### Component rendering
Another important part of components is how they are rendered in the **canvas**, this aspect is handled by the **View** of the component. It has nothing to do with the **final data**, you can return a big `<div>...</div>` string as HTML of your component but render it as a simple image in the canvas (think about placeholders for complex/dynamic data).
@ -204,7 +210,7 @@ Here below you can see the list of built-in component types, ordered by their po
Now that we know how components work, we can start exploring the process of creating new **Component Types**.
The first rule of defining new component types is to place the code inside a plugin. This is necessary if you want to load your custom types at the beginning, before any component initialization (eg. a template loaded from DB). The plugin is loaded before component fetch (eg. in case of Storage use) so it's a perfect place to define component types.
<u>The first rule of defining new component types is to place the code inside a plugin</u>. This is necessary if you want to load your custom types at the beginning, before any component initialization (eg. a template loaded from DB). The plugin is loaded before component fetch (eg. in case of Storage use) so it's a perfect place to define component types.
draggable: 'form, form *', // Can be dropped only inside `form` elements
droppable: false, // Can't drop other elements inside it
attributes: { // Default attributes
type: 'text',
name: 'default-name',
@ -251,6 +259,7 @@ To understand better how Traits work you should read its [dedicated page](Traits
:::
### isComponent
Let's see in detail what we have done so far. The first thing to notice is the `isComponent` function, we have mentioned it already in [this](#component-recognition-and-component-type-stack) section and we need it to make the editor understand `<input>` during the component recognition step.
One more tip, if you define a component type without the `isComponent`, the only way for the editor to see that component will be with a declared type (via object like `{ type: '...' }` or using `data-gjs-type`)
### Model
Now that we got how `isComponent` works we can start to explore the `model` property.
@ -474,6 +484,7 @@ You'll find other lifecycle methods, like `init`, [below](#lifecycle-hooks)
Now let's go back to our input component integration and see another useful part for the component customization
### View
Generally, when you create a component in GrapesJS you expect to see in the canvas the preview of what you've defined in the model. Indeed, by default, the editor does the exact thing and updates the element in the canvas when something in the model changes (eg. attributes, tag, etc.) to obtain the classic WYSIWYG (What You See Is What You Get) experience. Unfortunately, not always the simpliest thing is the right one, by building components for the builder you will notice that sometimes you'll need something more:
But now, how does the editor recognize which Component to bind to the `img` element and what to do with the `span` one?
Each Component inherits, from the base one, a particular static method
```js
/**
* @param {HTMLElement} el
* @return {Object}
*/
isComponent: function(el) {
...
}
```
This method gives us the possibility to recognize and bind component types to each HTMLElement (div, img, iframe, etc.). Each **HTML string/element** introduced inside the canvas will be processed by `isComponent` of all available types and if it matches, the object represented the type should be returned. The method `isComponent`**is skipped** if you add the component object (`{ type: 'my-custom-type', tagName: 'div', attribute: {...}, ...}`) or declare the type explicitly on the element (`<divdata-gjs-type="my-custom-type">...</div>`)
For example, with the image component this method looks like:
```js
// Image component
isComponent: function(el) {
if(el.tagName == 'IMG')
return {type: 'image'};
}
```
## Update Component Type
Let's try with something that might look a little bit tricky. What about a Google Map?!? Google Maps are generally embedded as `iframe`s, but the template can be composed by a lot of different `iframe`s. How can I tell the editor that a particular iframe is actually a Google's Map? Well, you'll have to figure out the right pattern, you have the `HTMLElement` so you can make all the checks you want. In this particular case this pattern is used:
Updating component types is quite easy, let's see how:
In addition to `tagName` check, we also used the `src` property, but you can actually override it with your own logic by extending the built-in component.
## Define new Component
Let's see an example with another HTML element that is not handled by default Component types. What about `input` elements?
const domc = editor.DomComponents;
With the default GrapesJS configuration `input`s are treated like any other element; you can move it around, style it, etc. However, we'd like to handle this type of element more specifically. In this case, we have to create a new Component type.
domc.addType('some-component', {
// You can update the isComponent logic or leave the one from `some-component`
// isComponent: (el) => false,
Let's define few specs for our new *Input* type:
* Can be dropped only inside `form` elements
* Can't drop other elements inside it
* Can change the type of the input (text, password, email, etc.)
* Can make it required for the form
To define a new Component type you need to choose from which built-in Component inherit its properties, in our case we just gonna choose the default one. Let's see a complete example of the new type definition
```js
// Get DomComponents module
var comps = editor.DomComponents;
// Get the model and the view from the default Component type
From the example above you can notice few interesting things: how to bind events, how to update directly the DOM and how to update the model. The difference between updating the DOM and the model is that the HTML code (the one you get with `editor.getHtml()`) is generated from the *Model* so updating directly the DOM will not affect it, it's just the change for the canvas.
## Update Component type
Here an example of how easily you can update/override the component
```js
var originalMap = comps.getType('map');
init() {
// Ovverride `init` function in `some-component`
}
},
comps.addType('map', {
model: originalMap.model.extend({
// Override how the component is rendered to HTML
toHTML: function() {
return '<div>My Custom Map</div>';
},
}, {
isComponent: function(el) {
// ... new logic for isComponent
},
}),
view: originalMap.view
// Update the view, if you need
view: {},
});
```
## Improvement over addType <Badgetext="0.14.50+"/>
Now, with the [0.14.50](https://github.com/artf/grapesjs/releases/tag/v0.14.50) release, defining new components or extending them is a bit easier (without breaking the old process)
* If you don't specify the type to extend, the `default` one will be used. In that case, you just
use objects for `model` and `view`
* The `defaults` property, in the `model`, will be merged automatically with defaults of the parent component
* If you use an object in `model` you can specify `isComponent` outside or omit it. In this case,
the `isComponent` is not mandatory but without it means the parser won't be able to identify the component
if not explicitly declared (eg. `<div data-gjs-type="new-component">...</div>`)
**Before**
```js
const defaultType = comps.getType('default');
comps.addType('new-component', {
model: defaultType.model.extend({
defaults: {
...defaultType.model.prototype.defaults,
someprop: 'somevalue',
},
...
}, {
// Even if it returns false, declaring isComponent is mandatory
isComponent(el) {
return false;
},
}),
view: defaultType.view.extend({ ... });
});
```
**After**
```js
comps.addType('new-component', {
// We can even omit isComponent here, as `false` return will be the default behavior
isComponent: el => false,
model: {
defaults: {
someprop: 'somevalue',
},
...
},
view: { ... };
});
```
* If you need to extend some component, you can use `extend` and `extendView` property.
* You can now omit `view` property if you don't need to change it
### Extend Component Type
**Before**
```js
const originalMap = comps.getType('map');
comps.addType('map', {
model: originalMap.model.extend({
...
}, {
isComponent(el) {
// ... usually, you'd reuse the same logic
},
}),
// Even if I do nothing in view, I have to specify it
view: originalMap.view
});
```
**After**
Sometimes you would need to create a new type by extending another one. Just use `extend` and `extendView` indicating the component to extend.
The `map` type is already defined, so it will be used as a base for the model and view.
We can skip `isComponent` if the recognition logic is the same of the extended component.
```js
comps.addType('map', {
model: { ... },
});
```
Extend the `model` and `view` with some other, already defined, components.
```js
comps.addType('map', {
comps.addType('my-new-component', {
isComponent: el => {/* ... */},
extend: 'other-defined-component',
model: { ... }, // Will extend 'other-defined-component'
view: { ... }, // Will extend 'other-defined-component'
// `isComponent` will be taken from `map`
model: { ... }, // Will extend the model from 'other-defined-component'
view: { ... }, // Will extend the view from 'other-defined-component'
});
```
```js
comps.addType('map', {
comps.addType('my-new-component', {
isComponent: el => {/* ... */},
extend: 'other-defined-component',
model: { ... }, // Will extend 'other-defined-component'
model: { ... }, // Will extend the model from 'other-defined-component'
extendView: 'other-defined-component-2',
view: { ... }, // Will extend 'other-defined-component-2'
// `isComponent` will be taken from `map`
view: { ... }, // Will extend the view from 'other-defined-component-2'
In the example above the editor will not get the new type from the HTML because the content is already parsed and appended, so it'll get it only with new components (eg. from Blocks)