npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@scottjarvis/component

v1.3.1

Published

A tiny reactive component library with state management, DOM diffing, tweening, server-side rendering and much more.

Downloads

18

Readme

npm version Node version Build Status bundle size Downloads

Component is a simple, "stateful component" thing.

It lets you create re-usable, "functional components" - basically functions that have a "state".

A "state" is a snapshot of your application data at a specific time.

Features

  • Easy setup, zero dependencies
  • 2.2kb, minified and gzipped
  • Simple syntax, quick to learn, easy to use:
    • plain JavaScript only
    • no compilation or build tools needed
    • no virtual DOM or JSX needed
    • should work with any test suite
  • Works client-side, in browsers:
    • auto re-render on state change
    • good (re)rendering/animation performance at 60fps, using requestAnimationFrame
    • DOM diffing uses real DOM Nodes (not VDOM)
  • Works server-side, in Node:
    • render your components as strings (HTML, stringified JSON)
    • render your components as data (JS objects or JSON)
  • Easy state management:
    • define "actions" to easily update the state in specific ways
    • log all states in a history, for debugging (optional):
      • rewind or fast-forward to any point in the state history
      • save/load current or any previous state as "snapshots"
  • Easy CSS component styling:
    • Automatic "scoping"/prefixing of your component CSS (optional)
    • Re-render styles on component CSS change (optional)
  • Supports "middleware" functions:
    • easily customise a components setState and re-render behaviour
  • Supports nested components
    • embed components in the "views" of other components
    • supports various methods and syntaxes
  • Works with these optional add-ons:
    • validator: validate states against a schema (like a simple PropTypes)
    • html/htmel: simpler, more powerful Template Literals (like a simple JSX)
    • emitter: an event emitter - share updates between components
    • tweenState: animate nicely from one state to the next
    • storage: enables persistent states (between page refreshes, etc)
    • syncTabs: Synchronize state updates & page renders between browser tabs
    • devtools: enables easier component debugging in the browser

Quickstart

Here's some quick examples to demo how it all looks, generally:

"Counter" app

const Counter = new Component({ count: 1 });

const add = num => Counter({ count: Counter.state.count + num })

Counter.view = props => htmel
  `<div>
    <h1>Counter: ${props.count}</h1>
    <button onclick="${e => add(+1)}"> + </button>
    <button onclick="${e => add(-1)}"> - </button>
  </div>`;

Counter.render('.container')

"Todo list" app

const Todo = new Component({ txt: '', list: [ "one" ] });

const setText = e => Todo({ txt: e.target.value });
const addItem = e => Todo({ list: [ ...Todo.state.list, Todo.state.txt ] });

Todo.view = props => htmel
  `<div>
    <h1>Todo</h1>
    <input  onkeyup="${setText}" value="${props.txt}" type="text" />
    <button onclick="${addItem}"> Add Note </button>
    <ul>
      ${props.list.map(i => `<li>${i}</li>`)}
    </ul>
  </div>`;

Todo.render('.container')

A re-usable HTML component:

Unlike the previous two examples, the one below is a function that generates re-usable components - a new component is created and returned each time it's called.

function Header(state) {
  const Header = new Component({ title: "Hello world", ...state });
  Header.view = props => `<h1>${props.title}</h1>`;
  return Header;
}

// And you use it like this:
const header1 = new Header();

// Add it to our page
header1.render('.container');

// Update the state, the heading will re-render for you
header1.setState({ title: "Hello again!" });

// Or set state via the component constructor
header1({ title: "Hello a 3rd time!" });

Nested components

Child components should be regular functions that return part of the view of the parent component:

const Foo = new Component({ title: "Hey!", items: [ "one", "two" ] });

const Header = txt => `<h2>${txt}</h2>`
const List   = i   => `<ul>${i.map(item => `<li>${i}</li>`).join('')}</ul>`

Foo.view = props => 
  `<div id="myapp">
    ${Header(props.title)}
    ${List(props.items)}
  </div>`

But you can also nest proper (stateful) components inside other components, too:

// create a re-usable button component
function Button(state) {
  const Button = new Component({ ...state });
  Button.view = props => html`<button onclick="${props.fn}">${props.txt}</button>`;
  return Button;
}

// create 3 buttons from the re-usable component
const btn1 = new Button({ txt: "1", fn: e => alert("btn1") });
const btn2 = new Button({ txt: "2", fn: e => alert("btn2") });
const btn3 = new Button({ txt: "3", fn: e => alert("btn3") });

// create the main (parent) component
const Menu = new Component({ txt: 'Click the buttons!' });

// create a view with our buttons included:
Menu.view = props => htmel`
  <div>
    <h2>${props.txt}</h2>
    ${btn1}
    ${btn2}
    ${btn3}
  </div>
`;

