Skip to content

A compiler tool for create neater control flow tags such as <If>/<For>/<Switch> for JSX/TSX.

License

Notifications You must be signed in to change notification settings

joe-sky/narrative

Repository files navigation

Narrative

Travis CI Status Codecov License

Packages

Package Badges
@narrative/control-flow NPM Version NPM Downloads
@narrative/babel-plugin-compiler NPM Version NPM Downloads
@narrative/swc-plugin-compiler NPM Version NPM Downloads
@narrative/vite-plugin-compiler NPM Version NPM Downloads

Introduction

Narrative(abbreviated as nt) is a compiler tool for create neater control flow tags such as <If>/<Switch>/<For> for React JSX/TSX. It does so by transforming component-like control flow tags to their JavaScript counterparts:

<If when={condition()}>Hello World!</If>;

// Compiled ↓ ↓ ↓ ↓ ↓ ↓

{
  condition() ? 'Hello World!' : null;
}

The inspiration mainly comes from jsx-control-statements, this tool can be seen as an alternative solution with syntactic differences to jsx-control-statements. It also only depends on Babel(or SWC), and it's compatible with React and React Native.

In addition, its API has also referenced the following projects:

Basic Overview

import { useState, FC } from 'react';
import { If, ElseIf, Else, For, Empty, Switch, Case, Default } from '@narrative/control-flow';

const App: FC = () => {
  const [todos, setTodos] = useState([]);

  const addTodo = () => {
    setTodos(todos.concat(`Item ${todos.length}`));
  };

  return (
    <div className="app">
      <ul>
        <For of={todos}>
          {(todo, { index }) => (
            <If when={index > 5}>
              <li key={index}>{todo * 3}</li>
              <ElseIf when={index > 10}>
                <li key={index}>{todo * 4}</li>
              </ElseIf>
              <Else>
                <li key={index}>{todo * 5}</li>
              </Else>
            </If>
          )}
          <Empty>
            <li>No data</li>
          </Empty>
        </For>
      </ul>
      <ul>
        <For in={{ a: 1, b: 2, c: 3 }}>
          {(item, { key }) => <li key={key}>{item}</li>}
          <Empty>
            <li>No data</li>
          </Empty>
        </For>
      </ul>
      <Switch value={todos.length}>
        <Case is={1}>1</Case>
        <Case is={2}>2</Case>
        <Case in={[3, 4, 5]}>3/4/5</Case>
        <Default>More than 2</Default>
      </Switch>
    </div>
  );
};

Highlights

Narrative has similar or different features as jsx-control-statements:

  • ✨ Tag names are more like native JavaScript control statements.
  • 💫 More concise syntax keywords of tags.
  • ⭐ Tags supports full TypeScript inference.
  • ⚡ No runtime code, just need compiler.
  • 🔥 Supports both Babel and SWC compilers.
  • 🔧 Support syntax error prompt for Babel and SWC.

Table of Contents

Examples

React + Vite(use Babel compiler)

