npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@zuze/react-ast

v0.1.0

Published

Render React from JSON

Downloads

6

Readme

@zuze/react-ast: For creating highly configurable react applications

NPM Coverage Status Build Status JavaScript Style Guide Bundle Phobia

Check out the 📕 official documentation 📕

What's this?

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.

Why?

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.

Install

# npm
npm install @zuze/react-ast

# yarn
yarn add @zuze/react-ast

Usage

Simple Example

import 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'));

Nesting Components

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>

Props

  • components: { [key: string]: ComponentDescriptor } - A map of ComponentDescriptors
  • root: = 'MAIN' - The key of the ComponentDescriptor to start rendering
  • resolver: ComponentResolver - Given a ComponentDescriptor, return a Component
  • Component?: ReactElement - Render a descriptor using a Component
  • render?: Renderer - Render a descriptor using a function
  • children?: Renderer - Alternate way to render a descriptor using a function

Advanced Props

  • cmpKey = '$cmp' - Object key the will yield dynamic components.
  • merge?: (...ComponentDescriptors) => ComponentDescriptor - merge function used to combine ComponentDescriptors when using interpolated $cmp's - see merging.
  • interpolate: RegExp = /\{\{(.+?)\}\}/g - Pattern which will be used to interpolate variables
  • context: {[key: string]: any} = {} - Context that can be used for interpolation patterns - e.g. {{some_string}}
  • contextKey: string = $ - When interpolating, do it from context - e.g. {{$string_from_context}}

Interpolation and 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>

Merging

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>

*/

Types

ComponentMap

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>

ComponentDescriptor

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>

DynamicComponents

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>

ComponentResolver

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!' ]
    }
  }}
/>

Code Splitting with Module Resolvers

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

Renderer

({ render: (props = {}) => any, descriptor: ComponentDescriptor, key: string }) => any

A 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}
/>

createComponentResolver

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

License

MIT © akmjenkins