Hi,
I'm studying the editor and I think I understand the difference between components and blocks. Now, I was wondering if GrapesJS offers the possibily to manage a block as an atomic entity or permits to define components as a tree with of nodes.
For example, I have a block called separator that basically is a table with a tbody, a tr and a td that are all components in gjs. I would like that the click on any component of this separator block in reality selects the table component at which I setup custom traits and properties that I then can split programmatically on the correct component of the block (e.g. setting the height on the table would in reality set the height of the nested td).
My aim is to give the users the feeling that they are dealing with a single atomic element even if at the source code level the properties they set for that element are distributed among the different nodes that compose that element.
The main issue that I can foresee is that the source code should be importable so as it is generated by the editor and I'm not sure if the editor can distinguish the separator element from any td component of a table component used for tabular data and if it can sniff all the styles to repopulate the traits and the properties in the editor.
Is such a thing possible?
Thank you.
The main issue that I can foresee is that the source code should be importable so as it is generated by the editor and I'm not sure if the editor can distinguish the separator element from any td component of a table component used for tabular data and if it can sniff all the styles to repopulate the traits and the properties in the editor.
Yeah this is a good question, basically, the editor has a stack of component types and every time you add a new one it's placed on top of the stack
[
youLastAddedComponent,
...
table,
image,
text,
default,
]
Let's say you need to add a table component inside the selected one (Component's API)
const selected = editor.getSelected();
// selected is the Component's model
selected.append(`<table>...</table>`);
Now when you pass the HTML, the string is parsed and each Node is translated in a component object (at the moment, just a JS object) and once appended to selected's inner components (selected.components().add({ ...component object... })) it becomes the Component. But to distinguish exactly which type of model is the Node, you iterate over the stack, execute on each type isComponent static method and check if it's valid (you generally return an object like { type: 'my-type', ... })
So isComponent method here is the key, there you can implement the correct logic to make the editor understand if it's a normal table or the separator one (the method takes the Node as the argument so you just use normal DOM's API for the logic).
You can also skip the recognition part by setting the type explicitly, in this way:
<table data-gjs-type="separator">...</table>
Be sure to check also the guide on how to correctly store and load templates, because if you use the same HTML, stored from the editor, for the editing, it's WRONG, you have to use the JSON data (which already contains all the info about your types, so you just skip the parsing and the recognition).
Hi, @artf thank you for the very detailed explanation.
So, if I understand correctly, the first component in the stack that returns its type with its isComponent method determines the type of the node that is currently analyzed by the editor, and the fact that it has returned a type for this node prevents the editor from testing the inner nodes of this node for a component type? Sticking to my example, when my table node has been recognized to be a separator component its inner nodes tbody, tr and td are not tested against any isComponent method, am I correct?
I'm very aware of the fact that is risky to init the editor from HTML code but at the moment I must to do it since the templates are stored as HTML files on the file system and for the time being they should be editable with the current system, too.
the fact that it has returned a type for this node prevents the editor from testing the inner nodes of this node for a component type?
No, inner nodes will be still traversed, but you can actually prevent that with this as a return to your isComponent
isComponent(el) {
...
return {
type: 'separator',
// If the returned object contains `components`,
// inner nodes will be skipped
components: [],
}
}
but, with this current setting you're telling that you have no components, so you'd actually get this rendered in you canvas:
<table ...></table>
empty table, so what you can do?!? With this:
components: [el.innerHTML],
it's the same like not putting components, because the string will be parsed and processed again. What you can actually do, what I think might be your case:
return {
type: 'separator',
content: el.innerHTML,
components: [],
}
content is for static stuff, the editor won't create Components and you won't even be able to select them (not being a Component you don't have the model)
Yeah, the content property could really solve my problem but I guess I have to manually keep it's value in sync (with string manipulation) with the changes made by the users to the properties and the traits of the separator component, and, since it is part of the model of a separator component, any change to it will trigger a view update, right?
Otherwise, if I let the inner nodes to be traversed but check the ancestors and/or the descendants to establish if e.g. the they are part of my separator component and make them not droppable, not draggable and not selectable (maybe defining separated components like separator-tr, separator-td, etc.)? Could this be a better solution?
Yeah, the content property could really solve my problem but I guess I have to manually keep it's value in sync (with string manipulation) with the changes made by the users to the properties and the traits of the separator component, and, since it is part of the model of a separator component, any change to it will trigger a view update, right?
Well as you said I would like that the click on any component of this separator block in reality selects the table I supposed that the separator was something static, without the need to change inner stuff, it depends what your traits are doing, eg. if you just change attributes of the component itself it's still in sync with its view
Otherwise, if I let the inner nodes to be traversed but check the ancestors and/or the descendants to establish if e.g. the they are part of my separator component and make them not droppable, not draggable and not selectable (maybe defining separated components like separator-tr, separator-td, etc.)? Could this be a better solution?
Yeah sure
My bad for not being clear enough in my opening post. I'll try to explain better what I would like to accomplish with an example.
The basic assumption is that my target user doesn't know anything about HTML, CSS, nodes, components and stuff but he understands that any block he drags in the white canvas has some properties he can edit and he expects to see the result of those changes in a real-time fashion in the canvas.
So... The user drags in the canvas the Separator block that is in the Blocks Panel. The source code of the Separator in the canvas is now something like:
<table class="separator" style="width: 100%; margin: 20px 0;">
<tbody>
<tr>
<td style="height: 1px; background: rgb(0, 0, 0)"></td>
</tr>
</tbody>
</table>
Now, the user wants to edit the Separator block so he clicks on the black line in the canvas and he doesn't (and must not) care that he clicked on the table, tbody, tr or td - since he expects the separator to be be an atomic entity. In this case, using one of the approaces described in the previous posts, I select the external node table so that the functions move/delete/copy of the editor work as expected.
Now, in his Style Panel he edits the properties Width, Height, Margin e Background and he sees the canvas updated accordingly. The tricky part is that at the source code level not all the properties need to affect the table node of the Separator block, since the height and background properties should update the named properties in the inner td node. In this case, I think I have to listen for any change at the properties of the separator component and trigger my own login to manage the model (or the style attribute if I'm using the content approach) of all the nodes of the component.
Assuming he wants the separator red, half of the its container width, with more spacing, centered horizontally e thickier then the result source code should be the following
<table class="separator" style="width: 50%; margin: 40px auto;">
<tbody>
<tr>
<td style="height: 2px; background: #FF0000"></td>
</tr>
</tbody>
</table>
My sole technical constraint is that the source code should be importable in the editor since it will be saved as generated by the editor on the file system (in reality I'll make some pre and post processing on it since I need a complete HTML page, but I think I can manage this).
I hope that this make everything more clear.
I thank you, genuinely.
Now, in his Style Panel he edits the properties Width, Height, Margin e Background and he sees the canvas updated accordingly. The tricky part is that at the source code level not all the properties need to affect the table node of the Separator block, since the height and background properties should update the named properties in the inner td node. In this case, I think I have to listen for any change at the properties of the separator component and trigger my own login to manage the model (or the style attribute if I'm using the content approach) of all the nodes of the component.
Correct, if you need to select inner models you can use find and css query selectors tableModel.find('tr td')[0].setStyle({ height: '100px' })
Hi, @artf, thank you as always for the detailed replies. I took some time to organize my mind and I almost have an idea of what to do. I just can't understand a couple of things and I'm not sure they are feasible.
Assuming I decide to use the components approach discussed a few posts ago (but the problems here described are valid even for the content approach) and considering that my idea is to use the Style Manager of the table component to style all the components of the separator...
how can I prevent the properties that are meant for the inner components to apply to the table component? e.g. if the user sets the height property, I can react to the event and update the model of the td component but now the table component itself has the same property while I don't want any height applied to the table component.
when I import an existing template, when the editor recognizes the table component as part of my separator how can I set in the Style Manager of the table component the values of the properties applied to its inner components? And is it even possible to do it without affecting the component table itself (as described in the point 1.)?
Hi @artf, I have found a suboptimal solution to both the problems using Traits. Basically I define all the properties of the inner components of the table component as traits of the table component itself. Then, using the initialization method of the inner components I can set the traits of the table component and at the same time I can activate the listeners for the changes of the values of the traits of the table component. I think this is a suboptimal solution because the user has to switch between the Style and the Traits panels to customize the component. I could define traits also for all the properties of the table component but then I need to implement all the logic to keep the things in sync (it does not seem complicated but with a lot of components which has lots of properties could be error prone). I couldn't find another way and I don't know neither the editor nor JavaScript in such a way to customize it to my needs.
Do you think this is a good solution?
Thank you very much for the help.
Yeah, I think it's the best way if you want to manage the component as a single block of elements.
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.