npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

alkali

v1.1.4

Published

Reactivity with native JavaScript objects and HTML elements

Downloads

54

Readme

Alkali

Join the chat at https://gitter.im/alkali-js/Lobby Build Status devDependency status

Alkali is a package for creating efficient, reactive data flow that drives native HTML elements. The namesake, alkali metals are a set of elements known for being extremely reactive, conductive, and lightweight, and likewise this library is designed to be a lightweight (20KB gzipped), dependency-free package for accessing simple pure native JavaScript objects with modeling and reactivity capabilities, and creating reactive UIs based on native DOM elements. Alkali is designed for speed and scalability, using a true functional reactive, cache and invalidation-based system that provides optimized on-demand rendering performance. This makes it possible to build highly efficient and fast applications, with UI components driven by standard JavaScript objects using modern functionally reactive techniques, and without any large framework impositions.

Table of Contents

Alkali Basics

The basic approach of using Alkali within your application, is to first create "Variables" that holds your source data. A variable is the central entity in Alkali and represents a value that may change and can be reacted to. Next, we can traverse or transform variables into other derived variables. These variables can be used in the browser or server-side (in Node). In the browser, we can use these transformations, or the orginal variables directly, in element constructors to create bindings to DOM elements. A simple example would look like:

import { reactive, Div, Span } from 'alkali'

// construct a variable
let greeting = reactive('Hi')
// create a new variable based on the first
let fullGreeting = greeting.to(greeting => greeting + ', World')
// construct a div, with the fullGreeting variable bound as the content
document.body.appendChild(new Div(fullGreeting))

This would create a new <div> bound to our greeting variable with an initial value of "Hi". From here we can make changes to variables, and changes will flow through:

greeting.put('Hello')

This will notify derived variables and bound elements. This will result in the element binding queuing up a rendering, which will later execute and execute any necessary transforms, showing "Hello, World" in our <div>.

Installation

Alkali can be installed with npm install alkali or cloned from the github. Alkali works with ES6, CommonJS, or AMD modules bundlers/loaders. Or, you can load dist/index.js (from the package or from github CDN) as a script and use the alkali global. Also, here is a codepen with a basic example to start trying out Alkali.

Compatibility

Alkali is tested and runs on IE11+ or any other modern browser, and can also on NodeJS (without DOM generation). There is some very limited support for IE9-10.

Variables

Again, the central entity in the data model system is a "Variable" (similar notion has variously been known by various names such as "reactive", "signal", "property", "stream", "observable", and others). This object represents and holds a value that may change in the future. A variable can also be likened to a promise, except it can continue to change, rather than resolving one time. Depending on the interface, we can read the value, be notified when it has changed, change the value, and get meta and error information about the type of the value.

The simplest way to create a variable is by calling the reactive(initialValue) function, which will provided value as the initial value of the variable.

Notifications of data changes are delivered by update notifications. When a downstream subscriber is interested in the results of a variable change, it can request the latest value. This is subtly distinct from "streams", in that unnecessary computations can be avoided and optimized when only the current state (rather than the history of every intermediate change) is of interest. Variables can also employ internal caching of calculated values. And Variables support bi-directional flow. They can be modified as well as monitored.

Variables also support promises as values, and the variable pipeline will handle waiting for a promises to resolve to do computations.

Alkali uses extendable element constructors and updaters that are designed to consume variables, binding to variables values, and reactively responding to variable changes. They can also bound to inputs that will update variables in respond to user changes.

The Variable class can be extended and variable classes can be used like variables, where the instance to be acted on, can be resolved as needed. This allows for structured variables and categorical relationships between variable and element classes to be defined, and resolved based on context.

The main/index module in alkali exports all of functionality in alkali. If you are using ES6 module format, you can import different constructors and utilities like:

import { reactive, Div } from 'alkali'

Alkali uses UMD format, so it can be consumed by CommonJS or AMD module systems as well.

Debugging/Interaction

See the Debugging section below for information on debugging tips with standard developer tools.

Todo Example

Alkali-Todo is a the TodoMVC application written with Alkali. This repository includes a walk-through for a good example-based approach to learning to use Alkali.

Typings

Alkali includes an index.d.ts file to provide a TypeScript type interface.

TypeScript Plugin for Reactive Expressions

The ts-transform-alkali can optionally be used to write and transform reactive properties and expressions that create alkali variables and properties with decorators.

Babel Plugin for Reactive Expressions

Also, the babel-plugin-transform-alkali can optionally be used to write and transform reactive expressions for babel.

Variable/reactive API

The Variable class and reactive function are the main API for creating variables with most data.

reactive(initialValue: any): Variable

This creates a new variable, using the intial value. The provided value will also be used to dictate the "type" or structure of the returned variable. Providing a primitive will return a variable with reactive methods mirroring the primitive methods. If you provide an object, it will return variable with reactive properties corresponding to the properties on the object. For example:

import { reactive } from 'alkali'
let greeting = reactive('hello')
let upperCaseHello = greeting.toUpperCase()
upperCaseHello.valueOf() -> "HELLO"
greeting.put('hi')
upperCaseHello.valueOf() -> "HI" // reactively updates

Likewise returned variables will have reactive properties:

let person = reactive({
	name: 'John',
})
let name = person.name
name.valueOf() -> 'John'
person.name = 'Jane'
name.valueOf() -> 'Jane'

Variable API

