Define Component
This page contains a detailed API reference for the defineComponent
function.
Import
- ES6
- CommonJS
- Browser (CDN)
import { defineComponent } from 'froyojs';
const { defineComponent } = require('froyojs');
window.froyojs.defineComponent;
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
- JavaScript
- TypeScript
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}`;
},
},
});
const FrozenYogurt = defineComponent<{
$root: HTMLElement,
$state: {
flavor: string
},
button: HTMLButtonElement,
message: HTMLElement,
}>({
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 thestate
option)...
: properties defined innodes
,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 isundefined
.required
(optional): Defines if the state property is required. In non-production environments, a warning will be thrown if the property value isundefined
.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
- JavaScript
- TypeScript
const FrozenYogurt = defineComponent({
state: {
price: {
type: Number,
default: 1.99,
},
flavor: {
type: String,
required: true,
},
},
});
const FrozenYogurt = defineComponent<{
$root: HTMLElement;
$state: {
price: number;
flavor: string;
};
}>({
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 ordocument
.
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 ordocument
.
custom
:node
: Declares custom node(s). The root element is passed as the first and only argument. When aNodeList
orHTMLCollection
is returned, the value is automatically converted into an array.
Example
- JavaScript
- TypeScript
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'),
},
},
});
const FrozenYogurt = defineComponent<{
$root: HTMLElement;
$state: {};
label: Text;
cone: HTMLDivElement;
logo: SVGPathElement;
topping: Element | null;
napkins: Element[];
spoon: HTMLElement | null;
}>({
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
- JavaScript
- TypeScript
const FrozenYogurt = defineComponent({
state: {
lickCount: {
default: 0,
},
},
methods: {
lickIceCream() {
this.$state.lickCount++;
},
},
hooks: {
$setup() {
this.lickIceCream();
},
},
});
const FrozenYogurt = defineComponent<
$root: HTMLElement,
$state: { lickCount: number },
lickIceCream(): void;
>({
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
- JavaScript
- TypeScript
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;
},
},
},
},
});
const Cone = defineComponent<
$root: HTMLElement,
$state: {
type: 'sugar' | 'waffle'
},
>({
state: {
type: {
type: String,
default: 'sugar'
},
},
});
const FrozenYogurt = defineComponent<
$root: HTMLElement,
$state: {
coneType: 'sugar' | 'waffle'
},
cone: InstanceType<typeof Cone>
>({
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 attributetrue
: Adds the attribute to the elementfalse
,null
: Removes the attribute from the elementundefined
: 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 namefalse
: 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 propertynull
: Removes the property from the elementundefined
: Ignores the entry and nothing changes
content
: Defines the content of the element. The value must be astring
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();