Git Product home page Git Product logo

initiative's Introduction

InitiativeJS

A React framework for professional developers to build your business logic in isolation, then compose your app UI with zero boilerplate code.

Get started | API docs

Case study

Let's look at an example component from an online shop app. This component renders a single item from the users shopping cart, and lets them remove the item if they no longer want to purchase it.

Here is how that component might be implemented traditionally:

function ShoppingCartItem({ item }) {
  const { id, name, quantity, totalPrice } = item;

  const deleteItemMutation = useDeleteItemMutation();
  const deleteItem = useCallback(
    () => deleteItemMutation(id),
    [(id, deleteItemMutation)],
  );

  const { t } = useTranslation();

  const [dialogOpen, setDialogOpen] = useState(false);
  const openDialog = useCallback(() => setDialogOpen(true), []);
  const closeDialog = useCallback(() => setDialogOpen(false), []);

  return (
    <tr>
      <td>{name}</td>
      <td>{t("shopping-cart.item.quantity", { quantity })}</td>
      <td>{t("shopping-cart.item.total-price", { totalPrice })}</td>
      <td>
        <IconButton
          aria-label={t("shopping-cart.item.delete-button")}
          onClick={openDialog}
        >
          <DeleteIcon />
        </IconButton>
        <Dialog open={dialogOpen} onClose={closeDialog}>
          <Typography variant="h6">
            {t("shopping-cart.delete-item-dialog.title")}
          </Typography>
          <Typography variant="body1">
            {t("shopping-cart.delete-item-dialog.description")}
          </Typography>
          <Flex flexDirection="row" justifyContent="end">
            <Button onClick={closeDialog}>
              {t("shopping-cart.delete-item-dialog.cancel-button")}
            </Button>
            <Button
              variant="filled"
              color="danger"
              startIcon={<DeleteIcon />}
              onClick={deleteItem}
            >
              {t("shopping-cart.delete-item-dialog.confirm-button")}
            </Button>
          </Flex>
        </Dialog>
      </td>
    </tr>
  );
}
.
.
.
⎫
⎪
⎬ 🛠️ business logic
⎪
⎭
.
} 🎨 style props
.
⎫
⎬ 🧵 ui state
⎭
.
.
⎫
⎪
⎬ 🏗️ composition &
⎪ 🎨 style props
⎪
⎪
⎭
} 🧵 ui state
⎫
⎬ 🏗️ composition
⎭
} 🧵 ui state
⎫
⎪
⎬ 🏗️ composition &
⎪ 🎨 style props
⎪
⎪
⎭
} 🧵 ui state
⎫
⎪
⎬ 🏗️ composition &
⎪ 🎨 style props
⎪
⎭
} 🛠️ business logic
⎫
⎪
⎬ 🏗️ composition &
⎪ 🎨 style props
⎪
⎪
⎭
.
.

For comparison, here is the same component implemented with InitiativeJS:

function ShoppingCartItemBloc({ item, slots, OutputsProvider }) {
  const { id, name, quantity, totalPrice } = item;

  const deleteItemMutation = useDeleteItemMutation();
  const deleteItem = useCallback(
    () => deleteItemMutation(id),
    [(id, deleteItemMutation)],
  );

  return (
    <OutputsProvider deleteItem={deleteItem}>
      <slots.Child />
    </OutputsProvider>
  );
}

Wait, what happened to the JSX?

The code you want to focus on during development, debugging and review is the business logic. But often times, component composition code and style props make up the overwhelming part of your component files, creating noise and distracting from the relevant lines of code.

InitiativeJS is built around a key observation: Unlike business logic code, component composition code is very standardized – so much so that it can easily be auto-generated. All you need is a way to describe your UI structure to the code generator.

And that's where the InitiativeJS editor comes into play. The editor is a core part of the framework that let's you compose and style your existing React components into pages and views.

image placeholder

The editor generates standard React code – here's an example output. The generated file exports a React component that you can import and render anywhere in your app. The component has the exact same behaviour as hand-written code, but you save yourself the tedious typing work, and keep your business logic free from UI clutter.

InitiativeJS at a glance

InitiativeJS is ...

  • An API layer for building isolated components. Declare schemas for your components that describe the component inputs and outputs – i.e. what data must be passed into your component through props, and what data your component exposes to its subtree (through an <OutputsProvider />).
  • A no-code editor to compose components. Compose your components into pages, views, or even new reusable components. Configure all style props inside the editor to keep your hand-written files clutter free: colors, font styles, margins and positions, labels, and translations.
  • Fully type safe. Inside the schemas you define types for your inputs and outputs, and the editor will ensure that you only assign compatible values to a component input. The schema doubles as a component props interface, so schema and component will always be in sync.
  • Compatible with the tools you know and love. Use any React package, any build tool, SSR.
  • Incrementally adoptable. You can migrate existing apps one page, view or component at a time.
  • Built for professional development workflows. Scenes are saved as tsx files and stored in your git repository. The generated code uses the schema types as well, so TypeScript will detect breaking changes in your generated scenes after you edit a schema or update an npm package.

Check out the tutorial to learn how InitiativeJS works in detail.