The Variable class defines the variable structure, their properties and methods, and provides a base class for creating new variable classes.

new Variable(initialValue: any)

This is the constructor for a variable. You may create a variable with an initial value, provided as the optional argument.

valueOf()

This returns the current value of the variable. This method also allows variables to be used directly in expressions in place of primitive values, where JavaScript coercion will automatically convert a value. For example a variable with the number 4 can be used:

import { reactive } from 'alkali' // assuming modern-ES or TS module transpilation
let four = reactive(4)
four * four -> 16
'#' + four -> '#4'
four < 5 -> true
four == 4 -> true

If the variable requires asynchronous resolution (async transform or source), this will return a promise.

then(onFulfilled, onRejected)

This also retrieves the current of the variable, using the standard promise API/callback. This method also means that all variables can be treated as promises/thenables, and used in places that accept promises, including the await operator.

put(value)

put allows us to update the value of a variable with a new value. This can be given a standard value, or you can pass in another variable, in which case this variable will be "linked" to the other, receiving all values and updates from the provided variable.

If the value passed in is not different than the current value, no changes will be made and this will return Variable.noChange. If the value can not be assigned, it will return Variable.deny.

property(propertyName, PropertyClass?)

This returns a variable representing the value of the property of the variable. If this variable's value is an object, the property variable's value will be the value of the given property name. This variable will respond to changes in the object, and putting a value in a property variable will update the corresponding property on the parent object. For example:

let object = {foo: 1};
let objectVar = reactive(object);
let foo = objectVar.property('foo');
foo.valueOf() -> 1
foo.put(2);
object.foo -> 2

