Small hooks library inspired by React hooks but for standard web components.
npm i @atomify/hooks
Atomify hooks is made for the modern browsers. Its recommended in legacy browsers to add the following while compiling to ES5 with Babel:
exclude: /node_modules\/(?!@atomify)/
Its recommended to use Web Components polyfill to support everything from Web Components spec in legacy browsers.
All the examples below are made in combination with @atomify/jsx. But its also possible to add your own custom renderer or use plain template literal strings.
Atomify is made to accept an overwrite in the renderer. Atomify uses string render by default. @atomify/jsx
is shipped with an JSX renderer that can be used within @atomify/hooks
. Import setupDefaultRender
and add it to the top of the application.
import { JSXRenderer } from '@atomify/jsx';
import { setupDefaultRender } from '@atomify/hooks';
setupDefaultRender(JSXRenderer);
@atomify/hooks
components are made out of functions and Typescript.
You can create new components by importing the defineElement from @atomify/hooks
. @atomify/jsx components come without Shadow DOM enabled. You can enable the Shadow DOM by setting the useShadowDom
argument as true.
import { h, Fragment } from '@atomify/jsx';
import { defineElement } from '@atomify/hooks';
const CustomElement = () => {
return (<Fragment>Hello World!</Fragment>);
};
defineElement('custom-element', CustomElement, {useShadowDom: true});
You can now use the component as following inside your HTML:
<custom-element></custom-element>
Since the functional component do not have access to the this, its possible to get access to the component update
function and the current element
. The update
function triggers a re-render of the component. We import FCE
from @atomify/hooks
to get access to those parameters:
import { h } from '@atomify/jsx';
import { defineElement, FCE } from '@atomify/hooks';
const CustomElement: FCE = ({ element, update }) => {
console.log('Element:', element);
return (<button onClick={() => update()}>Hello World!</button>);
};
defineElement('custom-element', CustomElement, {useShadowDom: true});
@atomify/hooks supports a similar API as React Hooks but mainly focussed on web components.
@atomify/hooks
comes with three different lifecycle hooks:
import { h, Fragment } from '@atomify/jsx';
import { defineElement, onUpdated, onDidLoad, onDidUnload } from '@atomify/hooks';
const CustomElement = () => {
onDidLoad(() => {
console.log('called when component did load');
});
onDidUnload(() => {
console.log('called when the component is removed')
});
onUpdated(() => {
console.log('called when the component is updated');
});
return (<Fragment>Hello World!</Fragment>);
};
defineElement('custom-element', CustomElement, {useShadowDom: true});
Properties are custom attributes or properties that can be used to pass data through components. Properties have the options to be reflected to attributes. You can expose properties by importing useProp
. The properties can be Number
, String
, Boolean
, Object
and Array
.
Besides importing the hook its also needed to define the prop and the the type of the prop. This is needed because Atomify
uses this to also create the observedAttributes
array. Besides that its also used to convert an attribute to a property through this type.
import { h, Fragment } from '@atomify/jsx';
import { defineElement, useProp, FCE } from '@atomify/hooks';
const CustomElement: FCE = () => {
const [name, setName] = useProp<string>('name', 'default name');
return (<Fragment>Hello {name}!</Fragment>);
};
CustomElement.props = {
name: {
type: String;
}
};
defineElement('custom-element', CustomElement, {useShadowDom: true});
You can track the state of the property by using the 3th index of the array:
const [name, setName, watchName] = useProp<string>('name', 'default name');
setName('other default name');
watchName((newValue, oldValue) => {
console.log(newValue); // other default name
console.log(oldValue) // default name
});
You can set the reflectToAttr
option in the Prop
definitions objects to true
to reflect the property to an attribute. The property will now be in sync with the attribute:
CustomElement.props = {
name: {
type: String;
reflectToAttr: true,
},
};
<custom-element name="default name"></custom-elementt>
The initial value of useProp
can be empty if it is sure that the initial value will always be set on the element. This is where we the required
boolean within the property map comes in place.
When the required
boolean isset it will check if the value is not undefined
and forces the initial value to be set on the component:
const [name] = useProp<string>('name');
CustomElement.props = {
name: {
type: String;
required: true,
},
};
To dispatch Custom Dom events from components, use the useEvent
hook. The example below will dispatch test
event:
const event = useEvent<Number>({eventName: 'test'});
event.emit(1);
The useEvent
hook has serveral options that can be used:
interface CustomEventOptions {
// Boolean that tells if the event can bubble up
bubbles?: boolean;
// Boolean that tells the event whether it can bubble up through the boundary between shadow DOM and DOM.
composed?: boolean;
// Boolean that tells if the event can be canceled
cancelable?: boolean;
// The default event name can be overwritten by using the eventName argument.
eventName?: string;
}
useElement
and useElements
are hooks that are executing querySelector
and querySelectorAll
on the shadowRoot if useShadowDom:true
and otherwise on the this. The hooks return a current object as reference this is needed because this object is getting updated once the component updates and its fully loaded.
const div = useElement<HTMLDivElement>('div');
const buttons = useElements<HTMLButtonElement[]>('button');
console.log(div.current); // outputs single div
console.log(buttons.current) // outputs array of buttons
*Binded to the this of the element (specially handy for dynamic or conditional elements)
Both of the hooks bind the queried element to the this of the custom element. Since we are using functional components you need to specifically tell Typescript
that these queried elements can be used:
import { h, Fragment } from '@atomify/jsx';
import { Component, FCE, useElement, defineElement } from '@atomify/hooks';
// Component returns the basic Atomify Component.
export interface CustomElement extends Component {
div: HTMLDivElement;
}
const CustomElement: FCE<CustomElement> = ({ element, update }) => {
const div = useElement<HTMLDivElement>('div');
return (
<div>Hello World!</div>
);
};
defineElement('custom-element', CustomElement, {useShadowDom: true});
// App.ts
const customElement = document.querySelector<CustomElement>('custom-element');
console.log(customElement.div); // returns the single div;
Assigning a different name: It can happen sometimes that the selector has a dash. Ex: custom-element this will give some weird syntaxes like below:
const div = useElement<HTMLElement>('custom-element');
console.log(element['custom-element']);
The options now has an "as" option that lets you change the name of this element:
const div = useElement<HTMLElement>('custom-element', { as: 'customElement' });
console.log(element.customElement);
Changing target query element
The useElement
and useElements
use the current custom element as their target root (ex: targetRoot.querySelector).
This can be changed by passing a different root within options object:
const div = useElement<HTMLElement>('custom-element', { as: 'customElement', target: document });
console.log(element.customElement);
The useListen
hook is used to listen to DOM events, it can also listen to the custom events that are being dispatched by the useEvent
hook:
....
const CustomElement: FCE<CustomElement> = ({ element, update }) => {
const button = useElement<HTMLButtonElement>('button');
const event = useEvent<Number>({eventName: 'test'});
// Listens to the custom event named test.
useListen(window, 'test', (e: CustomEvent) => {
console.log('useListen:', e.detail);
});
// Listens to the click event of the button
// and fires the custom event when the button is clicked.
useListen(button, 'click', () => {
event.emit(1);
});
return (
<button>Hello World!</button>
);
};
defineElement('custom-element', CustomElement, {useShadowDom: true})
Because we are using functional components its not possible to make methods available through the outside world. Thats where the useBindMethod
comes in:
export interface CustomElement extends Component {
log: () => void;
}
const CustomElement: FCE<CustomElement> = ({ element, update }) => {
useBindMethod('log', () => {
console.log('Hello world!')
});
onDidLoad(() => {
element.log();
});
return (
<button>Hello World!</button>
);
};
defineElement('custom-element', CustomElement, {useShadowDom: true});
// App.ts
const customElement = document.querySelector<CustomElement>('custom-element');
console.log(customElement.log()); // logs: Hello World!
The composition hooks are a set of addtive, function-based APIs that allow basic composition of functional components.
Takes an object and returns a reactive object.
const state = useReactive<{ count: number}>({count: 1});
state.count++;
console.log(state.count) // outputs 2
Takes a single value and creates a reactive object from a primitive or object.
const text = useRef('Some text');
console.log(text.current) // outputs Some text
Create a reactive objects that is synchronized with other reactive properties.
const state = useReactive<{ count: number}>({count: 1});
const double = useComputed(() => state.count * 2);
console.log(double); // outputs 2
Runs a function immedialty while reactively tracking the dependencies and re-runs whenever a value of a dependency is changed.
const state = useReactive<{ count: number}>({count: 1});
const double = useComputed(() => state.count * 2);
useWatch(() => {
console.log(state.count, double.current); // outputs 1, 2
});
Atomify has an buildin componentOnReady
promise that will resolve onces the component is loaded into the dom.
// element.tsx
export interface CustomElement extends Component {
}
const CustomElement: FCE<CustomElement> = ({ element, update }) => {
return (
...
);
};
defineElement('custom-element', CustomElement, {useShadowDom: true});
// app.tsx
import { CustomElement } from './element.tsx'
const customElement = document.querySelector<CustomElement>('custom-element')
customElement.componentOnReady().then(() => {
console.log('loaded:', customElement)
});
Most functionalities can be achieved with the provided hooks above or with @atomify/kit
. But you can also create your own hooks for custom functionality:
import { createHook } from '@atomify/hooks';
export const useHook = (name: string) =>
createHook<string>({
// Each callback gives the current element.
// The hooks of that current element .
// The index of that current element.
onDidLoad: (element, hooks, index) => {
// Fired when the component loaded.
return `Hello ${name}`;
},
onUpdate: () => {
// Fired when component is updated.
return `Welcome ${name}`
},
onDidUnload: (_, hooks) => {
// OnDidUnload is the only one called as a initializer (so its called at the start)
// Thats why the components have a callback function that is only ran in the phase its given
hooks.callbacks.push({
type: DID_UNLOAD_SYMBOL,
callback: cb,
});
}
});
const name = useHook('Atomify');
console.log(name) // logs Hello Atomify.