Git Product home page Git Product logo

innerself's Introduction

innerself

A tiny view + state management solution using innerHTML. Live demo.

innerHTML is fast. It's not fast enough if you're a Fortune 500 company or even if your app has more than just a handful of views. But it might be just fast enough for you if you care about code size.

I wrote innerself because I needed to make sense of the UI for a game I wrote for the js13kGames jam. The whole game had to fit into 13KB. I needed something extremely small which would not make me lose sanity. innerself clocks in at under 40 lines of code. That's around 400 bytes minified, ~250 gzipped.

innerself is inspired by React and Redux. It offers the following familiar concepts:

  • composable components,
  • a single store,
  • a dispatch function,
  • reducers,
  • and even an optional logging middleware for debugging!

It does all of this by serializing your component tree to a string and assigning it to innerHTML of a root element. I know this sounds like I'm crazy but it actually works quite nice for small and simple UIs.

If you don't care about size constraints, innerself might not be for you. Real frameworks like React have much more to offer, don't sacrifice ease of use nor performance, and you probably won't notice their size footprint.

innerself was a fun weekend project for me. Let me know what you think!

Install

$ npm install innerself

Caveats

You need to know a few things before you jump right in. innerself is a poor choice for form-heavy UIs. It tries to avoid unnecesary re-renders, but they still happen if the DOM needs even a tiniest update. Your form elements will keep losing focus because every re-render is essentially a new assignment to the root element's innerHTML.

When dealing with user input in serious scenarios, any use of innerHTML requires sanitization. innerself doesn't do anything to protect you or your users from XSS attacks. If you allow keyboard input or display data fetched from a database, please take special care to secure your app. The innerself/sanitize module provides a rudimentary sanitization function.

Perhaps the best use-case for innerself are simple mouse-only UIs with no keyboard input at all :)

Usage

innerself expects you to build a serialized version of your DOM which will then be assigned to innerHTML of a root element. The html helper allows you to easily interpolate Arrays.

import html from "innerself";
import ActiveTask from "./ActiveTask";

export default function ActiveList(tasks) {
    return html`
        <h2>My Active Tasks</h2>
        <ul>
            ${tasks.map(ActiveTask)}
        </ul>
    `;
}

The state of your app lives in a store, which you create by passing the reducer function to createStore:

const { attach, connect, dispatch } = createStore(reducer);
window.dispatch = dispatch;
export { attach, connect };

You need to make dispatch available globally in one way or another. You can rename it, namespace it or put it on a DOM Element. The reason why it needs to be global is that the entire structure of your app must be serializable to string at all times. This includes event handlers, too.

import html from "innerself";

export default function ActiveTask(text, index) {
    return html`
        <li>
            ${text} ${index}
            <button
                onclick="dispatch('COMPLETE_TASK', ${index})">
                Mark As Done</button>
        </li>
    `;
}

You can put any JavaScript into the on<event> attributes. The browser will wrap it in a function which takes the event as the first argument (in most cases) and in which this refers to the DOM Element on which the event has been registered.

The dispatch function takes an action name and a variable number of arguments. They are passed to the reducer which should return a new version of the state.

const init = {
    tasks: [],
    archive: []
};

export default function reducer(state = init, action, args) {
    switch (action) {
        case "ADD_TASK": {
            const {tasks} = state;
            const [value] = args;
            return Object.assign({}, state, {
                tasks: [...tasks, value],
            });
        }
        case "COMPLETE_TASK": {
            const {tasks, archive} = state;
            const [index] = args;
            const task = tasks[index];
            return Object.assign({}, state, {
                tasks: [
                    ...tasks.slice(0, index),
                    ...tasks.slice(index + 1)
                ],
                archive: [...archive, task]
            });
        }
        default:
            return state;
    }
}

If you need side-effects, you have three choices:

  • Put them right in the on<event> attributes.
  • Expose global action creators.
  • Put them in the reducer. (This is considered a bad practice in Redux because it makes the reducer unpredictable and harder to test.)

The dispatch function will also re-render the entire top-level component. In order to be able to do so, it needs to know where in the DOM to put the innerHTML the top-level component generated. This is what attach returned by createStore is for:

import { attach } from "./store";
import App from "./App";

attach(App, document.querySelector("#root"));

createStore also returns a connect function. Use it to avoid passing data from top-level components down to its children where it makes sense. In the first snippet above, ActiveList receives a tasks argument which must be passed by the top-level component.

Instead you can do this:

import html from "innerself";
import { connect } from "./store";
import ActiveTask from "./ActiveTask";
import TaskInput from "./TaskInput";

function ActiveList(state) {
    const { tasks } = state;
    return html`
        <h2>My Active Tasks</h2>
        <ul>
            ${tasks.map(ActiveTask)}
            <li>
                ${TaskInput()}
            </li>
        </ul>
    `;
}

export default connect(ActiveList);

You can then avoid passing the state explicitly in the top-level component:

import html from "innerself";
import { connect } from "./store";

import ActiveList from "./ActiveList";
import ArchivedList from "./ArchivedList";

export default function App(tasks) {
    return html`
        ${ActiveList()}
        ${ArchivedList()}
    `;
}

Connected components always receive the current state as their first argument, and then any other arguments passed explicitly by the parent.

Crazy, huh?

I know, I know. But it works! Check out the example in example01/ and at https://stasm.github.io/innerself/example01/.

Logging Middleware

innerself comes with an optional helper middleware which prints state changes to the console. To use it, simply decorate your reducer with the default export of the innerself/logger module:

import { createStore } from "innerself";
import withLogger from "innerself/logger";
import reducer from "./reducer"

const { attach, connect, dispatch } =
    createStore(withLogger(reducer));

innerself's People

Contributors

stasm avatar f1yn avatar

Watchers

James Cloos avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.