1more v0.1.18
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
tabindexinstead oftabIndexor string representation ofstyleattribute.
- 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
idattribute will be used nodeidproperty. So it's property-first approach, with fallback to attributes when property not found in node instance. - Assigning
nullorundefinedto 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
disabledor similar boolean attributes to force remove them on gettingfalsevalue. Sometimes using direct property has same effect, for example assigningnode.disabled = falsewill remove the attribute. CSS selectors seemed to use node's actual property values over HTML definition. For all other cases it's better to usenullorundefinedto perform removal.
- Special dynamic attributes:
class/className- accepts only strings. Assigningnull,undefinedor any non-string value will result in removal of this attribute.style- accepts objects with dashed CSS properties names. For examplebackground-colorinstead ofbackgroundColor. Browser prefixes and custom CSS properties are supported. Assigningnullorundefinedtostylewill remove this attribute. Assigningnull,undefinedor 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.innerHTMLis temporarily disabled.
- Event handlers should have name starting with
onand actual event name. For exampleonclickinstead ofonClick. Handlers are not attached to DOM nodes, instead library use automatic event delegation. - For Custom Elements:
- Element should be registered before call to
htmlwith template containing this element. - Property-first approach should work fine, as long as property is exposed in element instance. When assigning
nullorundefinedto element property, it is going to be directly assigned to element, not triggering removal. For attributesnullandundefinedwill 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
shadowRootdirectly, 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.
4 years ago
4 years ago
4 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago