Check out the 📕 official documentation 📕
A pretty straightforward recursive renderer with a tiny amount of syntactic sugar (using $cmp and interpolation!) to compile building block components into complete react applications. Used to develop low-code application frameworks OR to quickly deploy to "hotspots" in existing applications where you need maximum configurability.
Building high quality, highly reusable components is the best approach to react (or any other component-based UI framework).
@zuze/react-ast was built with the goal of having non-technical users build multiple, complete applications using low code concepts.
The rest of this README is geared towards technical users and how to implement @zuze/react-ast. After implementing @zuze/react-ast, you'll need to create your own documentation (hopefully we can help with that too!) about what components are available for your non-technical users to configure and how they can be configured.
# npm
npm install @zuze/react-ast
# yarn
yarn add @zuze/react-astimport React from 'react';
import ReactDOM from 'react-dom';
import ReactAST from '@zuze/react-ast'
const App = () => (
<ReactAST
components={{
MAIN: {
component: 'h1',
children: ['Hello World']
}
}}
/>
);
ReactDOM.render(<App/>, document.getElementById('root'));Creating components dynamically is done with the special key $cmp in your JSON (configurable via the cmpKey prop).
import React from 'react';
import ReactDOM from 'react-dom';
import ReactAST from '@zuze/react-ast'
const App = () => (
<ReactAST
components={{
MAIN: {
component: 'h1',
children: [
'Please click on this ',
{
$cmp: {
component: 'a'
props: {
href: 'https://www.google.com'
},
children:['link to Google']
}
}
]
}
}}
/>
);
ReactDOM.render(<App/>, document.getElementById('root'));
// Output <h1>Please click on this <a href="https://www.google.com">link to google</a></h1>components: { [key: string]: ComponentDescriptor }- A map ofComponentDescriptorsroot: = 'MAIN'- The key of theComponentDescriptorto start renderingresolver: ComponentResolver- Given aComponentDescriptor, return a ComponentComponent?: ReactElement- Render a descriptor using a Componentrender?: Renderer- Render a descriptor using a functionchildren?: Renderer- Alternate way to render a descriptor using a function
cmpKey = '$cmp'- Object key the will yield dynamic components.merge?: (...ComponentDescriptors) => ComponentDescriptor- merge function used to combineComponentDescriptorswhen using interpolated$cmp's - see merging.interpolate: RegExp = /\{\{(.+?)\}\}/g- Pattern which will be used to interpolate variablescontext: {[key: string]: any} = {}- Context that can be used for interpolation patterns - e.g.{{some_string}}contextKey: string = $- When interpolating, do it fromcontext- e.g.{{$string_from_context}}
Interpolation and context is at the very core of @zuze/react-ast. Simply, it works like this:
const App = () => (
<ReactAST
components={{
MAIN: {
component: 'div',
children: [ 'Hi {{$user}}!' ]
}
}}
context={{
user:'joe'
}}
/>
);
// outputs <div>Hi joe!</div>Interpolation is not just for strings. You can interpolate functions:
const App = () => (
<ReactAST
components={{
MAIN: {
component: 'div',
props: {
onClick: '{{$showAlert}}'
},
children: [ 'Hi {{$user}}!' ]
}
}}
context={{
user: 'joe'
showAlert: () => {
alert('clicked');
}
}}
/>
);
// outputs <div>Hi joe!</div>... arrays:
const App = () => (
<ReactAST
components={{
MAIN: {
component: 'div',
children: '{{$friends}}'
}
}}
context={{
friends: ['joe','bill','sam']
}}
/>
);
// outputs <div>joebillsam</div>...full objects:
const App = () => (
<ReactAST
components={{
MAIN: {
component: 'div',
children: [
'Hi {{$user}}!',
{
$cmp: '{{ANOTHER_DIV}}'
}
]
},
ANOTHER_DIV: {
component: 'div',
children: ['Im another div']
}
}}
/>
);
// outputs <div>Hi joe! <div>Im another div</div></div>... which can be used to create composable dynamic components (🎉🎉🎉):
const App = () => (
<ReactAST
components={{
MAIN: {
component: 'div',
children: [
{
$cmp: '{{TITLE_CMP}}',
children: ['Hi {{$user}}!']
}
]
}
TITLE_CMP: {
component: 'h1',
props: {
title: 'Im a heading'
}
}
}}
context={{
user: 'joe'
}}
/>
);
// outputs <div><h1 title="Im a heading">Hi joe!</h1></div>When combining component descriptors in the way detailed above, the merge prop is used. By default, component descriptors are just shallow merged with the exception of the props key. props will be merged one level deep:
// example
const App = () => (
<ReactAST
components={{
MAIN: {
component: 'div'
children: [
{
$cmp: "SECOND",
props: {
propA: 'New A',
propB: 'New B',
}
},
{
$cmp: "SECOND",
props: {
propD: 'New D',
},
children: ['Child 1','Child 2']
}
]
},
SECOND: {
component: 'span',
props: {
propA: 'Original A',
propD: 'Original D',
},
children: ['Original Child']
}
}}
/>
);
/*
<div>
<span propA="New A" propB="New B" propD="Original D">Original Child</span>
<span propA="OriginalA" propD="New D">Child1Child2</span>
</div>
*/A ComponentMap is a key-value map of ComponentDescriptors. The string keys are referred to as ComponentIdentifiers
{
[key: string]: ComponentDescriptor
}
// example
<ReactAST
components={{
MAIN: {
component:'div',
children:[
{
$cmp: '{{OTHER_COMPONENT}}}'
}
]
},
OTHER_COMPONENT: {
component: 'span',
children: [ 'Hello World!' ]
}
}}
/>
// output
// <span>Hello World!</span>Only props and children are treated as special keys in a ComponentDescriptor. It is encouraged to use component and module, but as long as your resolver can return a component given a ComponentDescriptor, then naming isn't critical.
{
component: string,
props?: { [key: string]: any },
children?: any[],
[key: string]: any
}
// example of a host component (no module)
<ReactAST
components={{
MAIN: {
component: 'span',
props: {
title: 'My Span Title'
},
children: [ 'Hello world!' ]
}
}}
/>
// output
// <span title="My Span Title">Hello World!</span>A DynamicComponent is simply a resolved ComponentDescriptor to a React Component. They are created using the $cmp keyword (configurable using the cmpKey prop).
{
$cmp: ComponentDescriptor
}
<ReactAST
components={{
MAIN: {
component: 'div',
children: [
{
$cmp: {
component: 'span',
props: {
title: 'My Span Title',
},
children: [ 'Hello World!' ]
}
}
]
}
}}
/>
// Output
// <div><span title="My Span Title">Hello World</span></div>So far, we've just been rendering HTML, pretty useless stuff. Let's supercharge our capabilities with a ComponentResolver
ComponentResolver: (descriptor: ComponentDescriptor) => ReactElement
Very simple, a ComponentResolver is passed the ComponentDescriptor and returns a React component.
// type
// (descriptor: ComponentDescriptor) => ReactElement;
// example using a module property on the component descriptor
import * as mui from `@material-ui/core`;
const resolver = ({component}) => {
if(component === 'MuiButton') return mui.Button;
throw new Error(`Component ${component} could not be found!`);
}
<ReactAST
resolver={resolver}
components={{
MAIN: {
component: 'MuiButton',
props: {
variant: 'contained'
},
children: [ 'Click Me!' ]
}
}}
/>A highly useful pattern is to specify a module property on your component descriptor:
// example using a module property on the component descriptor
import * as mui from `@material-ui/core`;
const resolver = ({module,component}) => {
if(module !== 'mui') throw new Error(`Component ${component} could not be found!`);
return mui[component];
}
<ReactAST
resolver={resolver}
components={{
MAIN: {
component: 'Button',
module:'mui',
props: {
variant: 'contained'
},
children: [ 'Click Me!' ]
}
}}
/>To set up your create-react-app for code splitting, all you need to do is put a jsconfig.json file in your base directory and put this in it:
{
"baseUrl": "src"
}Now you can use a dynamic import in your resolver:
import { createImporter, createComponentResolver } from '@zuze/react-ast';
const importer = createImporter(({ component }) => import(`./components/${component}`));
// the `resolver` prop
const resolver = createComponentResolver(importer);NOTE: In order to tell webpack what bundles need to be generated, it's critical that the dynamic import have a fixed prefix - hence ./components/${component}. If we just did import(component) - webpack would not be able to determine what bundles need to be generated.
NOTE 2: It is necessary to wrap your resolver in createComponentResolver when dynamically importing components. find out why
({ render: (props = {}) => any, descriptor: ComponentDescriptor, key: string }) => anyA Renderer - given as either a Component or the render or children prop - is used when extra processing that can't be encoded in the AST may need to be happen. For those familiar with material-ui, an example might be a theme:
import * as mui from `@material-ui/core`;
const resolver = ({module,component}) => {
if(module !== 'mui')
throw new Error('Can only use components exported by material-ui');
return mui[component];
}
const { ThemeProvider } = mui;
<ReactAST
components={{
MAIN: {
component: 'Button',
module: 'mui',
props: {
variant: 'contained',
},
children: [ 'Hello World MUI Button!' ],
theme: {
colors: {
primary: 'red'
}
}
}
}}
render={({
render,
descriptor,
key
}) => <ThemeProvider key={key} theme={descriptor.theme}>{render()}</ThemeProvider>}
/>NOTE: If you are wrapping the output of the render function in another component, like above, you'll need to pass in the key parameter to avoid warnings in development about not supplying keys.
You can additionally pass props to the render function that will end up in the component described in the AST:
<ReactAST
components={{
MAIN: {
component: 'Button',
module: 'mui',
props: {
variant: 'contained',
},
children: [ 'Hello World MUI Button!' ]
}
}}
render={({render}) => render({onPress:() => alert('Pressed')})}
/>As a Component (example using material-ui/styles solution):
import { makeStyles } from '@material-ui/styles';
const UseStylesCmp = ({useStyles,children,...rest}) => children({
...rest,
classes:useStyles()
})
const MyRendererComponent = ({key,render,descriptor}) => {
// when using a Component as a renderer, you can use hooks/state/context!
const someProps = useSomeCustomHook(descriptor);
return (
<UseStylesCmp
{...someProps}
useStyles={makeStyles(descriptor.styles)}
>{render}</UseStyles>
}
<ReactAST
components={{
MAIN: {
component: 'Button',
module: 'mui',
props: {
variant: 'contained',
},
styles: {
root: {
background: 'blue'
}
},
children: [ 'Hello World MUI Button!' ]
}
}}
Component={MyRendererComponent}
/>
It's critical that the same component is always returned from the resolver given the same descriptor. Otherwise, components will be unmounted and state will be lost. createComponentResolver maintains an internal cache that can be configured by supplying the second parameter to createComponentResolver
createComponentResolver(resolver: Resolver, cacheFn: (descriptor) => string = ({component}) => component)The cacheFn function accepts a ComponentDescriptor and must return a string. By default it uses the component field.
It's only necessary to use this function if you are dynamically importing components using createImporter because the importer creates a React.lazy component. This same React.lazy component instance must always be returned.
createImporter (and React.lazy) is just one way to utilize code-splitting, but it's not the only way. We utilize code splitting in our LazyAST and Snippet components for our documentation site without using React.lazy
MIT © akmjenkins