// add our main/parent component to page
Menu.render('.container');

See more short recipes in examples/recipes.js.


Installation

In browsers:

<script src="https://unpkg.com/@scottjarvis/component"></script>
<script>
  // use it here
</script>

In NodeJS:

npm i @scottjarvis/component

Then add it to your project:

ES6 Modules:

import { Component } from '@scottjarvis/component'

// use it here

Using NodeJS require():

var { Component } = require('@scottjarvis/component');

// use it here

See each add-on module (validator, html, htmel, emitter, storage and tweenState) for their respective installation instructions.


Usage

In browsers

Create interactive components for your web pages:

In NodeJS

Or you can do "server-side rendering" (SSR) of your components:

See examples/usage-in-node.js

Or run it in your terminal:

node examples/usage-in-node.js

Component API overview

These are the methods and properties attached to the components you create.

Methods:

  • .setState(obj): update the component state, triggers a re-render
  • .render(el): (re)render to the given element on state change (browser)
  • .toString(): render your component as a string on state change (NodeJS)
  • .view(props): receives a state and sets the component view to (re)render (optional)
  • .style(props): receives a state and sets the <style> to (re)render (optional)
  • .actions(obj): chainable methods that simplify updating the state (optional)
  • .tweenState(obj[, cfg]): set state on each frame, supports various easings (optional)
  • .middleware: an array of functions that run at the end of setState() (optional)
  • ...and more

Properties:

  • .state: an object, contains your app data, read-only - cannot be modified directly
  • .schema: an object against which to validate your component state (optional)
  • .container: the HTML Element into which the components view is rendered (optional)
  • .html: alias of .container
  • .css: the <style> Element which holds the component styles (optional)
  • .uid: a unique string, generated once, on creation of a component
  • .log: an array containing a history of all component states
  • ...and more

Settings:

  • .reactive: if false, disables auto re-rendering on state change
  • .scopedCss: if false, disables auto-prefixing .style() CSS with the class .${uid}
  • .debug: if true, a record of states changes are kept in .log

Advanced usage

Using "state validation"

You can validate your component state against a schema, before you set it or render anything.

If the data to set doesn't match a components state schema, an error will be thrown.

This is a similar concept to propTypes in React, but a bit simpler.

First, you must install a tiny (~300 bytes) additional dependency:

npm i @scottjarvis/validator

Then enable it:

var { Component } = require("@scottjarvis/component")
Component.validator = require("@scottjarvis/validator")

Define a schema object. For each property included, the value should be:

  • a typeof type name, as a string
  • or a validator function, that returns true or false

Then create your component, passing in the schema as the second parameter to Component().

function Foo(state) {
  const defaults = { 
    count: 0, 
    age: 20 
    items: [ "one", "two" ],
    foo: {
      bar: "whatever"
    }
  };

  const schema = { 
    count: "number",
    age: age => typeof age === "number" && age > 17,
    items: "array",
    foo: {
      bar: "string"
    }
  };

  // pass in the schema as the 2nd param
  const Foo = new Component({ ...defaults, ...state }, schema);

  return Foo;
}

If you try to set an invalid state, your component will throw an error:

const foo = new Foo();
foo.setState({ count: "a string" }) // this will throw an Error!

See @scottjarvis/validator for more usage info.

Using "middleware"

When you call myComponent.setState(), the page is re-rendered (in browser) and a history of state updates are kept.

Using "middleware" functions you can extend this behaviour further, and make other stuff happen at the end of the setState() method.

Middleware functions can be re-used across lots of components and, unlike "actions", are not tied to one component.

Here's how to use "middleware" functions to customise your components setState() behaviour:

  1. Define some "middleware" functions - these will be called at the end of setState()
  2. Add your "middleware" to a component as an array of functions:
// Define the middleware functions
const countLog = props => console.log("middleware -> count = ", props.count)

// Define a component that uses middleware
function Foo() {

  const Foo = new Component({ count: 0 })

  Foo.view = props => `<p>${props.count}</p>`

  // Add the middleware to your component
  Foo.middleware = [countLog]

  return Foo;
}

// ..let's use our component with middleware

const foo = new Foo();
foo.setState({ count: 1 }) // will run the middleware

In the above example, every time foo.setState({ ... }) is called, the countLog function will be called at the end of setState().

Note that your middleware functions receive the latest state of the host component, as props.

See examples/usage-in-node.js for the complete example.

Using the "state history"

Here is how to "time travel" to previous states, or jump forward to more recent ones.

Note: To enable the state history, app.debug must be true.

const foo = new Foo();

// enable logging of state history
foo.debug = true

// Take a "snapshot" (we'll use it later)
var snapshot = foo.state

// ...later

