A lightweight (below 5K compressed) reactive UI library via template literal tags.
Support in all modern Browsers.
To bootstrap a new app:
$ npm init hydro-app@latest <project> // or npx create-hydro-app@latest <project>
or integrate in an existing app:
$ npm install hydro-js
import { render, html } from 'hydro-js';
Alternatively you can use a CDN:
<script type="module">
import { render, html } from "https://unpkg.com/hydro-js";
</script>
- Simple Counter
- Reactive CSS
- Two Way Data Binding
- Show
- Destructure Attributes
- Ternary
- Promise Handling
- Nested Reactivity
- Nested Reactivity 2
There are multiple things this library can do. The first thing is generating HTML from strings. This is mostly done by the Range Web API
. There are already ways to do that, like Element.insertAdjacentHTML()
, but this has some drawbacks, as it does not create Table Elements, like <colgroup>
, <tbody>
, <tfoot>
, <thead>
and <tr>
. Furthermore, the html function deals with inline events, objects, Handlebars / {{ Mustache }} etc. Using this function will feel like writing JSX without a build step.
The render function is used for mounting and unmounting Elements to/from the DOM and for executing lifecycle hooks. Optionally, it can diff both HTML Trees and reuse Elements (optionally too). This is not using a virtual DOM.
The functions calls for render
and DOM Updates are queued and worked on during a browser's idle periods.
In order to make the DOM reactive, ES6 Proxy
objects are being used to map data against an array of DOM Elements. Whenever the setter is being called, the Proxy will check the mapping and update the DOM granularly. No re-renders are needed!
Almost all intern maps are using WeakMap
with DOM Elements or Proxy objects as keys and thus memory is cleared efficiently.
args: string
returns: DocumentFragment | Element | Text
Takes a string and transforms it to HTML. Used for internal bookkeeping too.
html`<p>Text</p>`;
args:
- new Element (
ReturnType<typeof html> | reactive Proxy
) - old Element (
ReturnType<typeof html> | string
) - shouldSchedule?:
boolean
(default: true)
returns: function
that unmounts the new Element
Accepts the return value of html
and replaces it with old Element. If it is a string, it will be resolved with querySelector
. If there is no second parameter, the Element will be appended to the body
.
render(html`<p>Text</p>`);
args: boolean
Will enable/disable the schedule logic for render
and DOM Updates. Intern value defaults to true
.
args: boolean
Will enable/disable the reuse of Elements in the diffing phase of render
. Intern value defaults to true
.
args: boolean
If enabled, it will insert the new DOM Tree to the DOM before diffing. This will asssure that reused Elements will not lose their state (e.g. <video>
in Chrome. Intern value defaults to false
.
args: Node
Inserts Proxy values in the template HTML. This is useful, when HTML already exists, i.e. in a HTML file and you want to set the hydro Proxy Objects for the handlebars. Also, this can set event listener and remove the inline listener. This is a good way to send HTML over the wire.
// <p id="value">{{value}}</p> in HTML
const template = $("#value");
hydro.value = "Hello World";
setReactivity(template);
// <p id="value" onclick="placeholder">{{value}}</p> in HTML
const template = $("#value")!;
hydro.value = "Hello World";
setReactivity(template, { placeholder: () => console.log("clicked") }); // placeholder should be unique
args:
function
- elem (
ReturnType<typeof html>
) - ...args for passed
function
Calls the passed in function
with ...args
, after the Element is being inserted by render
;
const elem = html`<p>Hello World</p>`;
onRender(() => console.log("rendered elem"), elem);
render(elem);
args:
function
- elem (
ReturnType<typeof html>
) - ...args for passed
function
Calls the passed in function
with ...args
, after the Element is being diffed out by render
or removed by unmount
;
const elem = html`<p>Hello World</p>`;
onCleanup(() => console.log("removed elem"), elem);
const unmount = render(elem);
unmount();
args: value: any
returns: unique Proxy
Returns a Proxy object that can be used within html
. The Proxy is wrapping a function that can set the value. There are two ways to call the function (see Nested Reactivity 2
. If the Proxy will be called with a function, then the argument of the passed in function will be provided as the current value for the Proxy, otherwise it will take the new argument as new value.
The actual value will be set on the hydro Proxy.
Special behaviour for (prev) functions: the old value will be kept, if the returned value is undefined.
const data = reactive({ value: 42 });
render(html`<p>${data.value} β¬</p>`);
data((prev) => (prev.value = 21)); // Change the value
args:
ReturnType<typeof reactive>
function
Calls the function whenever the value of reactive Proxy changes. This is only one layer deep but chaining properties on reactive Proxys will return a Proxy too. Observing a prop of an object will look like:
observe(person.name, ...)
const person = reactive({ name: "Steve" });
observe(person.name, (newValue) => console.log(`Name changed to ${newValue}`));
person.name.setter("Definitely not Steve"); // Change the value
args:
ReturnType<typeof reactive>
Removes all observers from the reactive Proxy. This will not be called recursively for properties.
args: function
returns: a stop function
This works similarly to Vue3 watchEffect: To apply and automatically re-apply a side effect based on reactive state, we can use the watchEffect method. It runs a function immediately while reactively tracking its dependencies and re-runs it whenever the dependencies are changed.
const count = reactive(0);
watchEffect(() => console.log(getValue(count)));
// -> logs 0
count(1);
// -> logs 1
args: ReturnType<typeof reactive>
returns: currently set value
Returns the value inside the the Proxy. getValue is needed because a reactive Proxy does not have access to the value.
const person = reactive({ name: "Steve" });
console.log(getValue(person.name)); // Get curent name
args:
ReturnType<typeof reactive>
boolean
Sets the schedule behavior for DOM Updates that are connected to this Proxy. This will not be called recursively for properties.
args: ReturnType
Deletes the Proxy object and removes all observers (both recursively). This is important for keeping memory low. This happens by setting the value to null
.
args:
- condition:
function | ReturnType<typeof reactive>
- trueVal:
any
- falseVal:
any
- proxy?:
ReturnType<typeof reactive>
returns: ReturnType<typeof reactive>
In order to track a ternary in a template literal, this function has to be used. The proxy parameter (4th) is optional, if the first parameter is a reactive Proxy. Otherwise, the condition function is being executed, whenever the Proxy value changes, which will update the DOM to either the trueVal or the falseVal, depening on the return value. If trueVal is a function, then it will be executed. The same applies for falseVal.
const toggleValue = reactive(true);
render(html` <button>${ternary(toggleValue, "ON", "OFF")}</button> `);
setTimeout(() => toggleValue(false), 1e3); // Will re-validate the ternary after 1s
The actual Proxy in the library. This cannot be used with getValue
, observe
, ternary
or unset
but it offers the same functionality in a different manner.
Special behaviour for promises: the library will await promises and will set its value to the unwrapped value. If the Promise rejects, the value will be unset.
Special behaviour for null: null will delete all properties and observer for a value
properties:
- isProxy:
boolean
(default: true) - asyncUpdate:
boolean
, (default: true, derived from globalSchedule) - observe:
function
, args:string
as key, fn:function
- unobserve:
function
, args:string | undefined
- unobserve key or all, , fn:function
- getObservers:
function
, returns: map with all observers
hydro.fruit = "Banana";
render(html`<span>{{ fruit }}</span>`);
Render the elements whenever the data changes. It will handle the operation for deletion, addition, swapping etc. This defaults to a non-keyed solution but it can be changed by calling setReuseElements
with false.
args:
- root:
string
(CSS selector) - data:
ReturnType<typeof reactive>
- renderFunction:
function
, args: item:any
, i:number
const data = reactive([{ id: 4, label: "Red Onions" }])
view('.table', data, (item, i) => <tr>Reactive: {data[i].id}, Non-reactive: {item.id}</tr>)
args:
- event:
string
- data:
any
- who:
EventTarget
- options:
object
(default:{ bubbles: true }
)
Emits an event from the EventTarget who. This event bubbles by default.
render(
html`<div onfav=${({ detail: cake }) => console.log(cake)}>
<p onclick=${({ target }) => emit("fav", "Cheesecake", target)}>
Click to emit your favorite cake π°
</p>
</div>`
);
// With event options
render(
html`<div onfav=${({ detail: cake }) => console.log(cake)}>
<p
onclick=${{
options: {
once: true,
},
event: ({ target }) => emit("fav", "Strawberry Cake", target),
}}
>
Click to emit your favorite cake π°
</p>
</div>`
);
Shortcut for querySelector
.
Shortcut for querySelectorAll
.
An object with internal data / functions for testing or deeper dives for developers. This only includes a compare
function for DOM Elements at the moment.
- bind: binds a piece of data to an element. This is only useful, when an element should be removed from the DOM, when the data is being set to null.
const data = reactive({ name: "Pet" });
render(html`<p bind=${data}>${data.name}</p>`);
setTimeout(() => unset(data), 1000); // will remove the element
To enable HTML highlighting in your files, you could use leet-html in VS Code.