azero
v1.2.1
Published
A lightweight front-end JavaScript framework.
Downloads
5
Readme
azero

A lightweight front-end JavaScript framework with built-in state management. The syntax is similar to htm used with vhtml. azero is more than just the syntax! It is the whole runtime transforming your syntax. Without a Virtual Tree.
Table Of Contents
- azero
Installation
The use of Vite with vanilla-ts (TypeScript template) and TailwindCSS is strongly recommended.
npm i azero
pnpm i azeroAfter that, update your dependencies.
npm update
pnpm upJSX-Like Syntax
azero uses JSX-like syntax like htm does. This is powered by tagged templates and works in any modern browser. This is the counter example shown in every modern framework in azero:
const counter = new Observable(0);
document.body.append(...html`
<div class="flex w-screen h-screen justify-center items-center text-gray-600 bg-white">
<button
onclick=${() => counter.value++}
class="px-4 py-2 bg-gray-100 font-semibold
rounded-full border border-gray-200 hover:border-gray-500 transition"
>
You have clicked me ${counter}
${counter.derive(count => count === 1 ? "time" : "times")}
</button>
</div>
`);After this README you will be able to understand and fully recreate the example above.
Inserting
Inserting Values
You can insert any value into the dom; it will be converted to a string:
const x = 23;
document.body.append(...html`
<div>${"a string"}</div>
<div>${true}</div>
<div>${x}</div>
`);Inserting Nodes
You can also insert types Node and Node[]:
const myNodes = html`
<div>Foo</div>
<div>Bar</div>
`;
document.body.append(...html`
<div>${myNodes}</div>
`);Inserting Observable Values
You can insert observable values easily:
const counter = new Observable(0);
document.body.append(...html`
<button onclick=${() => counter.value++}>You have clicked me ${counter} times</button>
`);Inserting Observable Nodes
You can also insert types Observable<Node> and Observable<Node[]>. Because this is rarely done there is not a recommended use case example. Furthermore, when using Observable<Node> it is required that the initial value of the Observable is of type Node and not null or undefined. That is due to the runtime exchanging the nodes. If you want such a functionality nonetheless, you could toggle a hidden class on the element or use an Observable<Node[]> where - initially - you assign an empty array and then - when you update - assign a new array containing your new node.
Read more about using
Observablewith mutable data under Mutable Data
Attributes
azero supports a rich attribute syntax:
document.body.append(...html`
<div title="Foo">Bar</div>
<div title='Foo'>Bar</div>
<div title=Foo>Bar</div> <!-- not recommended -->
`);Internally the attribute is setting the property of the node (key) to the given value. class keys are automatically converted to className. Here is an example:
document.body.append(...html`
<div title="Foo">Bar</div>
`);
// azero (index.js)
// ...
const node = document.createElement("div");
node["title"] = "Bar";
// ...Attributes on Components
Attributes on components are collected on an object that will be given to the function at invocation. The property children will contain an array of child nodes.
function MyComponent({something, children}: {something: number, children: Node[]}) {
return html`
<div>
something: ${something}
${children}
</div>
`;
}
document.body.append(...html`
<${MyComponent} title="Foo">Bar<//>
`);Inserting Values and Observables
You can also insert values and observables as attributes:
const x = "Hello";
const toggledClass = new Observable("hidden");
// ... update `toggledClass` ...
document.body.append(...html`
<div title=${"Foo"}>Bar</div>
<div title=${x}>Zen</div>
<div class="${toggledClass}">Zen</div> <!-- the same effect -->
<div class='${toggledClass}'>Zen</div> <!-- the same effect -->
<div class=${toggledClass}>Zen</div> <!-- the same effect -->
`);If an Observable is inserted, it auto-subscribes to that Observable and sets the attribute on update.
Inserting Values and Observables Into Strings
You can also insert values and observables into attribute strings:
const myStaticClass = "foo";
const toggledClass = new Observable("hidden");
// ... update `toggledClass` ...
document.body.append(...html`
<div class="bg-indigo-600 ${toggledClass} ${myStaticClass} etc...">Bar</div> <!-- the same effect -->
<div class='bg-indigo-600 ${toggledClass} ${myStaticClass} etc...'>Bar</div> <!-- the same effect -->
`);This is implemented using the reduce function.
Spread Props
Spread props are similar to JSX. All properties of the specified object are assigned to the element / component props.
document.body.append(...html`
<div ...${{title: "Foo"}}>Bar</div>
`);Components
Components are - similar to React - functions returning reusable UI and logic. If the function is being used as a component, it may only return types Node and Node[].
function Component({children}: {children: Node[]}) {
return html`
<div title="Foo">
Bar
${children}
</div>
`;
}azero gives you many ways to insert a component.
document.body.append(...html`
<${Component}/>
<${Component}>Children</${Component}>
<${Component}>Children<//>
${Component({children: []})} <!-- not recommended -->
${Component({children: [document.createTextNode("Children")]})} <!-- not recommended -->
`);State Management
azero gives you excellent built-in support for state management. The module azero/observable contains those classes:
Observable<T>ObservableArray<E>ObservableMap<T>
Note: This module can be used without using azero core.
Observable
This concept is similar to React
useState()and SvelteStore.
Observable<T> is a wrapper for a generic value T; primarily used with primitives. You can create an Observable with:
const observable = new Observable(0);The type (in this case number) will be inferred by TypeScript.
Changing the value
You can get / set the value through the ES6 class getter / setter property Observable.value:
observable.value = 23; // set
console.log(observable.value); // getSubscribing
This is where the magic happens. You can call subscribe on observable and the callback function provided is called whenever the value changes. This also returns another function, that unsubscribes the subscriber (when called).
const unsubscribe = observable.subscribe(value => console.log(value), false);
observable.value = 20; // logs `20`
// later
unsubscribe();When initially subscribing, the second parameter (assigned to false in the example) is a boolean specifying whether the callback should be called when initially subscribing. This parameter will default to true. This is especially useful if you are synchronizing data and want to set an initial value to your target. In other words: if you were to remove that false in the invocation, it would log 20 on the subscribe line.
Deriving
The Observable.derive function creates a new Observable that can be derived / generated / produced from the current:
const message = new Observable("Hello!");
const messageInUppercase = message.derive(message => message.toUpperCase());Whenever message updates, messageInUppercase updates too and will contain the uppercase version of the message. Another frequent use case is toggled content:
const hidden = new Observable(false);
document.body.append(...html`
<button onclick=${() => hidden.value = !hidden.value}>Toggle</button>
<div class="${hidden.derive(hidden => hidden ? "hidden" : "")} bg-blue-600 ...">Some content</div>
`);The string is derived from the boolean and inserted into the attribute.
Mutable Data
The use of Observables with objects and arrays for reactivity is discouraged, because - internally - it compares the assigned value and the existing value with ===. So when the same object is present, === does not care if it was modified, it has to be a different object. But it has great application for static content:
const split: Observable<string[]> = new Observable([]);
let input = "";
document.body.append(...html`
<input oninput=${(e: Event) => input = (e.target as HTMLInputElement).value}>
<button onclick=${() => split.value = input.split("")}>Split!</button>
${split.derive(split => split.map(char => html`
<div>Char: ${char}</div>
`[1]))}
`);In the example the split Observable<string[]> is derived from the user input (not directly, but when the user presses the button, the split string is assigned to split). Now whenever split updates, the split array (in the provided arrow function) is mapped to an array of nodes. Note, that because html returns Node[] and you are mapping string to Node not Node[], the trailing [1] must be added. This captures the <div/>. Even if there is no whitespace before the <div/>, there is always a text node. If you did not capture the index 1 of the array, Observable<string[]> would be mapped to Observable<Node[][]> (which the runtime cannot resolve).
Note, that those nodes will always be re-rendered when
splitchanges. Also, it relies on the creation of new objects. If you still want a reactive array, check out the next section.
ObservableArray
The ObservableArray<E> is a wrapper around an array of E. It has a completely different set of functions than Array.
const numbers = new ObservableArray([10, 20]); // You can provide an initializer array
numbers.add(30);
numbers.remove(10);
// ...In case you did not provide an initializer array you will need to annotate the type, because TypeScript will not be able to infer the type:
const numbers = new ObservableArray<number>();
// ...Atomic Operations
Any operation done by its interface comes down to those four atomic operations:
set(index: number, element: E): Sets the index to the element (overriding / replacing)insert(index: number, element: E): Inserts the element at the specified indexremoveAt(index: number): Removes the element at the specified indexmove(from: number, to: number): Moves the element at indexfromto indexto
Subscribing
You can subscribe to any of those operations and therefore are able to mirror the ObservableArray on your own array:
subscribeToSet(subscription: (index: number, element: E) => void): () => voidsubscribeToInsert(subscription: (index: number, element: E) => void): () => voidsubscribeToRemoveAt(subscription: (index: number, element: E) => void): () => voidsubscribeToMove(subscription: (from: number, to: number) => void): () => void
All of those functions return a function that - when called - unsubscribes your subscription.
Map
The map functions provides a way to integrate your ObservableArray into the dom:
map<E>(array: ObservableArray<E>, mapper: (element: E, index: number) => Node): Node[]
It maps each element to a node, so you can easily integrate your data. Internally, it subscribes to all atomic operations and therefore mirrors changes to the array with the dom. When nodes are added / removed, only new nodes are rendered. An example:
type Post = {
title: string,
content: string
}
const posts = new ObservableArray<Post>([
{
title: "Initial Post",
content: "Initial Content"
}
]);
document.body.append(...html`
<button onclick=${() => posts.add({
title: "A Post",
content: "Lorem Ipsum"
})}>Add Post</button>
${map(posts, post => html`
<div>
<h1>
${post.title} -
<button onclick=${() => posts.remove(post)}>Delete Post</button>
</h1>
<div>${post.content}</div>
</div>
`[1])}
`);Here the posts ObservableArray contains an initial post. New posts are added through posts.add({...}). Each post maps to a visual representation of the post data, including a button that uses posts.remove(post) to remove the post. Simple.
Reduce
This section technically belongs to
Observable.
This concept is similar to the Svelte
$: ...syntax.
The reduce function combines an array of Observables into one new Observable which can be derived / generated / produced from all the specified ones. This function could have easily been called derive but in a sense it is similar to the reduce operation on an array, so reduce was kept. A frequent use case is:
const a = new Observable(0);
const b = new Observable(0);
document.body.append(...html`
<div>${a} * ${b} = ${reduce([a, b], ([a, b]) => a * b)}</div>
`);The result of a * b is dependent on both Observables a and b. When each of them changes, the reducer (function) will be called to recalculate the result.
Note, that types are not available in reduce because reduce accepts variable type parameters.
Lifecycle
Components (normally) have this lifecycle:
Mounting -> Updating -> Unmounting
Because nodes and components in azero are updated on node-level, the Updating step is negligible for the component. Both Mounting and Unmounting happen on node-level too. azero gives you two optional properties
Node.onMount: () => voidNode.onUnmount: () => void
on every node, that will be called when this node is added / removed from the dom.
Automatic Lifecycle
When inserting an Observable, azero will subscribe on mount and unsubscribe on unmount, so that this node is not updated when it isn't being used. Example (a clock):
const time = new Observable(new Date());
let interval: number;
document.body.append(...html`
<div
onMount=${() => interval = setInterval(() => time.value = new Date(), 1000)}
onUnmount=${() => clearInterval(interval)}
>
Time: ${time}
</div>
`);You're done! Start making some apps!