foo.rw()         // go to initial state
foo.ff()         // go to latest state
foo.rw(2)        // rewind two steps to a previous state
foo.ff(2)        // fast-forward two steps to a more current state

// Set a previous state
foo.setState(foo.log[0].state)

// Set a "named" state, from a previous point in time
foo.setState(snapshot)

Styling your component

Use Foo.style() to define some styles for your components view (optional):

function Foo(state, schema) {

  // ... 

  Foo.style = (props) => `
    #myapp {
      border: 2px solid ${props.borderColor || 'red'};
      margin: 0 auto;
      max-width: ${props.maxWidth};
    }
    .btn {
      background-color: ${props.btnColor || 'red'};
      padding: 6px;
    }`;

  return Foo;
}

If a component is added to a page with foo.render('.container'), the CSS is prefixed with the id or className of its container.

This CSS "auto-scoping" will prevent a components styles affecting other parts on the page.

It also keeps your component CSS clean - no need to prefix anything with a unique ID or class yourself.

If your container has no class or id attributes, then a unique string, foo.uid, will be used instead.

You can disable automatic CSS "scoping"/prefixing by using foo.scopedCss = false.

When rendering your component in NodeJS, or using toString(), your CSS will not be auto prefixed.

To see style() in use, see examples/usage-in-browser.html

Using "actions"

Define "actions" to update your state in specific ways.

These are like regular methods, except they're always chainable, they hook into the emitter add-on automatically, and they're tagged by name in your components state history.

function Foo(state, schema) {
  const Foo = new Component({ count: 0, items: [] });
  // define the actions
  Foo.actions({
    update:     props => Foo({ props }), // same as calling Foo.setState()
    plus:       props => Foo({ count: Foo.state.count + props }),
    minus:      props => Foo({ count: Foo.state.count - props }),
    addItems:   props => Foo({ items: [ ...Foo.state.items, ...props ] }),
  });
  return Foo;
}

// ...later

const foo = new Foo();

// use "actions" to update specific parts of your state
foo.plus(105);
foo.minus(5);

// A components "actions" can be chained
foo.minus(1)
   .minus(1)
   .minus(1)
   .plus(3)
   .addItems([ { name: "one" }, { name: "two" } ]);

Using the add-on emitter module, components can listen for and react to these actions. This is an easy way to share states between components, and for components to "talk to each other".

Using the emitter module

Any time a components state is changed via an "action", it can emit an event that other components can listen for.

To achieve this, just include the emitter like so:

In browsers:

<script src="https://unpkg.com/@scottjarvis/component"></script>
<script src="https://unpkg.com/@scottjarvis/component/dist/emitter.min.js"></script>
<script>
  Component.emitter = emitter

  // use it here
</script>

In NodeJS:


var { Component, emitter } = require("@scottjarvis/component");
Component.emitter = emitter;

The emitter provides the following methods:

  • foo.on("actionName", props => { ... }) - every time actionName is emitted, run the given function
  • foo.once("actionName", props => { ... }) - run the given function only once
  • foo.off("actionName") - stop listening to actionName

Note, props is the latest state of the component that emitted the event.

Here's how to use the emitter:

// Define a component to listen to

function Foo(state) {
  const defaults = {
    count: 0,
    items: [{ name: "one" }]
  }
  const Foo = new Component({ ...defaults, ...state })

  // Define chainable "actions" that we can listen to
  Foo.actions({
    plus:     props => Foo.setState({ count: Foo.state.count + props }),
    minus:    props => Foo.setState({ count: Foo.state.count - props }),
    addItems: props => Foo.setState({ items: [...Foo.state.items, ...props] })
  })

  return Foo;
}

Now let's "listen" to foo using another component, called bar:

// Define some other component
const bar = new Component({})

// Define "listeners" for the actions above:
bar
  .once("minus",  props => console.log("Bar: 'minus'",    props.count))
  .on("plus",     props => console.log("Bar: 'plus'",     props.count))
  .on("addItems", props => console.log("Bar: 'addItems'", props.items))

// ...now we're ready to run the program..

const foo = new Foo();

// these actions will trigger Bar
foo.plus(105)
foo.minus(5)

// stop listening to the "plus" action, keep listening to others..
bar.off("plus")

foo.minus(1)
  .minus(1)
  .plus(3)
  .addItems([{ name: "two" }, { name: "three" }])

Also see examples/usage-emitter.js

Using your own Event Listeners

To add your own Event Listeners, you should add them to the container of your components:

// render a component into the page to get its HTML
foo.render('.container');

// now we have the `.html` property on our component, we can use it
foo.html.addEventListener("click", e => {
    // get the element clicked
    const el = e.target.className;
    // work out what to do next
    if (el === "foo") {
      // ...
    } else if (el === "bar") {
      // ...
    }
});

