I'm trying to build a basic Header component that lets you select H1 to H6 with a trait. But when an option is selected, the canvas doesn't update. The change is visible in the code view, and if I move the element in the canvas with the drag tool, the tag then changes. I have been reading the API docs as well as the source, but I can't make the component automatically re-render. I suspect that Grapes is not listening for a change of the component tag name. What is the appropriate way to force a re-render in this case?
var comps = editor.DomComponents;
var blocks = editor.BlockManager;
var textType = comps.getType('text');
var textModel = textType.model;
var textView = textType.view;
comps.addType('header', {
model: textModel.extend({
defaults: Object.assign({}, textModel.prototype.defaults, {
'custom-name': 'Header',
tagName: 'h1',
traits: [
{
type: 'select',
options: [
{value: 'h1', name: 'One (largest)'},
{value: 'h2', name: 'Two '},
{value: 'h3', name: 'Three '},
{value: 'h4', name: 'Four '},
{value: 'h5', name: 'Five '},
{value: 'h6', name: 'Six (smallest)'},
],
label: 'Size',
name: 'header-size',
changeProp: 1
}
]
}),
init() {
this.listenTo(this, 'change:header-size', this.changeTagName);
},
changeTagName() {
// view.tagName is a fn that returns model.tagName
this.set('tagName', this.get('header-size'));
}
}, {
isComponent: function(el) {
if(el && ['H1','H2','H3','H4','H5','H6'].includes(el.tagName)) {
return {type: 'header'};
}
}
}),
view: textView
});
blocks.add('header', {
label: 'Header',
category: 'Basic',
attributes: {class:'fa fa-header'},
content: {
type:'header',
content:'Insert your header text here',
activeOnRender: 1
}
});
Yeah this is because you can't change the tag name of an existing DOM element. Probably the solution would be, on tagName change, remove and replace the node (at the same position)
I've been trying to find a solution but can't. Do you mean alter the HTMLElement or the Component? The component model is the source of truth, and it's unsafe to edit the canvas DOM, right? Here is my shot at a component-based solution:
changeTagName() {
var em = this.em;
var ed = this.em.get('Editor');
// deselect the current header
ed.select(null);
// create a new component
var parent = this.parent();
parent.components().add({
tagName: this.get('header-size'),
type: 'header',
style: this.getStyle(),
attributes: this.getAttributes(),
content: this.view.el.innerHTML, // should only be text, since header comp extends text type
'header-size': this.get('header-size')
});
// find the index in the child list of the old header's parent
var parent_children = this.parent().components().models;
var element_i = null;
for(var i = 0; i < parent_children.length; i++) {
if(parent_children[i] == this) {
element_i = i;
}
}
//this.parent().components().remove(this);
this.destroy();
var new_comp = parent_children.pop(); // remove the new component from the end of list
parent_children.splice(element_i, 0, new_comp); // inject into child list at appropriate index
}
I'm having the same problem, though. The header is in the right spot in the code viewer and in the layers panel, but it's at the end of the list on the Canvas. How do I trigger a re-rendering of the canvas? Maybe it would be nice to have a Component API method to add a component at a certain index.
All advice is welcome. Thanks @artf
@z1lk I already added such a thing for the next release
// Model
// inside init
this.listenTo(this, 'change:tagName', this.tagUpdated);
// ...
tagUpdated() {
const coll = this.collection;
const at = coll.indexOf(this);
coll.remove(this);
coll.add(this, { at });
},
Great. I can now simply create a Header component with a trait like so:
{
type: 'select',
options: [
{value: 'h1', name: 'One (largest)'},
{value: 'h2', name: 'Two '},
{value: 'h3', name: 'Three '},
{value: 'h4', name: 'Four '},
{value: 'h5', name: 'Five '},
{value: 'h6', name: 'Six (smallest)'},
],
label: 'Size',
name: 'tagName',
changeProp: 1
}
Hi how to check after that the component in isComponent ?
@olivmonnier check the tag name:
isComponent: function(el) {
if(el && ['H1','H2','H3','H4','H5','H6'].includes(el.tagName)) {
return {type: 'header'};
}
}
With this code, el become a string so i can't check the tag name:
export default (editor, config = {}) => {
const domc = editor.DomComponents;
const defaultType = domc.getType('default');
const defaultModel = defaultType.model;
const defaultView = defaultType.view;
const textType = domc.getType('text');
const textModel = textType.model;
const textView = textType.view;
domc.addType('header', {
model: textModel.extend({
defaults: Object.assign({}, textModel.prototype.defaults, {
'custom-name': 'Header',
attributes: { 'data-type': 'header' },
tagName: 'h1',
traits: [
{
type: 'select',
options: [
{ value: 'h1', name: 'One (largest)'},
{ value: 'h2', name: 'Two' },
{ value: 'h3', name: 'Three' },
{ value: 'h4', name: 'Four' },
{ value: 'h5', name: 'Five' },
{ value: 'h6', name: 'Six (smallest)' }
],
label: 'Size',
name: 'tagName',
changeProp: 1
}
]
}),
init() {
this.listenTo(this, "change:tagName", this.tagUpdated);
},
tagUpdated() {
const coll = this.collection;
const at = coll.indexOf(this);
coll.remove(this);
coll.add(this, { at });
}
}, {
isComponent(el) {
if (typeof el == 'string') {
return { type: 'header' }
}
}
}),
view: textView
})
// ...
}
I'm not sure why el would be a string, but see the commit that closed the issue: https://github.com/artf/grapesjs/commit/e450cb98855d16ad819f1214350825a50e45e910
If you're using the latest Grapes version, the Component listens for a change of tagName and does the node replacement itself. So you should be able to remove your init and tagUpdated functions. The trait will update tagName and the Component will do the rest.
Edit:
Another thing I thought of is: sometimes the object passed to isComponent doesn't have the method that I call on it, and there will be an error thrown in that case. You could do a safety check first:
if(el && el.tagName && ['H1','H2','H3','H4','H5','H6'].includes(el.tagName)) {
...
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Most helpful comment
@z1lk I already added such a thing for the next release