Package | Badges |
---|---|
@narrative/control-flow | |
@narrative/babel-plugin-compiler | |
@narrative/swc-plugin-compiler | |
@narrative/vite-plugin-compiler |
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:
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>
);
};
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.
narrative-react-vite-demo[https://github.com/joe-sky/narrative-react-vite-demo]
narrative-react-vite-swc-demo[https://github.com/joe-sky/narrative-react-vite-swc-demo]
npm i @narrative/control-flow @narrative/babel-plugin-compiler
Configure Babel
:
{
"plugins": ["@narrative/compiler"]
}
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]
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 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 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;
}
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>
</>
);
}
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>
);
}
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 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>
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.
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;
}
<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>;
}
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 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>
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>
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 |
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 asindex
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 });
}
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);
}
🤖 RX-9 Narrative Gundam
, ready to launch!
MIT