Using the storage module

Use the storage module to make your components remember their state between page refreshes and sessions, using localStorage.

Note that storage can be polyfilled for NodeJS, so will work in Node too - by saving to JSON files.

In NodeJS, the state persists between script invocations, rather than page refreshes.

To use the storage add-on, include it in your project like so:

In browsers:

<script src="https://unpkg.com/@scottjarvis/component"></script>
<script src="https://unpkg.com/@scottjarvis/component/dist/storage.min.js"></script>
<script>
  Component.storage = storage

  // use it here
</script>

In NodeJS:

var { Component, storage } = require('@scottjarvis/component');
Component.storage = storage

// use it here

To enable persistent storage for a component, just define a store name (where to save your data) as myComponent.store = "something".

In a browser, this is how you add persistent storage to our Counter app:

const Counter = new Component({ count: 1 });

const add = num => Counter({ count: Counter.state.count + num })

Counter.view = props => htmel`
  <div>
    <h1>Counter: ${props.count}</h1>
    <button onclick="${e => add(+1)}"> + </button>
    <button onclick="${e => add(-1)}"> - </button>
  </div>`;

// simply define a "store name" (where to save your data) before you render
Counter.store = 'Counter';

// now we can render it into the page - this will load in the persistent state 
// from its store as the initial state for the component
Counter.render('.container')

In NodeJS, this is how you add persistent storage to our Counter app:

  • install the NodeJS localStorage polyfill: npm i node-localstorage -D
  • run your scripts with the -r node-localstorage/register option to enable it
  • the "local storage" used will be a local folder/file, called ./scratch/<store name>

Example command, running a component with a persistent state in NodeJS:

node -r node-localstorage/register examples/usage-persistant-state.js

See examples/usage-persistant-state.js for more info.

Using the syncTabs module

The syncTabs add-on uses localStorage and only works in browsers. It requires the storage add-on.

How to use it:

In browsers:

<script src="https://unpkg.com/@scottjarvis/component"></script>
<script src="https://unpkg.com/@scottjarvis/component/dist/storage.min.js"></script>
<script src="https://unpkg.com/@scottjarvis/component/dist/syncTabs.min.js"></script>
<script>
  Component.storage = storage
  Component.syncTabs = syncTabs

  // your components will now have a persistent state, that is synchronized 
  // across multiple browser tabs - an update in one tab will update the others
</script>

Using the tweenState module

With tweenState it's super easy to do animations that use requestAnimationFrame and DOM diffing.

Using tweenState() is much like using setState(), except:

  • you only pass in the state values you want to tween
  • you can pass in tween settings as a 2nd param (delay, duration, easing, callbacks)

How it works:

  • the tweened state values are passed to setState() on each frame (or whenever you choose, if using the shouldSetState() callback)
  • the state you passed in will be passed to setState() on the final frame

By default, tweenState() calls setState() on every frame of the tweened animation. You can override this behaviour by defining a shouldSetState() callback in your tween config, which is called on every frame - setState() will only be called on that frame if shouldSetState() returns true.

To use tweenState, import it along with Component, like so:

In browsers:

<script src="https://unpkg.com/@scottjarvis/component"></script>
<script src="https://unpkg.com/@scottjarvis/component/dist/tweenState.min.js"></script>
<script>
  Component.tweenState = tweenState

  // use it here
</script>

In NodeJS:

Note that tweenState includes polyfills for NodeJS, so works in Node too.

var { Component, tweenState } = require('@scottjarvis/component');
Component.tweenState = tweenState

// use it here

Example usage of tweenState:

// Define our app state
var state = {
  ignore: "me",
  count: 199,
  foo: 1,
  dontTween: "this property",
  bar: {
    zzz: 100
  }
}

// Define a stateful main component
var App = new Component(state)

// Tween (animate) from one state to the next:
// Give the state to tween to, with some tween options as the (optional) 2nd param

App.tweenState(
  // 1st param - object - the new state values to tween to...
  // pass in only the properties that you want to tween!
  { count: 200, foo: 10, bar: { zzz: 999 } },
  // 2nd param - object - the tween settings
  {
    delay: 0,
    duration: 500,
    ease: "linear",
    paused: false,
    // called on first frame:
    onStart: tweenProps => tweenProps,
    // called on every frame:
    onUpdate: tweenProps => tweenProps,
    // called on last frame:
    onComplete: tweenProps => tweenProps,
    // called on every frame, choose to set state or not (must return true or false)
    shouldSetState: tweenProps => tweenProps.frame % 2 > 0, // for example, this will set state only on odd frame numbers
    // called only on the frames where the state was updated:
    onSetState: tweenProps => tweenProps
  }
)