An optional class can be provided to define the class to use/instantiate for the property. Once the property variable has been created/accessed, it will be available as a property directly on the variable (as long as it doesn't conflict with any methods). In the case the property variable can be accessed from objectVar.foo.

to(function, reversal?)

This maps or transforms the value of the current variable to a new variable (that is returned), reflecting the current value of the variable (and any future changes) through the execution of the callback function. The callback function is called when the variable is changed and there is downstream interest in, or demand for it, and is called with the value and should return a value to be provided to the returned variable. For example:

let number = reactive(3);
number.valueOf() -> 3
let doubled = number.to((value) => value * 2);
doubled.valueOf() -> 6
number.put(5);
number.valueOf() -> 5
doubled.valueOf() -> 10

A to function can return variables as well, in which case you can effectively chain variables together, merging their changes. For example:

let a = reactive(1)
let b = reactive(2)
let sum = a.to((aValue) => {
	return b.to((bValue) => {
		return aValue + bValue;
	})
})
// sum will reactively update to changes in either a or b

The to function will also wait for any promise values to resolve before executing as well.

The optional reversal argument can define a function that will be called when the returned variable is changed (with put or child set), and can handle sending data back to the source.

get(propertyName)

This returns the value of the named property. The following are functionally equivalent:

variable.property(name).valueOf() === variable.get(name)

set(propertyName, value)

This sets the value of the named property. The following are functionally equivalent:

variable.property(name).put(value)

and

variable.set(name, value)

schema

This is a property that provides a variable representing the schema for the variable. The schema can include metadata and validation information (used for the validation property below). A schema can define sub-property schemas that will be applied to variable properties, as well. You can get and set this property. By default, this will return the constructor for plain variables, will get the schema.properties[propertyName] for property variables, and will use returned variables for derived variables.

validation

This is a property that provides a variable representing the validation of this variable. Alkali provides very basic validation, but generally you will want to implement your own validate method, which can use the schema to validate variable values. See the validation section below for more information.

subscribe(listener)

This adds a listener for any changes to the variable. If you provide a function, this will be called with an event object that has a value() method that can be called to get the current value. You can also use a subscriber object with a next(value) method, based on the proposed ES7 Observable API. However, use of subscribe to immediately access the value is generally discouraged, because it require immediate recomputation, rather than using alkali's optimized resource management. It is preferred to propagate changes through Variables to Elements and Renderers, as they provide more efficient resource management and avoid unnecessary computations. One of other distinction in these APIs is that when an object with next(value) is provided, and the variable resolves asynchrounsly (it or an upstream variable is assigned a promise), next will not be called until the promise is resolved the event listener will be called immediately (when the assignment takes place).

This method will return an object with an unsubscribe method, which you can call to stop a subscription.

Variable.for(subject)

This static method will return a variable instance mapped to the target object. This will return a stable reference to a variable instance, the first call for a given target object will create a new instance, and subsequently calls with return the same variable. This can be very useful if independent code will access the same object(s) and make changes to the object and ensure that the changes are communicated through the same variable.

all(array, transform?)

This function allows you to compose a new variable from an array of input variables, where the resulting variable will update in response to changes from any of the input variables. The return variable will hold an array of values that represent the value of each of the input variable's values (in the same order as the variables were provided). This is intended to mirror the Promise.all() API. For example:

import { all, reactive } from 'alkali'
let a = reactive(1);
let b = reactive(2);
let sum = all(a, b).to(([a, b]) => a + b);

all will also work with a set of arguments, instead of an array. It was will also work with an object, in which case each property value will be resolved, and the result will resolved to an object with the resolved values.

You can also provide an optional transform argument that will do a transform of the input values, which is essentially shorthand for all(...).to(...):

let sum = all([a, b], (a, b) => a + b);

Variables as Arrays (VArray)

Variables provide most of the array methods for when the value of a variable is an array, by using the VArray constructor. Methods including push, splice, pop, and forEach are all available on these variables, and will act on the underlying array, and send out the proper update notifications for modifications. When arrays are modified through variables, the update notifications are incremental, and can be much more efficient for downstream listeners that support them (including alkali element lists).

Functional array methods can also be applied, which will return a new variable, which will stay updated with results synced to the source array. These include filter, map, every, same, reduce, and reduceRight which take the same arguments and behave according to the standard methods.

Also, variables with arrays can be used as iterables in for-of loops. For example:

var letters = new VArray(['a', 'b', 'c']);
arrayVariable.push('d');
let lettersAfterB = letters.filter(letter => letter > 'b');
for (let letter of lettersAfterB) {
	...
}

In addition, VArray's implements most Set methods, and supports key-based interaction, for retrieving and removing values based on their id/primary-key. If entries in the array can be uniquely identified, support for this can be added by implementing a getId(entry) method.

keyBy and groupBy methods are also available: keyBy(getKey(element, emit(key, value))?, getValue(element)?) - This will index the values in array using the provided key retrieval, getKey, which can be a string to indicate a property, or a function to retrieve the key from the element. The getKey receives the element and can return the appropriate key. It also has an emit function that can optionally be used to add additional entries to the index. If omitted, value itself will be the key. In addition getValue can also provided to retrieve the value, if something other than the original array element is desired. This will return a Map variable, which can be used to retrieve values by id.

groupBy(getKey(element, emit), ?, getValue(element)?) - This behaves the same as keyBy but can be used when multiple elements may share the same key. This will put all the elements for a given in an array under the key. The returned Map variable will have array values.

Structured Variables

With variables we can define structured data that will be represented by a corresponding variable structure. We can create new extended Variables with their own object structure. This can be done by provided a structure in a call to Variable.with or a direct call (without new) which will return an subclassed Variable class/constructor. Once we have defined the structure, these property variables will be available as properties directly on the variable class and instances. For example:

let MyVariable = Variable.with({
	name: VString,
	// we can subclass and define structures and use these in properties
	subObject: Variable({
		subValue: VNumber,
		foo: OtherCustomVariable
	})
})

(JSFiddle example)

This is a useful pattern because it defines a structure for your data, and these sub-variables can easily be accessed as first class properties (rather than going through the property API). We can also values to these properties and they will be assigned to the value of the variable. For example:

let myVar = new MyVariable({ name: 'Alkali' });
myVar.name; // the "name" property variable
myVar.name.valueOf(); -> 'Alkali'
myVar.name.subscribe(event => console.log('new name', event.value()));
// assign a new value to the property, will trigger the listener
myVar.name = 'New name';
// assignment the same as this:
myVar.name.put('New name');
// we can traverse into the sub class/objects:
myVar.subObject.subValue.put(3);

We could go further and define list structures as well (here we demonstrate inline class definitions):

let MyVariable = Variable({
	myList: VArray.of(Variable({
		foo: Variable()
	}))
});

Variable collections

In addition, any time you create a new variable class (with Variable.with, Variable, or extending with class syntax), this class has its own implicit, default collection (and instance of VArray.of(Class)) that can be accessed from the static collection property, and instances can be added to and removed from it. The idiomatic way to create a model class with alkali is to create a variable class and then using it's collection to manage and interact with the set of instances. For example:

let MyVariable = Variable.with({
	name: VString,
	age: VNumber
});
let john = MyVariable.collection.push({ name: "John", age: 17})

Furthermore, for convenience in using this idiomatic pattern, the array methods are directly available as static methods on the model class:

MyVariable.push({ name: "John", age: 17})
let filtered = MyVariable.filter(person => person.age > 15)

Primitive Typed Variables

We can also define properties with a specific primitive type. Alkali exports classes for primitive typed variables:

  • VString
  • VNumber
  • VBoolean
  • VDate
  • VSet
  • VPromise
  • VMap

Each of these have methods corresponding to the methods on the original primitive. For non-mutating accessor methods, the method will return a new variable representing the result of applying the method to the value. For example:

let greeting = new VString('hello, world')
let greet = vs.slice(0, 5)
greet.valueOf() -> 'hello'
greeting.put('hi there')
greet.valueOf() -> 'hi th'

And the mutating methods (that change the value of the primitive) will change the primitive and notifying any listeners:

let set = new VSet(['a', 'b'])
let hasC = set.has('c')
hasC.valueOf() -> false
set.add('c')
hasC.valueOf() -> true

Subclassing Structured Variables

Variables can also be subclassed with native class syntax. Subclassed variables can not be directly called without new (no classes can), but you can derive new structured variables from variable subclasses using the .with(structure) method, just as you would above:

class MyVariable extends Variable {
  ...
}
let AnotherVariable = MyVariable.with({
  name: VString
})

EcmaScript Generator Support (react())

EcmaScript's new generator functions provide an elegant way to define reactive variable-based functions. Alkali provides a react() function that will take a generator function that yields variables and execute the function reactively, inputting variable values, and re-executing in response to changes. For example, we could create a function that computes the maximum of two other variables by simply writing:

import { react } from 'alkali'

let sumOfAAndB = react(function*() {
	return Math.max(yield a, yield b)
})

The resulting variable will reactively update in response to changes in the variable a or b.

This reactive function will also properly wait for promises; it can be used with variables that resolve to promises or even directly with promises themselves.

Generator Computed Properties

Reactive generators can also be directly defined in the variable structures. A computed variable property be assigned by providing a generator method used to calculate the variable value. This will generate a getter for the property that will return a variable based on the generator method. This method can be written in same way as the react generator functions described above, where you use the yield operator on each variable. The generator method also has access to this. Generator getters can be defined on variable classes or element classes. For example:

let MyVariable = Variable({
	*name() { // this will create a computed property for "name"
		return `${yield this.firstName} ${yield this.lastName}`
	},
	firstName: Variable,
	lastName: Variable
})
let v = new MyVariable({
	firstName: 'John',
	lastName: 'Doe'
})
let name = v.name // the name property will return a variable
name.valueOf() -> 'John Doe'
v.lastName = 'Smith' // this will update "name" to have a value of "John Smith"

See JSFiddle Example

Element Construction

Alkali includes functionality for constructing and extending from native DOM elements, and binding these elements to variables for reactive UIs. The alkali module exports the full set of native element constructors (see the list at the end of the documentation), as properties, to use for streamlined creation of DOM elements. For example, using EcmaScript's module format to import:

import { Div, Span, Anchor, TextInput } from 'alkali';

let divElement = new Div();
let spanElement = new Span();
document.body.appendChild(divElement).appendChild(spanElement);

In addition, an element has a create method that may be alternately called to create a new element. new Element() and Element.create() are equivalent.

These classes create native DOM elements that can be placed directly into the DOM (it is not a wrapper). All the standardized element types should be available from the module (they are all properties of the module, and if you are not using ES6, you can access them like Element.Div). These classes can take several arguments for constructing elements. The first argument is an optional string in CSS selector format that can be used to define the class, id, or tag. For example, to create a div with a class of 'my-class' and id of 'my-id':

let divElement = new Div('.my-class#my-id');

All remaining arguments can be in any order and be any of these types:

Properties Argument

An argument can be an object with properties that will be copied to the target element. For example, we could create <a> element with a link:

new Anchor({
	href: 'a url',
	textContent: 'click here'
});

Each of the property values will be assigned to the newly created element.

If any of the values are alkali variables, they will be automatically bound to the element, reactively updating the element in response to any changes to the variable. For example:

let a = reactive(1);
document.body.appendChild(new Div({title: a}));
a.put(2); // will update the title of the div

You can also use a variable for input value properties (value, valueAsNumber, and checked), and the variable will be auto-updated with user changes to the input.

You can alse use variable classes for property values as well. These are described below.

Alkali uses an optimized strategy for updating elements, by waiting for the next animation frame to update, and only updating elements that are connected to the DOM.

By default, properties are copied directly to the element that is being, or will be, created. However, Alkali also provides special handling for certain properties:

  • href, id, innerHTML, src, tabIndex, title, textContent, etc. - All the standard DOM element properties are copied to the target element, and any of these can contain a static value or a variable. A variable will auto-update the value of the property with the value of the variable.
  • content - This represents the main content of an element, and can depend on the type of element. For most elements, this represents the inner content of element. If it is a primitive, it will be the text content (inserted as a text node). If it is an array, it will generate a set of child elements. For inputs, the content corresponds to the value or checked property of the input, typed to the type of value that the input expects (numbers for number inputs), with bi-directional binding.
  • classes - This can be an object where each property corresponds to a class name that can be conditionally turned on or off for the element. For each property, the value can be a boolean or variable that returns a boolean (or anything that can be evaluated as truthy or falsy). This is typically the best way to enable and disable conditional styling for an element.
  • class, for, role - These are copied to their corresponding attributes.
  • attributes This can be an object, and the properties are copied to attributes on the element.
  • dataset - This can be an object, and the properties are copied to the elements dataset object to construct custom-user attributes.
  • style - This can be an object, and the properties are copied to the elements style object to construct inline styles.
  • CSS style properties - Inline style properties can be defined directly in the properties argument as well, and will create inline styles. When used as direct properties, booleans will be converted to named values for certain properties (for display, visibility), and numbers will be appended with px for dimensional properties.
  • render - This described below.
  • If a property is not recognized as one of these handled properties described above, the value will be copied to the target element (if the value is variable, the variable itself will be copied directly). To avoid any unexpected property collisions, Alkali keeps a whitelist of known/standard element and style properties, such that if an unknown property on an element exists, it will be overriden (and its behavior ignored).

In addition, custom handling of properties can be defined creating render methods or getters and setters as described below. See JSFiddle Example

Children Array Argument

An argument can be an array that defines a set of elements to use as the content or children of the created element. An array should consist of items where each item corresponds to the node that will be created as a child. This array can contain any of the following:

  • Element classes - These will generate new elements
  • Element instances - This is will be directly inserted
  • Variables or primitives - These will be converted to text nodes
  • Or nested arrays - This will result in nested elements (within the last element before the array). Sub-array elements will be added as children of the preceding element.

For example, we could create a table:

import { Table, TR, TD } from 'alkali';
let table = new Table([
	TR, [
		TD, ['Column 1, Row 1'],
		TD, ['Column 2, Row 1'],
	],
	TR, [
		TD, ['Column 1, Row 2'],
		TD, ['Column 2, Row 2'],
	]
]);

See JSFiddle Example

Variable Argument

A variable may be provided directly as an argument as well. This variable will be connected to the default content of the element. Again, for most elements, this variable will be mapped to the text content of the element. For example:

let greeting = reactive('Hello');
new Span(greeting);

And for input elements, the content of the element is the value of the input. This makes it easy to setup bi-direction bindings from inputs to variables. For example:

let a = reactive();
new TextInput(a);

The variable a will be mapped to this new input. This means that any changes made to a will cause the input to be updated, and any changes that the user makes to the input will update the variable (two-way binding).

When an element is detached from the DOM, it will no longer listen for variable changes (allowing the variables and all dependencies to be automatically cleaned up).

String (and numbers, booleans) Argument

You can also simply provide a string (or any primitive, including numbers or booleans), and this will also be directly inserted as a text node. For example:

new Div('Some text to put in the div');

Note that if you are using a string as the first argument, if it starts with a '.' or '#', it will be interpreted as a selector. Only the first argument can be selector, so a string-as-text can be safely used with any starting character for subsequent arguments. You can safely output variable or user-provided strings with explicit content (Div({content: someString})), or as an explicit child (Div([someString])).

null and undefined

Any null or undefined argument will be ignored. This can be useful for conditionally creating elements:

Div([
	maybeIncludeChild ? Span : null
])

Event Handlers

The properties argument may also define event handlers. These event handlers are simply functions defined with the same event handler names as used by event attributes (however, these are not implemented using "DOM0" event registration, Alkali uses modern event registration to setup these handlers). For example, we could create a span that listens for clicks:

new Span({
	onclick(event) {
		// click event occurred
	},
	onmouseover: mouseOverHandler
});

See Example

Extending Elements

The Alkali element classes are designed to be extended or derived so that you can easily create your own custom components and constructors. Extended element classes can define default properties, bindings, and children elements as well. When you call a class with the new operator or call the create method, you are creating a new element instance. But, you can also derive new constructors or classes. The first way to do this is by creating an extended constructor. By calling a class without the new operator or if you use the with method, you will create a new constructor of the original element class (or constructor). Creating new constructors works much like creating new instances, taking the same types of arguments, and defines a set of properties or event handlers to be assigned to an element instance on instantiation. An extended constructor constructs true extended native DOM element. For example, we could create a custom div constructor with a pre-defined HTML class attribute:

let MyDiv = Div('.my-class')
// and we can create new elements from this, just like with standard element classes
let myDivElement = new MyDiv()

We can also define default property values and define a children layout, to create our own complex components. For example, we could go further in extending a Div:

let MyComponent = Div({
	title: 'a default title'
}, [
	Span,
	P
]);

These resulting extended constructors can be used like any other element classes, including in child layouts, making it easy to create a hierarchy of layout:

let AnotherComponent = Div([
	H2(someVariable),
	MyComponent('.add-a-class', {
		onclick() { // ...
		}
	})
]);

And we can create element instances by using a new operator with nested constructors for a clean hierarchical syntax:

	new UList([
		LI([Span('.a-class', 'first')]),
		LI([Span('.a-class', 'second')]),
	]);

Creating/Extending Element Classes (Components)

True custom element classes or components can also be created by using the native JavaScript class extension mechanism, or any transpilation or class emulation (like Babel). We can extend from an existing element class (or constructor), and create a real new class with its own prototype (that inherits the native methods and properties). This is appropriate to use when creating new components with their own behavior defined in methods and event handlers. For example, we could write:

class MyDiv extends Div {
	onclick() {
		this.doSomething();
	}
	doSomething() {
		super.click();
	}
}

One of the advantages of using classes is that it allows you to use the super operator to call super class methods, permitting more sophisticated element class composition. Note that there are some limitations to using native class syntax. EcmaScript does not currently support properties, nor does it support direct constructor calls, so if you want to create a new derived constructor from a natively constructed class, this must be done through the with method (instances can still be created with the new operator). Assigning default properties or children can be done by calling with properties before or after class extending:

class MyDiv extends Div({title: 'default title'}) {
	// class methods
}

And we can create a constructor from MyDiv with properties to be assigned to the instances:

MyDivWithClass = MyDiv.with('.a-class', {title: 'a different title'});

Note that if you are using TypeScript, event handlers must be defined as class property (with a function value):

	onclick = function() {
		this.doSomething();
	}

See JSFiddle Example

Children

You can also define a set of children for by setting the static children property of a class:

MyDiv.children = [
	Div,
	Span
];

or

class MyDiv extends Div {
	static get children() {
		return [Div, Span];
	}
}

This is distinct from providing an array of elements or other values as the content (or as an argument) of an element. The children represents the intrinsic structure of an element, and the content is inserted after the children are created. Consequently we could define a structure like:

import { Div, Span, content } from 'alkali'
...
MyDiv.children = [
	Span('Hello'),
	content(Div)
]

new MyDiv([Span('.inner-span', 'World')])

Which would create a structure like:

<div>
	<span>Hello</span>
	<div>
		<span class="inner-span">World</span>
	</div>
</div>

Children can alternately be defined as a shadow DOM structure by using shadow instead of children.

See JSFiddle Example

Property Declaration

We can also declare properties on our elements through a constructor call, just as we would with a variable. When we define a property with a variable, this will make the property consistently available as a variable statically on the element and on the element instance. For example, we can declare that an element expects a title and body properties that we expect to be passed in on creation:

let MyDiv = Div({
  title: Variable,
  body: Variable,
  created(properties) {
    properties.title // this will always be a variable, if a title is provided it will be initialized that value
    // properties.body will be a variable as well
    properties.content = properties.body.to(body => 'Body: ' + body)
  }
})
let myDiv = new MyDiv({title: 3})
myDiv.title // this will be a variable that has been initialized to 3
myDiv.title.put(4) // we can update the variable
myDiv.body.put('new body') // this will be a variable as well, and we can update it

This variables will also be statically available, which can be useful for statically defining the element children/structure:

let MyDiv = Div({
  title: Variable,
  body: Variable
})

We can then easily reference those properties:

MyDiv.children = [
  H1(MyDiv.title),
  Div('.body', MyDiv.body),
]

See JSFiddle Example

Generator getter methods

Again, if you are developing in an ES6 compatible environment (Babel or restricted set of modern browsers), you can define generator getters in constructor arguments, making it very simple to construct element properties that react to other properties and variables. A generator getter will result a property variable for custom properties. For standard/native element properties, the generator getter will reactively assign its output to the standard element property. For example:

let MyLink = Anchor({
	*path() { // custom property
		return `${yield this.owner}/${this.repo}`
	},
	*href() { // defines the href for the <a> element
		return `https://${yield this.domain}/${yield this.path}`
	},
	*content() { // defines the contents of the <a> element
		return 'Link to ' + (yield this.path)
	}
})
let alkali = reactive('alkali')
new MyLink({
	domain: 'github.com', // these can be variables or static values
	owner: 'kriszyp',
	repo: alkali
})

Generator *render Method

In addition to getter generators, you can define a singular *render method to modify an element in response to variables. The *render method can be used on classes, constructors, or element instantiation. For example, without even creating a class we can write:

new Div({
	*render() {
		this.title = yield titleVariable;
		...
	}
})

See JSFiddle Example

Construction Lifecycle Methods

There are several methods that are called as part of the construction of an element that can be used to define additional behavior of an element. These include:

  • created(properties) - This is called for each new element instance prior to applying any properties or doing any rendering of the element or children, or attaching to the DOM. It is called with the properties that were provided to construct the element (merged arguments from construction, including original variables in the case of properties that contain variables). This method can modify the properties object, to apply different properties to the element during construction. This is the most common method for adding custom handling of elements. When an element contains children, the parent will be executed before the children created methods.
  • ready(properties) - This is called for each new element instance after the properties have been applied and rendering and construction of children have completed, and is called with the properties that were provided to construct the element (including original variables in the case of properties that contain variables). It is called after the properties and children have been assigned, but before the element is attached to a parent. Generally, DOM operations are faster prior to an element being attached. When an element contains children, the children will be executed before the parent ready method.
  • attached() (and attachedCallback()) - This is called when an element is attached to the document tree. This is useful for performing operations that may involve dimensional layout (measuring dimensions), requiring the element to be attached. When an element contains children, the parent will be executed before the children attached methods.
  • detached() - This is called when an element is detached from the document tree. This can be a useful place to perform cleanup operations. However, elements may be reattached as well (and attached would be called again).

For example:

class MyComponent extends Div {
	created(properties) {
		// we can interact with the properties that were passed in, and add to them
		properties.content = [
			H2('Hello, ' + properties.greeting)
		];
	}
	ready(properties) {
		// the properties have been set, do any additional manipulation
		this.appendChild(new Span('World'));
	}
	attached() {
		// we can measure now
		let myWidth = this.offsetWidth;
	}
}

Variable Classes

We can create our own variable classes that can be used to define properties and be referenced as well. When we use our own variable classes in property declarations, this will define a relationship between an element class and a variable class. Variable classes have the same api as normal variables, and you can then use the variable class within properties of the defined element, or any child elements. For example:

let Title = Variable()
let MyComponent = Div({
	title: Title // Define title to be our own variable type
}, [
	Span('.some-child-element', [
		Title // can even reference the variable class in child elements
	])
])

Now each instance of MyComponent that we create, will have a corresponding value/object for MyVariable, and those can even be accessed from child elements. We can also programmatically access the variable instance for a given element:

let myComponent = new MyComponent()
var title = Title.for(myComponent)
// will update the element instance
title.put('Hello')

Element classes themselves also act as variable classes. Element classes include a static property method, like variables, which maps to the properties of the elements themselves. This makes it convenient to declaratively use element properties in child elements.

Element Lists/Loops

Often you may want to create a set of child elements, based on an array or list of values or objects. You can provide an array, or a variable with an array, as the content of an element, and then define a child element structure to be generated for item in the array with an each property. The child element structure can then access the current item in the array loop through the Item variable class. For example, we could create a ul element with li children by doing:

import { UL, LI, Item } from 'alkali';

new UL({
	content: ['One', 'Two'],
	each: LI(Item)
});

Like any other variable class, we can access properties from the Item class as well, and create more sophisticated child structures. Here is how to create a select dropdown:

import { Select, Option, Item } from 'alkali';
new Select({
	content: [{id: 1, name: 'One'}, {id: 2, name: 'Two'}],
	each: Option({
		value: Item.property('id'),
		content: Item.property('name')
	})
});

Again, we can also use a variable that contains an array as the content to drive the list. When using a variable, the child elements will reactively be added, removed, or updated as the variable is modified in the future. If we use the array methods on the variable, the updates will be progressive or iterative, and will not require rerendering the whole list.

The generic Item class can be limiting in that it offers no connection back to a collection for updates. However, classes can extend a VArray and define a relationship with the class of the items within the array/collection. This can done setting a static collection property on the item class to reference the collection class. For example:

class Widgets extends VArray {...}
class Widget extends Variable {...}
Widget.collection = Widgets

One this has been the child class can be referenced in loops as well, and the changes can propagate to the collection:

new Div({
	content: Widgets,
	each: Input(Widget.property('selected'))
});

Alternately, you can define this relationship with Widgets.collectionOf = Widget

Another means of generating elements from list or array data is to use a map method:

new Select({
	content: options.map(item =>
		Option({
			value: item.id,
			content: item.name
		}))
})

This will also respond to changes (additions, removals) in the source array, if it is a variable.

Metadata and Validation

Alkali provides metadata/schema information, as well as validation functionality that can be associated with variables and their properties and derived variables. This can further facilitate the encapsulation of a property, allowing you variable-aware UI controls to interact with a variable or property's future value changes, as well as metadata and validation that further defines the property.

You can define the schema for a variable by setting the schema property on a variable or defining a getter for the property. If you don't define a schema, the default schema is the variable's constructor. In any case, you can define metadata on your schema that is available for downstream use. A schema can also define metadata for properties, which is generally more useful. This is done by putting property definitions, in a properties object, with each property defining a schema for the corresponding property. For example, we could define a variable class that specifies that the email property should have a metadata property of required: true:

let ValidatedVariable = Variable({
	email: Variable({
		required: true,
		description: 'Email address'
	})
}

Now we could define a UI control that makes use of this:

class FormField extends Label {

}
let content = FormField.property('content')
FormField.children = [
	content.schema.description, ': ',
	Input(content, {
		required: content.schema.required
	})
]

This form field class only relies on the variable/property to construct the label, input, and required attribute. We could then use it:

var entry = new ValidatedVariable({})
form.append(FormField(entry.property('email')))

We could also add a validate method that will be called to determine the validation of the variable:

let ValidatedVariable = Variable({
	validate(value, schema) {
		if (schema.pattern && !schema.pattern.test(value)) {
			return ['Value is not the right format']
		}
	}
	email: Variable({
		pattern: /\w+@\w+/,
		...
})
...
FormField.children = [
	...
	Input(content),
	Span('.errors', content.validation, {
		each: errors
}

Alkali Element API

All the element classes/constructors that are exported or generated by Alkali have the following static methods/properties:

  • create(...elementArguments) - Creates a new element instance
  • with(...elementArguments) - Creates a new element constructor
  • property(name) - Returns a generalized variable for the property of elements of this class
  • for(subject) - Gets an instance of the element for the given subject
  • children - This an array of the children (constructors, variables, or elements) that will be constructed on instantiation
  • inputEvents - This is an array of events types to listen for when a variable is connected to an input's value. By default this is ['change'].

Alkali Element Exports

Several additional exports are available from alkali for working with elements. The first is assign which allows the same type of element properties (like classes) that constructors can take, along with variable values, to be applied to an existing element or class/constructor. It takes the form: assign(target, properties)

The target can be an element, constructor, or class, and the properties should be an object with properties to be assigned.

These include two methods that can be added to HTMLElement.prototype to easily add elements to existing elements, using Alkali constructors and variables.

When added to elements their API is:

  • parentElement.append(...elementArguments) - This appends new child elements to the parent element using standard alkali arguments for children (constructors, variables, elements, etc.).
  • parentElement.prepend(...elementArguments) - This inserts new child elements in the parent element using standard alkali arguments for children (constructors, variables, elements, etc.), before other existing elements.

Both of these methods are compatible with proposed DOM4 methods. While augmenting native objects isn't recommended for consumption by other libraries, it is recommended for application developers, and can be done:

import { append, prepend } from 'alkali'

HTMLElement.prototype.append = append
HTMLElement.prototype.prepend = prepend

Options

Alkali also exports an options object. It has the following properties: options.moveLiveElementsEnabled - This indicates whether or not alkali supports elements with variables, restarting the variables when an element is reattached. This is enabled (true) by default, but it requires expensive mutation observation, and it is recommended that you disable it, and avoid reattaching alkali constructed elements (that should be created, attached, removed and left to be collected).

Renderers

Renderers are an additional mechanism for making UI components react to data changes. Renderers allow us to add reactive capabilities to existing components with minimal change. Renderers are given a variable to respond to, an element (or set of elements) to attach to, and rendering functionality to perform. When an renderer's variable changes, it will queue the rendering functionality, and render the change in the next rendering frame, if the element is still visible. A Renderer can be constructed with an options object that defines the source variable, the associated DOM element, and an renderUpdate method that will perform the rerender with the latest value from the variable.

For example, we could create a simple variable:

import { reactive } from 'alkali';

var greeting = reactive('Hi');

And then define an renderer:

import { Renderer } from 'alkali'

var greeting = reactive('Hi');
new Renderer({
	variable: greeting,
	element: someElement,
	renderUpdate: function (newValue) {
		element.innerHTML = newValue + '.';
	}
})

An Renderer will only update an element if it is visible, and will mark it as needing rerendering. If a hidden element is made visible again, you can trigger the rerendering by calling Renderer.onShowElement(element) on a parent element. You can also provide a custom definition for what constitutes a visible element that should be immediately rendered by defining a shouldRender(element) method, which should return true or false indicating if the element needs to be rendered.

Alternately, you may set alwaysUpdate to true on the Renderer options to force the Renderer to always render in response to changes.

If your variables use promises, alkali will wait for the promise to resolve before calling renderUpdate (and it will be called with the resolution of the promise). You may define a renderLoading to render something while a promise is waiting to be resolved.

Reverse Mappings

Alkali supports assymetric, bi-directional bindings, which means that we can variables can no only pass data downstream, but data can flow back upstream. For examples, a variable can be bound to an input, but that input may be changed by the user, causing a new value flow back up into the variable. If this flow goes through variable function mappings, that transform data downstream, you may want to define a reverse transform for data flowing back upstream. This can be done by defining a reverse function attached to the primary mapping function. This takes two arguments, the incoming output variable with the change upstream, and the downstream inputs variables that may need to be updated in response to the change. For example:

function double(value) {
	return value * 2
}
double.reverse = function(output, inputs) {
	// in reverse, we divide the value by 2
	inputs[0].put(output.valueOf() / 2)
}
let aNumber = reactive(4)
let doubled = aNumber.to(double)
doubled.valueOf() // -> returns 8
doubled.put(20) // change the output, this will feed back up, and change the original variable
aNumber.valueOf() // -> returns 10

Contextualization

The computations (and invalidations) can be all be executed with an optional context, which effectively allows variables to be parameterized. This means that a given variable does not have to be used to only represent a single value, but the variable may be used to represent set of different variables depending on their context. This also facilitates the construction of very powerful caching mechanisms that can intelligently cache based on determining which parameters may lead to different results.

Variable Copy-on-Write

Alkali variables that contain objects default to using copy-on-write semantics that protects object immutability. Whenever you set a property on a variable containing an object, a new copy of the object will be created and assigned the provide property value, leaving the original object untouched. For example:

let obj = {foo: 1}
let v = reactive(obj)
v.set('foo', 2)
v.valueOf() -> {foo: 2}
obj.foo -> 1

This behavior can be altered by setting the isWritable flag to false on a variable:

let obj = {foo: 1}
let v = reactive(obj)
v.isWritable = false
v.set('foo', 2)
obj.foo -> 2

Alkali variables can be assigned (with put) a value that is another variable. When this happens the first variable will receive the value of the assigned variable, and reflect any changes of the assigned or linked variable. The linked variable acts as an "upstream" source, and changes will propagate down. In a default assignment, changes will not propagate upstream, changes to the downstream variable will not affect the source. Again, a new copied object will be created to contain the changes of a downstream variable. For example:

let sourceVariable = reactive({foo: 1})
let containingVariable = reactive()
containingVariable.put(sourceVariable)
sourceVariable.set('foo', 2) // this will propagate down to containingVariable
containingVariable.set('foo', 3) // this will not affect the sourceVariable
containingVariable.get('foo') -> 3
sourceVariable.get('foo') -> 2

Variable Proxying

However, there may be situations where you want to explicitly define a variable as a proxy, such that changes propagate to the source, as well as to the proxying variable. This can be done by using the is method to assign the variable, or simply passing into the reactive function:

let sourceVariable = reactive({foo: 1})
let containingVariable = reactive(sourceVariable)
containingVariable.set('foo', 3) // this *will* affect the sourceVariable
sourceVariable.get('foo') -> 3
containingVariable.put({foo: 4}) // this will also affect the sourceVariable
sourceVariable.get('foo') -> 4

Note that when returning a variable from to variable transform, the resulting transform variable will use proxying behavior, by default, if there is no reverse transform that was defined.

Variable Copies

Alkali includes a variable Copy constructor, that allows you to maintain a copy of an object from another variable. Variable copies are very useful in situations where you want to reactively create a working copy of any object to edit and change, and potentially later save those changes back to the original object. For example, you may want to select an object to open in a form, and allow changes to be made in form. By using a working copy, the form edits can automatically be mapped to the object, but not committed back to the original object until later:

var selectedObject = reactive(); // this will be set to the currently selected object
var workingCopy = new Copy(selectedObject); // holds a copy of each object contained in selectedObject
var myForm = new MyForm({
	variable: workingCopy // we can pass this to a form, with changing the original object
});
myForm.on('submit', function() {
	workingCopy.save(); // now save the changes back to the original object
})

Creating Web Components/Custom Tag Named Elements

Custom elements (web components) can be registered with their own custom tag name as well. This can be done by extending an Element class, and calling defineElement with that class. This will set the tag name of the created elements, attempt to call customElements.define to register the element with the browser, if it is available in the browser, and return a class/constructor for the element. For example:

import { Element, defineElement } from 'alkali'

export default defineElement('custom-element',
class MyCustomElement extends Element { // on newer browsers we could extend other elements
	...
}

Note that this functionality currently will only work predictably on all browsers by extending the generic Element class, as other base elements with specific functionality, like inputs and tables, will not properly inherit their functionality in olders browsers (that do not support customElements.define). Using defineElement is recommended for classes that will be frequently used and can extend generic element functionality, or in newer browser environments.

Again, the returned element constructor can be called directly to construct new constructors, instantiated.

The provided name can include selectors as well as to define a class name or ids to assign on construction:

defineElement('custom-element.add-this-class-too')

Additional Variable Methods

The following methods are also available on variables (but mostly used internally):

updated(updateEvent)

This is called to indicate