From 829972c75ffa9fbad55710b5c9dfa378702c7c7d Mon Sep 17 00:00:00 2001 From: elliottisonfire Date: Fri, 6 Nov 2015 14:51:06 -0500 Subject: [PATCH 1/2] Add HTML tag support and update readme --- .eslintignore | 1 + README.md | 99 +++++++++++++++++++++++++++------ demo/components/StringField.jsx | 2 +- demo/index.html | 2 +- demo/index.jsx | 13 ++++- dist/react-json-schema.js | 30 +++++----- dist/react-json-schema.min.js | 2 +- lib/ReactJsonSchema.js | 33 ++++++----- package.json | 5 ++ spec/ReactJsonSchemaSpec.js | 14 +++++ 10 files changed, 152 insertions(+), 49 deletions(-) diff --git a/.eslintignore b/.eslintignore index 44c4f5b..24ba984 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ spec/spec.js +*.md diff --git a/README.md b/README.md index 1269944..45dab9a 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,21 @@ # 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", @@ -20,25 +23,89 @@ const schema = { "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 @@ -46,8 +113,6 @@ To run the demo ### 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` @@ -55,5 +120,3 @@ Please use a linter that recognizes eslint rules ### Roadmap * Support custom keys for children -* Support native html tags as components, with the option to add custom tag definitions -* Drop lodash dependency? diff --git a/demo/components/StringField.jsx b/demo/components/StringField.jsx index 122bf09..2349e9f 100644 --- a/demo/components/StringField.jsx +++ b/demo/components/StringField.jsx @@ -18,7 +18,7 @@ class StringField extends React.Component { render() { return ( - + ); } } diff --git a/demo/index.html b/demo/index.html index 3032949..c9f1098 100644 --- a/demo/index.html +++ b/demo/index.html @@ -6,7 +6,7 @@ -
+
diff --git a/demo/index.jsx b/demo/index.jsx index 4c05a41..d0ea900 100644 --- a/demo/index.jsx +++ b/demo/index.jsx @@ -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': [ @@ -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')); diff --git a/dist/react-json-schema.js b/dist/react-json-schema.js index afb9e2e..96dcbc9 100644 --- a/dist/react-json-schema.js +++ b/dist/react-json-schema.js @@ -12,17 +12,19 @@ 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; var ReactJsonSchema = (function () { function ReactJsonSchema() { _classCallCheck(this, ReactJsonSchema); + + console.log(_node_modulesReactLibReactDOM2['default']); } _createClass(ReactJsonSchema, [{ @@ -30,7 +32,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); @@ -43,7 +45,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)); }); @@ -52,21 +54,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.'); @@ -76,7 +80,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', diff --git a/dist/react-json-schema.min.js b/dist/react-json-schema.min.js index c575d87..73bd652 100644 --- a/dist/react-json-schema.min.js +++ b/dist/react-json-schema.min.js @@ -1 +1 @@ -"use strict";Object.defineProperty(exports,"__esModule",{value:true});var _createClass=function(){function defineProperties(target,props){for(var i=0;i { + forEach(subSchemas, (subSchema, index) => { subSchema.key = index; Components.push(this.parseSchema(subSchema)); }); @@ -26,29 +31,31 @@ export default class ReactJsonSchema { } 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) : []; } diff --git a/package.json b/package.json index 5b04160..fc89fa8 100644 --- a/package.json +++ b/package.json @@ -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" @@ -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", diff --git a/spec/ReactJsonSchemaSpec.js b/spec/ReactJsonSchemaSpec.js index 1e5997c..7fb24af 100644 --- a/spec/ReactJsonSchemaSpec.js +++ b/spec/ReactJsonSchemaSpec.js @@ -93,8 +93,22 @@ export default describe('ReactJsonSchema', () => { const actual = reactJsonSchema.resolveComponent(schema); expect(React.isValidElement()).toBe(true); }); + it('should resolve native HTML tags.', () => { + spyOn(React, 'createElement'); + const stringSchema = { component: 'h1' }; + const actual = reactJsonSchema.parseSchema(stringSchema); + expect(React.createElement).toHaveBeenCalledWith(stringSchema.component, jasmine.any(Object), jasmine.any(Array)); + }); }); describe('when resolving component children', () => { + it('should resolve text before resolving child components.', () => { + spyOn(React, 'createElement'); + spyOn(reactJsonSchema, 'resolveComponentChildren'); + const stringSchema = { component: 'h1', text: 'Hello World' }; + const actual = reactJsonSchema.parseSchema(stringSchema); + expect(React.createElement).toHaveBeenCalledWith(jasmine.any(String), jasmine.any(Object), stringSchema.text); + expect(reactJsonSchema.resolveComponentChildren).not.toHaveBeenCalled(); + }); it('should return an empty array if no child components are present.', () => { const actual = reactJsonSchema.resolveComponentChildren(schema); expect(Lang.isArray(actual)).toBe(true); From 508e74309b739a6161d7eec11ef3bc3c9e2741a9 Mon Sep 17 00:00:00 2001 From: elliottisonfire Date: Fri, 6 Nov 2015 14:53:59 -0500 Subject: [PATCH 2/2] Remove log --- dist/react-json-schema.js | 2 -- dist/react-json-schema.min.js | 2 +- lib/ReactJsonSchema.js | 4 ---- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/dist/react-json-schema.js b/dist/react-json-schema.js index 96dcbc9..1504351 100644 --- a/dist/react-json-schema.js +++ b/dist/react-json-schema.js @@ -23,8 +23,6 @@ var _componentMap = null; var ReactJsonSchema = (function () { function ReactJsonSchema() { _classCallCheck(this, ReactJsonSchema); - - console.log(_node_modulesReactLibReactDOM2['default']); } _createClass(ReactJsonSchema, [{ diff --git a/dist/react-json-schema.min.js b/dist/react-json-schema.min.js index 73bd652..6a2a569 100644 --- a/dist/react-json-schema.min.js +++ b/dist/react-json-schema.min.js @@ -1 +1 @@ -"use strict";Object.defineProperty(exports,"__esModule",{value:true});var _createClass=function(){function defineProperties(target,props){for(var i=0;i