The tween config (2nd param) takes the following properties:

  • delay in milliseconds (default: 0)
  • duration in milliseconds (default: 0)
  • ease - the name of a timing function (default: linear, see src/easings.js for the full list)
  • paused - true or false (default: false)
  • shouldSetState() - called on every frame, receives tweenProps, should return true or false
  • onSetState() - called only on frames where the state is updated, receives tweenProps
  • onStart() called on the first frame, receives tweenProps
  • onUpdate() called on every frame, receives tweenProps on each frame
  • onComplete() called after last frame, receives final tweenProps

The tweenProps object returned to callbacks provides the tweening values of the current frame, and includes:

  • progress - a number from 0 to 1 (so 0.5 is half-way through the tween)
  • frame - the current frame number
  • frameTotal - the total number of frames
  • values - an array of the tweened values
  • tweenedState - the state of your component on the current frame
  • and more...

Also see examples/usage-tweenState.js

Using html and htmel modules for easier HTML templating

To make it easier to build a good HTML "view" for your components, there are two optional add-on functions which provide a nicer way to write HTML in JavaScript "Template literals".

These return your components view as either a String or HTML Object, but are otherwise mostly inter-changeable:

  • html (~650 bytes) - returns your template as a String.
  • htmel (~800 bytes) - returns your template as an HTML Object (browser) or String (NodeJS).

Both html and htmel can be used standalone (without Component) for general HTML templating.

// Example of using `html` or `htmel` standalone, without any `Component` stuff:

const foo = "Hello world"

const str = html`<h1>${foo}</h1>` // `html`  returns a string
const el = htmel`<h1>${foo}</h1>` // `htmel` returns a DOM Node

// ..or in functions that return pre-defined HTML snippets from templates:

const para = text => htmel`<p>${text}</p>`

const list = array => htmel`<ul>${array.map(text => `<li>${text}</li>`)}</ul>`

// now generate the DOM elements
const p  = para("Put me in a paragraph.")
const ul = list([ "one", "two", "three" ])

To use html or htmel (or both), import them [along with Component], like so:

In browsers:

<script src="https://unpkg.com/@scottjarvis/component"></script>
<script src="https://unpkg.com/@scottjarvis/component/dist/html.min.js"></script>
<script src="https://unpkg.com/@scottjarvis/component/dist/htmel.min.js"></script>
<script>
  // use them here
</script>

In NodeJS:

var { Component, html, htmel } = require('@scottjarvis/component');

// use them here

Features of both html and htmel:

// embed JS object properties as valid HTML attributes  (ignores nested/child objects)
html`<p ${someObj}>some text</p>`
// embed JS object properties as CSS (ignores nested/child objects)
html`<p style="${someObj}">some text</p>`
// embed JS objects as JSON in HTML data attributes
html`<p data-json='${someObj}'>some text</p>`
// embed real DOM objects 
const elem = document.querySelector(".foo");
const elems = document.querySelectorAll(".bar");
html`<div>${elem}${elems}</div>` 
// embeds arrays properly, no need to use .join('')
html`<ul>${list.map(i => `<li>${i}</li>`)}</ul>`
// hides Falsey values, instead of printing "false", etc
html`<span>some ${foo && `<b>cool</b>`} text</span>`
// nested templates
var TableRows = props => props.map(row =>
  html`<tr>
    ${row.map((item, i) => `<td>${row[i]}</td>`)}
  </tr>`);

var Table = props => html`
  <table>
    <tbody>
      ${TableRows(props.data)}
    </tbody>
  </table>`;

Features of htmel:

In a browser, you can use htmel instead of html to return DOM Nodes (instead of strings):

// returns a text node
htmel`I’m simply text.`
// returns an HTML node
htmel`<p>I’m text in an element.</p>`
// supports HTML fragments
htmel`<td>foo</td>`

If using htmel in a browser, you can also embed functions as event attributes - they'll be attached to the relevant HTML Elements as proper Event listeners:

// embed functions - they'll be attached as Event listener methods
htmel`<p onclick="${e => console.log(e.target)}">some text</p>`

Example usage of htmel with Component:

// Let's define a component with a view, using `htmel`
function Foo(state, schema) {

  const defaults = {
    css: {
      "border": "2px solid red",
      list: {
        "padding": "8px",
      }
    },
    attrs: {
      "class": "foo bar",
      "z-index": 2,
    },
    text: "My Title:",
    list: [
      "one",
      "two",
    ],
    status: false,
    someFunc: function(event) { console.log(event); }
  };

  const Foo = new Component({ ...defaults, ...state }, schema);

  // now let's use `htmel` to construct an HTML view..
  Foo.view = props => htmel`
    <div style="${props.css}" ${props.attrs}>
      <h2>${props.title}</p>
      <p>${props.text}</p>
      <ul style="${props.css.list}">
        ${props.list.map(val => `<li>${val}</li>`)}
      </ul>
      ${props.status && `<p>${props.status}</p>`}
      <button onclick="${props.someFunc}">Click me</button>
    </div>
  `;

  return Foo;
} 
  

