Writing Genesis elements to work in the layout
Contained elements
This section concerns the behaviour of elements inside the layout. If you are using simple elements or Genesis-supplied elements, this is less of a concern; but if you are building complex custom components yourself you need this information.
Element lifecycle (gating)
Some actions that the user can perform with items in the layout will run the component lifecycle functions (connectedCallback
and disconnectedCallback
) when you don't want them to run:
- when an item is dragged around the layout
- potentially, when another item is removed from the layout
- potentially, when new items are added to the layout
- when an item is maximized or minimized
For example, if you have a component with a loaded resource on the layout (such as a grid with a grid-pro-genesis-datasource
) and you add a new item to the layout with the JavaScript API, then the component with the loaded resource will have to reload too. It is important that any such element accounts for this, including such requirements as caching data, or resizing correctly.
In the @genesislcap/foundation-utils
package, there is a mix-in class LifecycleMixin
which exposes two protected members:
shouldRunConnect
shouldRunDisconnect
These can be used to gate specific functionality.
For example, if there are parts of disconnectedCallback
that you don't want to run when the item is being dragged around the layout, you can gate it behind a (!this.shouldRunDisconnect) return;
early return. See example here.
At the very least, you must run super
calls to the lifecycle methods, or else your custom element will not work correctly.
Resource-intensive components
You do not need to de-register a component that is registered in the layout while it is not in use. However, if you have a component that is extremely resource-intensive, then you can use this lifecycle control method to ensure that it only consumes resources when it is in use.
-
When the element is part of the layout registry, then
shouldRunConnect
will be false and you can use this to ensure that your component isn't doing unnecessary work while part of the cache. -
Once the component is actually initialized in the layout on the DOM, then
shouldRunConnect
will be true, and you can then perform all the required initialization.
For more information, see this example of resource-intensive component resetting.
Managing the state
Items inside the layout can save and restore the state using various methods, but it can become difficult to manage the state if you're adding the same item to the layout multiple times (multiple instances of the same web component).
You can implement the LayoutComponentWithState interface, which enables you to save and load the state per instance of your components. See the linked interface and the associated functions API documentation for examples and explanations of usage.
Usage of this interface is optional; if you do not need to manage the state for your components in this way, then simply do not implement the interface. It is unlikely you'll need to use this functionality, and you can use other systems such as state management instead .
The layout system only interacts with the immediately contained items - so if you have components that contain other components, each top-level component must interact with the contained components to manage their states.
Each layout item can contain multiple components, and most of the time there are no extra considerations when doing this. However, the state of each component in an instance is saved in order of the components on the DOM, so if the serialized state is manually changed to have the items out of order with their state, then the incorrect states will be passed into each item. This should not occur during defined behaviour, but is possible if the end-user is able to change the state passed into loadLayout()
manually.
Element cloning
To enable you to add multiple items from the same registration
, the layout system clones elements to add to the layout.
This is the case both when items are added with .addItem()
, and when they are added using the declarative API. Under the hood, this uses the Node cloneNode api.
There are some limitations to this function, especially when using custom elements with the shadow DOM. See here.
As a general rule, if you need to have elements with Genesis bindings inside the layout, wrap them in custom elements.
If you are writing your own custom element that needs to work inside the layout, follow these steps.
- In the
@genesislcap/foundation-utils
package, there is a mix-in classLifecycleMixin
, which overrides thecloneNode
API.
// Make a call to `deepClone` and manually clone children
override cloneNode(deep?: boolean): Node {
const thisClone = this.deepClone();
if (deep) {
Array.from(this.childNodes).forEach((child) => {
thisClone.appendChild(child.cloneNode(true));
});
}
return thisClone;
}
// Create a new element of the same name and copy over attributes
deepClone(): Node {
const copy = document.createElement(this.tagName.toLowerCase());
this.getAttributeNames().forEach((at) => copy.setAttribute(at, this.getAttribute(at)));
return copy;
}
- You can then extend the cloning functionality for your specific requirements. For example, our charts component needs to copy over
config
anddata
parameters.
export class G2PlotChart extends LifecycleMixin(GenesisElement) {
...
override deepClone(): Node {
const copy = super.deepClone() as G2PlotChart;
copy.config = structuredClone(this.config);
copy.data = structuredClone(this.data);
return copy;
}
...
}
Some items you'll probably want to copy over are eventListeners
and other non-attribute configuration elements on your custom element.