Pricing and license terms

Refer to the license for full details, but here is a quick summary.

For app developers

InitiativeJS is free to use for for personal use and open source projects. You may also install and try out InitiativeJS for evaluation purposes.

If you use InitiativeJS in a commercial product, each developer on your team that uses the InitiativeJS Editor must purchase a license. Licenses come with 12 months of updates from the date of purchase. You may continue to use all versions of InitiativeJS that were released during that time period, but upgrading to later InitiativeJS versions requires a new license.

Both community and commercial license allow you to ship generated scene code to production. However, you're not permitted to ship code to production that was imported from @initiativejs/schema – like NodeSchema and t.Type. Refer to the docs to learn how you can configure your bundler to exclude these files from production builds.

For npm package authors

Building reusable InitiativeJS nodes, types and libraries and publishing them as npm packages is free. You can publish your package under any license you like, be it open source or for-profit.

When bundling your package code before publishing, make sure that your bundle doesn't include code from @initiativejs/schema – like NodeSchema or t.Type. Instead, your code may only include import statements to the @initiativejs/schema package. Refer to the docs to learn how you can configure your bundler to exclude these files from production builds.

Contributing

The best way you can contribute to the project right now is by sharing your feedback with us. Please tell us about anything you're unhappy with – everything from minor inconveniences to full showstoppers. Browse our Github issue tracker and upvote issues (by responding with 👍), comment your own requirements and ideas, or open a new issue.

If you want to contribute code or documentation, that awesome! But before you start working on a PR, please talk to us first. For local development and debugging, follow the setup instructions here.

initiative's People

Contributors

pschiffmann avatar brickless1 avatar

Watchers

 avatar  avatar  avatar

initiative's Issues

Nested scene folders in kragle workspace

The kragle editor only scans the top-level workspace directory for scene files. Instead, it should recursively scan all sub-directories inside the workspace. Scenes from sub-directories must be opened, saved, and new scenes created.

Entity property accessors

Consider this example entity:

import { t } from "@kragle/runtime";

interface User {
  readonly firstName: string;
  readonly lastName: string;
  getFullName(): string;
}

const userEntity = t.entity<User>("acme::User");

If we want to extract the user name and pass it to a node input of type string, we need to create a library function function getFirstName(user: User): string { return user.firstName; } for that. This approach doesn't scale for larger data models.

When defining an entity, it should be possible to declare properties, like so:

const userEntity = t.entity<User>("acme::User", {
  firstName: {
    type: t.string(),
  },
  lastName: {
    type: t.string(),
  },
  getFullName: {
    type: t.function()(t.string()),
  },
});

Remove undefined from the type system

I'm debating whether we should support t.undefined() in the Kragle type system. Advantages of removing undefined:

  1. null is a billion dollar mistake on it's own, and JavaScript has two flavors of null. We could encourage users to use only null, and never use undefined.
  2. Hopefully, an ecosystem of 3rd party packages will evolve around Kragle. Removing undefined could help with harmonizing node APIs and remove one pitfall that package authors no longer have to worry about.
  3. If expressions configured in the editor can only evaluate to null, then it's possible for a node component to detect whether an optional input was set to null, or not set at all. (Question: is that useful for node components?)

Editor support for scene inputs

Follow-up to #11. When assigning a node input value, users can choose from all scene inputs that are assignable to the input type. They can also choose the option "Create new scene input ...", this will prompt for an input name, then create a new scene input with the same type as the node input type.


In the editor node tree, the scene itself should be selectable as the tree root element. When selecting the scene itself, the node properties tool displays a list of scene inputs and let's the user assign a literal value, a library member, a function call, or a stage-prop.

stage-props are values that are passed to the <Stage /> component, like this:

root.render(
  <StrictMode>
    <Stage
      definitions={definitions}
      translate={{ type: t.function(t.string())(t.string()), value: myTranslateCallback }}
    />
  </StrictMode>
);

Editor undo&redo

Support undo and redo actions for changing node input expressions, and for changing the node tree structure.

Drag&drop moving of nodes in the node tree

New nodes can only be inserted into empty regular slots, or in the last position of a collection slot. After creating a node, it can't be moved to a different place; if the node needs to be moved, the user has only two options: 1. Delete the node and create a new one at the target location with the same properties and descendants, or 2. manually edit the scene.json file.

The editor needs to support drag&drop gestures inside the node tree, allowing users to move nodes to a new location.

Add a license

Task

At the moment, the project code is unlicensed, which makes it unusable by anyone but me. I need to decide on a license.

Background thoughts

  • I would love to open source the project completely.
  • I believe the project has a lot of potential, and I would like to work on it full-time. I already work a full-time job, but could quit it if this project somehow generates revenue to sustain me.
  • Profitability is not a top priority at this stage of the project; I'm fine with publishing the project as open source for now and figure out the business aspects later.
  • While I focus on building the technical foundation, and rest assured that nobody can exploit my work without collaborating with me.

Solution ideas