narrative-react-vite-demo[https://github.com/joe-sky/narrative-react-vite-demo]

React + Vite(use SWC compiler)

narrative-react-vite-swc-demo[https://github.com/joe-sky/narrative-react-vite-swc-demo]

Installation

Using with Babel

npm i @narrative/control-flow @narrative/babel-plugin-compiler

Configure Babel:

{
  "plugins": ["@narrative/compiler"]
}

Using with SWC

npm install @narrative/control-flow @narrative/swc-plugin-compiler

Configure SWC:

{
  "jsc": {
    "experimental": {
      "plugins": [["@narrative/swc-plugin-compiler", {}]]
    }
  }
}
  • If your swc_core version is lower than 0.8, please install the old version:
npm install @narrative/[email protected]

Usage

Each JSX tags must to be imported from @narrative/control-flow when use:

import { If, Else, ElseIf } from '@narrative/control-flow';

function render(no: number) {
  return (
    <If when={no === 1}>
      <span>1</span>
      <ElseIf when={no === 2}>
        <span>2</span>
      </ElseIf>
      <Else>
        <span>0</span>
      </Else>
    </If>
  );
}

As above the usage is similar to the regular React components. Each tags and its props also support TypeScript type checking.

If Tag

<If> tag is an alternative syntax for conditional logic in JSX, it is similar to the if statement in JavaScript. Also supports <ElseIf>, <Else>, and the syntax design fully supports JSX native formatting. Simple examples:

import { If } from '@narrative/control-flow';

// simple
<If when={true}>
  <span>IfBlock</span>
</If>

// using multiple child elements or expressions
<If when={no < 5}>
  one
  {"two"}
  <span>three</span>
  <span>four</span>
  <ElseIf when={no >= 5}>
    <span>five</span>
  </ElseIf>
</If>
Click here to view how the compiler works
<If when={index > 5}>
  <li>{todo * 2}</li>
  <ElseIf when={index > 10}>
    <li>{todo * 3}</li>
  </ElseIf>
</If>;

// Compiled ↓ ↓ ↓ ↓ ↓ ↓

{
  index > 5 ? <li>{todo * 2}</li> : index > 10 ? <li>{todo * 3}</li> : null;
}

<If>

If only use <If>, the children of <If> will be returned when the value of when prop is true.

Prop Name Prop Type Required
when boolean
import { If } from '@narrative/control-flow';

<If when={no > 1}>
  <span>IfBlock1</span>
  <span>IfBlock2</span>
</If>;

// Compiled ↓ ↓ ↓ ↓ ↓ ↓

{
  no > 1 ? (
    <>
      <span>IfBlock1</span>
      <span>IfBlock2</span>
    </>
  ) : null;
}

<Else>

Only one <Else> can be added within a <If>. If the when prop value of <If> is false, then the children of <Else> will be returned:

import { If, Else } from '@narrative/control-flow';

<If when={no > 1}>
  <span>IfBlock1</span>
  <span>IfBlock2</span>
  <Else>
    <span>IfBlock3</span>
    <span>IfBlock4</span>
  </Else>
</If>;

// Compiled ↓ ↓ ↓ ↓ ↓ ↓

{
  no > 1 ? (
    <>
      <span>IfBlock1</span>
      <span>IfBlock2</span>
    </>
  ) : (
    <>
      <span>IfBlock3</span>
      <span>IfBlock4</span>
    </>
  );
}

<ElseIf>

Multiple <ElseIf> can be added within a <If>, and any one of the tag children with a when prop of true will be returned:

import { If, Else, ElseIf } from '@narrative/control-flow';

<If when={no > 10}>
  <span>IfBlock1</span>
  <ElseIf when={no > 5}>
    <span>IfBlock2</span>
  </ElseIf>
  <ElseIf when={no > 1}>
    <span>IfBlock3</span>
  </ElseIf>
  <Else>
    <span>IfBlock4</span>
  </Else>
</If>;

// Compiled ↓ ↓ ↓ ↓ ↓ ↓

{
  no > 10 ? (
    <span>IfBlock1</span>
  ) : no > 5 ? (
    <span>IfBlock2</span>
  ) : no > 1 ? (
    <span>IfBlock3</span>
  ) : (
    <span>IfBlock4</span>
  );
}

Function children of <If> <ElseIf> <Else>

The children of <If>, <ElseIf>, <Else> also supports a function. It can be used for logic that calculates first and then renders:

import { If } from '@narrative/control-flow';

<If when={no > 1}>
  {() => {
    const blockName = 'IfBlock';

    return (
      <>
        <span>{blockName}1</span>
        <span>{blockName}2</span>
      </>
    );
  }}
  <Else>
    {() => {
      const blockName = 'ElseBlock';

      return (
        <>
          <span>{blockName}1</span>
          <span>{blockName}2</span>
        </>
      );
    }}
  </Else>
</If>;

// Compiled ↓ ↓ ↓ ↓ ↓ ↓

{
  no > 1
    ? (() => {
        const blockName = 'IfBlock';

        return (
          <>
            <span>{blockName}1</span>
            <span>{blockName}2</span>
          </>
        );
      })()
    : (() => {
        const blockName = 'ElseBlock';

        return (
          <>
            <span>{blockName}1</span>
            <span>{blockName}2</span>
          </>
        );
      })();
}

Switch Tag

<Switch> tag is an alternative syntax for multi branch conditional statements in JSX, it is similar to the switch statement in JavaScript. Also supports <Case>, <Default>. Simple examples:

import { Switch } from '@narrative/control-flow';

<Switch value={todos.length}>
  <Case is={1}>
    <span>1</span>
  </Case>
  <Case is={2}>
    <span>2</span>
  </Case>
  <Case is={3}>
    <span>3</span>
  </Case>
  <Default>
    <span>0</span>
  </Default>
</Switch>;

// Compiled ↓ ↓ ↓ ↓ ↓ ↓

{
  todos.length === 1 ? (
    <span>1</span>
  ) : todos.length === 2 ? (
    <span>2</span>
  ) : todos.length === 3 ? (
    <span>3</span>
  ) : (
    <span>0</span>
  );
}

<Switch> <Case>

<Switch> requires to set the value prop.

Prop Name Prop Type Required
value any

Each <Case> matches the value prop of <Switch> via is prop. At least one <Case> is required within the <Switch>.

Prop Name Prop Type Required
is any Either is or in is required

Example:

import { Switch, Case } from '@narrative/control-flow';

<Switch value={todos.length}>
  <Case is={1}>
    <span>1</span>
  </Case>
  <Case is={2}>
    <span>2</span>
  </Case>
</Switch>;

// Compiled ↓ ↓ ↓ ↓ ↓ ↓

{
  todos.length === 1 ? <span>1</span> : todos.length === 2 ? <span>2</span> : null;
}

As above, the <Case> use strict equality(===) when matching.

Multiple values of <Case>

If multiple values need to be matched in one <Case>, the in prop can be used.

Prop Name Prop Type Required
in ArrayLike<any> Either is or in is required

Example:

import { Switch, Case } from '@narrative/control-flow';

<Switch value={todos.length}>
  <Case is={1}>
    <span>1</span>
  </Case>
  <Case is={2}>
    <span>2</span>
  </Case>
  <Case in={[3, 4, 5]}>
    <span>3/4/5</span>
  </Case>
</Switch>;

// Compiled ↓ ↓ ↓ ↓ ↓ ↓

{
  todos.length === 1 ? (
    <span>1</span>
  ) : todos.length === 2 ? (
    <span>2</span>
  ) : [3, 4, 5].includes(todos.length) ? (
    <span>3/4/5</span>
  ) : null;
}

<Default>

<Switch> can have one <Default> inside it, which will be matched to the <Default> when all <Case> do not match:

import { Switch, Case, Default } from '@narrative/control-flow';

<Switch value={todos.length}>
  <Case is={1}>
    <span>1</span>
  </Case>
  <Case in={[2, 3]}>
    <span>2/3</span>
  </Case>
  <Default>
    <span>0</span>
  </Default>
</Switch>;

// Compiled ↓ ↓ ↓ ↓ ↓ ↓

{
  todos.length === 1 ? <span>1</span> : [2, 3].includes(todos.length) ? <span>2/3</span> : <span>0</span>;
}

Function children of <Case> <Default>

The children of <Case>, <Default> also supports a function. It can be used for logic that calculates first and then renders:

import { Switch, Case, Default } from '@narrative/control-flow';

<Switch value={todos.length}>
  <Case is={1}>
    {() => {
      const blockName = 'CaseBlock';

      return (
        <>
          <span>{blockName}1</span>
          <span>{blockName}2</span>
        </>
      );
    }}
  </Case>
  <Default>
    {() => {
      const blockName = 'DefaultBlock';

      return (
        <>
          <span>{blockName}1</span>
          <span>{blockName}2</span>
        </>
      );
    }}
  </Default>
</Switch>;

// Compiled ↓ ↓ ↓ ↓ ↓ ↓

{
  todos.length === 1
    ? (() => {
        const blockName = 'CaseBlock';

        return (
          <>
            <span>{blockName}1</span>
            <span>{blockName}2</span>
          </>
        );
      })()
    : (() => {
        const blockName = 'DefaultBlock';

        return (
          <>
            <span>{blockName}1</span>
            <span>{blockName}2</span>
          </>
        );
      })();
}

For Tag

<For> tag is an alternative syntax for loops logic in JSX. Example:

import { For } from '@narrative/control-flow';

<For of={todos}>
  {(todo, { index }) => <li key={index}>{todo}</li>}
  <Empty>
    <li>No data</li>
  </Empty>
</For>;

// Compiled ↓ ↓ ↓ ↓ ↓ ↓

{
  (__arr => {
    if (__arr?.length) {
      return __arr.map((todo, index) => <i key={index}>{todo}</i>, this);
    }
    return <li>No data</li>;
  })(todos);
}

<For of>

<For of> loops is similar to the for of statement in JavaScript, the of prop accepts Arrays and Array-likes.

Prop Name Prop Type Required
of ArrayLike<any>

The loop callback function is in children of <For of>, example:

import { For } from '@narrative/control-flow';

<For of={todos}>{(todo, { index }, arr) => <li key={index}>{todo}</li>}</For>;

// Compiled ↓ ↓ ↓ ↓ ↓ ↓

{
  todos?.map?.((todo, index, arr) => {
    return <i key={index}>{todo}</i>;
  }, this) || null;
}

As above the callback function parameters:

Parameter order Type Description Required
first type of Array items each items of Array
second Loop iteration metadata mainly using index of Array
third type of Array looping Array variable

<For in>

<For in> loops is similar to the for in statement in JavaScript, the in prop accepts an Object.

Prop Name Prop Type Required
in Record<any, any>

The loop callback function is in children of <For in>, example:

import { For } from '@narrative/control-flow';

<For in={{ a: 1, b: 2, c: 3 }}>{(item, { key }, obj) => <i key={key}>{item}</i>}</For>;

// Compiled ↓ ↓ ↓ ↓ ↓ ↓

{
  (__obj => {
    const __keys = __obj ? Object.keys(__obj) : [];

    if (__keys.length) {
      return __keys.map(key => {
        const item = __obj[key];
        const obj = __obj;
        return <i key={key}>{item}</i>;
      }, this);
    }
  })({ a: 1, b: 2, c: 3 });
}

As above the callback function parameters:

Parameter order Type Description Required
first type of Object values each values of source object
second Loop iteration metadata mainly using key and keys
third type of Object looping Object variable

Loop iteration metadata

Access additional information about each iteration by the second callback argument:

  • index: A number from 0 to the length of the Arrays or Objects.
  • key: The key for this item in Objects, same as index for Arrays.
  • keys: The keys Arrays for Objects.
import { For, If } from '@narrative/control-flow';

<For in={{ a: 1, b: 2, c: 3 }}>
  {(item, { key, index, keys }) => (
    <i key={key}>
      {item}
      <If when={keys.length > 2 && index > 1}>{index}</If>
    </i>
  )}
</For>;

// Compiled ↓ ↓ ↓ ↓ ↓ ↓

{
  (__obj => {
    const __keys = __obj ? Object.keys(__obj) : [];

    if (__keys.length) {
      return __keys.map((key, index) => {
        const item = __obj[key];
        const keys = __keys;
        return (
          <i key={key}>
            {item}
            {keys.length > 2 && index > 1 ? index : null}
          </i>
        );
      }, this);
    }
  })({ a: 1, b: 2, c: 3 });
}

<Empty>

A common pattern when rendering a collection is to render a special case when the collection is empty. Optionally provide a <Empty> to handle this case for both <For in> and <For of> loops. <Empty> should be set in the children of <For>, Example:

import { For, Empty } from '@narrative/control-flow';

const emptyObj = {};

<For in={emptyObj}>
  {(item, { key }) => <i key={key}>{item}</i>}
  <Empty>No Data</Empty>
</For>;

// Compiled ↓ ↓ ↓ ↓ ↓ ↓

{
  (__obj => {
    const __keys = __obj ? Object.keys(__obj) : [];

    if (__keys.length) {
      return __keys.map(key => {
        const item = __obj[key];
        return <i key={key}>{item}</i>;
      }, this);
    }

    return 'No Data';
  })(emptyObj);
}

The Origin of Name

🤖 RX-9 Narrative Gundam, ready to launch!

Narrative

License

MIT

About

A compiler tool for create neater control flow tags such as <If>/<For>/<Switch> for JSX/TSX.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published