trui

Tiny Reactive UI For JavaScript

If you are viewing this as a README.md, visit https://anywhichway.github.io/trui/ for a more interactive experience.

The closest comparable libraries are VanJS and htmx.

You can think of trui as what might happen if VanJS and htmx had a baby:

Learning and using trui

Installation

npm install trui

Then use the trui.js or trui.min.js file in the root directory.

Basic Examples

Reactive HTML Templates (rhtml.js 1316 bytes)

<p style="border:1px solid black;padding:5px">
<button onclick="this.state.counter||=0;this.state.counter++">Click Count: ${counter||0}</button>
</p>

Set age to greater than or equal 21 to see the Access Granted message.

<p id="person" style="border:1px solid black;padding:5px" data-name="Joe" data-age="21" title="21 and older are eligible">
    Name: ${name} Age: ${age}
    Age: <input name="age" type="number" value=${age} oninput="person.dataset.age=event.target.value">
    <span style="display:${age >=21 ? '' : 'none'}">Access is granted</div>
</p>

By loading rhtmx.js (804 bytes) you can get utility functions like $data, $state and $attribute to facilitate data updates and reactive rendering.

<p style="border:1px solid black;padding:5px" data-name="Joe" data-age="21" title="21 and older are eligible" oninput="$data(event,false)">
    Name: ${name} Age: ${age}
    <input name="age" type="number" value="${age}">
    <span style="display:${age >=21 ? '' : 'none'}">Access is granted</span>
</p>

Reactive JavaScript (rjs.js 1600 bytes) - similar to VanJS

Whereas states cannot be private with rhtml.js, they are a property of each element, with rjs.js they can be private.

var rjsCounter = () => {
        const state = rjs.state(0);
        return button({
            onclick() {
                state.value++;
            }
        }, () => {
            return `Click Count: ${state.value}`
        });
    }
document.currentScript.insertAdjacentElement("afterend", rjsCounter());

However, you can use the public state if you wish:

<script>
var rhtmlCounter = () => {
    return button({
        onclick(event) {
            this.state.count ||= 0;
            this.state.count++;
            // state would also be available as event.target.state
        }
    }, (state) => {
        return `Click Count: ${state.count||=0}`
    });
}
document.currentScript.insertAdjacentElement("afterend", rhtmlCounter());
</script>

Below we return to the eligibility example using private state instead of data attributes:

var rjsForm = () => {
    const state = rjs.state({name:"Joe",age:21});
    return p({
        oninput(event) {
            state[event.target.name] = event.target.value;
        },
        style: "border:1px solid black; padding:5px"
    }, () => {
        return `Name: ${state.name} Age: ${state.age}`;
    },() => { 
        return input({
            name: "age",
            type: "number",
            value: state.age
        });
    }, () => {
        return span({
            style: `display:${state.age >=21 ? '' : 'none'}`
        },() => {
            return " Access is granted";
        });
    });
}
document.currentScript.insertAdjacentElement("afterend", rjsForm());

Element Fetch (xfetch.js 1148 bytes) - similar to htmx load

<script src="./src/element-fetch.js"></script>

Xon (xon.js 1304 bytes) - similar to htmx triggers

<p style="border:1px solid black;padding:5px" target=">" x-on='every:1000' >${new Date()}</p>

The target attribute is used to specify the target of the xon operation. The > means inner. Additional options include:

Reactive HTML (rhtml.js)

With reactive html, you can use the JavaScript ${} syntax directly in your HTML.

Note: rhtml.js should be loaded in the head of your HTML document.

Reactive Attributes, Datasets, and States

<p style="border:1px solid black;padding:5px">
    <button name="Greetings!!"
            onclick="this.getAttribute('name')=='Greetings!!' ? this.setAttribute('name','Goodbye...') : this.setAttribute('name','Greetings!!')">
        ${name}
    </button> Attribute Update<br>
    <button data-counter="0" onclick="this.dataset.counter++">Click Count: ${counter}</button>  Dataset Update<br>
    <button onclick="this.state.counter||=0;this.state.counter++">Click Count: ${counter||0}</button> State Update<br>
    <button style="color:blue" onclick="this.style.color==='blue' ? this.style.color='red' : this.style.color='blue'">Click To Color</button> Style Update<br>
    <button data-counter="0" style="color:${counter%2 ? 'red' : 'blue'}" onclick="this.dataset.counter++">Click Count: ${counter}</button> Dataset Update With Computed Style
</p>

Resolving Reactive HTML

The best way to resolve reactive HTML is to call document.resolve() in a script immediately after the body tag of a document:

document.resolve() will hide the body, resolve the document, and then un-hide the body. If you do not want the body to be hidden, you can call document.resolve({hidden:false}); however, this will result in screen flicker.

document.resolve() can be called with a state and dataset argument for use in resolving reactive values in the document. Updating properties of document.body.state or document.body.dataset will then trigger a re-render of the document.

<body>
    <script>
        document.resolve({state:{greeting:"Hello World!",counter:0}});
        document.currentScript.remove();
    </script>
