Skip to main content

Define Component

This page contains a detailed API reference for the defineComponent function.

Import

import { defineComponent } from 'froyojs';

Reference

defineComponent

Define a new Froyo component.

Type

function defineComponent<
TThis extends ComponentThis,
TOptions extends ComponentOptions<TThis>
>(options: TOptions): ComponentConstructor;

Details

This function requires a single argument which is an object to configure the component. It returns a component constructor which can be used to create component instances.

If you are using TypeScript, both generic arguments are optional. Providing the first is highly recommended; it defines the types for the this keyword within options. When provided, this argument will add granular control, enhanced type safety, and rich autocomplete to the component options. It will also add more specific types for consumers interacting with the component constructor and instance. The second argument defines the type for the options themselves and is often unnecessary to be provided explicitly.

Example

const FrozenYogurt = defineComponent({
name: 'FrozenYogurt',
state: {
flavor: {
type: String,
default: 'Vanilla',
},
},
nodes: {
button: {
type: 'query',
selector: 'button.toggle',
};
message: {
type: 'query',
selector: '.message',
},
},
events: {
button() {
return {
click: () => {
this.$state.flavor = this.$state.flavor === 'Vanilla' ? 'Chocolate' : 'Vanilla';
};
};
},
},
render: {
message() {
return `My favorite flavor is: ${this.$state.flavor}`;
},
},
});

constructor

The constructor for a component instance.

Type

interface ComponentConstructor {
new (
root: string | Element,
state?: Record<string, any>
): ComponentInstance;
}

Details

A component constructor is generated by the defineComponent function. It is used to generate new instances for that component definition.

The first argument of the constructor is required. It must be an element or a query selector for a valid element within in the DOM. The second argument is optional and is responsible for setting the initial state of the component.

Example

const instance = new FrozenYogurt(document.querySelector('#root'), {
flavor: 'Chocolate',
});

displayName

The human-friendly name of the component.

Type

interface ComponentConstructor {
get displayName(): string;
}

Details

This readonly property is the name of the component defined by the name option.

Example

console.log(FrozenYogurt); // "FrozenYogurt"

this

The internal component instance.

Type

type ComponentThis<T> = {
$root: Element;
$state: Record<string, any>;
} & Record<string, any>;

Details

this is an object that is bound to the component options and contains the following properties:

  • $root: the root element (defined by the constructor)
  • $state: the reactive state (defined by the state option)
  • ...: properties defined in nodes, components, methods.

All functional component options (e.g. methods, components, events, render, hooks) are automatically bound to this object. This means that referencing the this keyword from within them will give you access to the data above.

Since the properties from the nodes, components and methods options are spread on this object, their keys must be unique or they will be overridden by one another.

State properties are reactive, updating the value of a state property assigned to this.$state will trigger a component update.

Non-state properties can also be added or updated directly, however doing so will not trigger a component update.

Example

defineComponent({
hooks: {
$setup() {
console.log(this); // { $root: Element, ... }
},
},
});

Configuration Options

name

Define name of the component.

Type

type ComponentOptions = {
name?: string;
};

Details

This optional property defines a human-friendly name for the component and is used primarily for error messaging.

If a name is not provided the component will be labelled: "Unnamed component".

Example

const FrozenYogurt = defineComponent({ name: 'FrozenYogurt' });

console.log(FrozenYogurt.displayName); // "FrozenYogurt"

state

Declare and configure the reactive state of the component.

Type

type ComponentOptions = {
state?: { [key: string]: StateOption };
};

type StateOption<T = any> = {
type?: { new (): T };
required?: true;
default?: T;
readonly?: true;
};

Details

The state option takes an object with each key representing the name of the property.

The value of each property must be an object with the following options:

  • type (optional): Specifies the expected type. Can be one of the following native constructors: String, Number, Boolean, Array, Object, Date, Function, or any custom constructor function. The value of each state is compared to this type and a warning will be thrown in non-production environments if it does not match.
  • default (optional): Specifies a default value. The property will be set to this value automatically when the value is undefined.
  • required (optional): Defines if the state property is required. In non-production environments, a warning will be thrown if the property value is undefined.
  • readonly (optional): Defines if the state property is readonly. If enabled, consumers will be able to read the value of the property, but will not be able to update the value of the property externally.

