agilehead / forgo

An ultra-light UI runtime

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

forgo

Forgo is a 4KB library that makes it super easy to create modern web apps using JSX (like React).

Unlike React, there are very few framework specific patterns and lingo to learn. Everything you already know about DOM APIs and JavaScript will easily carry over.

  • Use HTML DOM APIs for accessing elements
  • There are no synthetic events
  • Use closures for maintaining component state
  • There's no vDOM or DOM diffing
  • Renders are manually triggered

We'll be tiny. Always.

All of Forgo is in one small JS file (actually it's TypeScript). It is a goal of the project to remain within that single file.

Installation

npm i forgo

Starting a Forgo project

There are a couple ready-made templates on GitHub to help you with the initial project scaffolding. These templates use webpack as the bundler/build tool.

This process is easier with degit:

For JavaScript:

npx degit forgojs/forgo-template-javascript#main my-project

For TypeScript:

npx degit forgojs/forgo-template-typescript#main my-project

And then to run it:

# switch to the project directory
cd my-project
# Install dependencies
npm i
# run!
npm start

A Forgo Component

A Forgo Component must have a function (called Component Constructor) that returns an object with a render() function (called Component).

Here's an Example.

import { rerender } from "forgo";

function SimpleTimer(initialProps) {
  let seconds = 0; // Just a regular variable, no hooks!

  return {
    render(props, args) {
      setTimeout(() => {
        seconds++;
        rerender(args.element); // rerender
      }, 1000);

      return (
        <div>
          {seconds} seconds have elapsed... {props.firstName}!
        </div>
      );
    },
  };
}

The Component Constructor function and the Component's render() method are both called during the first render with the initial set of props. But for subsequent rerenders of the same component, only the render() gets called (with new props). So if you're using props, remember to get it from the render() method.

Mounting the Component

Use the mount() function once your document has loaded.

import { mount } from "forgo";

window.addEventListener("load", () => {
  mount(<SimpleTimer />, document.getElementById("root"));
});

You could also pass a selector instead of an element.

window.addEventListener("load", () => {
  mount(<SimpleTimer />, "#root");
});

Child Components and Passing Props

That works just as you'd have seen in React.

function Parent(initialProps) {
  return {
    render(props, args) {
      return (
        <div>
          <Greeter firstName="Jeswin" />
          <Greeter firstName="Kai" />
        </div>
      );
    },
  };
}

function Greeter(initialProps) {
  return {
    render(props, args) {
      return <div>Hello {props.firstName}</div>;
    },
  };
}

Reading Form Input Elements

To access the actual DOM elements corresponding to your markup (and the values contained within them), you need to use the ref attribute in the markup. An object referenced by the ref attribute in an element's markup will have its 'value' property set to the actual DOM element when it gets created.

Here's an example:

function Component(initialProps) {
  const myInputRef = {};

  return {
    render(props, args) {
      function onClick() {
        const inputElement = myInputRef.value;
        alert(inputElement.value); // Read the text input.
      }

      return (
        <div>
          <input type="text" ref={myInputRef} />
          <button onclick={onClick}>Click me!</button>
        </div>
      );
    },
  };
}

You can access and read form input elements using regular DOM APIs as well. For example, the following code will work just fine if you assign an id to the input element.

function onClick() {
  const inputElement = document.getElementById("myinput");
  alert(inputElement.value);
}

Lastly, you can pass an event handler to an input and extract the current value from the input event:

function Component(initialProps) {
  return {
    render(props, args) {
      function onInput(e) {
        e.preventDefault();
        alert(e.target.value);
      }

      return (
        <div>
          <input type="text" oninput={onInput} />
        </div>
      );
    },
  };
}

Lists and Keys

Keys help Forgo identify which items in a list have changed, are added, or are removed. While Forgo works well without keys, it is a good idea to add them since it avoids unnecessary component mounting and unmounting in some cases.

As long as they are unique, there is no restriction on what data type you may use for the key; keys could be strings, numbers or even objects. For string keys and numeric keys, Forgo compares them by value; while for object keys, a reference equality check is used.

function Parent() {
  return {
    render(props, args) {
      const people = [
        { firstName: "jeswin", id: 1 },
        { firstName: "kai", id: 2 },
      ];
      return (
        <div>
          {people.map((item) => (
            <Child key={item.key} firstName={item.firstName} />
          ))}
        </div>
      );
    },
  };
}

function Child(initialProps) {
  return {
    render(props) {
      return <div>Hello {props.firstName}</div>;
    },
  };
}

Fetching data asynchronously

Parts of your application might need to fetch data asynchronously, and refresh your component accordingly.

Here's an example of how to do this:

async function getMessages() {
  const data = await fetchMessagesFromServer();
  return data;
}

export function InboxComponent(initialProps) {
  // This will be empty initially.
  let messages = undefined;

  return {
    render(props, args) {
      // Messages are empty. Let's fetch them.
      if (!messages) {
        getMessages().then((data) => {
          messages = data.messages;
          rerender(args.element);
        });
        return <p>Loading data...</p>;
      }

      // We have messages to show.
      return (
        <div>
          <header>Your Inbox</header>
          <ul>
            {messages.map((message) => (
              <li>{message}</li>
            ))}
          </ul>
        </div>
      );
    },
  };
}

Component Unmount

When a component is unmounted, Forgo will invoke the unmount() function if defined for a component. It receives the current props and args as arguments, just as in the render() function. This can be used for any tear down you might want to do.

function Greeter(initialProps) {
  return {
    render(props, args) {
      return <div>Hello {props.firstName}</div>;
    },
    unmount(props, args) {
      console.log("Got unloaded.");
    },
  };
}

Component mount

You'd rarely have to use this. mount() gets called with the same arguments as render () but after getting mounted on a real DOM node. At this point you can expect args.element.node to be populated, where args is the second parameter to mount() and render().

function Greeter(initialProps) {
  return {
    render(props, args) {
      return <div id="hello">Hello {props.firstName}</div>;
    },
    mount(props, args) {
      console.log(`Mounted on node with id ${args.element.node.id}`);
    },
  };
}

Bailing out of a render

When the shouldUpdate() function is defined for a component, Forgo will call it with newProps and oldProps and check if the return value is true before rendering the component. Returning false will skip rendering the component.

function Greeter(initialProps) {
  return {
    render(props, args) {
      return <div>Hello {props.firstName}</div>;
    },
    shouldUpdate(newProps, oldProps) {
      return newProps.firstName !== oldProps.firstName;
    },
  };
}

Error handling

By defining the error() function, Forgo lets you catch errors in child components (at any level, and not necessarily immediate children).

// Here's a component which throws an error.
function BadComponent() {
  return {
    render() {
      throw new Error("Some error occurred :(");
    },
  };
}

// Parent can catch the error by defining the error() function.
function Parent(initialProps) {
  return {
    render() {
      return (
        <div>
          <BadComponent />
        </div>
      );
    },
    error(props, args) {
      return (
        <p>
          Error in {props.name}: {args.error.message}
        </p>
      );
    },
  };
}

Additional Rerender options

The most straight forward way to do rerender is by invoking it with args.element as the only argument - as follows.

function TodoList(initialProps) {
  let todos = [];

  return {
    render(props, args) {
      function addTodos(text) {
        todos.push(text);
        rerender(args.element);
      }

      return <div>markup goes here...</div>;
    },
  };
}

But there are a couple of handy options to rerender, 'newProps' and 'forceRerender'.

newProps let you pass a new set of props while rerendering. If you'd like previous props to be used, pass undefined here.

forceRerender defaults to true, but when set to false skips child component rendering if props haven't changed.

const newProps = { name: "Kai" };
const forceRerender = false;
rerender(args.element, newProps, forceRerender);

Rendering without mounting

Forgo also exports a render method that returns the rendered DOM node that could then be manually mounted.

import { render } from "forgo";

const { node } = render(<Component />);

window.addEventListener("load", () => {
  document.getElementById("root")!.firstElementChild!.replaceWith(node);
});

Routing

Forgo Router (forgo-router) is a tiny router for Forgo, and is just around 1KB gzipped. https://github.com/forgojs/forgo-router

Application State Management

Forgo State (forgo-state) is an easy-to-use application state management solution for Forgo (like Redux or MobX), and is less than 1KB gzipped. https://github.com/forgojs/forgo-state

Try it out on CodeSandbox

You can try the Todo List app with Forgo on CodeSandbox.

Or if you prefer Typescript, try Forgo TodoList in TypeScript.

There is also an example for using Forgo with forgo-router.

Building

Forgo uses the latest JSX createElement factory changes, so you might need to enable this with Babel. More details here: https://babeljs.io/docs/en/babel-plugin-transform-react-jsx

For your babel config:

{
  "plugins": [
    [
      "@babel/plugin-transform-react-jsx",
      {
        "throwIfNamespace": false,
        "runtime": "automatic",
        "importSource": "forgo"
      }
    ]
  ]
}

If you're using TypeScript, add the following lines to your tsconfig.json file.

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "forgo"
  }
}

Getting Help

You can reach out to me via twitter or email. If you find issues, please file a bug on Github.

About

An ultra-light UI runtime

License:MIT License


Languages

Language:TypeScript 99.7%Language:JavaScript 0.3%