Using JSON-LD (linked data)

Adding linked data to your components is easy - just define it as part of your view:

  Foo.view = props => `
    <div>
      <script type="application/ld+json">{ ... }</script>
      ...
    </div>`
  • add a JSON-LD script before your component HTML
  • use the props passed in to define/update whatever you need
  • your JSON-LD will be updated along with your view, whenever your component re-renders

Using "nested components"

Components that are nested inside other components are called child components.

There are two kinds of child component - stateless and stateful - and while they behave the same in most ways, they have slightly difference syntax and features.

All child components have the following in common:

  • you include the child component in the "view" of the parent component
  • child components do not trigger a re-render of the page
  • to re-render a child component that has changed, you must update the parent component
  • nested components work with or without the html/htmel add-on(s)

About "stateless" child components:

Stateless components are just regular functions that take props as input, and return a view - usually HTML as a string.

// a stateless child component is just a function that receives `props`, and returns a view
const h2 = text => `<h2>${text}</h2>`;

// ...used inside the view of another component:
Foo.view = props => `
  <div>
    ${h2(props.text)}
    <p> ... </p>
  </div>
`;

About stateful child components

Stateful components are any components with a state, usually created like so:

const Foo = new Component({ ...someData });

NOTE: When nested inside another component, even stateful components do not run setState() & render() - they simply return their view, just like stateless child components.

This has a number of implications:

  • better performance (fewer page re-renders)
  • enforces similar behaviour to stateless child components
    • only parent components trigger page re-renders
  • nested components have an undefined .container property
    • therefore calling the render() method of a child component (usually) does nothing
  • calling setState() of a child component will update its state and run its "middleware", but doesn't re-render

For code examples, see the nested component recipes in examples/recipes.js.

Using the devtools module

The optional devtools add-on provides a nice UI for inspecting and even editing your components directly in the page.

The devtools only works in the browser.

In browsers:

<script src="https://unpkg.com/@scottjarvis/component"></script>
<script src="https://unpkg.com/@scottjarvis/component/dist/devtools.min.js"></script>
<script>
  Component.devtools = devtools
  devtools.init();
</script>
<script>
// ..now define or import your components
</script>

It's a bit like React devtools but simpler:

  • Main Toolbar:
    • Choose component: click "Select component" button, then choose a component on the page
    • Choose layout: can switch between bottom (horizontal) view, and side (vertical) view
  • Overview tab:check a components current state and HTML
  • Details tab: view other info about the selected component
  • History tab: cycle through a components state history
  • Editor tab: edit a components view and styling

Contains modified versions of:

Devtools screenshots:

Vertical view:

Devtools - vertical view

Horizontal view:

Devtools - horizontal view

Server side rendering

If running a NodeJS server, you can render the components as HTML strings or JSON.

Just define a view - a function which receives the state as props and returns the view as a string, object, etc.

// create an HTML view, using template literals
var htmlView = props => `
    <div id=${props.id}>
      ${Heading("Total so far = " + props.count)}
      ${List(props.items)}
      ${Button("Click here", `App.clickBtn(${props.incrementBy})`)}
    </div>`

// or return the state itself (pure headless component)
var dataOnlyView = props => props

// Choose a view to render
App.view = htmlView

// render the component
App.render()

// ..other rendering options...

// print it to the terminal
console.log(App.render())

If rendering a component in NodeJS that has a .view() and .style(), or if calling .toString() directly, the output will be a string like this one:

"<style>
#myapp {
  border: 2px solid grey;
  margin: 0 auto;
  max-width: 360px;
}
.btn {
  background-color: black;
  color: white;
  padding: 6px;
}
</style>
<div id=\"foo-id\">
  <h1>Total so far = 101</h1>
  <ul><li>First</li><li>two</li><li>three</li></ul>
  <button class=\"btn\" onclick=App.clickBtn(5)>Click here</button>
</div>"

^ Any styles are wrapped in a <style> tag, and your view is rendered after that.

Note: your component CSS is not auto-prefixed or "scoped" with containers class/id until/unless it's added to a container element, client-side, using .render('.container').

Changelog

1.3.1

  • build a "tree-shakable" ES Module too:
    • added new file src/index.esm.js
    • updated the rollup configs
    • npm run build now also creates dist/index.esm.js
    • added a "module" field in package.json, that points to dist/index.esm.js

