Skip to content

Commit

Permalink
Merge pull request #3 from TechniqueSoftware/feature/html-tags
Browse files Browse the repository at this point in the history
HTML Tags
  • Loading branch information
Matthew R. Elliott committed Nov 6, 2015
2 parents e91a718 + 508e743 commit b00eec0
Show file tree
Hide file tree
Showing 10 changed files with 146 additions and 49 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
spec/spec.js
*.md
99 changes: 81 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,59 +1,122 @@
# react-json-schema

This library builds React elements from JSON by mapping JSON definitions to React components that you expose. The interest behind making this library is to allow non-programmers whip up a view using JSON, which can be stored and retrieved in a database. Use it as you'd like. (JSX not required)
This library builds React elements from JSON by mapping JSON definitions to React components that you expose. The interest behind making this library is to allow non-programmers to construct a view using JSON, which can be stored and retrieved in a database. Use it as you'd like.

### Documentation
JSX is not a dependency for react-json-schema.

For a quick reference, you can jump to [full example](#putting-it-all-together).

The first thing you'll need to do is define your schema in JSON (or a JavaScript object literal). When doing so, there are two things to note:
- A **component** key _must_ exist and be defined by a string or React Component
- A **children** key _may_ exist to define sub-components
### Documentation

Next, we have to make sure that react-json-schema can create elements from component definitions. If a schema's **component** is defined by an string, you will need to expose the components included in the schema to react-json-schema. This can be done by calling `setComponentMap` with an object that has keys that match the strings in your schema, to the components are to be resolved by these strings.
#### Schema

Finally, you'll need to call `parseSchema` to create elements from your schema. Now you have React elements at your disposal!
The primary resource needed is a defined schema in JSON or a JavaScript object literal. It's recommended that schema attributes mainly define React component props. The parser explicitly handles the following attributes:
- **component**: _MUST_ exist and be defined by a string or React component (must be a string if describing a native HTML tag)
- **children**: _MAY_ exist to define sub-components
- **text**: _MAY_ exist to as a string to define inner HTML text (overrides children)

Example (taken from /demo/index.jsx)
Example JSON schema (ES6)
```js
const schema = {
"component": "ContactForm",
"title": "Tell us a little about yourself...",
"children": [
{
"component": "StringField",
"label": "What's your name",
"help": "It's okay, don't be shy :)"
"label": "What's your name?"
},
{
"component": "a",
"href": "#faq",
"text": "I'm not sure why I'm filling this out"
}
]
}
```

Example JS literal (ES6)
```js
const schema = {
"component": ContactForm,
"title": "Tell us a little about yourself...",
"children": [
{
"component": StringField,
"label": "What's your name?"
},
{
"component": "a",
"href": "#faq",
"text": "I'm not sure why I'm filling this out"
}
]
}
```

##### Dynamic Children and Keys

When arrays of components exist (like children), react-json-schema will resolve a key for the element based on the array index, which follows the rules for [dynamic children](https://facebook.github.io/react/docs/multiple-components.html#dynamic-children). Custom keys cannot be defined at this time.

/* es6 object literal shorthand */
#### Component Mapping

React components need to be exposed to the react-json-schema so that the parser can create React elements. If the schema contains object literals with component references, the schema is exposing the React components and no additional configuration is needed. If the schema does not contain references to components, the components can be exposed via `setComponentMap`.

Example for exposing non-exposed components (ES6)
```js
/* es6 object literal shorthand: { ContactForm } == { ContactForm: ContactForm } */
contactForm.setComponentMap({ ContactForm, StringField });
```

#### Parsing

Use `parseSchema` to render React elements. It returns the root node. Note that if your schema's root is an array, you'll have to wrap the schema in an element.

Example (ES6)
```js
/* If using ReactDOM (0.14+), else use React */
ReactDOM.render(contactForm.parseSchema(schema),
document.getElementById('contact-form'));
```

##### Rendering

Also note react-json-schema also does not perform any rendering, so the method in which you want to render is up to you. For example, you can use ReactDOMServer.render, ReactDOM.renderToString, etc. if you'd like.

#### Putting it All Together

```js
import ReactDOM from 'react-dom';
import ReactJsonSchema from 'react-json-schema';

import FormStore from './stores/FormStore';
import ContactForm from './components/ContactForm';
import StringField from './components/StringField';

// For this example, let's pretend I already have data and am ignorant of actions
const schema = FormStore.getFormSchema();
const componentMap = { ContactForm, StringField }

const contactForm = new ReactJsonSchema();
contactForm.setComponentMap(componentMap);

React.render(contactForm.parseSchema(schema),
document.getElementById('json-react-schema'));
ReactDOM.render(contactForm.parseSchema(schema),
document.getElementById('contact-form'));
```

### Try the Demo

To run the demo
* have webpack and webpack-dev-server globally installed
* `npm install`
* `npm run demo`
* The app will be served at http://localhost:8080

### Contribution and Code of Conduct

Please use a linter that recognizes eslint rules

* have webpack, webpack-dev-server and jasmine globally installed
* `npm install`
* `npm test` (Jasmine's test report will output in /spec/index.html)
* `npm run build`

### Roadmap

* Support custom keys for children
* Support native html tags as components, with the option to add custom tag definitions
* Drop lodash dependency?
2 changes: 1 addition & 1 deletion demo/components/StringField.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class StringField extends React.Component {

render() {
return (
<Input type="text" onChange={this.validateInput.bind(this)} {...this.props} />
<Input type="text" onChange={this.validateInput.bind(this)} label={this.props.label} help={this.props.help} />
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.css">
</head>
<body>
<div id="content"></div>
<div id="welcome-banner"></div>
<div id="json-react-schema"></div>
</body>
<script src="bundle.js"></script>
Expand Down
13 changes: 11 additions & 2 deletions demo/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,16 @@ import CheckboxField from './components/CheckboxField';
// If a package dependency: import ReactJsonSchema from 'react-json-schema';
import ReactJsonSchema from '../dist/react-json-schema';

const schema = {
const welcomeSchema = {
'component': 'h2',
'className': 'text-center',
'text': 'Hello World!'
};

const welcomeBanner = new ReactJsonSchema();
React.render(welcomeBanner.parseSchema(welcomeSchema), document.getElementById('welcome-banner'));

const formSchema = {
'component': 'ContactForm',
'title': 'Tell us a little about yourself, we\'d appreciate it',
'children': [
Expand Down Expand Up @@ -34,4 +43,4 @@ const componentMap = { ContactForm, StringField, CheckboxField };
const contactForm = new ReactJsonSchema();
contactForm.setComponentMap(componentMap);

React.render(contactForm.parseSchema(schema), document.getElementById('json-react-schema'));
React.render(contactForm.parseSchema(formSchema), document.getElementById('json-react-schema'));
28 changes: 15 additions & 13 deletions dist/react-json-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons

var _react = require('react');

var _react2 = _interopRequireDefault(_react);
var _node_modulesReactLibReactDOM = require('../node_modules/react/lib/ReactDOM');

var _lodash = require('lodash');
var _node_modulesReactLibReactDOM2 = _interopRequireDefault(_node_modulesReactLibReactDOM);

var _lodash2 = _interopRequireDefault(_lodash);
var _lodash = require('lodash');

var _componentMap = null;

Expand All @@ -30,7 +30,7 @@ var ReactJsonSchema = (function () {
value: function parseSchema(schema) {
var element = null;
var elements = null;
if (_lodash2['default'].isArray(schema)) {
if ((0, _lodash.isArray)(schema)) {
elements = this.parseSubSchemas(schema);
} else {
element = this.createComponent(schema);
Expand All @@ -43,7 +43,7 @@ var ReactJsonSchema = (function () {
var _this = this;

var Components = [];
_lodash2['default'].forEach(subSchemas, function (subSchema, index) {
(0, _lodash.forEach)(subSchemas, function (subSchema, index) {
subSchema.key = index;
Components.push(_this.parseSchema(subSchema));
});
Expand All @@ -52,21 +52,23 @@ var ReactJsonSchema = (function () {
}, {
key: 'createComponent',
value: function createComponent(schema) {
var props = _lodash2['default'].clone(schema);
props = _lodash2['default'].omit(props, ['component', 'children']);
var props = (0, _lodash.clone)(schema);
props = (0, _lodash.omit)(props, ['component', 'children']);
var Component = this.resolveComponent(schema);
var Children = this.resolveComponentChildren(schema);
return _react2['default'].createElement(Component, props, Children);
var Children = props.text || this.resolveComponentChildren(schema);
return (0, _react.createElement)(Component, props, Children);
}
}, {
key: 'resolveComponent',
value: function resolveComponent(schema) {
var Component = null;
if (_lodash2['default'].has(schema, 'component')) {
if (_lodash2['default'].isObject(schema.component)) {
if ((0, _lodash.has)(schema, 'component')) {
if ((0, _lodash.isObject)(schema.component)) {
Component = schema.component;
} else if (_lodash2['default'].isString(schema.component)) {
} else if (_componentMap && _componentMap[schema.component]) {
Component = _componentMap[schema.component];
} else if ((0, _lodash.has)(_node_modulesReactLibReactDOM2['default'], schema.component)) {
Component = schema.component;
}
} else {
throw new Error('ReactJsonSchema could not resolve a component due to a missing component attribute in the schema.');
Expand All @@ -76,7 +78,7 @@ var ReactJsonSchema = (function () {
}, {
key: 'resolveComponentChildren',
value: function resolveComponentChildren(schema) {
return _lodash2['default'].has(schema, 'children') ? this.parseSchema(schema.children) : [];
return (0, _lodash.has)(schema, 'children') ? this.parseSchema(schema.children) : [];
}
}, {
key: 'getComponentMap',
Expand Down
2 changes: 1 addition & 1 deletion dist/react-json-schema.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 16 additions & 13 deletions lib/ReactJsonSchema.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import _ from 'lodash';
import { createElement } from 'react';
import ReactDOM from '../node_modules/react/lib/ReactDOM';
import { isFunction, isArray, forEach, omit, clone, has, isObject, isString } from 'lodash';

let _componentMap = null;

Expand All @@ -8,7 +9,7 @@ export default class ReactJsonSchema {
parseSchema(schema) {
let element = null;
let elements = null;
if (_.isArray(schema)) {
if (isArray(schema)) {
elements = this.parseSubSchemas(schema);
} else {
element = this.createComponent(schema);
Expand All @@ -18,37 +19,39 @@ export default class ReactJsonSchema {

parseSubSchemas(subSchemas) {
const Components = [];
_.forEach(subSchemas, (subSchema, index) => {
forEach(subSchemas, (subSchema, index) => {
subSchema.key = index;
Components.push(this.parseSchema(subSchema));
});
return Components;
}

createComponent(schema) {
let props = _.clone(schema);
props = _.omit(props, ['component', 'children']);
let props = clone(schema);
props = omit(props, ['component', 'children']);
const Component = this.resolveComponent(schema);
const Children = this.resolveComponentChildren(schema);
return React.createElement(Component, props, Children);
const Children = props.text || this.resolveComponentChildren(schema);
return createElement(Component, props, Children);
}

resolveComponent(schema) {
let Component = null;
if (_.has(schema, 'component')) {
if (_.isObject(schema.component)) {
if (has(schema, 'component')) {
if (isObject(schema.component)) {
Component = schema.component;
} else if (_.isString(schema.component)) {
} else if (_componentMap && _componentMap[schema.component]) {
Component = _componentMap[schema.component];
} else if (has(ReactDOM, schema.component)) {
Component = schema.component;
}
} else {
} else {
throw new Error('ReactJsonSchema could not resolve a component due to a missing component attribute in the schema.');
}
return Component;
}

resolveComponentChildren(schema) {
return (_.has(schema, 'children')) ?
return (has(schema, 'children')) ?
this.parseSchema(schema.children) : [];
}

Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"name": "elliottisonfire",
"url": "http://elliottisonfire.com"
},
"repository": {
"type" : "git",
"url" : "https://github.com/TechniqueSoftware/react-json-schema"
},
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/TechniqueSoftware/react-json-schema/issues"
Expand Down Expand Up @@ -41,6 +45,7 @@
"eslint-config-airbnb": "^0.1.0",
"eslint-plugin-react": "^3.5.0",
"file-loader": "^0.8.4",
"jasmine": "^2.3.2",
"jsx-loader": "^0.13.2",
"path": "^0.12.7",
"react-bootstrap": "^0.25.2",
Expand Down
Loading

0 comments on commit b00eec0

Please sign in to comment.