Example

const FrozenYogurt = defineComponent({
state: {
price: {
type: Number,
default: 1.99,
},
flavor: {
type: String,
required: true,
},
},
});

See also:


nodes

Declare and configure the DOM nodes for the component.

Type

interface ComponentOptions {
nodes?: { [key: string]: NodeOption };
}

type NodeOption =
| {
type: 'text';
value?: string;
}
| {
type: 'element';
tagName: string;
className?: string;
attributes?: Record<string, string>;
content?: string;
}
| {
type: 'svg';
tagName: string;
className?: string;
attributes?: Record<string, string>;
content?: string;
}
| {
type: 'query';
selector: string;
optional?: true;
scope?: ($root: Element) => Element | Document;
}
| {
type: 'query-all';
selector: string;
optional?: true;
scope?: ($root: Element) => Element | Document;
}
| {
type: 'custom';
node: (
$root: Element
) =>
| Text
| Element
| Element[]
| NodeListOf<Element>
| HTMLCollectionOf<Element>
| null;
};

Details

The nodes option takes an object with each key representing the name of the property.

The value of each property must be an object with a type property matching one of the following types:

  • text: Creates a new text node with the following options:
    • value (optional): Specifies the initial text value of the node.
  • element: Creates a new HTML element with the following options:
    • tagName: Specifies the type of element to be created.
    • className: Specifies a CSS class name to add to the element.
    • content: Specifies a string to render within the element.
    • attributes: Specifies an object of attributes to add to the element.
  • svg: Creates a new SVG element with the following options:
    • tagName: Specifies the type of element to be created.
    • className: Specifies a CSS class name to add to the element.
    • content: Specifies a string to render within the element.
    • attributes: Specifies an object of attributes to add to the element.
  • query: Retrieves an element from the DOM with the following options:
    • selector: Specifies the CSS selector used to find the element.
    • optional (optional): Defines if the element is optional. If enabled, a warning will not be thrown if no element is found.
    • scope (optional): Declares the target of the query. By default, the root element is used. Must return an element or document.
  • query-all: Retrieves an array of elements from the DOM with the following options:
    • selector: Specifies the CSS selector used to find the elements.
    • optional (optional): Defines if the element is optional. If enabled, a warning will not be thrown if no elements are found.
    • scope (optional): Declares the target of the query. By default, the root element is used. Must return an element or document.
  • custom:
    • node: Declares custom node(s). The root element is passed as the first and only argument. When a NodeList or HTMLCollection is returned, the value is automatically converted into an array.

Example

const FrozenYogurt = defineComponent({
nodes: {
label: {
type: 'text',
value: '$1.99',
},
cone: {
type: 'element',
tagName: 'div',
className: 'cone',
},
logo: {
type: 'svg',
tagName: 'svg',
content: '<path />',
attributes: {
width: '25px',
height: '25px',
},
},
topping: {
type: 'query',
selector: '.topping',
optional: true,
},
napkins: {
type: 'query-all',
selector: '.napkin',
scope: () => document,
},
spoon: {
type: 'custom',
node: ($root) => $root.querySelector('.spoon'),
},
},
});

See also:


methods

Declare reusable functions on the component.

Type

interface ComponentOptions {
methods?: { [key: string]: (...args: any) => any };
}

Details

The methods option takes an object with each each key representing the name of the property and each value being a function.

Each function declared on this property is added to this and is automatically bound to the internal component instance.

Example

const FrozenYogurt = defineComponent({
state: {
lickCount: {
default: 0,
},
},
methods: {
lickIceCream() {
this.$state.lickCount++;
},
},
hooks: {
$setup() {
this.lickIceCream();
},
},
});

components

Declare and configure other components that are used by the component.

Type

interface ComponentOptions {
components?: {
[key: string]: (this: ComponentThis) => ComponentOption;
};
}