1.3.0

  • new optional add-ons:
    • html JSX-like HTML templating, returns templates as a String
    • htmel JSX-like HTML templating, returns templates as HTMLObject (browser) or String (Node)
    • storage - persistent state between page refreshes (browser) or script invocations (Node)
    • devtools - easier debugging of your components (browser only)
  • updates to src/component.js:
    • fixed: in NodeJS, debounced logging now falls back to using setTimeout, if needed
    • added: support for all new add-ons
    • added: allow view to be HTML Object, not only String
    • added: App.actionsList property - the list of defined actions functions
    • added: App() will return the container (an HTML Element)
      • App({...}) still returns App (for chainable actions, etc)
    • added: App.html property - alias of App.container (returns an HTML Element)
  • update docs:
    • re-write README and examples to show re-usable components
    • add the new add-ons to README and examples
  • updated build configs

1.2.0

  • doing Foo = new Component(someState) now returns a function, not an object
  • the function returned calls setState in its constructor
    • this means you can now set state using a new syntax - via the constructor: Foo({ count: 1 })
    • this in turns allows for nicer nested stateful components:
      • nested stateful components can now use the same syntax as stateless (plain function) components
  • update in tweenState: allow shouldSetState to be boolean (not only func that returns boolean)
  • updated examples, README, package.json, dist/, etc

1.1.12

  • new feature: state validation
    • simply pass a schema as the 2nd param when creating a new component
    • then call setState() as usual - Component will throw an error if the state isn't valid
    • requires @scottjarvis/validator to be installed (~330 bytes minified & gzipped)
    • see @scottjarvis/validator for more usage info
  • updated README

1.1.11

  • updated rollup deps and rebuilt in dist/
  • nothing else

1.1.10

  • better rendering performance: debounce the render() function:
    • setState() can be called 1000s of times a second
    • setState() also calls the render() function
    • render() will only update the DOM at 60fps
  • see https://gomakethings.com/debouncing-your-javascript-events/
  • updated README

1.1.9

  • new feature: "middleware"
    • define an array of functions as myComponent.middleware = [ someFunc, otherFunc ]
    • each function will be run at the end of setState()
  • updated README, package.json, etc

1.1.8

  • fixes in package.json

1.1.7

  • new feature: added an event emitter
    • if emitter is installed, component "actions" will emit an event
    • other components can listen to it with myComponent.on('actionName', (props) => { ... })
      • props will contain the latest state of the component that emitted the event
  • added src/emitter.js, implemented as an optional, extra module
  • updated build process to also build dist/emitter.min.js
  • added examples and updated README
    • added examples/usage-emitter.js

1.1.6

  • added src/tweenState.js and related support files (src/raf.js, src/easings.js)
  • new build process:
    • added rollup to bundle the source files into dist/ (see rollup.config.js)
    • added src/index.js to allow easier importing of multiple modules, if desired
  • updated package.json: index.js is the file imported in Node by default
  • now requires Node 10 or later (was Node 8)
  • updated README
    • updated install instructions
    • added tweenState usage info

1.1.5

  • fixed: scoped CSS sometimes not applied on page load:
    • here is the new implementation:
      • prefix component CSS with the containers existing id or class, if any
      • fall back to previous behaviour only if container has no id or class:
        • add unique class to container
        • prefix component CSS with that same unique class
  • better performance:
    • when debug is true, don't console log state history on state change
    • to see the state history, access App.log yourself instead
  • see examples/usage-in-browser.html

1.1.4

  • new: automatic "scoping" of component CSS
  • prevents component styles affecting other parts of page
  • simplifies writing CSS for your components:
    • removes the need to define unique namespaces (IDs, classes) in your component CSS
  • you can disable automatic CSS scoping like so: App.scopedCss = false
  • see examples/usage-in-browser.html
  • README and example updates

1.1.3

  • better performance:
    • only access/update the DOM from inside a requestAnimationFrame
    • only record state history if .debug is true
    • set .debug to false by default
  • improved README:
    • added info about component settings (reactive, debug)

1.1.2

  • fix: can re-render component to new container
  • fix: can render multiple components to page
  • fix: can call setstate() before defining .view()
  • fix: can call render() before defining .view() (pointless for now)
  • fix: don't attempt any styling if .style() not defined

1.1.1

  • README fixes

1.1.0

  • improved performance:
    • added DOM diffing, using BAD-DOM
    • only re-render .view() if needed
    • only re-render <style> if needed
  • smaller filesize:
    • smaller method names:
      • .forward() => .ff()
      • .rewind() => .rw()
      • .history => .log
    • removed:
      • .undo() and .redo() (use .rw(1), and .ff(1))
  • better indentation for server-side rendering
  • minify CSS added to components <style> tag
  • updated examples
  • updated README

1.0.0

  • initial release

Making changes to Component

Look in src/, make any changes you like.

Rebuild to dist/ using the command npm run build

