├── docs
├── pages
│ ├── index.js
│ ├── examples
│ │ ├── arrays
│ │ │ ├── .gitignore
│ │ │ ├── spec.json
│ │ │ ├── index.md
│ │ │ ├── index.html
│ │ │ ├── webpack.config.js
│ │ │ ├── server.js
│ │ │ └── index.js
│ │ ├── index.js
│ │ ├── refs
│ │ │ ├── .gitignore
│ │ │ ├── spec.json
│ │ │ ├── index.md
│ │ │ ├── index.html
│ │ │ ├── webpack.config.js
│ │ │ ├── server.js
│ │ │ └── index.js
│ │ ├── simple
│ │ │ ├── .gitignore
│ │ │ ├── spec.json
│ │ │ ├── server.js
│ │ │ ├── index.html
│ │ │ ├── index.md
│ │ │ └── index.js
│ │ ├── all-widgets
│ │ │ ├── .gitignore
│ │ │ ├── index.md
│ │ │ ├── spec.json
│ │ │ ├── index.html
│ │ │ ├── webpack.config.js
│ │ │ ├── server.js
│ │ │ └── index.js
│ │ ├── change-layout
│ │ │ ├── .gitignore
│ │ │ ├── spec.json
│ │ │ ├── index.md
│ │ │ ├── index.html
│ │ │ ├── webpack.config.js
│ │ │ ├── server.js
│ │ │ └── index.js
│ │ ├── custom-themes
│ │ │ ├── .gitignore
│ │ │ ├── spec.json
│ │ │ ├── index.html
│ │ │ ├── webpack.config.js
│ │ │ ├── server.js
│ │ │ ├── index.js
│ │ │ └── index.md
│ │ ├── validation
│ │ │ ├── .gitignore
│ │ │ ├── spec.json
│ │ │ ├── index.md
│ │ │ ├── index.html
│ │ │ ├── webpack.config.js
│ │ │ ├── server.js
│ │ │ └── index.js
│ │ ├── combining-schemas
│ │ │ ├── .gitignore
│ │ │ ├── spec.json
│ │ │ ├── index.md
│ │ │ ├── index.html
│ │ │ ├── webpack.config.js
│ │ │ ├── server.js
│ │ │ └── index.js
│ │ ├── initial-values
│ │ │ ├── .gitignore
│ │ │ ├── spec.json
│ │ │ ├── index.md
│ │ │ ├── index.html
│ │ │ ├── webpack.config.js
│ │ │ ├── server.js
│ │ │ └── index.js
│ │ ├── custom-field-validation
│ │ │ ├── .gitignore
│ │ │ ├── spec.json
│ │ │ ├── index.html
│ │ │ ├── webpack.config.js
│ │ │ ├── index.md
│ │ │ ├── server.js
│ │ │ └── index.js
│ │ ├── index.md
│ │ └── spec.json
│ ├── spec.json
│ └── index.md
└── images
│ └── example-liform-react.png
├── .eslintignore
├── .eslintrc
├── .babelrc
├── src
├── __tests__
│ ├── tempPolyfills.js
│ ├── test-utils.js
│ ├── setup.js
│ ├── compileSchema.spec.js
│ ├── themes.bootstrap3.TextareaWidget.spec.js
│ ├── processSubmitErrors.spec.js
│ ├── themes.bootstrap3.compatibleDateWidget.spec.js
│ ├── themes.bootstrap3.ArrayWidget.spec.js
│ ├── themes.bootstrap3.CompatibleDateTimeWidget.spec.js
│ ├── themes.bootstrap3.CheckboxWidget.spec.js
│ ├── themes.bootstrap3.StringWidget.spec.js
│ ├── themes.bootstrap3.NumberWidget.spec.js
│ ├── themes.bootstrap3.FileWidget.spec.js
│ ├── themes.bootstrap3.UrlWidget.spec.js
│ ├── themes.bootstrap3.DateWidget.spec.js
│ ├── themes.bootstrap3.SearchWidget.spec.js
│ ├── themes.bootstrap3.TimeWidget.spec.js
│ ├── themes.bootstrap3.ColorWidget.spec.js
│ ├── themes.bootstrap3.EmailWidget.spec.js
│ ├── themes.bootstrap3.PasswordWidget.spec.js
│ ├── themes.bootstrap3.DateTimeWidget.spec.js
│ ├── themes.bootstrap3.ChoiceWidget.spec.js
│ ├── themes.bootstrap3.MoneyWidget.spec.js
│ ├── themes.bootstrap3.PercentWidget.spec.js
│ ├── renderFields.spec.js
│ ├── createLiformSpec.spec.js
│ └── buildSyncValidation.spec.js
├── themes
│ ├── bootstrap3
│ │ ├── DateWidget.js
│ │ ├── TimeWidget.js
│ │ ├── UrlWidget.js
│ │ ├── SearchWidget.js
│ │ ├── StringWidget.js
│ │ ├── PasswordWidget.js
│ │ ├── DateTimeWidget.js
│ │ ├── NumberWidget.js
│ │ ├── ColorWidget.js
│ │ ├── EmailWidget.js
│ │ ├── ObjectWidget.js
│ │ ├── DateSelector.js
│ │ ├── CheckboxWidget.js
│ │ ├── TextareaWidget.js
│ │ ├── BaseInputWidget.js
│ │ ├── FileWidget.js
│ │ ├── PercentWidget.js
│ │ ├── MoneyWidget.js
│ │ ├── index.js
│ │ ├── ChoiceExpandedWidget.js
│ │ ├── ChoiceWidget.js
│ │ ├── oneOfChoiceWidget.js
│ │ ├── ChoiceMultipleExpandedWidget.js
│ │ ├── ArrayWidget.js
│ │ ├── CompatibleDateWidget.js
│ │ └── CompatibleDateTimeWidget.js
│ └── index.js
├── renderFields.js
├── LICENSE
├── compileSchema.js
├── processSubmitErrors.js
├── renderField.js
├── index.js
└── buildSyncValidation.js
├── .npmignore
├── .gitignore
├── .travis.yml
├── LICENSE
├── webpack.config.js
├── README.md
└── package.json
/docs/pages/index.js:
--------------------------------------------------------------------------------
1 | console.log("hola");
2 |
--------------------------------------------------------------------------------
/docs/pages/examples/arrays/.gitignore:
--------------------------------------------------------------------------------
1 | bundle.js
2 |
--------------------------------------------------------------------------------
/docs/pages/examples/index.js:
--------------------------------------------------------------------------------
1 | console.log("hola");
2 |
--------------------------------------------------------------------------------
/docs/pages/examples/refs/.gitignore:
--------------------------------------------------------------------------------
1 | bundle.js
2 |
--------------------------------------------------------------------------------
/docs/pages/examples/simple/.gitignore:
--------------------------------------------------------------------------------
1 | bundle.js
2 |
--------------------------------------------------------------------------------
/docs/pages/examples/all-widgets/.gitignore:
--------------------------------------------------------------------------------
1 | bundle.js
2 |
--------------------------------------------------------------------------------
/docs/pages/examples/all-widgets/index.md:
--------------------------------------------------------------------------------
1 | Here we go.
2 |
--------------------------------------------------------------------------------
/docs/pages/examples/change-layout/.gitignore:
--------------------------------------------------------------------------------
1 | bundle.js
2 |
--------------------------------------------------------------------------------
/docs/pages/examples/custom-themes/.gitignore:
--------------------------------------------------------------------------------
1 | bundle.js
2 |
--------------------------------------------------------------------------------
/docs/pages/examples/validation/.gitignore:
--------------------------------------------------------------------------------
1 | bundle.js
2 |
--------------------------------------------------------------------------------
/docs/pages/spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "index"
3 | }
4 |
--------------------------------------------------------------------------------
/docs/pages/examples/combining-schemas/.gitignore:
--------------------------------------------------------------------------------
1 | bundle.js
2 |
--------------------------------------------------------------------------------
/docs/pages/examples/initial-values/.gitignore:
--------------------------------------------------------------------------------
1 | bundle.js
2 |
--------------------------------------------------------------------------------
/docs/pages/examples/custom-field-validation/.gitignore:
--------------------------------------------------------------------------------
1 | bundle.js
2 |
--------------------------------------------------------------------------------
/docs/pages/examples/index.md:
--------------------------------------------------------------------------------
1 | ##hi
2 |
3 | a ritual *salutation*
4 |
--------------------------------------------------------------------------------
/docs/pages/examples/refs/spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Refs"
3 | }
4 |
--------------------------------------------------------------------------------
/docs/pages/examples/spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Examples"
3 | }
4 |
--------------------------------------------------------------------------------
/docs/pages/examples/arrays/spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Arrays"
3 | }
4 |
--------------------------------------------------------------------------------
/docs/pages/examples/simple/spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Simple form"
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | webpack.config*.js
2 | src/__tests__
3 | node_modules
4 | examples
--------------------------------------------------------------------------------
/docs/pages/examples/validation/spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Validation"
3 | }
4 |
--------------------------------------------------------------------------------
/docs/pages/examples/all-widgets/spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "All the widgets"
3 | }
4 |
--------------------------------------------------------------------------------
/docs/pages/examples/change-layout/spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Change layout"
3 | }
4 |
--------------------------------------------------------------------------------
/docs/pages/examples/custom-themes/spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Custom themes"
3 | }
4 |
--------------------------------------------------------------------------------
/docs/pages/examples/initial-values/spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Initial values"
3 | }
4 |
--------------------------------------------------------------------------------
/docs/pages/examples/combining-schemas/spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Combining schemas"
3 | }
4 |
--------------------------------------------------------------------------------
/docs/pages/examples/custom-field-validation/spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Custom field validation"
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "react-app",
3 | "rules": {
4 | "jsx-a11y/href-no-hash": 0
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/docs/images/example-liform-react.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Limenius/liform-react/HEAD/docs/images/example-liform-react.png
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-0", "react"],
3 | "plugins": ["transform-object-rest-spread", "lodash"]
4 | }
5 |
6 |
--------------------------------------------------------------------------------
/src/__tests__/tempPolyfills.js:
--------------------------------------------------------------------------------
1 | const raf = global.requestAnimationFrame = (cb) => {
2 | setTimeout(cb, 0)
3 | }
4 |
5 | export default raf
--------------------------------------------------------------------------------
/docs/pages/examples/initial-values/index.md:
--------------------------------------------------------------------------------
1 | Simply by providing an object that matches our schema as the `prop` `initialValues` we can provide initial values.
2 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | examples
2 | scripts
3 | docs
4 | .babelrc
5 | .eslint*
6 | .idea
7 | .editorconfig
8 | .npmignore
9 | .nyc_output
10 | .travis.yml
11 | webpack.*
12 | coverage
13 |
14 |
--------------------------------------------------------------------------------
/docs/pages/examples/refs/index.md:
--------------------------------------------------------------------------------
1 | We can use `$ref` to include snippets defined elsewhere, to avoid repeated definitions.
2 |
3 | In this case we are defining the `address` and later using it.
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | dist
4 | lib
5 | es
6 | .DS_Store
7 | yarn.lock
8 | .nyc_output/
9 | coverage/
10 | examples/bundle*
11 | package-lock.json
12 | built_docs/
13 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | before_install:
4 | - npm install -g npm@latest
5 |
6 | node_js:
7 | - "8"
8 | - "6"
9 |
10 | script:
11 | - npm run lint
12 | - npm test
13 |
--------------------------------------------------------------------------------
/docs/pages/examples/combining-schemas/index.md:
--------------------------------------------------------------------------------
1 | Lifrm react supports oneOf and allOf, that are ways of combining schemas. [Read about them here](https://spacetelescope.github.io/understanding-json-schema/reference/combining.html).
2 |
3 |
--------------------------------------------------------------------------------
/docs/pages/examples/change-layout/index.md:
--------------------------------------------------------------------------------
1 | What if instead of writing our own widgets we want to change the form's layout?
2 | The default layout has a simple submit button with the text "Submit" and shows global errors just above of it.
3 |
--------------------------------------------------------------------------------
/src/themes/bootstrap3/DateWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import BaseInputWidget from "./BaseInputWidget";
3 |
4 | const DateWidget = props => {
5 | return ;
6 | };
7 |
8 | export default DateWidget;
9 |
--------------------------------------------------------------------------------
/src/themes/bootstrap3/TimeWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import BaseInputWidget from "./BaseInputWidget";
3 |
4 | const TimeWidget = props => {
5 | return ;
6 | };
7 |
8 | export default TimeWidget;
9 |
--------------------------------------------------------------------------------
/src/themes/bootstrap3/UrlWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import BaseInputWidget from "./BaseInputWidget";
3 |
4 | const UrlWidget = props => {
5 | return ;
6 | };
7 |
8 | export default UrlWidget;
9 |
--------------------------------------------------------------------------------
/src/themes/bootstrap3/SearchWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import BaseInputWidget from "./BaseInputWidget";
3 |
4 | const SearchWidget = props => {
5 | return ;
6 | };
7 |
8 | export default SearchWidget;
9 |
--------------------------------------------------------------------------------
/src/themes/bootstrap3/StringWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import BaseInputWidget from "./BaseInputWidget";
3 |
4 | const StringWidget = props => {
5 | return ;
6 | };
7 |
8 | export default StringWidget;
9 |
--------------------------------------------------------------------------------
/docs/pages/index.md:
--------------------------------------------------------------------------------
1 | #Doc root
2 |
3 | This is a placeholder of the main page of the docs, that will be generated soon and will replace the current documentation system.
4 |
5 | For now, just go to the current documentation https://limenius.github.io/liform-react/#/
6 |
--------------------------------------------------------------------------------
/src/themes/bootstrap3/PasswordWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import BaseInputWidget from "./BaseInputWidget";
3 |
4 | const PasswordWidget = props => {
5 | return ;
6 | };
7 |
8 | export default PasswordWidget;
9 |
--------------------------------------------------------------------------------
/src/themes/bootstrap3/DateTimeWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import BaseInputWidget from "./BaseInputWidget";
3 |
4 | const DateTimeWidget = props => {
5 | return ;
6 | };
7 |
8 | export default DateTimeWidget;
9 |
--------------------------------------------------------------------------------
/src/themes/bootstrap3/NumberWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import BaseInputWidget from "./BaseInputWidget";
3 |
4 | const NumberWidget = props => {
5 | return ;
6 | };
7 |
8 | export default NumberWidget;
9 |
--------------------------------------------------------------------------------
/docs/pages/examples/simple/server.js:
--------------------------------------------------------------------------------
1 | var express = require('express')
2 |
3 | var app = express()
4 |
5 | app.use(express.static('./'))
6 |
7 | app.listen(3000, 'localhost', function(err) {
8 | if (err) {
9 | console.log(err)
10 | return
11 | }
12 |
13 | console.log('Listening at http://localhost:3000')
14 | })
15 |
--------------------------------------------------------------------------------
/docs/pages/examples/arrays/index.md:
--------------------------------------------------------------------------------
1 | Arrays are just regular items. Currently, the default theme has two ways of represent them (you can of course write your own widget for arrays).
2 |
3 | An array of other objects will be presented as a collection where you can add more items or remove them.
4 |
5 | An array of strings with the restriction `uniqueItems` will be presented as multiple choice list.
6 |
--------------------------------------------------------------------------------
/docs/pages/examples/validation/index.md:
--------------------------------------------------------------------------------
1 | Liform relies by default on [*ajv*](https://github.com/epoberezkin/ajv) to perform on blur validation. You can pass your custom validator using the `prop` `validate`.
2 |
3 | For validation on submit you can provide a validator using the `onSubmit` `prop`. For this, check the documentation of *redux-form*. This example adapted directly from its documentation.
4 |
--------------------------------------------------------------------------------
/docs/pages/examples/refs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | `, you should see something like this:
58 |
59 | 
60 |
61 | # Running the Examples
62 |
63 | To run the examples in `doc/pages/examples`, clone this repository, then run:
64 |
65 | ```bash
66 | npm install
67 | webpack
68 |
69 | cd doc/pages/examples/simple # (for instance)
70 | node server.js
71 | ```
72 |
73 |
74 | # Material UI
75 |
76 | There is a promising work on a theme for Material UI done by [samuelbriole](https://github.com/samuelbriole/react-liform-material-ui-theme)
77 |
78 |
79 |
--------------------------------------------------------------------------------
/src/themes/bootstrap3/ChoiceWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import classNames from "classnames";
4 | import { Field } from "redux-form";
5 | import { zipObject as _zipObject, map as _map } from "lodash";
6 |
7 | const renderSelect = field => {
8 | const className = classNames([
9 | "form-group",
10 | { "has-error": field.meta.touched && field.meta.error }
11 | ]);
12 | const options = field.schema.enum;
13 | const optionNames = field.schema.enum_titles || options;
14 |
15 | const selectOptions = _zipObject(options, optionNames);
16 | return (
17 |
18 |
19 | {field.label}
20 |
21 |
28 | {!field.required &&
29 | !field.multiple && (
30 |
31 | {field.placeholder}
32 |
33 | )}
34 | {_map(selectOptions, (name, value) => {
35 | return (
36 |
37 | {name}
38 |
39 | );
40 | })}
41 |
42 |
43 | {field.meta.touched &&
44 | field.meta.error && (
45 | {field.meta.error}
46 | )}
47 | {field.description && (
48 | {field.description}
49 | )}
50 |
51 | );
52 | };
53 |
54 | const ChoiceWidget = props => {
55 | return (
56 |
67 | );
68 | };
69 |
70 | ChoiceWidget.propTypes = {
71 | schema: PropTypes.object.isRequired,
72 | fieldName: PropTypes.string,
73 | label: PropTypes.string,
74 | theme: PropTypes.object,
75 | multiple: PropTypes.bool,
76 | required: PropTypes.bool
77 | };
78 |
79 | export default ChoiceWidget;
80 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "liform-react",
3 | "version": "0.9.1",
4 | "description": "Generate forms from json-schema to use with React (and redux-form)",
5 | "main": "./lib/index.js",
6 | "scripts": {
7 | "build": "npm run build:lib",
8 | "build:lib": "babel src --out-dir lib",
9 | "lint": "eslint src",
10 | "test": "jest",
11 | "test:cov": "cross-env NODE_ENV=test nyc --reporter=lcov --reporter=text npm test"
12 | },
13 | "keywords": [
14 | "react",
15 | "json-schema",
16 | "form",
17 | "redux-form"
18 | ],
19 | "repository": {
20 | "type": "git",
21 | "url": "https://github.com/limenius/liform-react.git"
22 | },
23 | "author": "Nacho Martin",
24 | "license": "MIT",
25 | "devDependencies": {
26 | "babel-cli": "^6.7.5",
27 | "babel-core": "^6.3.26",
28 | "babel-eslint": "^8.0.3",
29 | "babel-loader": "^7.1.2",
30 | "babel-plugin-lodash": "^3.3.2",
31 | "babel-plugin-react-transform": "^3.0.0",
32 | "babel-plugin-transform-object-rest-spread": "^6.22.0",
33 | "babel-preset-es2015": "^6.6.0",
34 | "babel-preset-react": "^6.5.0",
35 | "babel-preset-stage-0": "^6.5.0",
36 | "babel-register": "^6.7.2",
37 | "cross-env": "^5.1.1",
38 | "enzyme": "^3.2.0",
39 | "enzyme-adapter-react-16": "^1.1.0",
40 | "eslint": "^4.12.1",
41 | "eslint-config-react-app": "^2.0.1",
42 | "eslint-plugin-flowtype": "^2.39.1",
43 | "eslint-plugin-import": "^2.8.0",
44 | "eslint-plugin-jsx-a11y": "^6.0.2",
45 | "eslint-plugin-react": "^7.5.1",
46 | "jest": "^21.2.1",
47 | "json-loader": "^0.5.7",
48 | "nyc": "^11.3.0",
49 | "prettier": "^1.8.2",
50 | "react-addons-test-utils": "^15.6.0",
51 | "webpack": "^3.10.0",
52 | "webpack-dev-server": "^2.9.5",
53 | "webpack-hot-middleware": "^2.21.0"
54 | },
55 | "dependencies": {
56 | "ajv": "^5.2.2",
57 | "classnames": "^2.2.5",
58 | "deepmerge": "^2.0.1",
59 | "prop-types": "^15.5.10",
60 | "react": "^16.2.0",
61 | "react-dom": "^16.2.0",
62 | "react-redux": "^5.0.6",
63 | "redux": "^3.5.2",
64 | "redux-form": "^7.2.0"
65 | },
66 | "jest": {
67 | "verbose": true,
68 | "collectCoverageFrom": [
69 | "src/**/*.{js}",
70 | "!**/node_modules/**",
71 | "!src/__tests__/**"
72 | ],
73 | "testRegex": "src/__tests__/.*spec\\.jsx?$",
74 | "setupFiles": [
75 | "./src/__tests__/setup.js"
76 | ]
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/__tests__/buildSyncValidation.spec.js:
--------------------------------------------------------------------------------
1 | import expect from "expect";
2 | import buildSyncValidation, { setError } from "../buildSyncValidation.js";
3 | import Ajv from "ajv";
4 |
5 | describe("sync validation", () => {
6 | it("Works with basic objects", () => {
7 | let schema = {
8 | properties: {
9 | name: {
10 | type: "string",
11 | minLength: 3
12 | }
13 | },
14 | required: ["name"]
15 | };
16 |
17 | let values = {};
18 | let errors = buildSyncValidation(schema)(values);
19 | expect(errors)
20 | .toHaveProperty("name");
21 | });
22 | it("Errors on arrays are in _error key of the array", () => {
23 | let schema = {
24 | properties: {
25 | columns: {
26 | type: "array",
27 | minItems: 1,
28 | items: {
29 | type: "string"
30 | }
31 | }
32 | },
33 | required: ["columns"]
34 | };
35 |
36 | let values = {};
37 | let errors = buildSyncValidation(schema)(values);
38 | expect(errors)
39 | .toHaveProperty("columns");
40 | expect(errors.columns)
41 | .toHaveProperty("_error");
42 | });
43 | it("Works with array elements", () => {
44 | let schema = {
45 | properties: {
46 | columns: {
47 | type: "array",
48 | minItems: 1,
49 | items: {
50 | type: "string",
51 | minLength: 3
52 | }
53 | }
54 | },
55 | required: ["columns"]
56 | };
57 |
58 | let values = { columns: ["a"] };
59 | let errors = buildSyncValidation(schema)(values);
60 | expect(errors)
61 | .toHaveProperty("columns");
62 | expect(errors.columns)
63 | .toHaveProperty("0");
64 | });
65 | it("Works with several errors", () => {
66 | let schema = {
67 | properties: {
68 | name: {
69 | type: "string",
70 | minLength: 3
71 | },
72 | columns: {
73 | type: "array",
74 | minItems: 1,
75 | items: {
76 | type: "string",
77 | minLength: 3
78 | }
79 | }
80 | },
81 | required: ["columns", "name"]
82 | };
83 |
84 | let values = { columns: ["a"], name: "aa" };
85 | let errors = buildSyncValidation(schema)(values);
86 | expect(errors)
87 | .toHaveProperty("columns");
88 | expect(errors)
89 | .toHaveProperty("name");
90 | expect(errors.columns)
91 | .toHaveProperty("0");
92 | });
93 | });
94 |
--------------------------------------------------------------------------------
/src/themes/bootstrap3/oneOfChoiceWidget.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 | import classNames from "classnames";
4 | import { change } from "redux-form";
5 | import { connect } from "react-redux";
6 | import renderField from "../../renderField";
7 | import { map as _map} from "lodash";
8 |
9 | class OneOfChoiceWidget extends Component {
10 | constructor(props) {
11 | super(props);
12 | this.state = {
13 | choice: 0
14 | };
15 | this.renderOption = this.renderOption.bind(this);
16 | this.selectItem = this.selectItem.bind(this);
17 | }
18 |
19 | render() {
20 | const field = this.props;
21 | const className = classNames(["form-group"]);
22 | const schema = field.schema;
23 | const options = schema.oneOf;
24 |
25 | return (
26 |
27 |
28 | {schema.title}
29 |
30 |
37 | {_map(options, (item, idx) => {
38 | return (
39 |
40 | {item.title || idx}
41 |
42 | );
43 | })}
44 |
45 |
{this.renderOption()}
46 | {field.description && (
47 |
{field.description}
48 | )}
49 |
50 | );
51 | }
52 |
53 | renderOption() {
54 | const field = this.props;
55 | const schema = field.schema.oneOf[this.state.choice];
56 | return renderField(
57 | schema,
58 | field.fieldName,
59 | field.theme,
60 | field.prefix,
61 | field.context
62 | );
63 | }
64 |
65 | selectItem(e) {
66 | const { schema, context, dispatch } = this.props;
67 | for (let property in schema.oneOf[this.state.choice].properties) {
68 | dispatch(change(context.formName, property, null));
69 | }
70 | this.setState({ choice: e.target.value });
71 | }
72 | }
73 |
74 | OneOfChoiceWidget.propTypes = {
75 | schema: PropTypes.object.isRequired,
76 | fieldName: PropTypes.string,
77 | label: PropTypes.string,
78 | theme: PropTypes.object,
79 | multiple: PropTypes.bool,
80 | required: PropTypes.bool
81 | };
82 |
83 | export default connect()(OneOfChoiceWidget);
84 |
--------------------------------------------------------------------------------
/src/themes/bootstrap3/ChoiceMultipleExpandedWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import classNames from "classnames";
3 | import { Field } from "redux-form";
4 |
5 | const zipObject = (props, values) =>
6 | props.reduce(
7 | (prev, prop, i) => Object.assign(prev, { [prop]: values[i] }),
8 | {}
9 | );
10 |
11 | const changeValue = (checked, item, onChange, currentValue = []) => {
12 | if (checked) {
13 | if (currentValue.indexOf(checked) === -1) {
14 | return onChange([...currentValue, item]);
15 | }
16 | } else {
17 | return onChange(currentValue.filter(items => it === item));
18 | }
19 | return onChange(currentValue);
20 | };
21 |
22 | const renderChoice = field => {
23 | const className = classNames([
24 | "form-group",
25 | { "has-error": field.meta.touched && field.meta.error }
26 | ]);
27 | const options = field.schema.items.enum;
28 | const optionNames = field.schema.items.enum_titles || options;
29 |
30 | const selectOptions = zipObject(options, optionNames);
31 | return (
32 |
33 |
34 | {field.label}
35 |
36 | {Object.entries(selectOptions).map(([value, name]) => (
37 |
38 |
39 |
44 | changeValue(
45 | e.target.checked,
46 | value,
47 | field.input.onChange,
48 | field.input.value
49 | )
50 | }
51 | />
52 | {name}
53 |
54 |
55 | ))}
56 |
57 | {field.meta.touched &&
58 | field.meta.error && (
59 |
{field.meta.error}
60 | )}
61 | {field.description && (
62 |
{field.description}
63 | )}
64 |
65 | );
66 | };
67 |
68 | const ChoiceMultipleExpandedWidget = props => {
69 | return (
70 |
81 | );
82 | };
83 |
84 | export default ChoiceMultipleExpandedWidget;
85 |
--------------------------------------------------------------------------------
/src/buildSyncValidation.js:
--------------------------------------------------------------------------------
1 | import Ajv from "ajv";
2 | import merge from "deepmerge";
3 | import { set as _set } from "lodash";
4 |
5 | const setError = (error, schema) => {
6 | // convert property accessor (.xxx[].xxx) notation to jsonPointers notation
7 | if (error.dataPath.charAt(0) === ".") {
8 | error.dataPath = error.dataPath.replace(/[.[]/gi, "/");
9 | error.dataPath = error.dataPath.replace(/[\]]/gi, "");
10 | }
11 | const dataPathParts = error.dataPath.split("/").slice(1);
12 | let dataPath = error.dataPath.slice(1).replace(/\//g, ".");
13 | const type = findTypeInSchema(schema, dataPathParts);
14 |
15 | let errorToSet;
16 | if (type === "array" || type === "allOf" || type === "oneOf") {
17 | errorToSet = { _error: error.message };
18 | } else {
19 | errorToSet = error.message;
20 | }
21 |
22 | let errors = {};
23 | _set(errors, dataPath, errorToSet);
24 | return errors;
25 | };
26 |
27 | const findTypeInSchema = (schema, dataPath) => {
28 | if (!schema) {
29 | return;
30 | } else if (dataPath.length === 0 && schema.hasOwnProperty("type")) {
31 | return schema.type;
32 | } else {
33 | if (schema.type === "array") {
34 | return findTypeInSchema(schema.items, dataPath.slice(1));
35 | } else if (schema.hasOwnProperty("allOf")) {
36 | if (dataPath.length === 0) return "allOf";
37 | schema = { ...schema, ...merge.all(schema.allOf) };
38 | delete schema.allOf;
39 | return findTypeInSchema(schema, dataPath);
40 | } else if (schema.hasOwnProperty("oneOf")) {
41 | if (dataPath.length === 0) return "oneOf";
42 | schema.oneOf.forEach(item => {
43 | let type = findTypeInSchema(item, dataPath);
44 | if (type) {
45 | return type;
46 | }
47 | });
48 | } else {
49 | return findTypeInSchema(
50 | schema.properties[dataPath[0]],
51 | dataPath.slice(1)
52 | );
53 | }
54 | }
55 | };
56 |
57 | const buildSyncValidation = (schema, ajvParam = null) => {
58 | let ajv = ajvParam;
59 | if (ajv === null) {
60 | ajv = new Ajv({
61 | errorDataPath: "property",
62 | allErrors: true,
63 | jsonPointers: false
64 | });
65 | }
66 | return values => {
67 | const valid = ajv.validate(schema, values);
68 | if (valid) {
69 | return {};
70 | }
71 | const ajvErrors = ajv.errors;
72 |
73 | let errors = ajvErrors.map(error => {
74 | return setError(error, schema);
75 | });
76 | // We need at least two elements
77 | errors.push({});
78 | errors.push({});
79 | return merge.all(errors);
80 | };
81 | };
82 |
83 | export default buildSyncValidation;
84 |
85 | export { setError };
86 |
--------------------------------------------------------------------------------
/docs/pages/examples/custom-themes/index.md:
--------------------------------------------------------------------------------
1 | Liform comes with a theme written for Bootstrap 3. However, you can of course write your own theme or modify another theme by providing widgets.
2 |
3 | There is no need to write a *full theme*. It is possible that you don't need a widget for `color`, because your data simply doesn't deal with colors. In that case, you can simply left it undefined in your theme.
4 |
5 | Themes are simly JavaScript objects. The DefaultTheme is something like:
6 |
7 | ```
8 | export default {
9 | object: ObjectWidget,
10 | string: StringWidget,
11 | textarea: TextareaWidget,
12 | email: EmailWidget,
13 | integer: NumberWidget,
14 | number: NumberWidget,
15 | money: MoneyWidget,
16 | percent: PercentWidget,
17 | array: ArrayWidget,
18 | boolean: CheckboxWidget,
19 | password: PasswordWidget,
20 | search: SearchWidget,
21 | url: UrlWidget,
22 | color: ColorWidget,
23 | choice: ChoiceWidget,
24 | }
25 | ```
26 |
27 | So you can simply write a Widget that will be used whenever a field of a given type or widget is required.
28 |
29 | ### Writing a widget
30 |
31 | Suppose that we want to override the widget for `string` with a simple `input` tag, with no Bootstrap markup. We have to define a field using `redux-form`. Check out the documentation of `redux-form` for more details on what is a `Field` or a `Field Array` if you are not familiarized with it.
32 |
33 | If you feel like you need to add additional options to your schema, just do so. In this case we are defining the option `labelColor` in the schema and making use of it. Since the field has access to the schema of the property, we can just access it.
34 |
35 |
36 | ```
37 | import { Field } from 'redux-form'
38 |
39 | const RenderInput = field => {
40 | return (
41 |
42 | {field.label}
43 |
44 | {field.meta.touched && field.meta.error && {field.meta.error} }
45 | {field.description && {field.description} }
46 |
47 | )
48 | }
49 |
50 | const MyStringWidget = (props) => {
51 | return (
52 |
62 | )
63 | }
64 |
65 | ```
66 |
67 | ### Creating a derived theme
68 |
69 | As themes are just objects, we can simply extend an existing theme with our new widget:
70 |
71 | ```
72 | const myTheme = {...DefaultTheme, string: MyStringWidget}
73 | ```
74 |
75 | And pass it as props when creating the `Liform` component:
76 |
77 | ```
78 |
{console.log(v)}}/>
79 | ```
80 |
81 |
--------------------------------------------------------------------------------
/docs/pages/examples/custom-field-validation/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { createStore, combineReducers } from 'redux'
4 | import { reducer as formReducer, Field } from 'redux-form'
5 | import { Provider } from 'react-redux'
6 | import Liform, { DefaultTheme } from '../../../../src/'
7 | import classNames from "classnames"
8 |
9 | const RenderPassword = field => {
10 | const className = classNames([
11 | 'form-group',
12 | { 'has-error' : field.meta.touched && (field.meta.error || field.meta.warning) }
13 | ])
14 | return (
15 |
16 | {field.label}
17 |
18 | {field.meta.touched && field.meta.error && {field.meta.error} }
19 | {field.meta.touched && field.meta.warning && {field.meta.warning} }
20 | {field.description && {field.description} }
21 |
22 | )
23 | }
24 |
25 | const violations = value => {
26 | let rulesViolated = 4
27 | const letter = /[a-z]/
28 | const upper =/[A-Z]/
29 | const number = /[0-9]/
30 | if (value && value.length > 6) {
31 | rulesViolated --
32 | }
33 | if (letter.test(value)) {
34 | rulesViolated --
35 | }
36 | if (upper.test(value)) {
37 | rulesViolated --
38 | }
39 | if (number.test(value)) {
40 | rulesViolated --
41 | }
42 | return rulesViolated
43 | }
44 |
45 | const validatePassword = value => {
46 | if (violations(value) > 2) {
47 | return 'Password is VERY weak'
48 | }
49 | }
50 |
51 | const warnPassword = value => {
52 | const rulesViolated = violations(value)
53 | if (rulesViolated > 0 && rulesViolated <= 2) {
54 | return 'Password is weak'
55 | }
56 | }
57 |
58 | const MyPasswordWidget = (props) => {
59 | return (
60 |
72 | )
73 | }
74 |
75 | const validateRepeatedPassword = (value, allValues) => {
76 | if (allValues.password != allValues.password_again) {
77 | return 'Passwords don\'t match'
78 | }
79 | }
80 |
81 | const MyPasswordRepeatedWidget = (props) => {
82 | return (
83 |
94 | )
95 | }
96 | const Demo = () => {
97 | const reducer = combineReducers({ form: formReducer })
98 | const store = createStore(reducer)
99 | const myTheme = { ...DefaultTheme, password: MyPasswordWidget, repeatedpassword: MyPasswordRepeatedWidget }
100 | const schema = {
101 | 'type':'object',
102 | 'properties': {
103 | 'password': { 'type':'string', 'title': 'Password', 'widget': 'password' },
104 | 'password_again': { 'type':'string', 'title': 'Repeat Password', 'widget': 'repeatedpassword' },
105 | }
106 | }
107 | return (
108 |
109 | {console.log(v)}}/>
110 |
111 | )
112 | }
113 |
114 | ReactDOM.render(
115 | ,
116 | document.getElementById('placeholder')
117 | )
118 |
119 |
--------------------------------------------------------------------------------
/src/themes/bootstrap3/ArrayWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import renderField from "../../renderField";
4 | import { FieldArray } from "redux-form";
5 | import { times as _times} from "lodash";
6 | import ChoiceWidget from "./ChoiceWidget";
7 | import classNames from "classnames";
8 |
9 | const renderArrayFields = (
10 | count,
11 | schema,
12 | theme,
13 | fieldName,
14 | remove,
15 | context,
16 | swap
17 | ) => {
18 | const prefix = fieldName + ".";
19 | if (count) {
20 | return _times(count, idx => {
21 | return (
22 |
23 |
24 | {idx !== count - 1 && count > 1 ? (
25 | {
28 | e.preventDefault();
29 | swap(idx, idx + 1);
30 | }}
31 | >
32 |
33 |
34 | ) : (
35 | ""
36 | )}
37 | {idx !== 0 && count > 1 ? (
38 | {
41 | e.preventDefault();
42 | swap(idx, idx - 1);
43 | }}
44 | >
45 |
46 |
47 | ) : (
48 | ""
49 | )}
50 |
51 | {
54 | e.preventDefault();
55 | remove(idx);
56 | }}
57 | >
58 |
59 |
60 |
61 | {renderField(
62 | { ...schema, showLabel: false },
63 | idx.toString(),
64 | theme,
65 | prefix,
66 | context
67 | )}
68 |
69 | );
70 | });
71 | } else {
72 | return null;
73 | }
74 | };
75 |
76 | const renderInput = field => {
77 | const className = classNames([
78 | "arrayType",
79 | { "has-error": field.meta.submitFailed && field.meta.error }
80 | ]);
81 |
82 | return (
83 |
84 |
{field.label}
85 | {field.meta.submitFailed &&
86 | field.meta.error && (
87 |
{field.meta.error}
88 | )}
89 | {renderArrayFields(
90 | field.fields.length,
91 | field.schema.items,
92 | field.theme,
93 | field.fieldName,
94 | idx => field.fields.remove(idx),
95 | field.context,
96 | (a, b) => {
97 | field.fields.swap(a, b);
98 | }
99 | )}
100 |
field.fields.push()}
104 | >
105 | Add
106 |
107 |
108 |
109 | );
110 | };
111 |
112 | const CollectionWidget = props => {
113 | return (
114 |
124 | );
125 | };
126 |
127 | const ArrayWidget = props => {
128 | // Arrays are tricky because they can be multiselects or collections
129 | if (
130 | props.schema.items.hasOwnProperty("enum") &&
131 | props.schema.hasOwnProperty("uniqueItems") &&
132 | props.schema.uniqueItems
133 | ) {
134 | return ChoiceWidget({
135 | ...props,
136 | schema: props.schema.items,
137 | multiple: true
138 | });
139 | } else {
140 | return CollectionWidget(props);
141 | }
142 | };
143 |
144 | ArrayWidget.propTypes = {
145 | schema: PropTypes.object.isRequired,
146 | fieldName: PropTypes.string,
147 | label: PropTypes.string,
148 | theme: PropTypes.object,
149 | context: PropTypes.object
150 | };
151 |
152 | export default ArrayWidget;
153 |
--------------------------------------------------------------------------------
/docs/pages/examples/all-widgets/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import { createStore, combineReducers } from 'redux'
4 | import { reducer as formReducer } from 'redux-form'
5 | import { Provider } from 'react-redux'
6 | import Liform from '../../../../src/'
7 |
8 | const Demo = () => {
9 | const reducer = combineReducers({ form: formReducer })
10 | const store = createStore(reducer)
11 | const schema = {
12 | 'type':'object',
13 | 'properties': {
14 | 'choice': {
15 | 'type': 'string',
16 | 'enum': [
17 | 'foo',
18 | 'bar'
19 | ]
20 | },
21 | 'string': {
22 | 'type': 'string'
23 | },
24 | 'checkbox': {
25 | 'type': 'boolean',
26 | },
27 | 'color': {
28 | 'type': 'string',
29 | 'widget': 'color'
30 | },
31 | 'date': {
32 | 'type': 'string',
33 | 'widget': 'date'
34 | },
35 | 'datetime': {
36 | 'type': 'string',
37 | 'widget': 'datetime'
38 | },
39 | 'compatible-date': {
40 | 'type': 'string',
41 | 'widget': 'compatible-date',
42 | 'format': 'date'
43 | },
44 | 'compatible-datetime': {
45 | 'type': 'string',
46 | 'widget': 'compatible-datetime',
47 | 'format': 'date-time'
48 | },
49 | 'email': {
50 | 'type': 'string',
51 | 'widget': 'email',
52 | 'format': 'email'
53 | },
54 | 'file': {
55 | 'type': 'string',
56 | 'widget': 'file'
57 | },
58 | 'money': {
59 | 'type': 'string',
60 | 'widget': 'money'
61 | },
62 | 'number': {
63 | 'type': 'number',
64 | 'widget': 'number'
65 | },
66 | 'password': {
67 | 'type': 'string',
68 | 'widget': 'password'
69 | },
70 | 'percent': {
71 | 'type': 'number',
72 | 'widget': 'percent'
73 | },
74 | 'search': {
75 | 'type': 'string',
76 | 'widget': 'search'
77 | },
78 | 'textarea': {
79 | 'type': 'string',
80 | 'widget': 'textarea'
81 | },
82 | 'url': {
83 | 'type': 'string',
84 | 'widget': 'url'
85 | },
86 | 'tasks': {
87 | 'type':'array',
88 | 'title': 'A list of objects',
89 | 'items': {
90 | 'type': 'object',
91 | 'properties': {
92 | 'name': {
93 | 'type': 'string',
94 | 'title': 'Name of the Task'
95 | },
96 | 'dueTo': {
97 | 'type': 'string',
98 | 'title': 'Due To',
99 | 'widget': 'datetime',
100 | 'format': 'date-time'
101 | }
102 | }
103 | }
104 | },
105 | 'multiple': {
106 | 'type': 'array',
107 | 'title': 'Multiple choices',
108 | 'items': {
109 | 'type': 'string',
110 | 'enum': [
111 | '1',
112 | '2'
113 | ],
114 | 'enum_titles': [ 'one', 'two' ]
115 | },
116 | 'uniqueItems': true
117 | },
118 | }
119 | }
120 | return (
121 |
122 | {console.log(v)}} initialValues={{
123 | 'tasks' : [
124 | { 'name' : 'first task' },
125 | ],
126 | 'multiple' : [ '1' ]
127 | }}/>
128 |
129 | )
130 | }
131 |
132 | ReactDOM.render(
133 | ,
134 | document.getElementById('placeholder')
135 | )
136 |
137 |
--------------------------------------------------------------------------------
/src/themes/bootstrap3/CompatibleDateWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import classNames from "classnames";
3 | import { Field } from "redux-form";
4 | import DateSelector from "./DateSelector";
5 |
6 | // produces an array [start..end-1]
7 | const range = (start, end) =>
8 | Array.from({ length: end - start }, (v, k) => k + start);
9 |
10 | // produces an array [start..end-1] padded with zeros, (two digits)
11 | const rangeZeroPad = (start, end) =>
12 | Array.from({ length: end - start }, (v, k) => ("0" + (k + start)).slice(-2));
13 |
14 | const extractYear = value => {
15 | return extractDateToken(value, 0);
16 | };
17 | const extractMonth = value => {
18 | return extractDateToken(value, 1);
19 | };
20 | const extractDay = value => {
21 | return extractDateToken(value, 2);
22 | };
23 |
24 | const extractDateToken = (value, index) => {
25 | if (!value) {
26 | return "";
27 | }
28 | const tokens = value.split(/-/);
29 | if (tokens.length !== 3) {
30 | return "";
31 | }
32 | return tokens[index];
33 | };
34 |
35 | class CompatibleDate extends React.Component {
36 | constructor(props, context) {
37 | super(props, context);
38 | this.state = {
39 | year: null,
40 | month: null,
41 | day: null,
42 | hour: null,
43 | minute: null,
44 | second: null
45 | };
46 | this.onBlur = this.onBlur.bind(this);
47 | }
48 |
49 | // Produces a RFC 3339 full-date from the state
50 | buildRfc3339Date() {
51 | const year = this.state.year || "";
52 | const month = this.state.month || "";
53 | const day = this.state.day || "";
54 | return year + "-" + month + "-" + day;
55 | }
56 |
57 | onChangeField(field, e) {
58 | const value = e.target.value;
59 | let changeset = {};
60 | changeset[field] = value;
61 | this.setState(changeset, () => {
62 | this.props.input.onChange(this.buildRfc3339Date());
63 | });
64 | }
65 |
66 | onBlur() {
67 | this.props.input.onBlur(this.buildRfc3339Date());
68 | }
69 |
70 | render() {
71 | const field = this.props;
72 | const className = classNames([
73 | "form-group",
74 | { "has-error": field.meta.touched && field.meta.error }
75 | ]);
76 | return (
77 |
78 |
79 | {field.label}
80 |
81 |
82 |
83 |
91 |
92 |
93 |
101 |
102 |
103 |
111 |
112 |
113 | {field.meta.touched &&
114 | field.meta.error && (
115 |
{field.meta.error}
116 | )}
117 | {field.description && (
118 |
{field.description}
119 | )}
120 |
121 | );
122 | }
123 | }
124 | const CompatibleDateWidget = props => {
125 | return (
126 |
138 | );
139 | };
140 |
141 | export default CompatibleDateWidget;
142 |
143 | // Only for testing purposes
144 | export { extractDateToken };
145 |
--------------------------------------------------------------------------------
/src/themes/bootstrap3/CompatibleDateTimeWidget.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import classNames from "classnames";
3 | import { Field } from "redux-form";
4 | import DateSelector from "./DateSelector";
5 |
6 | // produces an array [start..end-1]
7 | const range = (start, end) =>
8 | Array.from({ length: end - start }, (v, k) => k + start);
9 |
10 | // produces an array [start..end-1] padded with zeros, (two digits)
11 | const rangeZeroPad = (start, end) =>
12 | Array.from({ length: end - start }, (v, k) => ("0" + (k + start)).slice(-2));
13 |
14 | const extractYear = value => {
15 | return extractDateTimeToken(value, 0);
16 | };
17 | const extractMonth = value => {
18 | return extractDateTimeToken(value, 1);
19 | };
20 | const extractDay = value => {
21 | return extractDateTimeToken(value, 2);
22 | };
23 | const extractHour = value => {
24 | return extractDateTimeToken(value, 3);
25 | };
26 | const extractMinute = value => {
27 | return extractDateTimeToken(value, 4);
28 | };
29 | const extractSecond = value => {
30 | return extractDateTimeToken(value, 5);
31 | };
32 |
33 | const extractDateTimeToken = (value, index) => {
34 | if (!value) {
35 | return "";
36 | }
37 | // Remove timezone Z
38 | value = value.substring(0, value.length - 1);
39 | const tokens = value.split(/[-T:]/);
40 | if (tokens.length !== 6) {
41 | return "";
42 | }
43 | return tokens[index];
44 | };
45 |
46 | class CompatibleDateTime extends React.Component {
47 | constructor(props, context) {
48 | super(props, context);
49 | this.state = {
50 | year: null,
51 | month: null,
52 | day: null,
53 | hour: null,
54 | minute: null,
55 | second: null
56 | };
57 | this.onBlur = this.onBlur.bind(this);
58 | }
59 |
60 | // Produces a RFC 3339 full-date from the state
61 | buildRfc3339Date() {
62 | const year = this.state.year || "";
63 | const month = this.state.month || "";
64 | const day = this.state.day || "";
65 | return year + "-" + month + "-" + day;
66 | }
67 |
68 | // Produces a RFC 3339 datetime from the state
69 | buildRfc3339DateTime() {
70 | const date = this.buildRfc3339Date();
71 | const hour = this.state.hour || "";
72 | const minute = this.state.minute || "";
73 | const second = this.state.second || "";
74 | return date + "T" + hour + ":" + minute + ":" + second + "Z";
75 | }
76 |
77 | onChangeField(field, e) {
78 | const value = e.target.value;
79 | let changeset = {};
80 | changeset[field] = value;
81 | this.setState(changeset, () => {
82 | this.props.input.onChange(this.buildRfc3339DateTime());
83 | });
84 | }
85 | onBlur() {
86 | this.props.input.onBlur(this.buildRfc3339DateTime());
87 | }
88 | render() {
89 | const field = this.props;
90 | const className = classNames([
91 | "form-group",
92 | { "has-error": field.meta.touched && field.meta.error }
93 | ]);
94 | return (
95 |
96 |
97 | {field.label}
98 |
99 |
100 |
101 |
109 |
110 |
111 |
119 |
120 |
121 |
129 |
130 |
131 |
139 |
140 |
141 |
149 |
150 |
151 |
159 |
160 |
161 | {field.meta.touched &&
162 | field.meta.error && (
163 |
{field.meta.error}
164 | )}
165 | {field.description && (
166 |
{field.description}
167 | )}
168 |
169 | );
170 | }
171 | }
172 | const CompatibleDateTimeWidget = props => {
173 | return (
174 |
186 | );
187 | };
188 |
189 | export default CompatibleDateTimeWidget;
190 |
191 | // Only for testing purposes
192 | export { extractDateTimeToken };
193 |
--------------------------------------------------------------------------------