type ComponentOption = {
constructor: ComponentConstructor;
root: Element | string;
state?: Record<string, any>;
subscribe?: Record<string, (value: any, previousValue: any) => void>;
};

Details

The components options takes an object with each key representing the name of the property.

The value of each property must be function that returns an object with the following options:

  • constructor: Declares the component constructor to be used.
  • root: Declares the root for the component. Must be a valid element or CSS query selector.
  • state (optional): Defines the state of the component.
  • subscribe (optional): Subscribes observers to specified state properties.

Example

const Cone = defineComponent({
state: {
type: {
type: String,
default: 'sugar',
},
},
});

const FrozenYogurt = defineComponent({
state: {
coneType: {
type: String,
default: 'waffle',
},
},
components: {
topping: {
constructor: Cone,
root: document.querySelector('.cone'),
state: {
cone: this.$state.coneType,
},
subscribe: {
cone: (value) => {
this.$state.coneType = value;
},
},
},
},
});

See also:


events

Declare and attach events to DOM elements related to the component.

Type

interface ComponentOptions<T extends ComponentNodes> {
events?: {
$window: (this: ComponentThis) => EventOption;
$document: (this: ComponentThis) => EventOption;
$root: (this: ComponentThis) => EventOption;
} & {
[key: string]: (this: ComponentThis, index?: number) => EventOption;
};
}

type EventOption = {
[key: string]: (event: Event) => void;
};

Details

The events option takes an object with each key representing the name of the event target.

With the exception of $window, $document, and $root, each key must correspond to properties defined in the nodes option.

The value of each property must be a function that returns an object of event types and handlers which are assigned directly to the respective target(s).

For properties with an array of elements, the array index is passed as the first argument of the function.

Example

const FrozenYogurt = defineComponent({
nodes: {
element: {
type: 'element',
tagName: 'button',
},
},
events: {
$document() {
return {
click: (event) => {
// ...
},
};
},
button() {
return {
focus: (event) => {
// ...
},
};
},
},
});

See also:


render

Type

interface ComponentOptions {
render?: {
$root: (this: ComponentThis) => RenderOptionElement;
} & {
[key: string]: (
this: ComponentThis,
index?: number
) => string | RenderOptionElement;
};
}

type RenderOptionElement = {
attributes?: Record<string, string | boolean | null | undefined>;
classes?: Record<string, boolean>;
content?: string;
style?: Record<string, string | null | undefined>;
};

Details

The render option takes an object with each key representing the name of the property. The value must be a function that returns value relative to the node type.

For Text nodes, the function must return a string. This string is applied directly to the node's value.

For Element nodes, the function can return a string or an object. If a string is returned, it will be applied directly to the element's innerHTML. If an object is returned it can have any of the following options:

  • attributes: Defines an object with each key representing the name of the HTML attribute and its value matches one of the following types:
    • string: Applies the value directly to the attribute
    • true: Adds the attribute to the element
    • false, null: Removes the attribute from the element
    • undefined: Ignores the entry and nothing changes
  • classes: Defines an object with each key representing the name of a CSS class name and its value matches one of the following types:
    • true: Adds the class name
    • false: Removes the class name
  • style (HTML elements only): Defines an object of inline styles to apply to the element. Each key represents the class name to be applied, while the value must be must be one of the following:
    • string: Applies the value directly to the property
    • null: Removes the property from the element
    • undefined: Ignores the entry and nothing changes
  • content: Defines the content of the element. The value must be a string that is used to set the inner HTML of the element.

The $root property is reserved and should be used to render the root element. All other keys must correspond to properties defined in nodes.

For properties which are an array of elements, the data will be applied directly to every element. Additionally, the index of the element is passed as the first argument of the function.

Example

