0.1.18 • Published 3 years ago

1more v0.1.18

Weekly downloads
-
License
MIT
Repository
-
Last release
3 years ago

1more

Size

(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 of tabIndex or string representation of style attribute.
  • 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 node id property. So it's property-first approach, with fallback to attributes when property not found in node instance.
    • Assigning null or undefined 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 getting false value. Sometimes using direct property has same effect, for example assigning node.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 use null or undefined to perform removal.
  • Special dynamic attributes:
    • class / className - accepts only strings. Assigning null, undefined or any non-string value will result in removal of this attribute.
    • style - accepts objects with dashed CSS properties names. For example background-color instead of backgroundColor. Browser prefixes and custom CSS properties are supported. Assigning null or undefined to style will remove this attribute. Assigning null, 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 example onclick instead of onClick. 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 or undefined to element property, it is going to be directly assigned to element, not triggering removal. For attributes null and undefined 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.
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.
0.1.17

3 years ago

0.1.18

3 years ago

0.1.16

3 years ago

0.1.15

3 years ago

0.1.14

3 years ago

0.1.13

3 years ago

0.1.12

3 years ago

0.1.11

3 years ago

0.1.10

3 years ago

0.1.9

3 years ago

0.1.8

3 years ago

0.1.7

3 years ago

0.1.6

3 years ago

0.1.5

3 years ago

0.1.4

3 years ago

0.1.3

3 years ago

0.1.2

3 years ago

0.1.1

3 years ago

0.1.0

3 years ago

0.0.3

3 years ago

0.0.2

3 years ago

0.0.1

3 years ago