treason
v1.0.0
Published
Cheating on JSX with JSON based components
Downloads
14
Maintainers
Readme
Treason
Treason, that's how it feels when you cheat on React by using JSON instead of JSX. This allows you to easily an API, or external source to generate your views. Possibilities are endless.
So how does this work, we iterate over your supplied JSON payload, look for the
layout
key and iterate the array, creating new elements using
React.createElement
. To support deeply nested structures we automatically
generate the required key
properties, or you supply them your self in as
props for the components. To fully unlock all potential of this module we allow
you modify every step of the process using various of helper methods. Want to
use a stylesheet object instead of style props? Easy mode. Wrapping the app
with Redux store? No problem. Custom components, React-Native? Yep, all
supported.
Take a look at our examples folder for some usage patterns.
Table of Contents
Installation
The package is released in the public npm registry and can be installed by running:
npm install --save treason
Usage
The treason
module exposes the Treason
class which you need to initialize.
import Treason from 'treason';
const layout = new Treason();
The treason
instance allows a single optional argument:
layout
The property that of the supplied JSON that stores the layout structure according to our JSON specification. Defaults tolayout
.
Now that the Treason instance is created you can register the components that should be allowed to render.
layout.register('MyComponent', MyComponent);
Now that we have components register a payload can be decoded so we can render an output.
const view = layout.render(require('./you/json/payload.json'));
console.log(view);
And that it, your JSON payload is now rendered and stored in the view
variable. So for a more complete picture, here's it all together:
import { Text, View, ScrollView } from 'react-native';
import { Svg, Path, Circle } from 'react-native-svg';
import { Foo, Bar, Loading } from './components';
import React, { Component } from 'react';
import Treason from 'treason';
const treason = new Treason();
treason
.register('Text', Text)
.register('View', View)
.register('Path', Path)
.register('Foo', foo);
Now that the we've registered the elements we want to allow in our view, we can pass it some JSON to render.
const view = treason.render(require('./path/to/file.json'));
<Container>
{ view }
</Container>
But we can take it a step further, so why not load the JSON file from a remote service:
class Main extends Component {
constructor() {
super(...arguments);
this.state = {
view: null
};
}
componentDidMount() {
fetch('http://api.endpoint.here').then((data) => {
this.setState({ view: layout.parse(data) });
});
}
render() {
if (!this.state.view) return <Loading />
return this.state.view;
}
}
Now you have on the fly updating of your app's layout.
API
register
Registers a new custom component that can be rendered by the JSON structure. The method accepts 2 arguments:
name
Name of the component in which it's addressed in the JSON structure.Component
The component that should be inserted.
class Example extends React.Component {
render() {
return <p>This is an example</p>
}
}
treason.register('Example', Example);
The method returns it self so it can be used for chaining purposes.
use
The supplied function will receive the plugin API that will give authors access to the following methods:
treason.use(require('./your-custom-plugin-here'));
treason.use(function (api) {
// api.register();
// api.modify();
// api.before();
// api.after();
});
The treason
library ships with the following plugins by default:
treason/svgs
Thesvgs
library allows you to create Svgs that work in React and React-Native. This plugin will register all components so they can be used by the layout.treason/react-native
Registers all available components that are exposed by thereact-native
library.
treason.use(require('treason/svgs'));
modify
The modify
functions allows you to transform specific properties. It requires
the following properties:
prop
The name of the property that needs to be intercepted.callback
Function that is invoked when the given property is encountered.
The callback
receives the following arguments:
value
The value of the property.config
An object with some additional information on where it's encounteredComponent
: The component that is being rendered with the property.props
: Reference to all props on the component.children
: Children of theComponent
.data
: Only set when you use the special@
syntax.
The callback should return
the new value for the property.
For example to prevent the dangerouslySetInnerHTML
from being used as
property on any of the components, you could forcefully remove it by modifying
the value from the HTML payload to undefined
:
treason.modify('dangerouslySetInnerHTML', function modify(value, config) {
return undefined;
});
The modify
function allows you to reference the additional data structures
that you've send together with your layout
payload. You can reference this
data by prefixing the name of the properties with an @
.
Because these properties are references data structures that were received in
the JSON structure they will be removed automatically after they have been
encountered in the props
of the component.
treason.modify('@style', function modify(value, config) {
// config.props
// config.Component
// config.children
// config.data
config.props.style = config.style[value];
});
treason.render({
style: { /* this will be set as config.data in the function above */ },
layout: ['div' { '@style': 'hello' }, [
['div', { '@style': 'hello' }]
]]
});
In the example above, the @style
modifier will be called for the div
, and
receive hello
as value. The config.data
is propagated with the contents of
the style
property of the JSON payload that you supplied to treason.render
.
So in this example we're re-using a single style
object instead of having to
supply the same style information multiple times in the JSON structure. Allowing
you to create a smaller and more efficient payload.
But we can do better, you can combine this with the before
method
that will pre-process the referenced style
object. So if you where to do this
on React-Native, you could transform the style
into a StyleSheet
instance:
import { StyleSheet } from 'react-native';
treason.before('style', function (payload) {
return StyleSheet.create(payload);
});
before
Pre-process the data in the JSON.
name
The name of the property in the initial JSON payload.callback
Function that is invoked when the given property is encountered.
The callback
receives the following arguments:
data
Data that thename
referenced in the JSON payload.layout
The layout structure before they are transformed into React elements.
The callback should return
the payload that you received as first argument.
Anything that you return will be now used as value.
import { createStore } from 'redux';
import reducer from './reducer';
treason.before('store', function before(data, layout) {
const store = createStore(reducer, data);
return store;
});
PRO TIP: As the layout is stored with a layout
key, you also modify that
when it's received:
treason.before('layout', function before(data, layout) {
//
// data == layout, as we're pre-processing the `layout` key from the
// supplied JSON payload. But we can now modify the whole JSON structure,
// add, remove elements as you wish. So in this case, we're going to
// wrap the received elements in a `<div class="container">` element.
//
return ['div', { className: 'container' }, layout];
});
after
Allows you to apply any additional post processing after the layout has been transformed into React elements. The method requires 2 arguments:
name
The name of the property in the initial JSON payload.callback
Function that is invoked when the given property is encountered.
The callback
receives the following arguments:
elements
The rendered React Elements tree.data
Data that thename
referenced in the JSON payload.
The callback should return
the React Elements tree, or a new modified tree:
treason.after('messages', function after(elements, messages) {
return (
<IntlProvider locale={ navigator.language } messages={ messages }>
{ elements }
</IntlProvider>
);
});
treason.render({ messages: {}, layout: ['Foo'] });
In the example above we have our React Intl messages stored as messages
key
in the initial payload, so after the elements are transformed we wrap the
generated React Tree and pass it the initial messages payload.
PRO TIP:* Just like the before
method, you can also process the layout
key and apply additional transformation to the elements tree.
render
This method transforms your given JSON string, or object structure into the
actual React elements. It accepts a single argument, the JSON string, or object
that needs to be transformed into React Elements. It should have a layout
key
that follows our JSON structure specification. Additional keys may be
supplied as their content can be referenced using the before
, after
and
modify
functions.
It's a synchronous process so the method will return created elements:
const elements = layout.render({
layout: ['ComponentName', { props: 'here' }]
});
As it's returning React Element's it can be used inside components as well.
class Ads extends React.Component {
constructor() {
super(...arguments);
this.state = {
view: null
};
}
componentDidMount() {
fetch('https://my.ads.server/treason').then(function parse(response) {
return response.json();
})
.then(function update(myJson) {
this.setState({ view: treason.render(myJson) });
});
}
render() {
if (!this.state.view) return <div className="placeholder" />
return (
<div className="ads">
{ this.state.view }
</div>
)
}
}
Word of caution Your JSON, your problem, we do not sanitize your data. Any of the registered components as well as normal elements can be rendered through the payload so do not allow user input as JSON/layout values.
clear
Clear the instance of all of it's registered components and before, after, use, modify functions.
treason.clear();
JSON
The JSON structure is simple and straightforward. The root element of the JSON
structure should be an object, with a key layout
. The layout should contain
the layout that destructured as Array
{
layout: []
}
["ComponentName", { props: "here" }, [
// children of the ComponentName
["ChildComponent", { props: "foo"} ],
["AnotherChild", { props: "bar"}]
]]
- First item is the name of the component that needs to be rendered. This name
should correspond to the name that you specified in the register
method. If the first item is not a string, we will assume that it's
Fragment
that needs to be rendered. - Next item is an object with the props that should be applied to the specified component. This object can omitted if there are no props to apply.
- Last item is an array of children that needs to be rendered within the component. This array should have children that are specified in exactly the same way as the parent component.
If you are familiar with AssetSystem, it uses the same structure. Below are some examples where the different use's and their outputs are rendered.
INPUT:
[{ foo: bar}, ["Example"]]
OUTPUT:
<Fragment foo="bar">
<Example />
</Fragment>
INPUT:
['Svg', [
["Rect", { height: 100, width: 100, fillColor: "red" }],
["Path", { d: "17n098d" }],
["G", [
["Circle", { x: 0, y: 0 }],
["Circle", { x: 0, y: 10 }]
]]
]]
OUTPUT:
<Svg>
<Rect height={ 100 } width={ 100 } fillColor="red" />
<Path d="17n098d" />
<G>
<Circle x={ 0 } y={ 0 } />
<Circle x={ 0 } y={ 10 } />
</G>
</Svg>
You can go as deep as you want with the structures, supply as many props as want.