1more
v0.1.18
Published
TBD
Downloads
28
Readme
1more
(One more) R&D project to bring performant DOM rendering to acceptable developer experience using template literals. Works completely in-browser, doesn't require compiler.
Hello world
import { html, render } from "1more";
const Hello = name => html`<div>Hello ${name}</div>`;
render(Hello("World"), document.getElementById("app"));
You can try it on Codesandbox
API Reference
Rendering
html
import { html, render } from "1more";
const world = "World";
const element = html`<div>Hello ${world}</div>`;
render(element, document.body);
Returns TemplateNode, containing given props and compiled HTML template. It does not create real DOM node, it's primary use is for diffing and applying updates during rendering phase. Fragments are supported (template with multiple root nodes).
Properties and Attributes
It's possible to control element properties and attributes, and use event handlers with nodes:
import { html } from "1more";
const on = true;
html`
<div
class=${on ? "turned-on" : null}
onclick=${() => console.log("Clicked")}
></div>
`;
- Static attributes (that does not have dynamic bindings):
- Should be in the same form as in plain HTML code. Library does not perform processing or normalization of static content. For example
tabindex
instead oftabIndex
or string representation ofstyle
attribute.
- Should be in the same form as in plain HTML code. Library does not perform processing or normalization of static content. For example
- Dynamic attributes (that have dynamic bindings):
- Should not have quotes around them;
- Only one binding per attribute can be used, partial attributes are not supported;
- Supports both property and HTML attribute names. For example:
tabIndex
/tabindex
. - No name conversion performed. Names are going to be used exactly as specified in the template.
- If property and its corresponding HTML attribute has same name, values will be assigned to property. For example for
id
attribute will be used nodeid
property. So it's property-first approach, with fallback to attributes when property not found in node instance. - Assigning
null
orundefined
to any property or attribute will result in removal of this attribute. For properties on native elements, library converts property name into corresponding attribute name to perform removal. - There is no behavior around
disabled
or similar boolean attributes to force remove them on gettingfalse
value. Sometimes using direct property has same effect, for example assigningnode.disabled = false
will remove the attribute. CSS selectors seemed to use node's actual property values over HTML definition. For all other cases it's better to usenull
orundefined
to perform removal.
- Special dynamic attributes:
class
/className
- accepts only strings. Assigningnull
,undefined
or any non-string value will result in removal of this attribute.style
- accepts objects with dashed CSS properties names. For examplebackground-color
instead ofbackgroundColor
. Browser prefixes and custom CSS properties are supported. Assigningnull
orundefined
tostyle
will remove this attribute. Assigningnull
,undefined
or empty string to CSS property value will remove it from element's style declaration.defaultValue
/defaultChecked
- can be used to assign corresponding value to node on first mount, and skipping it on updates. Thus it's possible to create uncontrolled form elements.innerHTML
is temporarily disabled.
- Event handlers should have name starting with
on
and actual event name. For exampleonclick
instead ofonClick
. Handlers are not attached to DOM nodes, instead library use automatic event delegation. - For Custom Elements:
- Element should be registered before call to
html
with template containing this element. - Property-first approach should work fine, as long as property is exposed in element instance. When assigning
null
orundefined
to element property, it is going to be directly assigned to element, not triggering removal. For attributesnull
andundefined
will work as usual, removing attribute from element. - Delegated events will work fine from both inside and outside of Shadow DOM content (even in closed mode) and doesn't require for events to be
composed
. Also, system ensures correct event propagation order for slotted content. - It's possible to render to
shadowRoot
directly, without any container element.
- Element should be registered before call to
Children
Valid childrens are: string, number, null, undefined, boolean, TemplateNode, ComponentNode and arrays of any of these types (including nested arrays).
Note: null, undefined, true, false values render nothing, just like in React.
import { html, component } from "1more";
const SomeComponent = component(() => {
return value => {
return html`<div>${value}</div>`;
};
});
// prettier-ignore
html`
<div>
${1}
${"Lorem ipsum"}
${null}
${false && html`<div></div>`}
${SomeComponent("Content")}
${items.map(i => html`<div>${item.label}</div>`)}
</div>
`;
component
import { component, html, render } from "1more";
const App = component(c => {
return ({ text }) => {
return html`<div>${text}</div>`;
};
});
render(App({ text: "Hello" }), document.body);
Creates component factory, that returns ComponentNode when called. Components are needed to use hooks, local state and shouldComponentUpdate optimizations.
Its only argument is rendering callback, that accepts component reference object and returns rendering function. Rendering function accepts provided props and returns any valid children type, including switching return types based on different conditions.
When calling created component function, rendering callback is not invoked immediately. Instead, it's invoked during rendering phase. Outer function going to be executed only once during first render, after that only returned render function will be invoked.
Note: Trying to render component with props object, that referentially equal to the one that was used in previous render, will result in no update. This is shouldComponentUpdate optimization.
render
import { render } from "1more";
render(App(), document.getElementById("app"));
When called first time, render
going to mount HTML document created from component to provided container. After that, on each render
call, it will perform diffing new component structure with previous one and apply updates as necessary. This behavior is similar to React and other virtual dom libraries. Render accepts any valid children types.
key
import { html, key } from "1more";
// prettier-ignore
html`
<div>
${items.map(item => key(item.id, Item(item)))}
</div>
`;
Creates KeyNode with given value inside. These keys are used in nodes reconciliation algorithm, to differentiate nodes from each other and perform proper updates. Valid keys are strings and numbers.
Note: It is possible to use keys with primitive or nullable types if needed. Arrays are not limited to only keyed nodes, it is possible to mix them if necessary. Nodes without keys are going to be updated (or replaced) in place.
import { render, key, html } from "1more";
render(
[
null,
undefined,
key(0, true),
false,
key(1, html`<div>First node</div>`),
html`<div>After</div>`,
],
document.body,
);
invalidate
import { component, html, invalidate } from "1more";
const SomeComponent = component(c => {
let localState = 1;
return () => {
return html`
<button
onclick=${() => {
localState++;
invalidate(c);
}}
>
${localState}
</button>
`;
};
});
invalidate
accepts component reference object and will schedule update of this component. It allows to react on changes locally, without re-rendering the whole app.
On invalidate, component render function will be called and results will be diffed and applied accordingly.
Note: invalidate
does not trigger update immediately. Instead update delayed till the end of current call stack. It allows to schedule multiple updates for different components and ensure that components are re-rendered only once and no unnecessary DOM modifications applied. If updates scheduled for multiple components, they going to be applied in order of depth, i.e. parent going to be re-rendered before its children.
useUnmount
import { component, html, useUnmount } from "1more";
const SomeComponent = component(c => {
useUnmount(c, () => {
console.log("Component unmounted");
});
return () => {
return html`<div>Some</div>`;
};
});
Allows to attach callback, that going to be called before component unmounted.
Contexts
Context API can be used to provide static values for children elements. Context providers are not part of the rendering tree, instead they attached to some host components.
Context API consists of three functions:
createContext(defaultValue)
- creates context configuration that can be used to provide and discover them in the tree.addContext(component, context, value)
- create provider for given context and attach it to the host component.useContext(component, context)
- get value from the closest context provider in the rendered tree or return context's default value.
import {
html,
component,
render,
createContext,
addContext,
useContext,
} from "1more";
const ThemeContext = createContext();
const Child = component(c => {
const theme = useContext(c, ThemeContext);
return () => {
// prettier-ignore
return html`
<div style=${{ color: theme.textColor }}>
Hello world!
</div>
`;
};
});
const App = component(c => {
addContext(c, ThemeContext, { textColor: "#111111" });
return () => {
return Child();
};
});
render(App(), container);
Note: contexts do not support propagating and changing values in them. Since this is one of the main performance problems when using them in React, and it can be solved in a lot of different ways, system defaults to focus only on providing and discovering static values. Reactivity can be achieved by using additional libraries that provide some way to subscribe to changes and putting their "stores" into context provider.
Observables
box
import { box, read, write, subscribe } from "1more/box";
const state = box(1);
read(state); // Returns 1
const unsub = subscribe(value => {
console.log("Current value: ", value);
}, state);
write(2, state); // Logs "Current value: 2"
Complementary primitive observable implementation, used mainly to support library in benchmarks. Tries to have low memory footprint, uses linked-lists to effectively manage subscriptions. Can be used as a cheap state management or as reference for integrating other libraries and writing hooks.
useSubscription
import { component, html } from "1more";
import { box, useSubscription } from "1more/box";
const items = box([]);
const SomeComponent = component(c => {
const getItemsCount = useSubscription(
// Component reference
c,
// Source
items,
// Optional selector
(items, prop) => items.count,
);
return prop => {
return html`<div>${getItemsCount(prop)}</div>`;
};
});
Setup subscription to source observable and returns getter function to read current value. When observable emits new value, it triggers update of the component.
usePropSubscription
import { component, html, render } from "1more";
import { box, read, usePropSubscription } from "1more/box";
const item = {
value: box(""),
};
const Item = component(c => {
const getValue = usePropSubscription(c);
return item => {
return html`<div>${getValue(item.label)}</div>`;
};
});
render(Item(item), document.body);
Allows to consume observable from component props. When receiving observable, it sets up subscription to it.
Delegated events
Rendered DOM nodes don't have attached event listeners. Instead renderer attaches delegated event handlers to rendering root (container argument in render
function) for each event type, then will use rendered app instance to discover target event handler.
Events that have bubbles: true
will be handled in bubble
phase (from bottom to top), with proper handling of stopPropagation
calls.
Events that have bubbles: false
(like focus
event) will be handled in their capture
phase on the target. This should not affect normal usage, but worth keep in mind when debugging.
Note: All event handlers are active (with passive: false
), and system doesn't have built-in support to handle events in capture
phase.
Does this implementation use Virtual DOM?
It is similar to vdom. On each render app generates immutable virtual tree structure that is used to diff against previous tree to calculate changed parts. Comparing to vdom, template nodes handled as one single entity with insertion points. This allows to compact tree structure in memory, separate static parts from dynamic, and as a result speed up diffing phase.
Examples
Credits
- ivi - inspired component and hooks API and a lot of hi-perf optimizations.