${greeting}
</body>

IMPORTANT: You MUST make the call to remove the current script, or the resolve in this script will override any state or dataset values set elsewhere in the document.

$attribute (rhtmlx.js)

$attribute is a utility function that can be used to update the attributes of an element. It is capable of accomplishing 3 things:

  1. Binding to the element for which the attribute needs to be updated
  2. Mapping the name of an input element to the attribute key, or taking an override
  3. Setting an attribute value (which may cause a reactive response)

$attribute is polymorphic. These are the signatures:

event is the event that triggered the update

element is the element on from which the closest matching parent should be found, unless selector is false, in which case it will be the element on which the attribute value will be set. Values for property (the attribute name) and value must be provided.

selector is used to find the element to which the attribute should be applied.

See $data below for an example.

$data (rhtmlx.js)

$data accomplishes the same three things as $attribute but for the dataset.

Here is the same content as used in the simple examples, but with a selector to find the p element with data attribute:

<p style="border:1px solid black;padding:5px" data-name="Joe" data-age="21" title="21 and older are eligible">
    Name: ${name} Age: ${age}
    <input name="age" type="number" value=${age} oninput="$data(event,'p:has(> input)')">
    <span style="display:${age >=21 ? '' : 'none'}">Access is granted</span>
</p>

If you have a complex structure, you can create dynamic selectors with string templates, e.g. p[data-${event.target.name}]:has(> input).

$state (rhtmlx.js)

$state accomplishes the same three things as $attribute but for the state.

Reactive JavaScript (rjs.js)

Can be used standalone or with any other trui files.

The functionality of rjs.js is similar to VanJS.

Extended Fetch (xfetch.js)

Can be used standalone or with any other trui files.

When xfetch.js is loaded:

Clicking on Privacy Policy below will load the content of the privacy-policy.html file into the p element.

<p x-href="./examples/privacy-policy.html#content" onclick="xfetch(event)">Privacy Policy</p>

Dispensing with SSR

If you are not trying to build a single page application, you can dispense with SSR by making a relatively minor change to your HTML authoring approach.

Since href is not standard on all elements and x-href is not standard at all, web crawlers and indexing services may not find all your content if you author like this:

<p x-href="./examples/privacy-policy.html#content" onclick="xfetch(event)">Privacy Policy</p>

However, by adding an anchor with the class xfetch you can make the privacy policy visible to all web crawlers and indexing services even without the user clicking on the link, and will also make the content available to users with JavaScript disabled and ensure proper behavior of the browser back button:

<p title="Privacy Policy" onclick="xfetch(event)" x-target=">">
    <a class="xfetch" href="./examples/privacy-policy.html#content" target="_top">Privacy Policy</a>
</p>

Note the targets above. A target or x-target on the p element will be used as the target for the fetch operation. Whereas the target on the a element will be used when JavaScript is not enabled. If you do not provide a target on the container element, it will fall through to the target on the a element. If neither is provided, the default target is for the container is >, i.e. inner content.

Using a hash in the href allows the loaded content to be an entire webpage with header, footer, and other content, but only the content with the id specified in the hash will be loaded into the target element. But, when JavaScript is disabled, the entire page will be loaded and simply scrolled to the target.

Xon (xon.js)

Can be used standalone or with any other trui files.

Comparing trui To Other Libraries

THe most relevant libraries to compare trui to are VanJS and htmx. Neither VanJS nor htmx provide reactive HTML templating like rhtml.js and rhtmlx.js. You could use either one of these libraries in conjunction with just rhtml.js and rhtmlx.js.

rjs.js and xrjs.js vs VanJS

xon.js and xfetch.js vs htmx

Roadmap

Post ideas at https://github.com/anywhichway/trui/issues

License

MIT

Release History (Reverse chronological order)

v0.0.19a 2024-04-17

Fixed issue with xfetch trying to resolve body when rhtml.js is not loaded.

v0.0.18a 2024-04-12

Fix for undefined function dp (Object.defineProperty) in xhtml.js

v0.0.17a 2024-04-11

Added document.resolve() to hide body, resolve document, and unhide body instead of just automatically resolving body.

v0.0.16a 2024-04-11

more handling of nested ${ with HTML as its result value

v0.0.15a 2024-04-11

more handling of nested ${ with HTML as its result value

v0.0.14a 2024-04-11

more handling of nested ${ with HTML as its result value

v0.0.13a 2024-04-11

more handling of nested ${ with HTML as its result value

v0.0.12a 2024-04-11

more handling of nested ${ with HTML as its result value

v0.0.11a 2024-04-11

more handling of nested ${ with HTML as its result value

v0.0.10a 2024-04-11

Addressed issue with properly handling nested ${ with HTML as its result value

v0.0.8a 2024-04-01

v0.0.7a 2024-04-01

v0.0.6a 2024-04-31

v0.0.5a 2024-04-31

v0.0.4a 2024-04-28

v0.0.3a 2024-04-27

v0.0.2a 2024-04-27

v0.0.1a 2024-04-27 Initial release