defineComponent({
nodes: {
contentOnly: {
type: 'element',
tagName: 'span',
},
optionsObject: {
type: 'element',
tagName: 'svg',
},
},
render: {
contentOnly() {
return '...'; // applies to innerHTML
},
optionsObject() {
return {
attributes: {
'<attribute-name>': '...', // sets the value
'<attribute-name>': true, // adds boolean attribute
'<attribute-name>': false, // removes boolean attribute
'<attribute-name>': null, // removes the attribute
'<attribute-name>': undefined, // ignores the update
},
classes: {
'<class-name>': true, // adds class
'<class-name>': false, // removes class
},
style: {
'<property-name>': '...', // sets the value
'<property-name>': null, // removes the property
'<property-name>': undefined, // ignores the update
},
content: '...', // applies to innerHTML
};
},
},
});

See also:


hooks

Define functions that hook into component lifecycle events and react to state changes.

Type

interface ComponentOptions {
hooks?: {
$setup: (this: ComponentThis) => void;
$teardown: (this: ComponentThis) => void;
} & {
[key: string]: (
this: ComponentThis,
value: any,
previousValue: any
) => void;
};
}

type ComponentThis = ComponentState & ComponentNodes & ComponentData;

Details

The hooks option takes an object with each key representing the name of the hook and each value is a function.

A handful of properties have reserved names and are called at specific times during the component lifecycle;

  • $setup: called once when the component initializes
  • $teardown: called once when the component is destroyed

All other keys must correspond to properties defined in state. These functions are called when the value of that state has changed. The current and previous value are passed to the function as the first and second argument respectively.

Example

const FrozenYogurt = defineComponent({
state: {
flavor: {
type: String,
default: 'vanilla',
},
},
hooks: {
$setup() {
// Perform setup tasks ...
},
$teardown() {
// Perform cleanup tasks ...
},
flavor(value, previousValue) {
// The value of "flavor" changed, do something ...
},
},
});

See also:


Component Instance

state

Retrieve the current state of the component.

Type

export interface ComponentInstance {
get state(): Record<string, any>;
}

Details

This property returns a readonly copy of the current state of the component instance.

Example

const instance = new FrozenYogurt('#root');

console.log(instance.state); // { flavor: 'vanilla', ... }

root

Retrieve the root element.

Type

export interface ComponentInstance {
get root(): Element;
}

Details

This property returns a reference to the root element.

Example

const instance = new FrozenYogurt('#root');

console.log(instance.root); // Element

setState

Imperatively set the internal state of the component instance.

Type

interface ComponentInstance {
setState(stateChanges: Record<string, any>): void;
}

Details

This method updates the state of the component instance. The one required argument must be an object that represents the state properties to be set. Each key must match a property name from the component's state definition.

If a particular state property is configured to be "readonly", any attempt to update it via this method will be ignored and a warning will be thrown.

Example

const instance = new FrozenYogurt('#root');

instance.setState({ flavor: 'chocolate' });

See also:


subscribe

Bind a callback function to the changes of a specified state property.

Type

interface ComponentInstance {
subscribe(
property: string,
observer: (value: any, previousValue: any) => void
): void;
}

See also:

Details

This method will bind the given observer function to a given state property. When the value of that property is changed, this function will be called with the current and previous value as the first and second argument respectively.

Example

const instance = new FrozenYogurt('#root');

const observeFlavorChange = (value: string) => {
// "flavor" changed, do something ...
};

instance.subscribe('flavor', observeFlavorChange);

unsubscribe

Unbind a callback function from a specified state property.

Type

interface ComponentInstance {
unsubscribe(
property: string,
observer: (value: any, previousValue: any) => void
): void;
}

Details

This method will unbind an observer function that was previously bound to a particular state property.

Example

const instance = new FrozenYogurt('#root');

const observeFlavorChange = (value: string, previousValue: string) => {
// "flavor" changed, do something ...
};

instance.unsubscribe('flavor', observeFlavorChange);

destroy

Imperatively destroy the component instance.

Type

interface ComponentInstance {
destroy(): void;
}

Details

When called, this method will immediately "destroy" the component instance by removing all event listeners, clearing all observers, and firing all end-of-lifecycle hooks.

Example

const instance = new FrozenYogurt('#root');

instance.destroy();