From 0f5a60882bba7e19eadff289d8300a9348c3d485 Mon Sep 17 00:00:00 2001 From: Genta Demnushaj Date: Thu, 31 Oct 2024 11:52:48 +0000 Subject: [PATCH 1/9] chore: added custom components initial content --- .../023_custom-components/01_quick-start | 82 +++++ .../023_custom-components/02_lifecycle | 75 ++++ .../023_custom-components/03_state-management | 342 ++++++++++++++++++ .../023_custom-components/04_event_handling | 76 ++++ .../023_custom-components/05_directives | 26 ++ .../023_custom-components/06_styling | 4 + 6 files changed, 605 insertions(+) create mode 100644 docs/001_develop/03_client-capabilities/023_custom-components/01_quick-start create mode 100644 docs/001_develop/03_client-capabilities/023_custom-components/02_lifecycle create mode 100644 docs/001_develop/03_client-capabilities/023_custom-components/03_state-management create mode 100644 docs/001_develop/03_client-capabilities/023_custom-components/04_event_handling create mode 100644 docs/001_develop/03_client-capabilities/023_custom-components/05_directives create mode 100644 docs/001_develop/03_client-capabilities/023_custom-components/06_styling diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/01_quick-start b/docs/001_develop/03_client-capabilities/023_custom-components/01_quick-start new file mode 100644 index 0000000000..7f1a31c13f --- /dev/null +++ b/docs/001_develop/03_client-capabilities/023_custom-components/01_quick-start @@ -0,0 +1,82 @@ +# Quick Start + +Let's create a simple web component using Genesis. There are three parts that make up a component: the HTML template, the CSS styles and the component logic. Web components can vary in complexity, from a simple button to a very detailed interactive experience. + +## Create Custom Element + +Start by importing all necessary part: + +```shell +import { css, customElement, GenesisElement, html } from '@genesislcap/web-core'; +``` + +```shell + +# Create an HTML template using the html tag template literal +const template = html``; + +# Create CSS styles using the css tag template literal +const styles = css` + h1 { + color: grey; + } +`; + +# The @customElement decorator is a function that helps define and register a custom HTML element. It associates a class with a specific custom tag name in the DOM. + +@customElement({ + name: 'my-button', + template, + styles, +}) + +export class HelloWorld extends GenesisElement {} + +``` + +We’ve created a custom component called , which currently displays a basic button in the browser with the text "Click Me." However, to make it more useful, let's add an attribute that allows us to customize the button's label and give it more dynamic functionality. + +:::important + +Web Component names are required to include a hyphen (-) to avoid conflicts with native HTML elements and to provide a way to namespace components from various libraries. + +::: + +## Add an Attribute + +We can add an `attr` decorator which allows us to link a class property to an HTML attribute, ensuring that changes in the attribute are automatically reflected in the JavaScript property, and vice versa, allowing for clean and declarative state management of custom elements. + +Let's declare an attribute called **_name_** and then change that value to see it reflected: + +```shell +import { attr, css, customElement, GenesisElement, html } from '@genesislcap/web-core'; + +@customElement({ + name: 'my-button', + template: html``;, + styles, +}) + +export class MyButton extends GenesisElement { + @attr label = 'Submit'; +} + +``` + +Now let's update the template to display the value: + +```shell + +const template = html`` +``` + +## Add it to your project + +Now that the component is created, it can be added in your HTML this way: + +```shell + + +``` + +In this example, we created a custom web component using GenesisElement. We defined a button template and added a label attribute using the @attr decorator, allowing the button text to be customized via HTML. We implemented the labelChanged method, which is automatically called whenever the label value changes. The template dynamically updates with the new value, ensuring the button remains responsive to changes in its attributes. diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/02_lifecycle b/docs/001_develop/03_client-capabilities/023_custom-components/02_lifecycle new file mode 100644 index 0000000000..a5a5c1fde1 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/023_custom-components/02_lifecycle @@ -0,0 +1,75 @@ +# Component Lifecycles + +Web Components go through several lifecycle stages from creation to removal. Understanding these lifecycles is crucial for proper component initialization, cleanup, and state management. + +Here are some lifecycle methods that we have access to through `GenesisElement`: + +### Connected & Disconnected + +`connectedCallback` lifecycle runs when component is added to DOM - perfect time for setup + +`disconnectedCallback` lifecycle runs when component is removed from DOM - perfect time for cleanup. + +```shell +import { attr, css, customElement, GenesisElement, html } from '@genesislcap/web-core'; + +const template = html``; + +@customElement({ + name: 'my-button', + template, + styles, +}) + +export class MyButton extends GenesisElement { + @attr label: string = "Submit"; + @attr disabled: boolean = false; + + connectedCallback() { + super.connectedCallback(); # Let GenesisElement do its setup first + console.log('Button is now connected to the DOM and ready for interaction'); + } + + disconnectedCallback() { + super.disconnectedCallback(); # Let GenesisElement do its cleanup first + console.log('Button is no longer interactive and removed from DOM'); + } +} +``` + +### Changed Callbacks + +Changed Callback lifecycle runs when attributes/properties update - handle state changes. The name varies depending on the name of the attribute. + +:::note + +In the following example you'll notice we have not included the `connectedCallback` and `disconnectedCallback`. It's important to remember that these two lifecycles are only necessary for things that need DOM presence. We can skip them for basic component setup that don't need DOM. + +::: + +```shell +import { attr, css, customElement, GenesisElement, html } from '@genesislcap/web-core'; + +const template = html``; + +@customElement({ + name: 'my-button', + template, + styles, +}) + +export class MyButton extends GenesisElement { + @attr label: string = "Submit"; + @attr disabled: boolean = false; + + labelChanged(oldValue, newValue) { + console.log(`Button text changed from ${oldValue} to ${newValue}`); + } +} +``` + +The `labelChanged` method will be triggered and consequently log the following statement when the label attribute is changed in the template: + +`` + +> Button text changed from Submit to Cancel diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/03_state-management b/docs/001_develop/03_client-capabilities/023_custom-components/03_state-management new file mode 100644 index 0000000000..ff3649f8aa --- /dev/null +++ b/docs/001_develop/03_client-capabilities/023_custom-components/03_state-management @@ -0,0 +1,342 @@ +# State Management + + + +Components manage state through several key mechanisms: attributes and properties for external state and observables for internal state. + +## Attributes + +As seen in the earlier examples, in order to create an attribute you simply use the `@attr` decorator. When you use the `@attr` decorator, you are creating a property-attribute pair. This creates a binding so that any change in one will update the other, keeping them in sync. + +```shell +import { attr, css, customElement, GenesisElement, html } from '@genesislcap/web-core'; + +const template = html``; + +@customElement({ + name: 'my-button', + template, + styles, +}) + +export class MyButton extends GenesisElement { + @attr label: string = "Submit"; + + labelChanged(oldValue, newValue) { + console.log(`Button text changed from ${oldValue} to ${newValue}`); + } +} +``` + +The `label` property is tied to an HTML attribute, so to override the value of `label` we can simply replace it in the HTML. + +The default value of `label` is 'Submit', if no value is provided in the HTML. + +```shell + +``` + +:::note + +The `labelChanged` method is only useful if you want to perform additional actions when the label attribute changes, such as logging or triggering other side effects. Since the `@attr` decorator already handles the synchronization between the attribute and property (updating the DOM whenever label changes), the labelChanged method isn’t strictly necessary. + +::: + +:::note +Here's a list of attribute characteristics: + +- Always strings in HTML +- Used in HTML markup +- Visible in DOM inspector +- Used for initial state +- Slower for dynamic updates + ::: + +### Attribute Value Binding + +For most attributes, we can directly bind their values to expressions in the template, allowing them to update dynamically based on properties in the component. + +The label attribute controls the text shown on the button. To bind label to a dynamic value, we use an expression within the template. Here’s how this might look in a component: + +```shell +import { attr, customElement, GenesisElement, html } from '@genesislcap/web-core'; + +const template = html` + +`; + +@customElement({ + name: 'my-button', + template, + styles, +}) + +export class MyButton extends GenesisElement { + @attr label = 'Click Me'; + # Here we are binding the text inside the button to the `label` attribute. +}); +``` + +#### Usage in HTML + +```shell + +``` + +- Here, the label attribute binds directly to the button's text via `${x => x.label}`. +- If label is set to "Cancel" in HTML `()`, the button will display "Cancel". +- When the label value is changed dynamically, the button text will automatically update.- + +### Interpolated Attribute Values + +In cases where an attribute value is partially static and partially dynamic, we can combine static and dynamic parts using interpolated expressions. This approach is useful for creating customized button labels or CSS classes. + +Suppose we want to add a counter to the button label that shows the number of times it has been clicked. We could use an expression to combine the static text ("Clicked: "), and a dynamic value, such as the clickCount property: + +```shell +import { attr, customElement, GenesisElement, html } from '@genesislcap/web-core'; + +const template = html` + +`; + +@customElement({ + name: 'my-button', + template, + styles, +}) + +export class MyButton extends FASTElement { + @attr label = 'Click Me'; // Bind to 'label' attribute + clickCount = 0; + + handleClick() { + this.clickCount++; + } +} +``` + +### Boolean Attribute Binding + +Boolean attributes are special because their presence/absence determines their value, rather than their string value. In addition to the `@attr` option, an attribute can also take a configuration with the `mode` option. + +If the attribute is a boolean and the mode is set to "boolean" this allows GenesisElement to add/remove the attribute from the element + +If a boolean attribute is present in the HTML markup (e.g., ``), it is interpreted as true. +If the attribute is absent (e.g., ``), it is interpreted as false. + +We can use a ? prefix to handle boolean attributes so they’re added or removed from the DOM based on a condition. + +> The disabled attribute controls whether the button is clickable. Using `?disabled=${x => x.isDisabled}`, we can bind it to the component’s `isDisabled` property and dynamically control whether the button is enabled or disabled. + +```shell +import { attr, customElement, GenesisElement, html } from '@genesislcap/web-core'; + +const template = html` + +`; + +@customElement({ + name: 'my-button', + template, + styles, +}) + +export class MyButton extends FASTElement { + @attr label = 'Click Me'; + @attr({ mode: 'boolean' }) isDisabled = false; + + handleClick() { + if (!this.isDisabled) { + console.log('Button clicked'); + } + } +} +``` + +#### Usage in HTML + +```shell + +``` + +- The ?disabled="${x => x.isDisabled}" syntax lets the templating engine handle boolean attributes automatically. + - If isDisabled is true, the disabled attribute is added to the button (` + `, + styles, +}) +export class MyButton extends GenesisElement { + @observable label = "Clicks"; # Label text for the button + @observable count = 0; # Tracks the number of clicks + + handleClick(event: Event) { + this.count++; # Increment the click count + + console.log("Event Target:", event.target); # Log the event target element + console.log("Component Label:", this.label); # Log the current label + + # Emit a custom "button-clicked" event with the current label and count + this.$emit("button-clicked", { + label: this.label, + count: this.count + }); + } +} +``` + +Now let's show how we can listen in another component providing for component communication: + +```shell +import { GenesisElement, customElement, html } from "@genesislcap/web-core"; +import "./my-button"; # Import the my-button component + +@customElement({ + name: "parent-component", + template: html` + + `, +}) +export class ParentComponent extends GenesisElement { + handleButtonClicked(event: CustomEvent) { + console.log("Custom event received in ParentComponent:", event.detail); + # Output: { label: "Clicks", count: X } + } +} +``` + +Explanation of key elements in the above example: + +Component Communication: +- This allows to communicate with its parent or other parts of the application by notifying them whenever it’s clicked. +- Using the `$emit` helper method, emits a button-clicked event that any parent component or listener can handle. + +Custom Events: +- `button-clicked` is a custom event that’s unique to . It’s created to indicate that the button has been clicked, providing a specific trigger for listeners. +- The `$emit` method simplifies the creation and dispatch of the button-clicked event with bubbles: true and composed: true, making it available to parent components and global listeners. + +Event Data Passing: +- The custom event includes data (the label and count properties) to provide context on the button’s state when the event was emitted. +- Implementation: The $emit method’s second parameter passes { label: this.label, count: this.count } as event.detail, enabling any listener to access this information for further processing. \ No newline at end of file diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/05_directives b/docs/001_develop/03_client-capabilities/023_custom-components/05_directives new file mode 100644 index 0000000000..99b9b58ba9 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/023_custom-components/05_directives @@ -0,0 +1,26 @@ +# Advanced Rendering: Using Directives +## DIRECTIVES + +- ref +- slotted +- children +- when +- whenElse +- repeat (as well as c as context object) +- sync + + +Directives +A. Content & Template Directives + +ref (DOM references) +slotted (content projection) +children (child elements) +'c' context object + +B. Flow Control Directives + +when +whenElse +repeat +sync \ No newline at end of file diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/06_styling b/docs/001_develop/03_client-capabilities/023_custom-components/06_styling new file mode 100644 index 0000000000..dc0a7e3add --- /dev/null +++ b/docs/001_develop/03_client-capabilities/023_custom-components/06_styling @@ -0,0 +1,4 @@ +# Styling & Structure: CSS Templates +Component styles +Theme integration +CSS customization \ No newline at end of file From d5a137d96dddee6526fb2b41ab75a476dc029c21 Mon Sep 17 00:00:00 2001 From: Genta Demnushaj Date: Thu, 31 Oct 2024 15:57:19 +0000 Subject: [PATCH 2/9] added directives --- .../023_custom-components/03_state-management | 8 +- .../023_custom-components/04_event_handling | 2 +- .../023_custom-components/05_directives | 305 ++++++++++++++++-- 3 files changed, 290 insertions(+), 25 deletions(-) diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/03_state-management b/docs/001_develop/03_client-capabilities/023_custom-components/03_state-management index ff3649f8aa..e7e7c01ccd 100644 --- a/docs/001_develop/03_client-capabilities/023_custom-components/03_state-management +++ b/docs/001_develop/03_client-capabilities/023_custom-components/03_state-management @@ -218,7 +218,7 @@ See `clickCount` @customElement({ name: 'my-button', template: html` - + < `;, styles, }) @@ -251,7 +251,7 @@ Observables are a powerful tool for creating reactive components. They allow pro @customElement({ name: 'my-button', template: html` - + `;, styles, }) @@ -283,9 +283,9 @@ import { observable, customElement, GenesisElement, html } from '@genesislcap/we @customElement({ name: 'my-button', template: html` - + `;, styles, }) diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/04_event_handling b/docs/001_develop/03_client-capabilities/023_custom-components/04_event_handling index 97491aadf0..71fcb9e18b 100644 --- a/docs/001_develop/03_client-capabilities/023_custom-components/04_event_handling +++ b/docs/001_develop/03_client-capabilities/023_custom-components/04_event_handling @@ -50,7 +50,7 @@ import "./my-button"; # Import the my-button component @customElement({ name: "parent-component", template: html` - + `, }) export class ParentComponent extends GenesisElement { diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/05_directives b/docs/001_develop/03_client-capabilities/023_custom-components/05_directives index 99b9b58ba9..ba1d770b2c 100644 --- a/docs/001_develop/03_client-capabilities/023_custom-components/05_directives +++ b/docs/001_develop/03_client-capabilities/023_custom-components/05_directives @@ -1,26 +1,291 @@ -# Advanced Rendering: Using Directives -## DIRECTIVES +# DIRECTIVES -- ref -- slotted -- children -- when -- whenElse -- repeat (as well as c as context object) -- sync +## Ref +The ref directive lets you directly reference a specific DOM element within your component's template, enabling direct interaction with that element. -Directives -A. Content & Template Directives +Here’s how to use ref in to reference the button element, making it accessible for direct manipulation in code. -ref (DOM references) -slotted (content projection) -children (child elements) -'c' context object +```shell +import {customElement, GenesisElement, html, ref } from '@genesislcap/web-core'; -B. Flow Control Directives +@customElement({ + name: "my-button", + template: html` + + `, +}) +export class MyButton extends GenesisElement { + buttonElement: HTMLButtonElement; -when -whenElse -repeat -sync \ No newline at end of file + connectedCallback() { + super.connectedCallback(); + console.log("Button text:", this.buttonElement.textContent); + } +} + +``` + +- `ref("buttonElement")`: The ref directive assigns the ` + `, +}) +export class MyButton extends GenesisElement { + @observable slotContent: Node[]; // Holds all content passed into the slot + + slotContentChanged() { + console.log("Slot content changed:", this.slotContent); + } +} +``` + +- The `` element inside ` + `, +}) +export class MyButton extends GenesisElement { + # // Array to hold all nodes inside the
+ @observable childNodes: Node[]; + + childNodesChanged() { + console.log("Updated child nodes:", this.childNodes); + } + + connectedCallback() { + super.connectedCallback(); + console.log("Initial child nodes:", this.childNodes); + } +} +``` + +- By specifying `children("childNodes")`, the `childNodes` property will contain all nodes inside the `
`, including text nodes, comment nodes, and any HTML elements. +- As before, `@observable` on `childNodes` ensures it updates whenever there’s a change in the `
`’s child nodes. +- Each time the slot content changes, `childNodesChanged` logs the updated list of child nodes. + +### Usage in HTML + +```shell + + Item 1 + Text node +
Another element
+ Item 2 +
+``` + +-childNodes will contain everything inside
, including elements, text nodes like "Text node", and
elements. + +- Any changes in the slot content (e.g., adding or removing nodes) will trigger childNodesChanged and log the updated list. + +### Syntax breakdown + +```shell +children(propertyName, options); +``` +- Property Name: 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. + +```shell +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` +
+ ${when( + x => !x.ready, + html`Loading...` + )} + ${when( + x => x.ready, + html`` + )} +
+ `, +}) +export class MyButton extends FASTElement { + @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 `` is first added to the DOM, it displays "Loading..." for 2 seconds (simulated delay). + +After 2 Seconds: + +- Once the delay completes, ready is set to true, and the button appears with the text "Data Loaded: Hello, Genesis!". + +### Syntax breakdown + +```shell +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 + +In the previous example with `when`, we demonstrated how to handle conditional rendering for 'if-else' scenarios by using `when` twice. However, there's a more efficient approach: the `whenElse` directive. With `whenElse`, you can manage if-else conditions in a single directive, avoiding the need to use `when` multiple times. + + + +### Usage in HTML + +## 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. + +`` 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. + +```shell +import { GenesisElement, customElement, observable, html, repeat } from "@genesislcap/web-core"; + +@customElement({ + name: "my-button-list", + template: html` +

Button List

+
    + ${repeat(x => x.buttons, html` +
  • + `)} +
+ `, +}) +export class MyButtonList extends GenesisElement { + @observable buttons: string[] = ["Click Me", "Submit", "Reset"]; // Static list of button labels +} +``` + +- The repeat directive iterates over the buttons array and renders each item as a ``; - -# Create CSS styles using the css tag template literal -const styles = css` - h1 { - color: grey; - } -`; - -# The @customElement decorator is a function that helps define and register a custom HTML element. It associates a class with a specific custom tag name in the DOM. - -@customElement({ - name: 'my-button', - template, - styles, -}) - -export class HelloWorld extends GenesisElement {} - -``` - -We’ve created a custom component called , which currently displays a basic button in the browser with the text "Click Me." However, to make it more useful, let's add an attribute that allows us to customize the button's label and give it more dynamic functionality. - -:::important - -Web Component names are required to include a hyphen (-) to avoid conflicts with native HTML elements and to provide a way to namespace components from various libraries. - -::: - -## Add an Attribute - -We can add an `attr` decorator which allows us to link a class property to an HTML attribute, ensuring that changes in the attribute are automatically reflected in the JavaScript property, and vice versa, allowing for clean and declarative state management of custom elements. - -Let's declare an attribute called **_name_** and then change that value to see it reflected: - -```shell -import { attr, css, customElement, GenesisElement, html } from '@genesislcap/web-core'; - -@customElement({ - name: 'my-button', - template: html``;, - styles, -}) - -export class MyButton extends GenesisElement { - @attr label = 'Submit'; -} - -``` - -Now let's update the template to display the value: - -```shell - -const template = html`` -``` - -## Add it to your project - -Now that the component is created, it can be added in your HTML this way: - -```shell - - -``` - -In this example, we created a custom web component using GenesisElement. We defined a button template and added a label attribute using the @attr decorator, allowing the button text to be customized via HTML. We implemented the labelChanged method, which is automatically called whenever the label value changes. The template dynamically updates with the new value, ensuring the button remains responsive to changes in its attributes. diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/01_quick-start.mdx b/docs/001_develop/03_client-capabilities/023_custom-components/01_quick-start.mdx new file mode 100644 index 0000000000..67555b1b26 --- /dev/null +++ b/docs/001_develop/03_client-capabilities/023_custom-components/01_quick-start.mdx @@ -0,0 +1,102 @@ +--- +title: 'quickstart' +sidebar_label: 'Quick Start' +id: quick-start + +sidebar_position: 3 +--- + +# Quick Start + +Let's create a simple web component using Genesis. There are three parts that make up a component: the HTML template, the CSS styles and the component logic. Web components can vary in complexity, from a simple button to a very detailed interactive experience. + +## Create Custom Element + +Start by importing all necessary parts: + +```typescript +import { css, customElement, GenesisElement, html } from '@genesislcap/web-core'; +``` + +```typescript + +// Create an HTML template using the html tag template literal +const template = html``; + +// Create CSS styles using the css tag template literal +const styles = css` + h1 { + color: grey; + } +`; +// The @customElement decorator is a function that helps define and register a custom HTML element. It associates a class with a specific custom tag name in the DOM. + +@customElement({ + name: 'my-button', + template, + styles, +}) + +export class MyButton extends GenesisElement {}; + +``` + +We’ve created a custom component called ``, which currently displays a basic button in the browser with the text "Click Me." To make it more useful, let's add an attribute that allows us to customize the button's label and give it more dynamic functionality. + +:::important + +Like web Components, Genesis Elements are required to include a hyphen (-) in their name in order to avoid conflicts with native HTML elements and to provide a way to namespace components from various libraries. + +::: + +## Add an Attribute + +We can add an `attr` decorator which allows us to link a class property to an HTML attribute, ensuring that changes in the attribute are automatically reflected in the JavaScript property, and vice versa, allowing for clean and declarative state management of custom elements. + +You can define a default value for an attribute directly within the class definition. This ensures that if no value is provided, the attribute will fall back to the specified default. + +Let's declare an attribute called **_name_** and then change that value to see it reflected: + +```typescript +import { attr, css, customElement, GenesisElement, html } from '@genesislcap/web-core'; + +@customElement({ + name: 'my-button', + template: html``;, + styles, +}) + +export class MyButton extends GenesisElement { + @attr label = 'Submit'; // setting Submit as default value +} + +``` + +Now let's update the template to display the value: + +```typescript + +const template = html`` +``` + +## Add it to your project + +:::note +After defining your custom component, you need to import it somewhere in your application's code to ensure it gets registered. +::: + +By importing the file, `MyButton` will be registered with the browser, allowing you to use the `` element in your HTML. + +```typescript +import './MyButton'; +``` + + +Now it can be added in your HTML this way: + +```typescript + + +``` + +>In this example, we created a custom web component using `GenesisElement`. We defined a button template and added a label attribute using the `@attr` decorator, allowing the button text to be customized via HTML. We implemented the `labelChanged` method, which is automatically called whenever the label value changes. The template dynamically updates with the new value, ensuring the button remains responsive to changes in its attributes. diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/02_lifecycle b/docs/001_develop/03_client-capabilities/023_custom-components/02_lifecycle.mdx similarity index 73% rename from docs/001_develop/03_client-capabilities/023_custom-components/02_lifecycle rename to docs/001_develop/03_client-capabilities/023_custom-components/02_lifecycle.mdx index a5a5c1fde1..48412fd3a1 100644 --- a/docs/001_develop/03_client-capabilities/023_custom-components/02_lifecycle +++ b/docs/001_develop/03_client-capabilities/023_custom-components/02_lifecycle.mdx @@ -1,16 +1,24 @@ +--- +title: 'Lifecycle' +sidebar_label: 'Lifecycle' +id: lifecycle + +sidebar_position: 3 +--- + # Component Lifecycles -Web Components go through several lifecycle stages from creation to removal. Understanding these lifecycles is crucial for proper component initialization, cleanup, and state management. +Genesis Elements go through several lifecycle stages from creation to removal. Understanding these lifecycles is crucial for proper component initialization, cleanup, and state management. Here are some lifecycle methods that we have access to through `GenesisElement`: ### Connected & Disconnected -`connectedCallback` lifecycle runs when component is added to DOM - perfect time for setup +`connectedCallback` lifecycle runs when component is added to DOM - perfect time for setup. `disconnectedCallback` lifecycle runs when component is removed from DOM - perfect time for cleanup. -```shell +```typescript import { attr, css, customElement, GenesisElement, html } from '@genesislcap/web-core'; const template = html``; @@ -26,12 +34,12 @@ export class MyButton extends GenesisElement { @attr disabled: boolean = false; connectedCallback() { - super.connectedCallback(); # Let GenesisElement do its setup first + super.connectedCallback(); // Let GenesisElement do its setup first console.log('Button is now connected to the DOM and ready for interaction'); } disconnectedCallback() { - super.disconnectedCallback(); # Let GenesisElement do its cleanup first + super.disconnectedCallback(); // Let GenesisElement do its cleanup first console.log('Button is no longer interactive and removed from DOM'); } } @@ -43,11 +51,11 @@ Changed Callback lifecycle runs when attributes/properties update - handle state :::note -In the following example you'll notice we have not included the `connectedCallback` and `disconnectedCallback`. It's important to remember that these two lifecycles are only necessary for things that need DOM presence. We can skip them for basic component setup that don't need DOM. +In the following example you'll notice we have not included the `connectedCallback` and `disconnectedCallback`. You only need to add them explicitly if you need to run your own code in the lifecycles, you can omit them otherwise. ::: -```shell +```typescript import { attr, css, customElement, GenesisElement, html } from '@genesislcap/web-core'; const template = html``; diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/03_state-management b/docs/001_develop/03_client-capabilities/023_custom-components/03_state-management.mdx similarity index 66% rename from docs/001_develop/03_client-capabilities/023_custom-components/03_state-management rename to docs/001_develop/03_client-capabilities/023_custom-components/03_state-management.mdx index e7e7c01ccd..acdae5b5a4 100644 --- a/docs/001_develop/03_client-capabilities/023_custom-components/03_state-management +++ b/docs/001_develop/03_client-capabilities/023_custom-components/03_state-management.mdx @@ -1,33 +1,33 @@ -# State Management +--- +title: "statemanagement" +sidebar_label: "State Management" +id: state-management + +sidebar_position: 3 +--- - +# State Management -Components manage state through several key mechanisms: attributes and properties for external state and observables for internal state. +Components manage state through several key concepts: attributes, properties, and observables. +Attributes are defined on the HTML markup of the element, whereas properties can be accessed via the DOM object in Javascript (or via a special property binding syntax on the markup). +Observable properties and attributes configure the component to watch for changes, allowing the associated html templte to update as they are updated. ## Attributes As seen in the earlier examples, in order to create an attribute you simply use the `@attr` decorator. When you use the `@attr` decorator, you are creating a property-attribute pair. This creates a binding so that any change in one will update the other, keeping them in sync. -```shell -import { attr, css, customElement, GenesisElement, html } from '@genesislcap/web-core'; - -const template = html``; - +```typescript @customElement({ - name: 'my-button', - template, + name: "my-button", + template: html``, styles, }) - export class MyButton extends GenesisElement { - @attr label: string = "Submit"; + @attr label: string = "Submit"; - labelChanged(oldValue, newValue) { - console.log(`Button text changed from ${oldValue} to ${newValue}`); - } + labelChanged(oldValue, newValue) { + console.log(`Button text changed from ${oldValue} to ${newValue}`); + } } ``` @@ -35,7 +35,7 @@ The `label` property is tied to an HTML attribute, so to override the value of ` The default value of `label` is 'Submit', if no value is provided in the HTML. -```shell +```typescript ``` @@ -59,36 +59,31 @@ Here's a list of attribute characteristics: For most attributes, we can directly bind their values to expressions in the template, allowing them to update dynamically based on properties in the component. -The label attribute controls the text shown on the button. To bind label to a dynamic value, we use an expression within the template. Here’s how this might look in a component: - -```shell -import { attr, customElement, GenesisElement, html } from '@genesislcap/web-core'; +The label attribute controls the text shown on the button. To bind label to a dynamic value, we use an expression within the template. Here’s how this might look in a `` component: -const template = html` - -`; +```typescript @customElement({ name: 'my-button', - template, + template: html``, styles, }) export class MyButton extends GenesisElement { @attr label = 'Click Me'; - # Here we are binding the text inside the button to the `label` attribute. + // Here we are binding the text inside the button to the `label` attribute. }); ``` #### Usage in HTML -```shell +```typescript ``` - Here, the label attribute binds directly to the button's text via `${x => x.label}`. - If label is set to "Cancel" in HTML `()`, the button will display "Cancel". -- When the label value is changed dynamically, the button text will automatically update.- +- When the label value is changed dynamically, the button text will automatically update. ### Interpolated Attribute Values @@ -96,26 +91,20 @@ In cases where an attribute value is partially static and partially dynamic, we Suppose we want to add a counter to the button label that shows the number of times it has been clicked. We could use an expression to combine the static text ("Clicked: "), and a dynamic value, such as the clickCount property: -```shell -import { attr, customElement, GenesisElement, html } from '@genesislcap/web-core'; - -const template = html` - -`; - +```typescript @customElement({ - name: 'my-button', - template, + name: "my-button", + template: html` `, styles, }) - -export class MyButton extends FASTElement { - @attr label = 'Click Me'; // Bind to 'label' attribute - clickCount = 0; - - handleClick() { - this.clickCount++; - } +export class MyButton extends GenesisElement { + @attr label = "Go"; // Button text label + @attr action = "Save"; } ``` @@ -132,51 +121,46 @@ We can use a ? prefix to handle boolean attributes so they’re added or removed > The disabled attribute controls whether the button is clickable. Using `?disabled=${x => x.isDisabled}`, we can bind it to the component’s `isDisabled` property and dynamically control whether the button is enabled or disabled. -```shell -import { attr, customElement, GenesisElement, html } from '@genesislcap/web-core'; - -const template = html` - -`; - +```typescript @customElement({ - name: 'my-button', - template, + name: "my-button", + template: html` + + `, styles, }) +export class MyButton extends GenesisElement { + @attr label = "Click Me"; + @attr({ mode: "boolean" }) isDisabled = false; -export class MyButton extends FASTElement { - @attr label = 'Click Me'; - @attr({ mode: 'boolean' }) isDisabled = false; - - handleClick() { - if (!this.isDisabled) { - console.log('Button clicked'); - } + handleClick() { + if (!this.isDisabled) { + console.log("Button clicked"); } + } } ``` #### Usage in HTML -```shell +```typescript ``` -- The ?disabled="${x => x.isDisabled}" syntax lets the templating engine handle boolean attributes automatically. - - If isDisabled is true, the disabled attribute is added to the button (` + `, + styles, +}) +export class MyButton extends GenesisElement { + @attr label = "Click Me"; // Linked attribute for label + clickCount = 0; // Internal property, not linked to an attribute - handleClick() { - this.clickCount++; // Update internal state - console.log(`Button clicked ${this.clickCount} times`); - } + handleClick() { + this.clickCount++; // Update internal state + console.log(`Button clicked ${this.clickCount} times`); + } } ``` - They can be declared using @attr which provides reflection between the JavaScript property and the HTML attribute. Changes to the attribute update the property, and vice versa. -```shell -export class MyButton extends FASTElement { - @attr label = "Click Me"; // Declared as a linked property +```typescript +export class MyButton extends GenesisElement { + @attr label = "Click Me"; // Declared as a linked property } - ``` -- The : symbol is used for property binding, allowing you to bind values directly to DOM properties instead of HTML attributes. This is useful when you want to bind complex data types or frequently changing properties that shouldn’t be reflected as attributes in the DOM. +- The _:_ symbol is used for property binding, allowing you to bind values directly to DOM properties instead of HTML attributes. This is useful when you want to bind complex data types or frequently changing properties that shouldn’t be reflected as attributes in the DOM. See `clickCount` -```shell +```typescript @customElement({ name: 'my-button', template: html` - < + `;, styles, }) -export class MyButton extends GenesisElemen { +export class MyButton extends GenesisElement { @attr label = "Click Me"; // Linked attribute for label clickCount = 0; // Internal property, not linked to an attribute @@ -235,10 +223,6 @@ export class MyButton extends GenesisElemen { ``` - - ## Observables Observables are a powerful tool for creating reactive components. They allow properties to trigger reactivity within a component whenever the data changes, which is essential for dynamic and interactive elements. Observables are declared with the `@observable`. @@ -246,7 +230,7 @@ Observables are a powerful tool for creating reactive components. They allow pro - Observable properties automatically trigger updates to any part of the template or component that depends on them. Whenever an observable property changes, the UI or other bound elements update without additional code. - Use `@observable` to mark a property as observable. This ensures that changes to the property automatically trigger any relevant updates in the component. -```shell +```typescript @customElement({ name: 'my-button', @@ -256,7 +240,7 @@ Observables are a powerful tool for creating reactive components. They allow pro styles, }) -export class MyButton extends GenesisElemen { +export class MyButton extends GenesisElement { @observable value = "Initial Value"; } @@ -264,20 +248,19 @@ export class MyButton extends GenesisElemen { - For each observable property, we have a built-in mechanism to execute code whenever the property’s value changes. This callback method is named after the observable property, with the suffix Changed. -```shell -export class MyButton extends GenesisElemen { +```typescript +export class MyButton extends GenesisElement { @observable value = "Initial Value"; -valueChanged(oldValue, newValue) { + valueChanged(oldValue, newValue) { console.log(`Value changed from ${oldValue} to ${newValue}`); } } - ``` -Let’s look at an example where we create a component with an observable count property that updates dynamically every time a button is clicked. We’ll use the `@observable` decorator and a `countChanged` callback to log each change. +Let’s look at an example where we create a `` component with an observable count property that updates dynamically every time a button is clicked. We’ll use the `@observable` decorator and a `countChanged` callback to log each change. -```shell +```typescript import { observable, customElement, GenesisElement, html } from '@genesislcap/web-core'; @customElement({ @@ -290,15 +273,15 @@ import { observable, customElement, GenesisElement, html } from '@genesislcap/we styles, }) -export class MyButton extends FASTElement { +export class MyButton extends GenesisElement { @observable count = 0; # Observable count property - # Method to increment the count + // Method to increment the count increment() { this.count++; // Increases the count by 1 } - # Callback triggered whenever `count` changes + // Callback triggered whenever `count` changes countChanged(oldValue, newValue) { console.log(`Count changed from ${oldValue} to ${newValue}`); } @@ -306,21 +289,20 @@ export class MyButton extends FASTElement { ``` -- The @observable decorator makes count reactive, meaning any change in count automatically updates the bound elements in the template. -- The template displays the current count as part of the button text. Whenever count changes, FAST automatically re-renders this binding, showing the latest value without extra code. +- The `@observable` decorator makes count reactive, meaning any change in count automatically updates the bound elements in the template. +- The template displays the current count as part of the button text. Whenever count changes, this binding is re-rendered, showing the latest value without extra code. -The countChanged method is automatically called whenever count changes. - This callback receives the oldValue and newValue, allowing you to track and log each change in the console. #### Usage in HTML -```shell +```typescript ``` -Initial State: - - The button displays "Clicked: 0 times". +Initial State: + The button displays "Clicked: 0 times". After Clicks: @@ -329,14 +311,11 @@ After Clicks: - And so on, incrementing with each click. - The button displays "Clicked: 0 times". -Each time count updates, countChanged logs the old and new values to the console: +Each time count updates, `countChanged` logs the old and new values to the console: -```shell +```typescript Count changed from 0 to 1 Count changed from 1 to 2 ``` @observable is an effective way to manage component state, allowing properties to reactively update the UI, trigger callbacks, and handle complex data in a streamlined way. - diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/04_event_handling b/docs/001_develop/03_client-capabilities/023_custom-components/04_event-handling.mdx similarity index 61% rename from docs/001_develop/03_client-capabilities/023_custom-components/04_event_handling rename to docs/001_develop/03_client-capabilities/023_custom-components/04_event-handling.mdx index 71fcb9e18b..a2826460f3 100644 --- a/docs/001_develop/03_client-capabilities/023_custom-components/04_event_handling +++ b/docs/001_develop/03_client-capabilities/023_custom-components/04_event-handling.mdx @@ -1,8 +1,17 @@ +--- +title: 'Event Handling' +sidebar_label: 'Event Handling' +id: event-handling + +sidebar_position: 3 +--- + + ### Event Handling In this section we will be covering the core aspects of event handling: -- Component Communication: Enables a component (like ) to notify or interact with parent components or other parts of the application. Here, we achieve this by emitting a custom event from when it’s clicked. +- Component Communication: Enables a component (like ``) to notify or interact with parent components or other parts of the application. Here, we achieve this by emitting a custom event from `` when it’s clicked. - Custom Events: Custom events are unique, developer-defined events (like button-clicked in this example) that communicate specific actions or states. We use `$emit` helper to create and dispatch a custom event. @@ -10,7 +19,7 @@ In this section we will be covering the core aspects of event handling: See implementation in the example below: -```shell +```typescript import { GenesisElement, customElement, html, observable } from "@genesislcap/web-core"; @customElement({ @@ -23,14 +32,14 @@ import { GenesisElement, customElement, html, observable } from "@genesislcap/we styles, }) export class MyButton extends GenesisElement { - @observable label = "Clicks"; # Label text for the button - @observable count = 0; # Tracks the number of clicks + @observable label = "Clicks"; // Label text for the button + @observable count = 0; // Tracks the number of clicks handleClick(event: Event) { - this.count++; # Increment the click count + this.count++; // Increment the click count - console.log("Event Target:", event.target); # Log the event target element - console.log("Component Label:", this.label); # Log the current label + console.log("Event Target:", event.target); // Log the event target element + console.log("Component Label:", this.label); // Log the current label # Emit a custom "button-clicked" event with the current label and count this.$emit("button-clicked", { @@ -43,7 +52,7 @@ export class MyButton extends GenesisElement { Now let's show how we can listen in another component providing for component communication: -```shell +```typescript import { GenesisElement, customElement, html } from "@genesislcap/web-core"; import "./my-button"; # Import the my-button component @@ -56,7 +65,7 @@ import "./my-button"; # Import the my-button component export class ParentComponent extends GenesisElement { handleButtonClicked(event: CustomEvent) { console.log("Custom event received in ParentComponent:", event.detail); - # Output: { label: "Clicks", count: X } + // Output: { label: "Clicks", count: X } } } ``` @@ -64,13 +73,13 @@ export class ParentComponent extends GenesisElement { Explanation of key elements in the above example: Component Communication: -- This allows to communicate with its parent or other parts of the application by notifying them whenever it’s clicked. -- Using the `$emit` helper method, emits a button-clicked event that any parent component or listener can handle. +- This allows `` to communicate with its parent or other parts of the application by notifying them whenever it’s clicked. +- Using the `$emit` helper method, `` emits a button-clicked event that any parent component or listener can handle. Custom Events: -- `button-clicked` is a custom event that’s unique to . It’s created to indicate that the button has been clicked, providing a specific trigger for listeners. +- `button-clicked` is a custom event that’s unique to ``. It’s created to indicate that the button has been clicked, providing a specific trigger for listeners. - The `$emit` method simplifies the creation and dispatch of the button-clicked event with bubbles: true and composed: true, making it available to parent components and global listeners. Event Data Passing: - The custom event includes data (the label and count properties) to provide context on the button’s state when the event was emitted. -- Implementation: The $emit method’s second parameter passes { label: this.label, count: this.count } as event.detail, enabling any listener to access this information for further processing. \ No newline at end of file +- Implementation: The `$emit` method’s second parameter passes `{ label: this.label, count: this.count }` as event.detail, enabling any listener to access this information for further processing. \ No newline at end of file diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/05_directives b/docs/001_develop/03_client-capabilities/023_custom-components/05_directives.mdx similarity index 94% rename from docs/001_develop/03_client-capabilities/023_custom-components/05_directives rename to docs/001_develop/03_client-capabilities/023_custom-components/05_directives.mdx index ba1d770b2c..e71a62a9bc 100644 --- a/docs/001_develop/03_client-capabilities/023_custom-components/05_directives +++ b/docs/001_develop/03_client-capabilities/023_custom-components/05_directives.mdx @@ -1,12 +1,20 @@ -# DIRECTIVES +--- +title: 'directives' +sidebar_label: 'Directives' +id: directives + +sidebar_position: 3 +--- + +# 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 to reference the button element, making it accessible for direct manipulation in code. +Here’s how to use ref in `` to reference the button element, making it accessible for direct manipulation in code. -```shell +```typescript import {customElement, GenesisElement, html, ref } from '@genesislcap/web-core'; @customElement({ @@ -38,7 +46,7 @@ Even though the `` +const template = html``; ``` ## Add it to your project :::note -After defining your custom component, you need to import it somewhere in your application's code to ensure it gets registered. +After defining your custom component, you need to import it somewhere in your application's code to ensure it gets registered. ::: By importing the file, `MyButton` will be registered with the browser, allowing you to use the `` element in your HTML. ```typescript -import './MyButton'; +import "./MyButton"; ``` - Now it can be added in your HTML this way: ```typescript @@ -99,4 +119,14 @@ Now it can be added in your HTML this way: ``` ->In this example, we created a custom web component using `GenesisElement`. We defined a button template and added a label attribute using the `@attr` decorator, allowing the button text to be customized via HTML. We implemented the `labelChanged` method, which is automatically called whenever the label value changes. The template dynamically updates with the new value, ensuring the button remains responsive to changes in its attributes. +> In this example, we created a custom web component using `GenesisElement`. We defined a button template and added a label attribute using the `@attr` decorator, allowing the button text to be customized via HTML. We implemented the `labelChanged` method, which is automatically called whenever the label value changes. The template dynamically updates with the new value, ensuring the button remains responsive to changes in its attributes. + +##Typed Templates + +Templates can be strongly typed to ensure they align with the data model they are rendering. In TypeScript, you specify the type directly within the template declaration using the syntax `html`. This approach provides better type safety and ensures that the data passed into the template matches the expected structure of the associated component or model. + +```typescript +import { html } from "@genesislcap/web-core"; + +const template = html``; +``` diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/02_lifecycle.mdx b/docs/001_develop/03_client-capabilities/023_custom-components/02_lifecycle.mdx index 48412fd3a1..cfb77d67cb 100644 --- a/docs/001_develop/03_client-capabilities/023_custom-components/02_lifecycle.mdx +++ b/docs/001_develop/03_client-capabilities/023_custom-components/02_lifecycle.mdx @@ -1,8 +1,24 @@ --- -title: 'Lifecycle' -sidebar_label: 'Lifecycle' +title: "Lifecycle" +sidebar_label: "Lifecycle" id: lifecycle - +keywords: + [ + connectedCallback, + disconnectedCallback, + propertyNameChanged, + DOM Interaction, + Reactive Updates, + Component Lifecycle, + Initialization and Cleanup, + Lifecycle Methods, + ] + tags: + - @connectedCallback + - @disconnectedCallback + - @attributeChangedCallback + - Reactive Lifecycle + - Lifecycle Events sidebar_position: 3 --- @@ -19,29 +35,34 @@ Here are some lifecycle methods that we have access to through `GenesisElement`: `disconnectedCallback` lifecycle runs when component is removed from DOM - perfect time for cleanup. ```typescript -import { attr, css, customElement, GenesisElement, html } from '@genesislcap/web-core'; +import { + attr, + css, + customElement, + GenesisElement, + html, +} from "@genesislcap/web-core"; -const template = html``; +const template = html``; @customElement({ - name: 'my-button', + name: "my-button", template, styles, }) - export class MyButton extends GenesisElement { - @attr label: string = "Submit"; - @attr disabled: boolean = false; - - connectedCallback() { - super.connectedCallback(); // Let GenesisElement do its setup first - console.log('Button is now connected to the DOM and ready for interaction'); - } - - disconnectedCallback() { - super.disconnectedCallback(); // Let GenesisElement do its cleanup first - console.log('Button is no longer interactive and removed from DOM'); - } + @attr label: string = "Submit"; + @attr disabled: boolean = false; + + connectedCallback() { + super.connectedCallback(); // Let GenesisElement do its setup first + console.log("Button is now connected to the DOM and ready for interaction"); + } + + disconnectedCallback() { + super.disconnectedCallback(); // Let GenesisElement do its cleanup first + console.log("Button is no longer interactive and removed from DOM"); + } } ``` @@ -56,23 +77,28 @@ In the following example you'll notice we have not included the `connectedCallba ::: ```typescript -import { attr, css, customElement, GenesisElement, html } from '@genesislcap/web-core'; +import { + attr, + css, + customElement, + GenesisElement, + html, +} from "@genesislcap/web-core"; -const template = html``; +const template = html``; @customElement({ - name: 'my-button', + name: "my-button", template, styles, }) - export class MyButton extends GenesisElement { - @attr label: string = "Submit"; - @attr disabled: boolean = false; + @attr label: string = "Submit"; + @attr disabled: boolean = false; - labelChanged(oldValue, newValue) { - console.log(`Button text changed from ${oldValue} to ${newValue}`); - } + labelChanged(oldValue, newValue) { + console.log(`Button text changed from ${oldValue} to ${newValue}`); + } } ``` diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/03_state-management.mdx b/docs/001_develop/03_client-capabilities/023_custom-components/03_state-management.mdx index acdae5b5a4..b255f0adc9 100644 --- a/docs/001_develop/03_client-capabilities/023_custom-components/03_state-management.mdx +++ b/docs/001_develop/03_client-capabilities/023_custom-components/03_state-management.mdx @@ -2,14 +2,34 @@ title: "statemanagement" sidebar_label: "State Management" id: state-management - +keywords: + [ + Attributes, + Dynamic Attributes, + Boolean Attributes, + Attribute Binding, + Properties, + Observables, + Reactive State, + State Updates, + Template Bindings, + Property Observables, + ] + tags: + - @attr + - @observable + - Attribute Binding + - Dynamic Attributes + - Boolean States + - Property Change + - Reactive Data sidebar_position: 3 --- # State Management Components manage state through several key concepts: attributes, properties, and observables. -Attributes are defined on the HTML markup of the element, whereas properties can be accessed via the DOM object in Javascript (or via a special property binding syntax on the markup). +Attributes are defined on the HTML markup of the element, whereas properties can be accessed via the DOM object in Javascript (or via a special property binding syntax on the markup). Observable properties and attributes configure the component to watch for changes, allowing the associated html templte to update as they are updated. ## Attributes @@ -19,7 +39,7 @@ As seen in the earlier examples, in order to create an attribute you simply use ```typescript @customElement({ name: "my-button", - template: html``, + template: html``, styles, }) export class MyButton extends GenesisElement { @@ -65,7 +85,7 @@ The label attribute controls the text shown on the button. To bind label to a dy @customElement({ name: 'my-button', - template: html``, + template: html``, styles, }) @@ -81,7 +101,7 @@ export class MyButton extends GenesisElement { ``` -- Here, the label attribute binds directly to the button's text via `${x => x.label}`. +- Here, the label attribute binds directly to the button's text via `${(x) => x.label}`. - If label is set to "Cancel" in HTML `()`, the button will display "Cancel". - When the label value is changed dynamically, the button text will automatically update. @@ -94,7 +114,7 @@ Suppose we want to add a counter to the button label that shows the number of ti ```typescript @customElement({ name: "my-button", - template: html` + template: html` + `;, styles, }) @@ -216,7 +241,7 @@ export class MyButton extends GenesisElement { clickCount = 0; // Internal property, not linked to an attribute handleClick() { - this.clickCount++; // Update internal state + this.clickCount += 1; // Update internal state console.log(`Button clicked ${this.clickCount} times`); } } @@ -225,39 +250,13 @@ export class MyButton extends GenesisElement { ## Observables -Observables are a powerful tool for creating reactive components. They allow properties to trigger reactivity within a component whenever the data changes, which is essential for dynamic and interactive elements. Observables are declared with the `@observable`. +Observables are a powerful tool for creating reactive components. They allow properties to automatically update the associated view (html template) within a component whenever the data changes, which is essential for dynamic and interactive elements. Observables are declared with the `@observable`. - Observable properties automatically trigger updates to any part of the template or component that depends on them. Whenever an observable property changes, the UI or other bound elements update without additional code. - Use `@observable` to mark a property as observable. This ensures that changes to the property automatically trigger any relevant updates in the component. -```typescript - -@customElement({ - name: 'my-button', - template: html` - -`;, - styles, -}) - -export class MyButton extends GenesisElement { - @observable value = "Initial Value"; -} - -``` - - For each observable property, we have a built-in mechanism to execute code whenever the property’s value changes. This callback method is named after the observable property, with the suffix Changed. -```typescript -export class MyButton extends GenesisElement { - @observable value = "Initial Value"; - - valueChanged(oldValue, newValue) { - console.log(`Value changed from ${oldValue} to ${newValue}`); - } -} -``` - Let’s look at an example where we create a `` component with an observable count property that updates dynamically every time a button is clicked. We’ll use the `@observable` decorator and a `countChanged` callback to log each change. ```typescript @@ -265,20 +264,20 @@ import { observable, customElement, GenesisElement, html } from '@genesislcap/we @customElement({ name: 'my-button', - template: html` - `;, styles, }) export class MyButton extends GenesisElement { - @observable count = 0; # Observable count property + @observable count = 0; // Observable count property // Method to increment the count increment() { - this.count++; // Increases the count by 1 + this.count+= 1; // Increases the count by 1 } // Callback triggered whenever `count` changes @@ -291,8 +290,8 @@ export class MyButton extends GenesisElement { - The `@observable` decorator makes count reactive, meaning any change in count automatically updates the bound elements in the template. - The template displays the current count as part of the button text. Whenever count changes, this binding is re-rendered, showing the latest value without extra code. - -The countChanged method is automatically called whenever count changes. -- This callback receives the oldValue and newValue, allowing you to track and log each change in the console. + - The `countChanged` method is automatically called whenever count changes. +- This callback receives the `oldValue` and `newValue`, allowing you to track and log each change in the console. #### Usage in HTML @@ -311,7 +310,7 @@ After Clicks: - And so on, incrementing with each click. - The button displays "Clicked: 0 times". -Each time count updates, `countChanged` logs the old and new values to the console: +Each time `count` updates, `countChanged` logs the old and new values to the console: ```typescript Count changed from 0 to 1 diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/04_event-handling.mdx b/docs/001_develop/03_client-capabilities/023_custom-components/04_event-handling.mdx index a2826460f3..d797caece8 100644 --- a/docs/001_develop/03_client-capabilities/023_custom-components/04_event-handling.mdx +++ b/docs/001_develop/03_client-capabilities/023_custom-components/04_event-handling.mdx @@ -1,12 +1,25 @@ --- -title: 'Event Handling' -sidebar_label: 'Event Handling' +title: "Event Handling" +sidebar_label: "Event Handling" id: event-handling - +keywords: + [ + DOM Events, + Custom Events, + Event Listeners, + Emitting Events, + Event Binding, + $emit Helper, + ] + tags: + - @emit + - @customEvent + - Event Lifecycle + - Event Listeners + - Event Data sidebar_position: 3 --- - ### Event Handling In this section we will be covering the core aspects of event handling: @@ -20,33 +33,38 @@ In this section we will be covering the core aspects of event handling: See implementation in the example below: ```typescript -import { GenesisElement, customElement, html, observable } from "@genesislcap/web-core"; +import { + GenesisElement, + customElement, + html, + observable, +} from "@genesislcap/web-core"; @customElement({ - name: "my-button", - template: html` - - `, - styles, + name: "my-button", + template: html` + + `, + styles, }) export class MyButton extends GenesisElement { - @observable label = "Clicks"; // Label text for the button - @observable count = 0; // Tracks the number of clicks + @observable label = "Clicks"; // Label text for the button + @observable count = 0; // Tracks the number of clicks - handleClick(event: Event) { - this.count++; // Increment the click count + handleClick(event: Event) { + this.count += 1; // Increment the click count - console.log("Event Target:", event.target); // Log the event target element - console.log("Component Label:", this.label); // Log the current label + console.log("Event Target:", event.target); // Log the event target element + console.log("Component Label:", this.label); // Log the current label - # Emit a custom "button-clicked" event with the current label and count - this.$emit("button-clicked", { - label: this.label, - count: this.count - }); - } + // Emit a custom "button-clicked" event with the current label and count + this.$emit("button-clicked", { + label: this.label, + count: this.count, + }); + } } ``` @@ -54,32 +72,95 @@ Now let's show how we can listen in another component providing for component co ```typescript import { GenesisElement, customElement, html } from "@genesislcap/web-core"; -import "./my-button"; # Import the my-button component +import MyButton from "./my-button"; // Import my-button component + +MyButton; @customElement({ - name: "parent-component", - template: html` - - `, + name: "parent-component", + template: html` + + `, }) export class ParentComponent extends GenesisElement { - handleButtonClicked(event: CustomEvent) { - console.log("Custom event received in ParentComponent:", event.detail); - // Output: { label: "Clicks", count: X } - } + handleButtonClicked(event: CustomEvent) { + console.log("Custom event received in ParentComponent:", event.detail); + // Output: { label: "Clicks", count: X } + } } ``` Explanation of key elements in the above example: -Component Communication: +Component Communication: + - This allows `` to communicate with its parent or other parts of the application by notifying them whenever it’s clicked. - Using the `$emit` helper method, `` emits a button-clicked event that any parent component or listener can handle. Custom Events: + - `button-clicked` is a custom event that’s unique to ``. It’s created to indicate that the button has been clicked, providing a specific trigger for listeners. - The `$emit` method simplifies the creation and dispatch of the button-clicked event with bubbles: true and composed: true, making it available to parent components and global listeners. Event Data Passing: + - The custom event includes data (the label and count properties) to provide context on the button’s state when the event was emitted. -- Implementation: The `$emit` method’s second parameter passes `{ label: this.label, count: this.count }` as event.detail, enabling any listener to access this information for further processing. \ No newline at end of file +- Implementation: The `$emit` method’s second parameter passes `{ label: this.label, count: this.count }` as event.detail, enabling any listener to access this information for further processing. + +### Event Bubbling + +Event bubbling lets an event travel up the DOM from the component that triggered it, allowing parent components to handle it. + +- It allows components higher up in the DOM tree to listen for and react to events emitted by child components. +- Reduces the need for tightly coupling child and parent components. + +When a custom event is emitted with `$emit`, it bubbles up the DOM by default unless bubbles is explicitly set to false. + +```typescript +import { + GenesisElement, + customElement, + html, + observable, +} from "@genesislcap/web-core"; + +@customElement({ + name: "my-button", + template: html` + + `, +}) +export class MyButton extends GenesisElement { + @observable label: string = "Click Me"; + @observable count: number = 0; + + emitCustomEvent() { + this.count += 1; + this.$emit("button-clicked", { + label: this.label, + count: this.count, + }); // By default, this event bubbles up the DOM. + } +} +``` + +Listening for events on a parent component: + +```typescript +@customElement({ + name: "parent-container", + template: html` +
+ +
+ `, +}) +export class ParentContainer extends GenesisElement { + handleButtonClick(event: CustomEvent) { + console.log("Button clicked event bubbled to parent:", event.detail); + // Logs: { label: "Click Me", count: } + } +} +``` \ No newline at end of file diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/05_directives.mdx b/docs/001_develop/03_client-capabilities/023_custom-components/05_directives.mdx index e71a62a9bc..e5d051c786 100644 --- a/docs/001_develop/03_client-capabilities/023_custom-components/05_directives.mdx +++ b/docs/001_develop/03_client-capabilities/023_custom-components/05_directives.mdx @@ -2,12 +2,35 @@ title: 'directives' sidebar_label: 'Directives' id: directives - +keywords: [ + Template Directives, + Reactive Templates, + Conditional Rendering, + List Rendering, + Dynamic Content, + DOM References, + Slot Management, + Data Synchronization, + Element Binding +] +tags: +- @ref +- @slotted +- @children +- @when +- @whenElse +- @repeat +- @sync +- Template Binding +- Conditional Directives +- Dynamic Directives sidebar_position: 3 --- # Directives +Directives are special functions used in templates to control rendering or manipulate elements dynamically. + ## Ref The ref directive lets you directly reference a specific DOM element within your component's template, enabling direct interaction with that element. @@ -15,26 +38,28 @@ The ref directive lets you directly reference a specific DOM element within your Here’s how to use ref in `` to reference the button element, making it accessible for direct manipulation in code. ```typescript -import {customElement, GenesisElement, html, ref } from '@genesislcap/web-core'; +import { + customElement, + GenesisElement, + html, + ref, +} from "@genesislcap/web-core"; @customElement({ - name: "my-button", - template: html` - - `, + name: "my-button", + template: html` `, }) export class MyButton extends GenesisElement { - buttonElement: HTMLButtonElement; + buttonElement: HTMLButtonElement; - connectedCallback() { - super.connectedCallback(); - console.log("Button text:", this.buttonElement.textContent); - } + connectedCallback() { + super.connectedCallback(); + console.log("Button text:", this.buttonElement.textContent); + } } - ``` -- `ref("buttonElement")`: The ref directive assigns the ` - `, + name: "my-button", + template: html` + + `, }) export class MyButton extends GenesisElement { - @observable slotContent: Node[]; // Holds all content passed into the slot + @observable slotContent: Node[]; // Holds all content passed into the slot - slotContentChanged() { - console.log("Slot content changed:", this.slotContent); - } + slotContentChanged() { + console.log("Slot content changed:", this.slotContent); + } } ``` - The `` element inside `` + (x) => x.ready, + html`` )}
`, }) -export class MyButton extends FASTElement { +export class MyButton extends GenesisElement { @observable ready: boolean = false; # Track loading state @observable data: { message: string } = null; # Store hardcoded data @@ -230,11 +264,73 @@ when(condition, template); ## whenElse -In the previous example with `when`, we demonstrated how to handle conditional rendering for 'if-else' scenarios by using `when` twice. However, there's a more efficient approach: the `whenElse` directive. With `whenElse`, you can manage if-else conditions in a single directive, avoiding the need to use `when` multiple times. +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`: -### Usage in HTML +```typescript +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` +
+ ${whenElse( + (x) => !x.ready, + html`Loading...`, + html`` + )} +
+ `, +}) +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 `` 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 + +```typescript +whenElse(condition, trueTemplate, falseTemplate); +``` + +- Condition: A boolean expression that determines which template to render. +- True Template: The template rendered if the condition evaluates to true. +- False Template: The template rendered if the condition evaluates to false. ## Repeat @@ -243,21 +339,28 @@ The `repeat` directive is used to dynamically render a list of items. This direc `` 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. ```typescript -import { GenesisElement, customElement, observable, html, repeat } from "@genesislcap/web-core"; +import { + GenesisElement, + customElement, + observable, + html, + repeat, +} from "@genesislcap/web-core"; @customElement({ - name: "my-button-list", - template: html` -

Button List

-
    - ${repeat(x => x.buttons, html` -
  • - `)} -
- `, + name: "my-button-list", + template: html` +

Button List

+
    + ${repeat( + (x) => x.buttons, + html`
  • ` + )} +
+ `, }) export class MyButtonList extends GenesisElement { - @observable buttons: string[] = ["Click Me", "Submit", "Reset"]; // Static list of button labels + buttons: string[] = ["Click Me", "Submit", "Reset"]; // Static list of button labels } ``` @@ -282,6 +385,64 @@ To test ``, add it to your HTML page: ``` +### 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: + +```typescript +
    + $ + {repeat( + (x) => x.buttons, + html`
  • ${(x, c) => `${c.index + 1}. ${x}`}
  • `, + { positioning: true } + )} +
+``` + +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. + +:::note + +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: + +```typescript +
    + $ + {repeat((x) => x.buttons, html`
  • ${(x) => x}
  • `, { + recycle: false, + })} +
+``` + ### Syntax breakdown ```typescript @@ -290,10 +451,178 @@ repeat(expression, itemTemplate, options); - Expression: Specifies the data source for repeat. - Item Template: Defines the HTML structure for each item. -- Options: Adds control over item positioning and view recycling (optional). +- Options (optional): + - positioning: true: Enables index and positional metadata like index, isFirst, and isLast. + - 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: + +```typescript +import { + GenesisElement, + customElement, + observable, + html, + sync, +} from "@genesislcap/web-core"; + +@customElement({ + name: "my-checkbox", + template: html` +
+ +
+ `, +}) +export class MyCheckbox extends GenesisElement { + @observable isSelected = true; + + isSelectedChanged() { + console.log(`isSelected changed to: ${this.isSelected}`); + } +} +``` + +- 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. + +```typescript +@customElement({ + name: "my-form", + template: html` +
+ +
+
+ +
+
+ +
+ `, +}) +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 + +```typescript + + + + +``` + +Let's look at an example that uses the `sync` directive with all three parameters: `binding`, `conversionType`, and `eventName` for a checkbox. + +::note + +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 + +::: + +```typescript +import { + GenesisElement, + customElement, + observable, + html, + sync, +} from "@genesislcap/web-core"; + +@customElement({ + name: "checkbox-example", + template: html` +
+ +
+ `, +}) +export class CheckboxExample extends GenesisElement { + @observable isChecked: boolean = false; + + isCheckedChanged() { + console.log(`Checkbox state changed to: ${this.isChecked}`); + } +} +``` + +### Syntax breakdown + +```typescript +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 allows `sync` 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 by `sync` 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. diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/06_styling-components.mdx b/docs/001_develop/03_client-capabilities/023_custom-components/06_styling-components.mdx index 1a3f5dafc1..8691cb9056 100644 --- a/docs/001_develop/03_client-capabilities/023_custom-components/06_styling-components.mdx +++ b/docs/001_develop/03_client-capabilities/023_custom-components/06_styling-components.mdx @@ -2,11 +2,23 @@ title: 'styling components' sidebar_label: 'Styling Components' id: styling-components - +keywords: [ + Component Styling, + Shadow DOM CSS, + Scoped Styles, + CSS Variables, + Theming, + Styling Templates +] +tags: +- @css +- CSS Variables +- Shadow DOM Styles sidebar_position: 3 --- # Styling & Structure: CSS Templates + Component styles Theme integration -CSS customization \ No newline at end of file +CSS customization From ba7432707e8559600aa20a540df656491e0bd5de Mon Sep 17 00:00:00 2001 From: Genta Demnushaj Date: Fri, 22 Nov 2024 18:51:05 +0000 Subject: [PATCH 6/9] chore: added content for styling custom elements --- .../023_custom-components/01_quick-start.mdx | 6 +- .../023_custom-components/02_lifecycle.mdx | 6 +- .../03_state-management.mdx | 4 +- .../04_event-handling.mdx | 4 +- .../023_custom-components/05_directives.mdx | 14 +- .../06_styling-components.mdx | 577 +++++++++++++++++- 6 files changed, 584 insertions(+), 27 deletions(-) diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/01_quick-start.mdx b/docs/001_develop/03_client-capabilities/023_custom-components/01_quick-start.mdx index 1fc942d90b..c8e4fc34cc 100644 --- a/docs/001_develop/03_client-capabilities/023_custom-components/01_quick-start.mdx +++ b/docs/001_develop/03_client-capabilities/023_custom-components/01_quick-start.mdx @@ -14,9 +14,9 @@ keywords: [ Component Initialization] tags: - quickstart - - @GenesisElement - - @customElement - - @template + - GenesisElement + - customElement + - template - shadowRoot - Hello World - My Button diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/02_lifecycle.mdx b/docs/001_develop/03_client-capabilities/023_custom-components/02_lifecycle.mdx index cfb77d67cb..cc83800a4c 100644 --- a/docs/001_develop/03_client-capabilities/023_custom-components/02_lifecycle.mdx +++ b/docs/001_develop/03_client-capabilities/023_custom-components/02_lifecycle.mdx @@ -14,9 +14,9 @@ keywords: Lifecycle Methods, ] tags: - - @connectedCallback - - @disconnectedCallback - - @attributeChangedCallback + - connectedCallback + - disconnectedCallback + - attributeChangedCallback - Reactive Lifecycle - Lifecycle Events sidebar_position: 3 diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/03_state-management.mdx b/docs/001_develop/03_client-capabilities/023_custom-components/03_state-management.mdx index b255f0adc9..04675995f0 100644 --- a/docs/001_develop/03_client-capabilities/023_custom-components/03_state-management.mdx +++ b/docs/001_develop/03_client-capabilities/023_custom-components/03_state-management.mdx @@ -16,8 +16,8 @@ keywords: Property Observables, ] tags: - - @attr - - @observable + - attr + - observable - Attribute Binding - Dynamic Attributes - Boolean States diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/04_event-handling.mdx b/docs/001_develop/03_client-capabilities/023_custom-components/04_event-handling.mdx index d797caece8..080dbcdfea 100644 --- a/docs/001_develop/03_client-capabilities/023_custom-components/04_event-handling.mdx +++ b/docs/001_develop/03_client-capabilities/023_custom-components/04_event-handling.mdx @@ -12,8 +12,8 @@ keywords: $emit Helper, ] tags: - - @emit - - @customEvent + - emit + - customEvent - Event Lifecycle - Event Listeners - Event Data diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/05_directives.mdx b/docs/001_develop/03_client-capabilities/023_custom-components/05_directives.mdx index e5d051c786..b498b9d71d 100644 --- a/docs/001_develop/03_client-capabilities/023_custom-components/05_directives.mdx +++ b/docs/001_develop/03_client-capabilities/023_custom-components/05_directives.mdx @@ -14,13 +14,13 @@ keywords: [ Element Binding ] tags: -- @ref -- @slotted -- @children -- @when -- @whenElse -- @repeat -- @sync +- ref +- slotted +- children +- when +- whenElse +- repeat +- sync - Template Binding - Conditional Directives - Dynamic Directives diff --git a/docs/001_develop/03_client-capabilities/023_custom-components/06_styling-components.mdx b/docs/001_develop/03_client-capabilities/023_custom-components/06_styling-components.mdx index 8691cb9056..24b3d7f2b5 100644 --- a/docs/001_develop/03_client-capabilities/023_custom-components/06_styling-components.mdx +++ b/docs/001_develop/03_client-capabilities/023_custom-components/06_styling-components.mdx @@ -3,22 +3,579 @@ title: 'styling components' sidebar_label: 'Styling Components' id: styling-components keywords: [ - Component Styling, + Custom element styles, + Dynamic CSS, + Scoped styles, + Element lifecycle, + Hiding undefined elements, + FOUC (Flash of Unstyled Content) Shadow DOM CSS, Scoped Styles, - CSS Variables, - Theming, - Styling Templates + CSS Custom Properties, + Normalize CSS, + Reusable styles, + Encapsulation, + Content containment (contain: content), + Styling Templates, + css helper, + cssPartial utility ] tags: -- @css -- CSS Variables -- Shadow DOM Styles +- @CSS +- Shadow DOM +- Dynamic Styles +- Custom Elements +- :host +- Styles and Lifecycle +- Encapsulation +- Performance +- FOUC + sidebar_position: 3 --- # Styling & Structure: CSS Templates -Component styles -Theme integration -CSS customization +In this section we will demonstrate how to style the `my-button` component, covering the following items: + +- basic styles, +- composing styles, +- partial CSS, +- dynamic behavior with CSSDirective, +- Shadow DOM styling, +- slotted content, and +- style lifecycle management. + +## Basic Styles + +In the previous sections and examples you might have noticed a `css` tag we imported from `@genesislcap/web-core` along with html. + +Quick Reminder on how to create a custom element using `@customElement`: + +```typescript +import { + css, customElement, html} from "@genesislcap/web-core"; + +@customElement({ + name: "my-button", + template, + styles // focusing on styles, +}) +``` + +So far we have seen how we can attach styles using the styles property in the `@customElement` decorator. But let's dive deeper and explore how we can define styles: + +### Defining Styles: + +In the example below we show how the `css` tagged template helper allows creating and re-using CSS for custom elements. +These styles are **encapsulated in the Shadow DOM** and attached via the styles property in the @customElement decorator. + +```typescript +import { + html, + css, + customElement, + GenesisElement, +} from "@genesislcap/web-core"; + +const styles = css` + :host { /* The :host selector applies styles to the custom element itself ().*/ + display: inline-block; + padding: 10px; + background-color: gray; + } + + :host([primary]) { /* :host([primary]): Styles the element differently if it has the primary attribute. */ + background-color: blue; + } + + :host([disabled]) { /* :host([disabled]): Styles the element with reduced opacity and disabled interaction if the disabled attribute is present.*/ + opacity: 0.5; + pointer-events: none; + } +`; + +@customElement({ + name: "my-button", + template: html``,, + styles, +}) +export class MyButton extends GenesisElement { + @attr primary: boolean = false; + @attr disabled: boolean = false; +} +``` + +### Usage in HTML + +```typescript + + +``` + +- The first button will have a blue background (primary attribute applied). +- The second button will have reduced opacity and disabled pointer events (disabled attribute applied). + +In the example above we have: + +- Used the `styles` property in the `@customElement` decorator. +- Ensured styles are encapsulated within the Shadow DOM. +- Used `:host` and `:host([attribute])` to apply styles based on the custom element's attributes. + +## Composing Styles + +We can also compose reusable styles and utility styles into our component. This approach encourages consistency across components and reduces duplication by enabling shared styles. + +Let's say we want to compose our own utility style that ensures consistent baseline styles across browsers. +This can include setting consistent margin, padding, line-height, and other foundational properties. + +```typescript +// normalize.ts +import { css } from "@genesislcap/web-core"; + +export const normalize = css` + html { + box-sizing: border-box; + } + *, + *::before, + *::after { + box-sizing: inherit; + margin: 0; + padding: 0; + } + button { + all: unset; + display: inline-block; + cursor: pointer; + } +`; +``` + +Here's how we can now use normalize utlity along with additional reusable style blocks to style `my-button` component. + +```typescript +import { + css, + html, + customElement, + GenesisElement, +} from "@genesislcap/web-core"; +import { normalize } from "./normalize"; // Import normalize styles + +// Define reusable base button styles +const buttonBaseStyles = css` + button { + font-family: Arial, sans-serif; + font-size: 16px; + padding: 10px 20px; + border-radius: 4px; + text-align: center; + border: 1px solid transparent; + } +`; + +@customElement({ + name: "my-button", + template: html` `, + styles: css` + ${normalize} /* Include normalize styles */ + ${buttonBaseStyles} /* Include reusable button styles */ + /*normalize and buttonBaseStyles combined for consistent design across components*/ + :host { + display: inline-block; + } + + button { + background-color: blue; + color: white; + transition: background-color 0.3s ease-in-out; + } + + button:hover { + background-color: darkblue; + } + + button:active { + background-color: navy; + } + `, +}) +export class MyButton extends GenesisElement { + label: string = "Click Me"; +} +``` + +In the example above we have: + +- Seen how to reuse and combine shared style blocks like normalize or utility styles. +- Combined multiple styles using the css helper. + +### Partial CSS + +In addition to reusing styles dynamically, we can also organize styles into separate files and this is when partial CSS comes in handy. + +To achieve this we can leverage `cssPartial`. See the example below: + +```typescript +import { css, cssPartial } from "@genesislcap/web-core"; + +const partial = cssPartial`padding: 10px 20px;`; // We've created partial styles using cssPartial +const styles = css` + :host { + ${partial} /* we can attach partial into host */ + } +`; + +@customElement({ + name: "my-button", + template: html``, + styles: styles, +}) +``` + +In the example above we have: + +- Defined reusable snippets of CSS using cssPartial. +- Reused blocks of CSS properties that aren’t standalone styles. + +## CSSDirective + +A CSSDirective allows us to create dynamic CSS styles and behaviors. +Instead of using static styles, you can programmatically control how the styles are applied to your component, +making it powerful for things like animations, dynamic layouts, or user interactions. + +We can achieve this in a few simple steps: + +Import the required Modules: + +```typescript +import { CSSDirective } from "@genesislcap/web-core"; +``` + +Define the Custom Directive + +```typescript +class RandomWidth extends CSSDirective { + private property = "--button-width"; +} +``` + +- A new class `RandomWidth` is created, extending `CSSDirective`. This makes it a directive that can generate styles dynamically. +- `private property = "--button-width"` defines a CSS custom property (--button-width) that will hold the dynamically generated width value. + +Generate CSS Dynamically + +```typescript + createCSS() { + return `width: var(${this.property});`; // value of --button-width + } +``` + +- The `createCSS()` method specifies what CSS should be added to the element. +- Here, it generates a rule to set the width of the element based on the value of the `--button-width` custom property (which will be dynamically updated). + +Define behavior + +```typescript + createBehavior() { + return { + bind(el) { + el.style.setProperty(this.property, `${Math.random() * 100 + 100}px`); + }, + unbind(el) { + el.style.removeProperty(this.property); + }, + }; + } +``` + +- The `createBehavior()` method defines the dynamic behavior of the directive: + + - `bind(el)`: When the element is connected to the DOM, it sets the `--button-width` property to a random value between 100px and 200px. + - `unbind(el)`: When the element is disconnected, it removes the `--button-width` property to clean up. + +Attach the Directive to Styles + +```typescript +import { + css, + html, + customElement, + GenesisElement, +} from "@genesislcap/web-core"; + +@customElement({ + name: "my-button", + template: html` `, + styles: css` + button { + ${new RandomWidth()} + background-color: blue; + color: white; + } + `, +}) +export class MyButton extends GenesisElement { + label: string = "Click Me"; +} +``` + +- The `RandomWidth` directive is used in the styles property for the `my-button` component. + - When the button is rendered: + - The `RandomWidth` directive dynamically sets the width of the button using the random value generated by `bind()`. + - The button also gets a blue background and white text from the other static styles. + +When the code runs: + +- When the my-button component is created and added to the DOM, the `bind()` method runs. +- A random width is calculated (e.g., 150px) and applied to the `--button-width` property. +- The `createCSS()` method uses this property to set the button's width. +- The button element will have a random width (e.g., 150px) and the other static styles like blue background and white text. +- If the element is removed from the DOM, the `unbind()` method is called, which removes the `--button-width` property, ensuring there are no leftover styles. + +In this section we have applied dynamic styles by: + +- Adding behavior-driven styles to custom elements using two key methods: + - `createCSS()`: Generates CSS dynamically. + - `createBehavior()`: Attaches dynamic behavior (e.g., runtime style updates) to an element. + +## Shadow DOM Styling + +Shadow DOM encapsulation ensures styles do not affect the global DOM. Meaning they apply only to your custom element and do not "leak" into the global styles. +Conversely, styles from the global scope cannot accidentally modify the appearance of your custom element. + +Key Considerations for Shadow DOM Styling + +- By default, custom elements behave as inline elements. You might want to define a specific display property like inline-block or block to control their layout behavior. +- Setting `contain: content` improves performance by signaling to the browser that the element's layout, painting, and size calculations are self-contained. + This ensures the browser doesn't unnecessarily re-calculate styles or layout changes for the entire document if something inside the component changes. +- Adding support for the hidden attribute ensures your component properly respects the display: `none` behavior when hidden is applied. This is a common practice for ensuring that components behave predictably. + +The `:host` selector allows us to apply styles directly to our custom element. + +```typescript +import { + css, + html, + customElement, + GenesisElement, +} from "@genesislcap/web-core"; + +const styles = css` + /* Encapsulation and default display */ + :host { + display: inline-block; /* Ensure consistent layout behavior */ + contain: content; /* Optimize performance */ + } + + /* Support for the "hidden" attribute */ + :host([hidden]) { + display: none; /* Completely remove the component from the visual flow */ + } + /* Styles for the internal button */ + button { + background-color: blue; + color: white; + padding: 10px 20px; + border: none; + border-radius: 4px; + cursor: pointer; + } +`; +@customElement({ + name: "my-button", + template: html` + + + `, + styles, +}) +export class MyButton extends GenesisElement {} +``` + +- :host ensures that styles like display and contain apply only to the element. + - This prevents global CSS rules from interfering with the component and vice versa. +- When `