I'm thinking of publishing the project under these licensing terms:

  • Buildings apps (private, open source, closed source, and commercial) with the engine is free.
  • Building node libraries and IDE extensions (private, open source, closed source, and commercial) is free.
  • Embedding the IDE, building a new IDE on the existing type system and runtime, or forking the project, is allowed IF AND ONLY IF the new project is published under a strict open source license that prevents commercial use, like GPL.
    • The idea here is to give the community a safety net in case the project is not commercially successful, and I eventually have to stop supporting it. In this case, it could still be maintained by volunteers.
    • If a 3rd party wants to embed the IDE into a commercial product, or build a new IDE on the existing type system, it could be allowed through individually negotiated multi-licensing.

Next steps

  • Research how other open source projects are financed (e.g. Remix).
  • Contact a lawyer.

Edit operation: create&replace node

The editor node tree should support an edit operation to turn the left node tree into the right node tree, without having to delete and re-create node C (including all descendants of C).

A              A
└╴C            └╴B
  ├╴D    -->     └╴C
  └╴E              ├╴D
                   └╴E

For collection slots, #6 will offer a decent workaround: create B as a sibling of C, then move C into B. This doesn't work for regular slots, because B can't be created while C still occupies the slot.

One possible solution is to replace the "add child" button of regular slots with a "replace child" button while the slot is filled. Pressing this button will create the selected node type, insert it into that slot, and insert the previous slot child into the first slot of the newly created node.

Nested scenes

When pressing the "add child" button in the editor node tree, a dialog opens that prompts the user to select a node type for the new node. In addition to selecting a node type, it should also be possible to select an existing scene that is rendered into the slot.

The nested scene is only referenced, not copied. It is not possible to change the structure of the nested scene from within the host scene.

It is possible to bind scene inputs to node outputs from the host scene.

NodeSchema inputs/outputs doc comments

Add a description property of type string to NodeSchemaInput. Display the text in the editor inside the tooltip when a user hovers over the "help" icon. (At least for inputs; we can display the output description inside the data flow analyzer, once it's implemented.)

export const MuiButtonSchema = new NodeSchema(
  "@kragle/template-mui-material::Button",
  {
    inputs: {
      label: {
        description: `
          Buttons allow users to take actions, and make choices, with a single
          tap.

          Buttons communicate actions that users can take. They are typically
          placed throughout your UI, in places like:
           * Modal windows
           * Forms
           * Cards
           * Toolbars`,
        type: t.string(),
      },
    },
  }
);

At the moment, we use the native HTML title attribute to display tooltips, so we can only render simple strings without formatting. For the scope of this issue, implement a simple text transformation for the description:

  • Remove the common leading whitespace from all lines.
  • Concatenate adjacent non-empty lines.

The result of this transformation, applied to the example above, results in this string:

Buttons allow users to take actions, and make choices, with a single tap.

Buttons communicate actions that users can take. They are typically placed throughout your UI, in places like:
 * Modal windows
 * Forms
 * Cards
 * Toolbars

This is the text that needs to be passed to the form controls as a help tooltip.


Later on, we want to support full Markdown rendering. First we need to implement HTML tooltips though; let's hope that Open UI makes some progress fast. :)

recursive entity types

Follow-up to #9. It should be possible to define recursive entities. Here is an idea how the API could look like:

// The result of `t.entity<User>` becomes `() => t.Entity<User>`, but every call
// returns the identical `t.Entity` object.
const userEntity = t.entity<User>("acme::User", () => ({
  name: {
    type: t.string(),
  },
  friends: {
    type: t.array(userEntity()),
  },
}));

Scene inputs

At the moment, generated scene components can't have props. This means that all external state must be passed into the scene through React context, then unpacked and exposed as outputs by BLoC components that are placed at the root of the scene node tree.

Instead, it should be possible to declare scene inputs inside of a scene. Scene inputs are serialized and stored inside the scene.json file, like this:

{
  "sceneInputs": {
    "colorScheme": {
      "type": {
        "type": "union",
        "elements": [
          {
            "type": "string",
            "value": "light"
          },
          {
            "type": "string",
            "value": "dark"
          }
        ]
      },
      "stageValue": {
        "type": "string-literal",
        "value": "light"
      }
    },
    "translate": {
      "type": {
        "type": "function",
        "parameters": [
          {
            "type": "string"
          }
        ],
        "returns": {
          "type": "string"
        }
      },
      "stageValue": {
        "type": "stage-prop",
        "prop": "translate"
      }
    }
  },
  "rootNode": "MyBloc",
  "nodes": {
    ...
  }
}

The generated scene.tsx file will contain the following code:

export interface MySceneProps {
  colorScheme: "light" | "dark";
  translate: (p1: string) => string;
}

export function MyScene({ colorScheme, translate }: MySceneProps) {
  ...
}

For the production scene.tsx code, the scene input values can be provided by the user when the scene component is rendered. Inside the stage, that doesn't work. Instead, the user must be able to assign values to the scene inputs through the editor UI. The editor UI changes are out of scope of this issue, and will be covered in a separate issue. But this issue needs to lay the foundation by implementing a mechanism where the stage runtime evaluates Expressions and passes the values to the scene inputs. The expressions are persisted in the stageValue key of scene.json.

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.