Future improvements

  • Store manager

    • like redux, storeon, etc
  • Better SSR

    • an toEnvelope() add-on method, to render components as JSON envelopes:
      • render as JSON
      • include view, actions & style as stringified HTML, CSS and JS
    • pass in config to toString(), to choose what to render:
      • Component lib itself
      • actions
      • methods
      • styles
      • views
    • client-side:
      • ability to "hydrate" or takeover a view that already exists in the page:
        • simply don't define a view, and call App.render('.container'):
          • any id or class attributes will become items in App.state
          • the contents will be grabbed and used for a new .view()
        • for (re)attaching events, see yo-yo
  • Scroll-based re-rendering/animation:

    • create a Component.scroll({ ... }) add-on module:
      • hooks setState into scroll progress values for components container
      • see sc0ttj/scrollstory
        • e.g. progress = data => App.tweenState({ width: ${data.progress * 100}% })
  • Better animations:

    • create a physics based timing functions module:
      • like current easings, but more flexible/dynamic
      • can pass params like friction, magnitude, etc, to make the anims more/less "pronounced"
      • see react-motion, react-spring, react-move, pose, etc
      • maybe do a Component.motion add-on that attaches extra info to component state:
        • component positioning and motion info:
          • (x,y, in view or not, scroll speed & direction, accel, current momentum, etc)
          • is inside/outside other component/element
          • collision detection
  • Universal rendering (add-ons):

  • Support for custom elements/Web Components

Related projects:

DOM and DOM diffing

  • BAD-DOM - a tiny (800 bytes) lazy DOM diffing function (used by this project)
  • set-dom - tiny dom diffing library
  • morphdom - a nice, fast DOM differ (not vdom, real DOM)

JSX-like syntax in Template Literals (alternatives to html/htmel)

  • developit/htm - JSX-like syntax in ES6 templates, generates vdom from Template Literals
  • htl- by Mike Bostock, events, attr/styles as object, other syntactic sugar, 2kb
  • zspecza/common-tags: html function - makes it easier to write properly indented HTML in your templates

Template strings to real DOM nodes

  • htl- by Mike Bostock, events, attr/styles as object, other syntactic sugar, 2kb
  • developit/htm - JSX-like syntax in ES6 templates, generates vdom from Template Literals
  • fast-html-parser - generate a simplified DOM tree from string, with basic element querying
  • domify-template-strings - get real DOM nodes from HTML template strings, browser only, no SSR
  • genel - create real DOM nodes from template string, browser only (no SSR), 639 bytes

Template strings to VDOM and VDOM diffing

  • snabby - use HTML template strings to generate vdom, use with snabbdom
  • snabbdom - the vdom diffing library used by Vue.js
  • petit-dom - tiny vdom diffing and patching library
  • hyperx - tagged templates to vdom (used by nanohtml

CSS in JS

  • twirl - a tag for template literals, turns CSS into objects
  • /(^|,)[\.#a-z][a-zA-Z0-9_\-:]*/gm - match first part of CSS selector in a CSS string, useful for scoping (see https://regexr.com/524pu)
  • document.styleSheets[0].cssRules[0].style.cssText.split(/\;[\s.]/).slice(0,-1) - convert cssText to array containing CSS proprties and values, like "margin:0 auto;"
  • str.replace(/([A-Z])/g, "-$1").toLowerCase() - camelCase to hyphen-case converter
  • var cssClassMatchRegex = new RegExp(/\.(-?[_a-zA-Z]+[_a-zA-Z0-9-]*)(?![^\{]*\})/g) - regex to match all classes in a string (maybe useful for "scoping")
  • Math.random().toString(36).split(".")[1] - unique ID generator (create a unique id for each new component)
  • cssString.replace(/\n/g, '').replace(/\s\s+/g, ' ') - minify string of CSS

Animation

  • react-tween-state - tween from one state to another (where I got tweenState idea from)
  • phena - a petit tweening engine based on requestAnimationFrame (adapted version inside src/tweenState.js)
  • easing functions - excellent set of easing functions (used by this project in src/easings.js)
  • react-tweenful - tweening and animation for React
  • react-state-stream - instead of one state, set all the states that will ever be, aka a lazy state stream

Routers

  • router - a tiny, isomorphic router & web server, supports express middleware, runs in browsers, lambdas, NodeJS.

Other tiny component libraries

  • yo-yo - tiny UI library, DOM diffing (uses morphdom), Event handling, ES6 tagged template literals
  • choojs - A 4kb framework for creating sturdy frontend applications, uses yo-yo
  • hyperapp - The tiny framework for building web interfaces, React-like, uses h(), VDOM, etc
  • preact - a 3.1kb alternative to React.js
  • nanohtml - 8kb, uses vdom, can attach event listers, can do SSR

Further reading