An ultra-light UI runtime

Related tags

UI Framework forgo
Overview

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's code is in one single TypeScript file. It is a goal of the project to remain within that single file.

Installation

npm i forgo

Starting a Forgo project

The easiest way to get started is with the 'create-forgo-app' utility. This relies on git, so you should have git installed on your machine.

npx create-forgo-app my-project

It supports TypeScript too:

npx create-forgo-app my-project --template typescript

And then to run it:

# switch to the project directory
cd my-project

# run!
npm start

# To make a production build
npm run build

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";

function ready(fn) {
  if (document.readyState !== "loading") {
    fn();
  } else {
    document.addEventListener("DOMContentLoaded", fn);
  }
}

ready(() => {
  mount(<App />, document.getElementById("root"));
});

You could also pass a selector instead of an element to the mount() function.

ready(() => {
  mount(<App />, "#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.id} 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>
      );
    },
  };
}

The Unmount Event

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.");
    },
  };
}

The Mount Event

If you're an application developer, you'd rarely have to use this. It might however be useful if you're developing libraries or frameworks which use Forgo. mount() gets called with the same arguments as render(), but after getting mounted on a real DOM node. It gets called only once.

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}`);
    },
  };
}

The AfterRender Event

Again, if you're an application developer you'd rarely need to use this. The afterRender() event runs every time after the render() runs, but after the rendered elements have been attached to actual DOM nodes. The 'previousNode' property of args will give you the node to which the component was previously attached, if it has changed due to the render().

function Greeter(initialProps) {
  return {
    render(props, args) {
      return <div id="hello">Hello {props.firstName}</div>;
    },
    afterRender(props, args) {
      console.log(
        `This component is mounted on ${args.element.node.id}, and previously to ${args.previousNode.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 you could pass newProps as well while rerendering. If you'd like previous props to be used, pass undefined here.

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

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. Read more at https://github.com/forgojs/forgo-router

Here's an example:

import { Router, Link, matchExactUrl, matchUrl } from "forgo-router";

function App() {
  return {
    render() {
      return (
        <Router>
          <Link href="/">Go to Home Page</Link>
          {matchExactUrl("/", () => <Home />) ||
            matchUrl("/customers", () => <Customers />) ||
            matchUrl("/about", () => <AboutPage />)}
        </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. Read more at https://github.com/forgojs/forgo-state

Here's an example:

import { bindToStates, defineState } from "forgo-state";

// Define one (or more) application state containers.
const mailboxState = defineState({
  messages: [],
  drafts: [],
  spam: [],
  unread: 0,
});

// A Forgo component
function MailboxView() {
  const component = {
    render(props: any, args: ForgoRenderArgs) {
      return (
        <div>
          {mailboxState.messages.length ? (
            mailboxState.messages.map((m) => <p>{m}</p>)
          ) : (
            <p>There are no messages for {signinState.username}.</p>
          )}
        </div>
      );
    },
  };
  // MailboxView must change whenever mailboxState changes.
  return bindToStates([mailboxState], component);
}

async function updateInbox() {
  const data = await fetchInboxData();
  // The next line causes a rerender of the MailboxView component
  mailboxState.messages = data;
}

Lazy Loading

You can achieve lazy loading with the forgo-lazy package. Read more at https://github.com/jacob-ebey/forgo-lazy

It's as simple as this:

import lazy, { Suspense } from "forgo-lazy";

const LazyComponent = lazy(() => import("./lazy-component"));

const App = () => ({
  render: () => (
    <Suspense fallback={() => "Loading..."}>
      <LazyComponent title="It's that easy :D" />
    </Suspense>
  ),
});

Integrating Forgo into an existing app

Forgo is quite easy to integrate into an existing web app written with other frameworks or with older libraries like jQuery.

To help with that, the forgo-powertoys library (less than 1KB in size) exposes a rerenderElement() function which can rerender a mounted Forgo component with just a CSS selector (from outside the Forgo app). Read more at https://github.com/forgojs/forgo-powertoys

Here's an example:

import { rerenderElement } from "forgo-powertoys";

// A forgo component.
function LiveScores() {
  return {
    render(props) {
      return <p id="live-scores">Top score is {props.topscore}</p>;
    },
  };
}

//mount it on a DOM node as usual
window.addEventListener("load", () => {
  mount(<SimpleTimer />, document.getElementById("root"));
});

// Now you can rerender the component from anywhere, anytime!
rerenderElement("#live-scores", { topscore: 244 });

Server-side Rendering (SSR)

You can render components to an html (string) with the forgo-ssr package. This allows you to prerender components on the server and will work with Node.JS servers like Koa, Express etc. Read more at https://github.com/forgojs/forgo-ssr

Here's an example:

import render from "forgo-ssr";

// A forgo component.
function MyComponent() {
  return {
    render() {
      return <div>Hello world</div>;
    },
  };
}

// Get the html (string) and serve it via koa, express etc.
const html = render(<MyComponent />);

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

Most users are better off just using create-forgo-app to create the project skeleton - in which case all of this is already set up for you. We strongly recommend doing it this way.

But for people who are doing it manually, we'll cover webpack-specific configuration here. Other bundlers would need similar configuration.

esbuild-loader with JavaScript/JSX

Add these lines to webpack.config.js:

module.exports = {
  // remaining config omitted for brevity.
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        loader: "esbuild-loader",
        options: {
          loader: "jsx",
          target: "es2015",
          jsxFactory: "forgo.createElement",
          jsxFragment: "forgo.Fragment",
        },
      },
    ],
  },
};

esbuild-loader with TypeScript/TSX

Add these lines to webpack.config.js:

module.exports = {
  // remaining config omitted for brevity.
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        exclude: /node_modules/,
        loader: "esbuild-loader",
        options: {
          loader: "tsx",
          target: "es2015",
          jsxFactory: "forgo.createElement",
          jsxFragment: "forgo.Fragment",
        },
      },
    ],
  },
};

