Directives
Directives in Genesis Elements are special functions used in the templates that enable you to create specific functionality. This section looks at the most commonly used directives:
Ref
The ref
directive enables you to reference a specific DOM element within your component's template, so that you can interact directly with that element.
Here’s how to use ref
in <my-button>
to reference the button element, making it accessible for direct manipulation in code.
import {
customElement,
GenesisElement,
html,
ref,
} from "@genesislcap/web-core";
@customElement({
name: "my-button",
template: html<MyButton>` <button ${ref("buttonElement")}>Click Me</button> `,
})
export class MyButton extends GenesisElement {
buttonElement: HTMLButtonElement;
connectedCallback() {
super.connectedCallback();
console.log("Button text:", this.buttonElement.textContent); // Button text: Click Me
}
}
In the example above, the directive ref("buttonElement")
creates a reference to the <button>
element and assigns it to the buttonElement
property of <my-button>
component instance, allowing direct interaction with the DOM element.
In connectedCallback
, this.buttonElement
is a direct reference to the <button>
DOM node, allowing us to access properties like textContent
. In this case, it logs the button’s text to the console.
Even though the <button>
element is part of our <my-button>
component’s template, you can’t directly access DOM elements inside the template without a reference. The ref
directive provides this direct access, enabling you to manipulate or retrieve information from a specific DOM element.
Syntax breakdown
ref(propertyName);
- Property Name: The name of the property that stores the DOM reference in the component. The element with
ref(propertyName)
will be assigned to this property.
Slotted
A slot (<slot>
) is a placeholder inside a component. Any content you add between <my-button>
tags in HTML will appear in this slot. This makes the <my-button>
component more flexible, because it can display different content depending on how it’s used.
The example below creates a <my-button>
component that can accept any content passed into it, such as text or other HTML elements. The slot can handle dynamic content, and the slotted
directive captures and monitors any content placed inside this slot.
The slotted
directive captures all nodes assigned to a slot in <my-button>
.
import {
GenesisElement,
customElement,
html,
slotted,
observable,
} from "@genesislcap/web-core";
@customElement({
name: "my-button",
template: html<MyButton>`
<button>
<slot ${slotted("slotContent")}></slot>
</button>
`,
})
export class MyButton extends GenesisElement {
@observable slotContent: Node[]; // Holds all content passed into the slot
slotContentChanged() {
console.log("Slot content changed:", this.slotContent);
}
}
In the example above, the <slot>
element inside <button>
is a placeholder. Any content added within <my-button>
in HTML will appear here.
Look further at slotted("slotContent")
:
- The
slotted
directive links the slot’s content to theslotContent
property. - This means
slotContent
will be an array containing all the nodes (text, elements, etc.) passed into the slot. - If
slotContent
changes (e.g., content is added or removed from the slot),slotContentChanged()
will be called automatically, logging the updated content.
TheslotContentChanged()
method is triggered whenever theslotContent
changes, so you can respond to changes in the slot content. In this example, it simply logs the current content of the slot to the console.
Usage in HTML
Once you have defined <my-button>
with slotted content, as above, you can use it in your HTML.
<my-button>
Click <strong>Here</strong>!
</my-button>
Children
The children
directive enables you to capture and manage multiple child nodes within a specific element. This is useful when you want to work with all child elements under a certain parent node inside your component.
The example below modifies <my-button>
to capture any child elements placed inside it. The children
directive gets all child nodes within the <div>
container.
import { GenesisElement, customElement, html, children, observable } from "@genesislcap/web-core";
@customElement({
name: "my-button",
template: html<MyButton>`
<button>
<div ${children("childNodes")}>
<slot></slot>
</div>
</button>
`,
})
export class MyButton extends GenesisElement {
// Array to hold all nodes inside the <div>
@observable childNodes: Node[];
childNodesChanged() {
console.log("Updated child nodes:", this.childNodes);
}
connectedCallback() {
super.connectedCallback();
console.log("Initial child nodes:", this.childNodes);
}
}
The example above specifies children("childNodes")
, so that the childNodes
property will contain all nodes inside the <div>
, including text nodes, comment nodes, and any HTML elements.
- Note the
@observable
onchildNodes
, which ensures it updates whenever there’s a change in the<div>
’s child nodes. - Each time the slot content changes,
childNodesChanged
logs the updated list of child nodes.
Usage in HTML
Once you have defined <my-button>
to capture and manage child nodes, as above, you can use it in your HTML.
<my-button>
<span>Item 1</span>
Text node
<div>Another element</div>
<span>Item 2</span>
</my-button>
-
childNodes
will contain everything inside<div>
, including<span>
and<div>
elements, text nodes like "Text node", as well as custom elements such as<rapid-button>
. -
Any changes in the slot content (e.g., adding or removing nodes) will trigger
childNodesChanged
and log the updated list.
Syntax breakdown
children(propertyName, options);
propertyName
specifies the property that will store the array of child nodes.options
(optional) provides additional settings for filtering or structuring child nodes. This can include:filter
restricts the collection to specific elements (e.g., elements('span')).positioning
includes index and other contextual properties if true.
When
The when
directive enables you to render sections of HTML conditionally.
When you provide a condition to when
, it displays the specified template in the DOM when the condition is true and removes it when false. If the condition never becomes true, the template is not rendered at all.
Here is an example:
import { GenesisElement, customElement, observable, html, when } from "@genesislcap/web-core";
function delay(ms) {
// this function simulates a delay so we can see the conditional rendering on the UI
return new Promise(resolve => setTimeout(resolve, ms));
}
@customElement({
name: "my-button",
template: html<MyButton>`
<div>
${when(
(x) => !x.ready,
html<MyButton>`Loading...`
)}
${when(
(x) => x.ready,
html<MyButton>`<button>Data Loaded: ${(x) => x.data.message}</button>`
)}
</div>
`,
})
export class MyButton extends GenesisElement {
@observable ready: boolean = false; // Track loading state
@observable data: { message: string } = null; // Store hardcoded data
connectedCallback() {
super.connectedCallback();
console.log("connectedCallback called");
this.simulateDataLoad();
}
async simulateDataLoad() {
console.log("simulateDataLoad called");
await delay(2000); // 2-second delay
this.data = { message: "Hello, Genesis!" }; // Hardcoded data
this.ready = true; // Indicate loading is complete
}
}
In the example above:
Initial state
- When
<my-button>
is first added to the DOM, it displaysLoading...
for two seconds (simulated delay).
After two seconds
- Once the two-second delay has elapsed,
ready
is set totrue
, and the button appears with the text:Data Loaded: Hello, Genesis!
.
Syntax breakdown
when(condition, template);
condition
is a boolean expression that determines if the template should be rendered. If true, the template is added to the DOM; if false, it’s removed.template
defines the HTML structure to render when the condition is true.
whenElse
The whenElse
directive simplifies conditional rendering; it allows you to specify both the "true" and "false" templates in a single directive. This is useful for if-else scenarios, where different content needs to be rendered based on a condition.
Let's look at the same example as above but with whenElse
:
import {
GenesisElement,
customElement,
observable,
html,
whenElse,
} from "@genesislcap/web-core";
function delay(ms) {
// This function simulates a delay so we can observe the conditional rendering
return new Promise((resolve) => setTimeout(resolve, ms));
}
@customElement({
name: "my-button",
template: html<MyButton>`
<div>
${whenElse(
(x) => !x.ready,
html<MyButton>`Loading...`,
html<MyButton>`<button>Data Loaded: ${(x) => x.data.message}</button>`
)}
</div>
`,
})
export class MyButton extends GenesisElement {
@observable ready: boolean = false; // Tracks loading state
@observable data: { message: string } = null; // Stores hardcoded data
connectedCallback() {
super.connectedCallback();
console.log("connectedCallback called");
this.simulateDataLoad();
}
async simulateDataLoad() {
console.log("simulateDataLoad called");
await delay(2000); // 2-second delay
this.data = { message: "Hello, Genesis!" }; // Hardcoded data
this.ready = true; // Indicate loading is complete
}
}
Initial state
- When
<my>-button>
is added to the DOM, the condition!x.ready
evaluates to true, so theLoading...
template is rendered.
After two seconds
- The
simulateDataLoad
method sets ready to true, causing the condition!x.ready
to evaluate to false. - The second template (the button) is then rendered with the message:
Data Loaded: Hello, Genesis!
.
Syntax breakdown
whenElse(condition, trueTemplate, falseTemplate);
condition
is a boolean expression that determines which template to render.trueTemplate
is the template rendered if the condition evaluates to true.falseTemplate
is the template rendered if the condition evaluates to false.
Repeat
The repeat
directive is used to render a list of items dynamically. This is highly useful for rendering lists, especially when you need to manage and display data dynamically.
In the example below, <my-button-list>
displays a static list of button labels, each rendered as a button. This example shows how to use repeat
to render each item in the list.
import {
GenesisElement,
customElement,
html,
repeat,
} from "@genesislcap/web-core";
@customElement({
name: "my-button-list",
template: html<MyButtonList>`
<h2>Button List</h2>
<ul>
${repeat(
(x) => x.buttons, // Data source: the buttons array
// item represents each button label in the array
// index ensures each item can have a unique identifier if needed
(item, index) =>
html`` // Template generator
)}
</ul>
`,
})
export class MyButtonList extends GenesisElement {
buttons: string[] = ["Click Me", "Submit", "Reset"]; // Static list of button labels
}
In the example above:
- The
repeat
directive iterates over thebuttons
array and renders each item as a<button>
inside an<li>
. - Each
button
displays the label from thebuttons
array ("Click Me"
,"Submit"
,"Reset"
). - The
buttons
array is initialized with static values for simplicity. Each item in the array is displayed as a button in the rendered HTML. - The
repeat
directive efficiently handles rendering, using the array indices as keys to minimize DOM updates. - The resulting structure is an unordered list (
<ul>
) where each<li>
contains a<button>
with the appropriate label from the array.
Usage in HTML
To test <my-button-list>
, add it to your HTML page:
<body>
<my-button-list></my-button-list>
</body>
Advanced features of repeat
The repeat
directive also includes advanced features for handling more complex scenarios. Here are two important options:
Opting into positioning
By default, the repeat
directive doesn’t track the position (index)
or metadata of each item in the array. However, you can achieve this by passing { positioning: true }
in the options. This enables you to use additional context properties such as:
index
- the index of the current item.isFirst
- true if the current item is the first in the array.isLast
- true if the current item is the last in the array.
Here is an example that uses positioning:
<ul>
$
{repeat(
(x) => x.buttons,
html`<li>${(x, c) => `${c.index + 1}. ${x}`}</li>`,
{ positioning: true }
)}
</ul>
In the example above, the c.index
provides the current index of the item, enabling you to render numbered buttons (e.g., 1. Click Me, 2. Submit).
{ positioning: true }
enables these additional context properties.
View recycling
The repeat
directive can reuse DOM elements that have already been rendered for array items. This improves performance by avoiding unnecessary DOM creation and destruction.
recycle:
enables you to set whether the DOM elements are reused when the array changes.
recycle: true
When the array changes, only DOM elements that are directly affected by the changes are recreated. DOM elements that have already been rendered and which are not directly affected by the array change are left unchanged. This is the default, which optimizes performance, especially for large lists.recycle: false
All views are discarded and recreated whenever the array changes.
Recycling components can cause issues if you have a set of components with different attributes or properties and you plan to re-render the list while changing the components, or their attributes and properties.
If you see that components are not changing as you expect, then try disabling recycling {recycle: false}
.
Let's look at an example without recycling:
<ul>
$
{repeat((x) => x.buttons, html<string>`<li>${(x) => x}</li>`, {
recycle: false,
})}
</ul>
Syntax breakdown
repeat(expression, itemTemplate, options);
expression
specifies the data source for repeat.itemTemplate
defines the HTML structure for each item.options
(optional):positioning: true
enables index and positional metadata likeindex
,isFirst
, andisLast
.recycle: false
prevents view recycling for cases where re-initializing DOM elements is required.
Sync
The sync
directive establishes two-way data binding between the model (component state) and the view (template). Unlike one-way data binding, where data flows only from the model to the view, the sync
directive ensures that changes in the view (e.g., user interaction) automatically update the model, keeping the two synchronized.
The main advantages of this are:
- You don't need to create manual event listeners to update the model.
- The view and model stay synchronized, which improves consistency.
- It automatically triggers
propertyNameChanged
methods for reactive behaviour. - It supports automatic type conversion for numbers, booleans, strings, and more.
The sync
directive accepts three parameters:
binding
conversionType
(optional)eventName
(optional)
The following example demonstrates how the sync
directive binds a checkbox’s checked state to a model property:
import {
GenesisElement,
customElement,
observable,
html,
sync,
} from "@genesislcap/web-core";
@customElement({
name: "my-checkbox",
template: html<MyCheckbox>`
<div>
<label>
<input
type="checkbox"
?checked=${sync((x) => x.isSelected, "boolean")}
/>
Enable Feature
</label>
</div>
`,
})
export class MyCheckbox extends GenesisElement {
@observable isSelected = true;
isSelectedChanged() {
console.log(`isSelected changed to: ${this.isSelected}`);
}
}
In the example above:
- The
isSelected
property sets the initial state of the checkbox (checked = true). - When the user toggles the checkbox, the
isSelected
property is updated automatically. - The
isSelectedChanged
method is triggered whenever the property changes, logging the new state to the console.
Using Sync with multiple input types
The sync
directive works seamlessly with various input types, such as text fields, number fields, and checkboxes.
@customElement({
name: "my-form",
template: html<MyForm>`
<div>
<label>
Text:
<input type="text" :value=${sync((x) => x.textValue)} /> // for text inputs, the default behavior of sync is already optimized for strings
</label>
</div>
<div>
<label>
Number:
<input type="number" :value=${sync((x) => x.numberValue, "number")} />
</label>
</div>
<div>
<label>
<input
type="checkbox"
?checked=${sync((x) => x.isChecked, "boolean")}
/>
Enable Notifications
</label>
</div>
`,
})
export class MyForm extends GenesisElement {
@observable textValue: string = "Default Text";
@observable numberValue: number = 42;
@observable isChecked: boolean = false;
textValueChanged() {
console.log(`Text value changed to: ${this.textValue}`);
}
numberValueChanged() {
console.log(`Number value changed to: ${this.numberValue}`);
}
isCheckedChanged() {
console.log(`Checkbox state changed to: ${this.isChecked}`);
}
}
Usage in HTML
<body>
<my-form></my-form>
<my-checkbox></my-checkbox>
</body>
Let's look at an example that uses the sync
directive with all three parameters: binding
, conversionType
, and eventName
for a checkbox.
The change
event is a standard DOM event that fires when the value of an element is committed, such as when the user checks or unchecks a checkbox. This event is built into the browser, and the sync
directive listens for it when specified as the third parameter (eventName
).
There's no need to define it explicitly in the code.
import {
GenesisElement,
customElement,
observable,
html,
sync,
} from "@genesislcap/web-core";
@customElement({
name: "checkbox-example",
template: html<CheckboxExample>`
<div>
<label>
<input
type="checkbox"
?checked=${sync((x) => x.isChecked, "boolean", "change")}
/>
Enable Feature
</label>
</div>
`,
})
export class CheckboxExample extends GenesisElement {
@observable isChecked: boolean = false;
isCheckedChanged() {
console.log(`Checkbox state changed to: ${this.isChecked}`);
}
}
Syntax breakdown
sync(binding, conversionType, eventName);
-
binding
is a function that defines the binding to the chosen variable on the model.- Example:
sync((x) => x.propertyName)
.
- Example:
-
conversionType
(optional) enablessync
to convert the underlying data type automatically from the string on the variable to the correct type on the model. The types available are:- string (this is the default)
- number
- time
- boolean
-
eventName
(optional) is used bysync
to know what event to listen to from the component and to reflect the data back to the model.input
change
default