Directives
Directives in Genesis Elements are special functions used in the templates that add specific functionality. Below is a summary of commonly used directives:
Ref
The ref
directive lets you directly reference a specific DOM element within your component's template, enabling direct interaction 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
}
}
Key Points:
ref("buttonElement")
: Theref
directive creates a reference to the<button>
element and assigns it to thebuttonElement
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 liketextContent
. Here, we log 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, allowing you to manipulate or retrieve information from a specific DOM element.
Syntax breakdown
ref(propertyName);
- Property Name: The name of the property that will store the DOM reference in the component. The element with
ref(propertyName)
will be assigned to this property.
Slotted
The goal is to create a <my-button>
component that can accept any content passed into it, such as text or other HTML elements. We use a slot to allow dynamic content, and the slotted
directive to capture and monitor any content placed inside this slot.
The slotted
directive captures all nodes assigned to a slot in <my-button>
.
A <slot>
is like a placeholder inside a component. Any content you add between <my-button>
tags in HTML will appear in this slot. This setup makes the <my-button>
component more flexible, as it can display different content depending on how it’s used.
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);
}
}
Key Points:
- The
<slot>
element inside<button>
is a placeholder. Any content added within<my-button>
in HTML will appear here.
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.
slotContentChanged()
method is triggered whenever theslotContent
changes, allowing you to respond to changes in the slot content. In this example, we’re simply logging the current content of the slot to the console.
Usage in HTML
<my-button>
Click <strong>Here</strong>!
</my-button>
Children
The children
directive allows us 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.
In this example, we’ll modify <my-button>
to capture any child elements placed inside it, using the children
directive to get 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);
}
}
Key Points:
- By specifying
children("childNodes")
, thechildNodes
property will contain all nodes inside the<div>
, including text nodes, comment nodes, and any HTML elements.- As before,
@observable
onchildNodes
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
<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>
elements, text nodes like "Text node", and<div>
elements.
- 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): 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 allows you to conditionally render sections of HTML. By providing a condition to when
, it will display the specified template in the DOM when the condition is true and remove it when false. If the condition never becomes true, the template won’t be rendered at all.
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
}
}
Initial State:
- When
<my-button>
is first added to the DOM, it displays "Loading..." for 2 seconds (simulated delay).
After 2 Seconds:
- Once the delay completes,
ready
is set totrue
, and the button appears with the text: "Data Loaded: Hello, Genesis!".
Syntax breakdown
when(condition, template);
condition
: 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 by allowing 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 the "Loading..." template is rendered.
After 2 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
: A boolean expression that determines which template to render.trueTemplate
: The template rendered if the condition evaluates to true.falseTemplate
: The template rendered if the condition evaluates to false.
Repeat
The repeat
directive is used to dynamically render a list of items. This directive is highly useful for rendering lists, especially when you need to manage and display data dynamically.
<my-button-list>
will display 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
}
Key Points:
- 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 enable this by passing { positioning: true }
in the options. This allows 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.
Example with Positioning:
<ul>
$
{repeat(
(x) => x.buttons,
html`<li>${(x, c) => `${c.index + 1}. ${x}`}</li>`,
{ positioning: true }
)}
</ul>
The c.index
provides the current index of the item, allowing you to render numbered buttons (e.g., 1. Click Me, 2. Submit).
{ positioning: true }
enables these additional context properties.
View Recycling
View recycling determines whether previously rendered DOM elements are reused when the array changes.
By default, repeat
enables view recycling to optimize performance, especially for large lists.
Recycling Enabled (recycle: true):
The repeat
directive may reuse DOM elements that have already been rendered for array items.
This improves performance by avoiding unnecessary DOM creation and destruction.
Recycling Disabled (recycle: false):
All views are discarded and recreated whenever the array changes.
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 enables 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 both synchronized.
This eliminates the need for manual event handling, simplifying code and ensuring consistency between the UI and the underlying data.
The sync
directive accepts three parameters: binding
, conversionType
(optional) and 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}`);
}
}
Key Points:
- 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
: A function that defines the binding to the chosen variable on the model.
- Example:
sync((x) => x.propertyName)
.
conversionType
: This optional parameter allowssync
to automatically convert the underlying data type from the string on the variable to the correct type on the model.
- string, this is the default
- number
- time
- boolean
eventName
: this optional parameter is used bysync
to know what event to listen to from the component and reflect the data back to the model.
input
change
default
Main Benefits:
- No need for manual event listeners to update the model.
- Ensures the view and model stay in sync, improving consistency.
- Automatically triggers
propertyNameChanged
methods for reactive behavior.- Supports automatic type conversion for numbers, booleans, strings, and more.