While using TypeScript, also add the following lines to your tsconfig.json. This lets you do tsc --noEmit for type checking, which esbuild-loader doesn't do.

Add these lines to tsconfig.json:

{
  "compilerOptions": {
    "jsx": "react",
    "jsxFactory": "forgo.createElement",
    "jsxFragmentFactory": "forgo.Fragment"
  }
}

babel-loader with JSX

This is slower than esbuild-loader, so use only as needed.

Add these lines to webpack.config.js:

module.exports = {
  // remaining config omitted for brevity.
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: ["babel-loader"],
      },
    ],
  },
};

Add these lines to babel.config.json:

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

TSX with ts-loader

Add these lines to webpack.config.js:

module.exports = {
  // remaining config omitted for brevity.
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: "ts-loader",
        exclude: /node_modules/,
      },
    ],
  },
};

Add these lines to tsconfig.json:

{
  "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.

Comments
  • Runtime-modifiable lifecycle events

    Runtime-modifiable lifecycle events

    Initial discussion at https://github.com/forgojs/forgo/issues/54

    Initial proposal by @spiffytech

    function MyComponent() {
      return (component: ComponentBuilder) => {
        component.mount(() => {
          const interval = setInterval(doStuff, 1_000);
          component.unmount(() => clearInterval(interval));
          const socket = makeWebsocket();
          component.unmount(() => socket.close());
        });
        component.render(() => <div>Hello world</div>);
      };
    }
    

    This style feels good to me. What do we get from nesting the arrow function within MyComponent?

    This style starts to feel a bit like SolidJS, which might be a good direction to go. Their approach seems like a logical evolution of what major frameworks have turned into.

    I like this style more than trying to add events on top of the existing monolith object API.

    We might need to make some multicast and some not and eat that as a documentation issue. transformAttrs would be the same as render - no point in having two of them, you'd really want (manually?) composed functions instead.

    enhancement 
    opened by jeswin 61
  • Key reordering does not preserve most components' states

    Key reordering does not preserve most components' states

    Sandbox

    If components in a list are reordered, the most of the moved components lose their state. I'm not totally sure what determines which ones keep/lose state, but some components that were rearranged definitely lose state.

    opened by spiffytech 30
  • Error from orphaned elements trying to render

    Error from orphaned elements trying to render

    I'm getting this error in a certain code path is my application:

    The rerender() function was called on a node without a parent element.

    I can't manage to create a minimal reproduction for this, and I've spent a bunch of hours trying to figure out what's going on here, even had a friend help debug the forgo source code tonight, but I haven't figured it out. I'm hoping you have an idea.

    It seems that when my component rerenders and returns null, Forgo isn't correctly updating its bookkeeping. It holds onto the old detached div in args.element, which eventually makes the rerender throw an error. The component's unmount() method isn't called in this scenario, either.

    The page visually renders fine, but I previously managed to get this error in an infinite loop that locked up page, and since I don't understand what's going wrong I can't just let it go.

    I have a parent and a child that both depend on the same forgo-state state, and am using forgo-router.

    The application flow that's happening looks like this: MyComponent onclick -> ajax -> update forgo-state -> MyComponent.render -> queueMicrotask(navigateTo('/app')), return null -> OtherComponent.render -> ajax => update forgo-state -> MyComponent.render

    MyComponent shouldn't get that second render, because the page has navigated away. It should be unmounted. But because it wasn't, forgo-state tries to rerender MyComponent, but because it's holding onto the old div (even though the last render returns null) Forgo tries to rerender it and blows up.

    (Why queueMicrotask? because forgo-router errors out if the very first render the app does includes an inline navigateTo() call because no components have corresponding DOM nodes yet).

    If MyComponent, instead of returning null, returns something like <p>hello</p> I don't get the error. If I do the navigateTo() immediately in the render rather than in queueMicrotask, I don't get the error.

    I see the error printed several times, if that means anything.

    Top of render() in MyComponent:

        render() {
          if (
            authnstate.accessExpiresInHours === null ||
            authnstate.accessExpiresInHours > 0
          ) {
            queueMicrotask(() => navigateTo("/app"));
            //return <p>Redirecting...</p>;
            return null;
          }
    

    The component is declared with bindToStates([authnstate], ...).

    MyComponent lives under App, which is also bound to the same state. App contains the router:

                 {...
                  matchExactUrl(myComponentUrl, () => <MyComponent />) ||
                 <p>Welcome!</p>}
    

    Please let me know if I can provide any other info that'll help. I'm sorry that I can't trigger this in a minimal sandbox, only in my full application.

    opened by spiffytech 16
  • SVG-family elements not receiving attrs

    SVG-family elements not receiving attrs

    If I copy/paste an SVG file into a Forgo component the elements get rendered, but their attrs are missing in my DevTools, so nothing is visible on my page.

    Sandbox

    opened by spiffytech 16
  • Consuming projects can't find `JSX.IntrinsicElements` after v2.1.2

    Consuming projects can't find `JSX.IntrinsicElements` after v2.1.2

    In v2.1.2 consuming projects can't find the JSX.IntrinsicElements interface. forgo is exporting JSX, but forgo.createElement isn't detecting that.

    This was introduced by the merge of 812177f (my commit, my bad). I'm not sure why this didn't turn up when I tested of that commit.

    I'll have a PR up once I identify a fix.

    Prior to v2.1.2, forgo declared namespace createElement with namespace JSX inside of it. 812177f switched the full module import (import "./jsxTypes") to a types import (import type "./jsxTypes") to fix an esbuild issue, and that means the old way of declaring the createElement namespace doesn't work.

    I'm trying to determine a new way to do that, and also why that's necessary in the first place. I can't find any documentation indicating TS JSX needs a createElement namespace. So presumably there's another way to go about the problem.

    opened by spiffytech 13
  • API change for rerender

    API change for rerender

    Continuing from #28

    Instead of asking the developer to import rerender and manually wire it with args.element, you could just pass this to the render callback as the third argument, so the developer doesn't need to know about args.element (all they care about is causing their component to rerender).

    So the render callback would look like:

    ...
    return {
        render(props, args, rerender) {   // rerender is already primed with args.element so the dev doesn't need to wire it themselves
    ...
    rerender() // <- didn't need to pass args.element
    

    So to achieve that you just need to wrap the rerender function sth like:

    
      // Get a new element by calling render on existing component.
            const newForgoNode = newComponentState.component.render(
              forgoElement.props,
              newComponentState.args,
              () => rerender(newComponentState.args.element)) // this third argument can now be called in userland by simply calling rerender() provided as the third argument of the callback
            );
    
    opened by edmulraney 12
  • feat(server): added hydrate functionality

    feat(server): added hydrate functionality

    Added hydrate functionality to work in conjunction with https://www.npmjs.com/package/forgo-render-to-string

    Updated package.json to work in multiple environments (windows)

    opened by jacob-ebey 11
  • Components are double-rendering elements

    Components are double-rendering elements

    I'm creating a form that adds/removes fields based on a radio selection. The initial render is fine, but changing the radio value causes the radio buttons/labels to double-render and one of the form fields to double-render. I can't spot any mistakes in my component code that would explain this behavior.

    Here's a sandbox demonstrating the issue.

    opened by spiffytech 10
  • Handling async/event-driven renders?

    Handling async/event-driven renders?

    How should I handle components that need to rerender when a promise resolves, or when an event fires? I'd figured I could set up my promise/event handler inside mount() and call rerender(), but in the context of #14 I now realize that the args.element provided to mount() will be stale by the time something tries to use it to rerender. The docs show some event handlers, but only ones used in the context of element references that are safe to recreate each render, and not situations where a handler should last the lifetime of the component.

    Is there a recommended way to handle this?

    opened by spiffytech 9
  • Incorrect procedure during component root element tag change

    Incorrect procedure during component root element tag change

    Sandbox with forgo-state Sandbox without forgo-state

    I think this may be what triggered of forgo/forgo-state forgojs/forgo-state#2; this bug was introduced in the same commit mentioned there, and now knowing the more specific cause I can reproduce this bug even after [email protected].

    When a component renders and returns a different root tag than before, the old tag gets marked for unloading but never actually gets cleaned up. So every time the component switches tags, the parentElement's list of unloadable nodes grows longer an longer.

    The next time the parent rerenders, the node gets unmounted, which prompts forgo-state to unbind it from the state. Except the component never gets remounted, so the new element never gets bound back to the state, and it just becomes unresponsive to state changes, while the parent's unloadable nodes list grows without bound.

    Because the new tag got rendered, the UI looks like it's correct until you try interacting with the updated element. And if you're not using forgo-state, you might not even notice, since the component holding the new tag can correctly ask itself to rerender. The deletedNodes/unmount part happens even without forgo-state, but things seem like they're working fine as long as the component doesn't have an unmount method.

    I'm investigating how to fix this, with these goals:

    • Prevent the unloadable nodes list from growing unboundedly
    • Stop unmounting the component just because its tag changed
    opened by spiffytech 8
  • Determine strategy for making Forgo extensible without expanding framework core

    Determine strategy for making Forgo extensible without expanding framework core

    There will be bountiful cases where an app wants some extended version of what Forgo does today, but since Forgo aims to be simple and small, we want to arrive at a stable core that doesn't need constant tinkering and expansion to support new use cases.

    What's the best way to facilitate this?

    forgo-state is a good example of a framework extension. It's effective at its job, but I don't think it's a repeatable pattern. It has a lot of knowledge about Forgo's internals, which is both cumbersome and a barrier for non-core developers to do something similar. A hypothetical forgo-mobx library seems intense to implement right now.

    Some examples of ideas requiring more than Forgo does today:

    • Automatically await callbacks and rerender when they return
    • Allow arbitrary functions to be run inside a component's error boundary, so that the UI updates appropriately if a click handler explodes
    • Hooks-esque stuff. Not really the way React does them (no magic variables, don't-call-it-stateful function components, etc), but e.g., if I use a setInterval(), I have to add mount + unmount methods and wire up both of those, plus the place I actually use the interval, touching three separate places of code. Right now it's not possible to make that setup/teardown reusable, short of creating a whole wrapped component. If args supported something akin to event listeners, that'd be easy for libraries to build on.
    • I've got a Portal component I need to contribute, but right now it depends on forgo-state which isn't ideal. It's easy to do without forgo-state, except I like that forgo-state minimizes what gets rerendered in a call tree. Making that logic reusable would be great.
    • How could an app be offered a Context API?

    A few things I'm thinking through:

    • How can we let reusable code tinker with a component's lifecycle methods? What does that look like? If we support that, do we still need the lifecycle methods? Are we even willing to consider major API changes in the first place, even if the end result is still small and simple?
      • If wrapping components is still the way forward, how can we make that not boilerplate-y and error-prone? I'd guess a complex component might want 5-10 hooks-y things.
      • Maybe make component wrapping feel more like using middleware?
    • If some code wants to hook into attrs (e.g., to wrap click handlers), how would Forgo support that? Technically today you could just wrap each of your handlers manually, but what if you wanted that applied across your whole project? With arbitrary opt-outs?
    • How can we make forgo's core less of a leaky abstraction? Can we e.g., make userland blind to implementation details like args.element?
    • What other stuff like forgo-state's call tree evaluation makes sense to make reusable?
    enhancement question 
    opened by spiffytech 7
  • Fragment usage

    Fragment usage

    I'm trying to render a list as shown in the example but it assumes rendering a single element.

    const list = [
      { key: 'a', value: '1' },
      { key: 'b', value: '2' }
    ];
    
    const Example = () => new forgo.Component({
      render() {
        return list.map(item => [
          <p>{item.key}</p>,
          <p>{item.value}</p>
        ])
      }
    });
    

    In React, you could wrap listed items like this into a fragment which you can attach a key to.

    return list.map(item => [
      <Fragment key={item.key}>
        {[
          <p>{item.key}</p>,
          <p>{item.value}</p>
        ]}
      </Fragment>
    ])
    

    How would you do this in forgo?

    documentation enhancement question medium-priority 
    opened by chronoDave 3
  • forgo.mount() typing not matching with behaviour

    forgo.mount() typing not matching with behaviour

    When trying to run the following code I got an unexpected error:

    import * as forgo from "forgo";
    
    const App= () =>
      new forgo.Component({
        render() {
          return <p>Tooltip</p>;
        }
      });
    
    forgo.mount(document.getElementById("root"), <App />);
    

    forgo.min.js:520 Uncaught Error: The container argument to the mount() function should be an HTML element.

    Now, because of the typing of mount(), this doesn't throw an error, though I'm not sure why.

    export function mount(
      forgoNode: ForgoNode,
      container: Element | string | null
    ): RenderResult {
      return forgoInstance.mount(forgoNode, container);
    }
    

    null however is never a valid container because if (parentElement) will always return false.

    opened by chronoDave 3
  • Checklist for The Big Update

    Checklist for The Big Update

    Stuff we need to do before we're ready for our "make some noise" announcement. @jeswin, feel free to edit this.

    Probably critical

    • [x] #59
    • [ ] #46
      • We know this will be an ongoing project, but we want to get some wins ahead of the release.
    • [x] #50
    • [x] #62

    Nice to have

    • [x] #39
      • Having problems returning null from render() feels weird coming from other frameworks. It'd be nice to knock this out before drawing attention to Forgo.
    • [ ] #58
      • Marking this because it minimizes "what-about"-ism from prospective users
    • [ ] Making the error boundary general-purpose
    documentation 
    opened by spiffytech 0
  • Contexts

    Contexts

    See previous discussion at https://github.com/forgojs/forgo/issues/54

    Copying some parts of it below:

    Initial Proposal by @spiffytech

    How could an app be offered a Context API?

    Jeswin said: So, would it suffice to do a regular import (of a file such as the following) into the component?

    This is how I'm handling it now (typically with forgo-state so I don't have to track what has to rerender), but static exports only work if the use case is amenable to a singleton. If you need something reusable (a repeatable component hierarchy, or a reusable application pattern), current Forgo requires prop drilling. Context APIs allow context to be created at arbitrary points in the component hierarchy, rather than as app globals.

    I think it's also suboptimal to make state an app-wide singleton as a default. Sometimes that turns out to be a mistake, and then it's a whole big thing to change how it works. If the default is to make a Context, then if you suddenly need two independent copies of some component hierarchy, that's no problem.

    The one big hassle with Contexts is types. A component expects to be run with certain Context values available, and I'm not sure how to express that in a type-safe way, guaranteeing a component is only instantiated in the correct context.

    Example use case: in my app, I have a big list of cards with buttons on them. Click a button and refetch updated data from the server. I don't want the user to click buttons too fast, because when the network request finishes the layout in the affected region will shift, so whats under their thumb could change as they're reaching to press it.

    So I want to disable all buttons in the affected region while the request is in flight, plus 500ms afterwards. There are 3-4 levels of component in between the top of the region and all of the buttons, so prop drilling the disabled state + the disable function would be a pain. And once I implement this, I'd like it to be reusable across parts of my app.

    I don't want to make this a global singleton, because I want to gray out disabled buttons, and graying out the whole UI would be confusing, and also the user may want to go click things that have nothing to do with the affected region while the request is in-flight.

    With Contexts I could make a MagicButton component that watches the context, replace all my stock

    opened by jeswin 0
  • Attribute Transformers

    Attribute Transformers

    Initial discussion at https://github.com/forgojs/forgo/issues/54

    Parts of the discussion are copied below for context:

    Initial proposal by @spiffytech

    Forgo could allow libraries to implement this if component had a method to modify attributes before they're put onto the DOM. Something like:

    function someFunc() {
      transformAttr(name, value, args) {
        if (name.startsWith('on')) {
          return autoRedraw(value, args);
        }
        return value;
      },
      render() {...}
    }
    

    Then you could imagine a library exposing this the same way forgo-state accepts a component and returns a component with modified lifecycle methods.

    Pair this with wanting click handlers to run inside the component's error boundary and we've got two examples where modifying DOM node attributes is useful.

    enhancement 
    opened by jeswin 0
Owner
null
Ultra lightweight, usable, beautiful autocomplete with zero dependencies.

Awesomplete https://leaverou.github.io/awesomplete/ Awesomplete is an ultra lightweight, customizable, simple autocomplete widget with zero dependenci

Lea Verou 6.9k Jan 2, 2023
Ultra-high performance reactive programming

________________________________ ___ |/ /_ __ \_ ___/__ __/ __ /|_/ /_ / / /____ \__ / _ / / / / /_/ /____/ /_ / /_/ /_/ \____/__

The Javascript Architectural Toolkit 3.5k Dec 28, 2022
A simple and minimal, ultra-lightweight vanilla JS framework with 0 deps.

piss.js A simple and minimal, ultra-lightweight vanilla JS framework with 0 deps, containing one function, piss. This function has the background colo

grian 15 Oct 21, 2022
Ultra Math Preview for VS Code

Ultra Math Preview for VS Code Real-time math preview for latex and markdown. Usage Install this extension, and then put your cursor into math block i

yfzhao 20 Dec 19, 2022
A (mostly) blank Ultra project

A (mostly) blank Ultra project

Exhibitionist 31 Aug 12, 2022
🍭 search-buddy ultra lightweight javascript plugin that can help you create instant search and/or facilitate navigation between pages.

?? search-buddy search-buddy is an open‑source ultra lightweight javascript plugin (* <1kb). It can help you create instant search and/or facilitate n

Michael 4 Jun 16, 2022
An ultra-lightweight self-hosted CI solution with a dashboard and containerized runners

An extremely simple containerized CI server. Ecosystem The Candor ecosystem is straightforward, and entirely containerized. Docker runs on the host ma

Paul Huebner 8 Nov 20, 2022
⏱️ Ultra-simple Stopwatch App using Phoenix LiveView

Stopwatch Create new phoenix "barebone" Phonenix application: mix phx.new stopwatch --no-mailer --no-dashboard --no-gettext --no-ecto Create folders a

dwyl 9 Nov 5, 2022
An ultra-high performance stream reader for browser and Node.js

QuickReader An ultra-high performance stream reader for browser and Node.js, easy-to-use, zero dependency. Install npm i quickreader Demo import {Quic

EtherDream 156 Nov 28, 2022
Runtime type checking for JS with Hindley Milner signatures

Hindley Milner Definitions The hm-def package allows you to enforce runtime type checking for JavaScript functions using Haskell-alike Hindley Milner

XOD 195 Dec 15, 2022
Dynamically set remote origins at runtime within hosts

external-remotes-plugin Host webpack.config const config = { ...otherConfigs plugins: [ new ModuleFederationPlugin({ name: "app1",

Module Federation 42 Nov 25, 2022
A web client port-scanner written in GO, that supports the WASM/WASI interface for Browser WebAssembly runtime execution.

WebAssembly Port Scanner Written in Go with target WASM/WASI. The WASM main function scans all the open ports in the specified range (see main.go), vi

Avi Lumelsky 74 Dec 27, 2022
frida runtime (no python required, only a single file),One-click support for ios smashing shell

fd 简要介绍 要是你看不懂中文可以使用chrome翻译功能 frida 运行时(不需要python,只有单一个文件) fd 使用fd前请确认手机上有frida-server a brief introdction English can use chrome translation frida r

null 181 Dec 30, 2022
Animator Core is the runtime and rendering engine for Haiku Animator and the components you create with Animator

Animator Core is the runtime and rendering engine for Haiku Animator and the components you create with Animator. This engine is a dependency for any Haiku Animator components that are run on the web.

Haiku 757 Nov 27, 2022
io-ts Typed Event Bus for the runtime of your Node.js application. A core for any event-driven architecture based app.

Typed Event Bus Based on io-ts types, this bus provides a handy interface to publish and consume events in the current runtime of the Node.js process.

Konstantin Knyazev 3 May 23, 2022
Small (0.5 Kb) react hook for getting media breakpoints state info in runtime

tiny-use-media Small (0.5 Kb) react hook for getting media breakpoints state info in runtime Usage npm i tiny-use-media --save Adn in your react code

Valeriy Komlev 51 Dec 13, 2022
Runtime object parsing and validation with static TypeScript typing.

TypeParse Runtime object transformation, parsing and validation with inferred static TypeScript typing. Install Using npm npm install typeparse Using

Kenneth Herrera 4 May 5, 2022
Get Vite's `import.meta.hot` at runtime

vite-hot-client Get Vite's import.meta.hot at runtime. You don't normally need this library directly. It's designed for embedded UI on top of Vite for

Anthony Fu 29 May 3, 2022
Egg Framework Boilerplate for NodeJS Runtime.

EGG.JS FRAMEWORK Tomato Work Personal Affairs Management System WEB Applets Built with Node >= 14.16.0 Node Version Release Egg Application - Document

Huỳnh Lê Minh Thịnh (Edgar Huynh) 15 Jul 8, 2022
Simple, lightweight at-runtime type checking functions, with full TypeScript support

pheno Simple, lightweight at-runtime type checking functions, with full TypeScript support Features Full TypeScript integration: TypeScript understand

Lily Scott 127 Sep 5, 2022