├── .babelrc
├── .gitignore
├── .npmignore
├── .nvmrc
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── docs
├── migrate-1.2-to-1.3.md
└── schema-onchange.md
├── jsdoc2md
└── README.hbs
├── package-lock.json
├── package.json
├── src
├── autoform_state.js
├── coercing.js
├── createSchema.js
├── index.js
├── index_base.js
├── pubsub.js
├── translate.js
├── translation_utils.js
├── translations
│ ├── en.js
│ ├── es.js
│ └── index.js
├── ui
│ ├── Autofield.jsx
│ ├── AutofieldContainer.jsx
│ ├── Autoform.jsx
│ ├── AutoformBase.jsx
│ ├── baseSkin.js
│ ├── componentRender.jsx
│ ├── components
│ │ ├── Button.jsx
│ │ ├── Checkbox.jsx
│ │ ├── FieldPropsOverride.jsx
│ │ ├── InputArrayPanel.jsx
│ │ ├── InputArrayTable.jsx
│ │ ├── InputArrayWrap.jsx
│ │ ├── InputWrap.jsx
│ │ ├── Panel.jsx
│ │ ├── Radio.jsx
│ │ ├── RadiosWrap.jsx
│ │ ├── Select.jsx
│ │ ├── Submodel.jsx
│ │ └── index.js
│ ├── defaultSkin.jsx
│ ├── deletedMark.js
│ ├── ducks
│ │ ├── index.js
│ │ └── inputArray.js
│ └── svgs
│ │ ├── AddGlyph.jsx
│ │ ├── RemoveGlyph.jsx
│ │ └── svgUtils.js
└── utils.js
├── test
├── addProps.react.test.js
├── coerce.react.test.js
├── controlled.react.test.js
├── custom-element.react.test.js
├── default-values.react.test.js
├── defaultSkin
│ ├── array.react.test.js
│ ├── boolean.react.test.js
│ ├── number.react.test.js
│ ├── password.react.test.js
│ ├── radios.react.test.js
│ ├── range.react.test.js
│ ├── select.react.test.js
│ └── subschema.react.test.js
├── field-override.react.test.js
├── forceErrors.react.test.js
├── form.react.test.js
├── helperText.react.test.js
├── imperative.react.test.js
├── initialization.react.test.js
├── inputArray.react.test.js
├── noAutocomplete.react.test.js
├── options.react.test.js
├── schema-onChange.react.test.js
├── submit.react.test.js
├── throws.test.js
├── translation.test.js
├── utils
│ ├── ErrorBoundary.jsx
│ ├── _mutationObserverHack.js
│ ├── buttonHack.js
│ ├── changeField.js
│ ├── createParenter.js
│ ├── createSubmitMocks.js
│ └── enzymeConfig.js
└── validations.react.test.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["@babel/preset-env", {
4 | "targets": {
5 | "node": true
6 | }
7 | }],
8 | [ "@babel/preset-react", {
9 | "runtime": "automatic"
10 | }]
11 | ],
12 | "plugins": [
13 | "@babel/plugin-transform-spread",
14 | "@babel/plugin-transform-object-rest-spread",
15 | "@babel/plugin-transform-class-properties",
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | lib
3 | node_modules
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | lib
2 | node_modules
3 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v20
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - "11.10.1"
5 |
6 | install:
7 | - npm install
8 |
9 | script:
10 | - npm test
11 |
12 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 1.3.15
4 |
5 | * Update dependencies
6 |
7 | ## 1.3.14
8 |
9 | ### Fixed
10 |
11 | * `validate` validation will correctly translated return value (`true` for "ok")
12 | * Passing `errorText` to wrapped array handler
13 |
14 | ## 1.3.13
15 |
16 | ### Changed
17 |
18 | * Array values are overwritten from initialValues instead of appending them.
19 | * Array mode can be overridden in fieldSchema.
20 | * Array field name digs into `_field` string id.
21 |
22 | ## 1.3.12
23 |
24 | ### Added
25 |
26 | * `getValues()` to the imperative handlers that returns the complete coerced form values
27 | * Tests for the rest of the imperative functions
28 | * Test for controlled components
29 |
30 | ### Fixed
31 |
32 | * Fixed imperative `reset()`
33 |
34 | ## 1.3.11
35 |
36 | ### Added
37 |
38 | * Allow React 18 as peer dependency
39 |
40 | ## 1.3.10
41 |
42 | ### Fixed
43 |
44 | * Externals errors with `forceErrors` now work with nested fields.
45 |
46 | ## 1.3.9
47 |
48 | ### Added
49 |
50 | * Be able to set errors from outside (for example, server) with `forceErrors` param in `Autoform`
51 |
52 | ## 1.3.8
53 |
54 | ### Fixed
55 |
56 | * Fixed mistake that locked react to 17 in peerDependencies.
57 |
58 | ## 1.3.7
59 |
60 | ### Changed
61 |
62 | * Updated some modules to fix security advisories
63 | * In webpack, separated mode from minification so there can be no minimized build that is still production like `npm run build:umd`
64 |
65 | ## 1.3.6
66 |
67 | * Put labels in some readme functions
68 | * Allow to add strings to specific language
69 | * Allow to change string base path with `trPathSetBase`
70 | * ``'s `onChange` prop to listen to any document changes
71 | * Renamed skin `render` to `props` to make it clearer. The older `render` still works and I don't plan on deprecating it.
72 |
73 | ## 1.3.5
74 |
75 | * Correct formatting in `docs/schema-onchange.md`
76 | * `setVisible` also prevents field occurrence in the submit results
77 | * Pass correctly `ref` in default skin in order for checkboxes to work
78 |
79 | ## 1.3.4
80 |
81 | ### Fixed
82 |
83 | * default skin select will receive correct initial values
84 |
85 | ## 1.3.3
86 |
87 | ### Added
88 |
89 | * Pass `inputRef` to skin function components to allow them to have the value changed without need them to be controlled
90 | * Pass `helperText` to skin function components too. Helps material-ui.
91 |
92 | ## 1.3.2
93 |
94 | ### Added
95 |
96 | * Tests for schema `onChange`
97 | * Talk about creating errors with `onChange` and test it
98 | * The helper text is specified with `setHelperText` from `onChange`, then schema `helperText` then model translated string `models..._helper` then blank.
99 |
100 | ### Changed
101 |
102 | * `FieldPropsOverride`'s `onChange` now works like schema's and receives [form control context](https://github.com/dgonz64/react-hook-form-auto/blob/master/docs/schema-onchange.md#context).
103 | * `onChange` is not considered experimental anymore
104 |
105 | ### Fixed
106 |
107 | * Select type schema `onChange` value passing corrected
108 | * More resilient way to create autofield state control
109 |
110 | ## 1.3.1
111 |
112 | ### Fixed
113 |
114 | * Stop ignoring `dist` folder
115 |
116 | ## 1.3.0
117 |
118 | ### Added
119 |
120 | * Compatibility with react-hook-form 7
121 | * [Guide for skin migrations](docs/migrate-1.2-to-1.3.md)
122 | * Inputs are bound individually to errors. That reduces whole form re-renders.
123 | * EXPERIMENTAL: Allows field value reaction with `onChange` in the schema definition. [Documentation](docs/schema-onchange.md)
124 | * Schemas can set a helper text for the field with `helperText`
125 | * (skins) `react-hook-form-auto` now takes care of register and not the skin. The skin will receive `onChange` and `onBlur`
126 | * (skins) Can take care of the logic for controlled components. Just add `controlled: true` to your skin entry and the skin component will receive also `value`.
127 | * (skins) Can choose a name for error field with `nameForErrors` skin attribute
128 |
129 | ### Changed
130 |
131 | * `onErrors` prop of `Autoform` is only called in submit
132 | * label's `for` and field's `id` now include the schema name in order to avoid potential collisions
133 | * `InputWrap` is now in charge of `array` type error display
134 | * Skin documentation was updated
135 | * (skins) `errors` are now only one and it's a string called `errorText`.
136 | * (skins) `array` the fields now unregister removed subobjects
137 | * (internal) `objectTraverse` now supports `returnValue` option that returns the value instead of its context
138 | * (internal) Tests updated for the new DOM layout
139 | * (internal) `componentRender` refactor, separated to `componentRender`, `AutofieldContainer` and `AutoField`
140 | * (internal, important for tests) field names follow the new `react-hook-form` dotted syntax. You can continue to use the bracked syntax in `FieldPropsOverride` for example.
141 | * (internal) `coerceRef` becomes `stateRef` and holds state, not only value. Also its keys are now paths instead of a fully structured doc.
142 |
143 | ### Fixed
144 |
145 | * Make clear in docs that you need version 6 of `react-hook-form` for older versions of this library in the deprecation section
146 | * Uncontrolled components with only update individually when there are errors instead of rendering the whole tree
147 | * `array` type component now unregister
148 |
149 | ### Removed
150 |
151 | * Removed typescript from project roadmap. Pull requests are welcome.
152 |
153 | ## 1.2.11
154 |
155 | ### Added
156 |
157 | * React 7 support
158 |
159 | ### Fixed
160 |
161 | * Updated some modules for security
162 | * Removed unneeded field triggers
163 |
164 | ## 1.2.10
165 |
166 | ### Fixed
167 |
168 | * Fixed FieldPropsOverride overriding everything
169 |
170 | ## 1.2.9
171 |
172 | ### Fixed
173 |
174 | * Fixed FieldPropsOverride identification after minification
175 |
176 | ## 1.2.8
177 |
178 | ### Added
179 |
180 | * You can affect whole array fields with a more semantic `FieldPropsOverride` name.
181 |
182 | ## 1.2.7
183 |
184 | ### Added
185 |
186 | * Actual example as to how to specify skin overrides.
187 | * Update React Native status.
188 | * Pass overrides also separately in the `overrides` prop.
189 |
190 | ### Fixed
191 |
192 | * Some security concerns
193 |
194 | ## 1.2.6
195 |
196 | ### Added
197 |
198 | * Now passing `styles` to `addGlyph` and `removeGlyph` skin components.
199 | * Talk about React Native project.
200 |
201 | ## 1.2.5
202 |
203 | ### Added
204 |
205 | * Validation rules are passed to skin component in order to help with ``.
206 | * Instance method `reset()` that works with ``.
207 |
208 | ### Fixed
209 |
210 | * Now exporting render functions. Rarely needed but documented.
211 | * (Internal) Moved `trPath()` to `translation_utils.js`.
212 |
213 | ## 1.2.4
214 |
215 | ### Added
216 |
217 | * Exporting AutoformBase to allow for more generic skins: Library is now compatible with ReactNative.
218 | * Every component is now configurable.
219 |
220 | ## 1.2.3
221 |
222 | ### Added
223 |
224 | * Some basic error messages on obvious things that we programmers usually forget, like not passing a `schema`.
225 | * Implemented minChildren and maxChildren
226 |
227 | ### Fixed
228 |
229 | * Forgot to talk about blueprint ui in README
230 | * Actually export `InputWrap` as documentation refers to it.
231 | * Array children now receive validation errors correctly.
232 |
233 | ## 1.2.2
234 |
235 | ### Added
236 |
237 | * Now you can control autocomplete="off" from Autoform (general) and schema (individual) see README.md
238 | * You can call `stringExists(id)` to see if it exists
239 | * Be able to pass any prop from schema to wrapper and input
240 | * Talk about brand new blueprintjs skin
241 |
242 | ### Changed
243 |
244 | * The X symbol has been removed from the errored input
245 |
246 | ## 1.2.1
247 |
248 | * Fixing package.lock
249 |
250 | ## 1.2.0
251 |
252 | ### Added
253 |
254 | * Allow wrapper to be specified also per component type in skin
255 |
256 | ### Changed
257 |
258 | * Updated dependencies
259 | * Using ReactHookForm 6
260 | * defaultValue is calculated before skinning and not after
261 | * Slightly better required string
262 |
263 | ### Fixed
264 |
265 | * Better boolean result for boolean fields
266 | * minimist security concern
267 | * acorn security concern
268 | * Updated docs with sandbox demos
269 |
270 | ## 1.1.2
271 |
272 | ### Changed
273 |
274 | * `register` will be passed to skin components with validation already set up
275 | * Documented the way to use `register` in skins
276 |
277 | ## 1.1.1
278 |
279 | ### Added
280 |
281 | * Error translation moved to translation_utils.js
282 | * Export more functions to help constructing skins
283 | * trField to translate field names
284 | * Allow processOptions to manage with standard control props
285 | * Allow button and form skinage
286 | * Pass submit to the form button onClick in order to facilitate imperative use
287 | * Added more components to skin to allow easier and more granular skinage
288 |
289 | ### Fixed
290 |
291 | * Integrate all README changes into generator (jsdoc2md/README.hbs) that were put in the README by mistake.
292 | * Incorrect translations for min, max, minLength and maxLength
293 |
294 | ## 1.1.0
295 |
296 | ### Added
297 |
298 | * You can change document values outside of Autoform
299 | * Documentation about Autoform's ref
300 | * Passing ReacHookForm's formHook to form components
301 | * Components get autoform props in autoformProps prop
302 | * Components get information about their place in arrays
303 | * Works with ReactHookForm 4
304 |
305 | ## 1.0.3 (12/11/2019)
306 |
307 | ### Fixed
308 |
309 | * Fixed checkboxes
310 | * Some documentation formating concerning translations
311 |
312 | ### Added
313 |
314 | * Tests for more components in the submit test
315 |
316 | ## 1.0.2 (26/10/2019)
317 |
318 | ### Added
319 |
320 | * Add and remove items from arrays is done with svg now
321 | * Sample and documentation for styling with Bootstrap 4
322 |
323 | ## 1.0.1 (14/10/2019)
324 |
325 | ### Updated
326 |
327 | * Better documentation concerning styles
328 |
329 | ## 1.0.0 (13/10/2019)
330 |
331 | Initial! :metal:
332 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | #### Bugs
2 |
3 | If you find a bug, open an issue. There will be probably needed code samples or, better, a project that reproduces the bug. In any case, provide as much information as you can about the expectations vs reality.
4 |
5 | #### Coding
6 |
7 | Some pointers in [README](https://github.com/dgonz64/react-hook-form-auto#help-wanted--contribute)
8 |
9 | ## Thank you very much!
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 David González
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docs/migrate-1.2-to-1.3.md:
--------------------------------------------------------------------------------
1 | # Affects skins
2 |
3 | After a [good question](https://github.com/dgonz64/react-hook-form-auto/issues/16) I though the best way to manage field change reaction was to upgrade to react-hook-form 7 because tracking `ref` felt kind of hacky. If you don't have skin components, you are good to go. Just update corresponding the library of the family, and react-hook-form.
4 |
5 | If you made a skin, I hope this migration guide helps.
6 |
7 | When I refer to component I mean the `skin[type].component` (or `skin[type].props.component`) entry.
8 |
9 | ## register vs onChange and onBlur
10 |
11 | The biggest difference is that now inputs are automatically registered and input component has `onChange` and `onBlur` as props that come from `const { field } = register()`. You should use those in the skin instead of `ref` or calling `register()` manually.
12 |
13 | ```diff
14 | component: (props) => {
15 | const {
16 | id,
17 | name,
18 | - register,
19 | defaultValue,
20 | + onChange,
21 | + onBlur
22 | } = props
23 |
24 | return (
25 |
33 | )
34 | }
35 | ```
36 |
37 | ## setValue
38 |
39 | Still works for plain value (not event), both for controlled and uncontrolled inputs.
40 |
41 | ```diff
42 | component: (props) => {
43 | const {
44 | name,
45 | defaultValue,
46 | + setValue,
47 | + onBlur
48 | } = props
49 |
50 | + const handleChange = newValue => {
51 | + setValue(name, newValue)
52 | + }
53 | +
54 | return (
55 |
61 | )
62 | }
63 | }
64 |
65 | ```
66 |
67 | ## id
68 |
69 | Components now receive id
70 |
71 | ```diff
72 | component: (props) => {
73 | const {
74 | + id,
75 | name,
76 | } = props
77 |
78 | return (
79 |
84 | )
85 | }
86 |
87 | ```
88 |
89 | ## Controlled
90 |
91 | If the skin resolver is declared as controlled, example:
92 |
93 | ```javascript
94 | string: {
95 | controlled: true,
96 | component: ({ id, name, onChange, onBlur, value }) => {
97 | ...
98 | }
99 | },
100 | ```
101 |
102 | Then `skin[type].component` (or `skin[type].props.component`) will be rendered with `value` prop updated each time it changes.
103 |
104 | ## errorText
105 |
106 | Instead of an error object skin receives now `errorText` as an already translated string.
107 |
108 | ```diff
109 | const ControlAdaptor = ({
110 | name,
111 | - errors,
112 | + errorText,
113 | }) => {
114 | - const error = errors[field]
115 | - const errorText = typeof error == 'object' ? tr(error.message, fieldSchema) : ''
116 |
117 | return
{errorText}
118 | }
119 | ```
120 |
--------------------------------------------------------------------------------
/docs/schema-onchange.md:
--------------------------------------------------------------------------------
1 | # Schema's onChange
2 |
3 | Now schemas can make the form react to changes. To acomplish that you need to set `onChange` accordingly.
4 |
5 | It's done by intercepting both `onChange` and `setValue`.
6 |
7 | Consider this example:
8 |
9 | ```javascript
10 | const pet = createSchema('pet', {
11 | name: {
12 | type: String,
13 | required: true,
14 | maxLength: 8
15 | },
16 | heads: {
17 | type: Number,
18 | onChange: (value, { arrayControl }) => {
19 | if (value == '42')
20 | arrayControl.add()
21 | if (value == '13')
22 | arrayControl.remove(arrayControl.index)
23 | },
24 | helperText: 'Enter 42 to add and 13 to remove'
25 | },
26 | hair: {
27 | type: 'select',
28 | options: ['blue', 'yellow'],
29 | onChange: (value, { name, setHelperText }) => {
30 | setHelperText(name, `Better not choose ${value}`)
31 | }
32 | },
33 | });
34 |
35 | return createSchema('owner', {
36 | name: {
37 | type: 'string',
38 | required: true,
39 | },
40 | height: {
41 | type: 'radios',
42 | options: ['tall', 'short'],
43 | onChange: (value, { setValue }) => {
44 | if (value == 'tall')
45 | setValue('name', 'André the Giant')
46 | }
47 | },
48 | usesHat: {
49 | type: 'boolean',
50 | onChange: (value, { setVisible }) => {
51 | setVisible('hatColor', value)
52 | }
53 | },
54 | hatColor: {
55 | type: 'select',
56 | options: ['black', 'red'],
57 | initiallyVisible: false
58 | },
59 | pets: {
60 | type: [pet],
61 | minChildren: 1,
62 | maxChildren: 2
63 | }
64 | });
65 | ```
66 |
67 | You can copy and paste it directly in [the demo](https://dgonz64.github.io/react-hook-form-auto-demo/demo/). You will be able to:
68 |
69 | * Automatically set owner's name when you set tall as height
70 | * Show an extra field when owner wears hat
71 | * Add pets when one pet has 42 heads
72 | * Remove pet when it has 13 heads
73 | * Receive conflicting advice when selecting pet hair color
74 |
75 | # API
76 |
77 | Schema's `onChange` receives the following arguments:
78 |
79 | | Param | Type | Description |
80 | | --- | --- | --- |
81 | | value | any | New plain value already set for the field the schema refers to |
82 | | context | object | Utilities to change state |
83 |
84 | # `context`
85 |
86 | The `context` has information and allows you to operate fields. It's done with a rudimentary pub-sub system, so only the affected field is updated. It has the following attributes:
87 |
88 | | Param | Type | Description |
89 | | --- | --- | --- |
90 | | name | string | Full field path. Name compatible with all the utilities |
91 | | setVisible | function | `setVisible(name, visible)` changes the visibility of the field |
92 | | setHelperText | function | `setHelperText(name, text)` changes the text that appears below the field |
93 | | formHook | object | As returned by [React Hook Form `useForm`](https://react-hook-form.com/api/useform) (`register`, `unregister`, etc) |
94 | | setValue | function | `setValue(name, value)` Sets the value of the field |
95 | | arrayControl | object | Utilities to change array in the case the field is an array |
96 |
97 | With `formHook` you can, for example, create errors:
98 |
99 | ```javascript
100 | const owner = createSchema('owner', {
101 | name: {
102 | // ...
103 | onChange: (value, { formHook }) => {
104 | if (value == 'errorsy') {
105 | formHook.setError('height', {
106 | type: 'focus',
107 | message: 'Something something error'
108 | })
109 | }
110 | }
111 | }
112 | })
113 | ```
114 |
115 | ## `arrayControl`
116 |
117 | If the field is an array, `arrayControl` has the following attributes
118 |
119 | | Param | Type | Description |
120 | | --- | --- | --- |
121 | | items | array | Items state |
122 | | index | number | Index of the current field |
123 | | remove | function | `remove(index)` removes element with index `index` |
124 | | add | function | Adds a new element to the array |
125 |
--------------------------------------------------------------------------------
/jsdoc2md/README.hbs:
--------------------------------------------------------------------------------
1 | [](https://travis-ci.org/dgonz64/react-hook-form-auto)
2 |
3 | This library allows your React application to automatically generate forms using [ReactHookForm](https://react-hook-form.com/). The form and validations are generated following a schema inspired by [SimpleSchema](https://github.com/aldeed/simple-schema-js).
4 |
5 | ## New in 1.3.0
6 |
7 | Now works with React Hook Form 7 🎉
8 |
9 | All the members of the family, like `rhfa-react-native` are updated too. You can update the corresponding one and it should work. If not, please open an issue.
10 |
11 | The exception is if you made a skin for your project, then you should follow [this guide](docs/migrate-1.2-to-1.3.md).
12 |
13 | ## Contents
14 |
15 | * [Installation](#installation)
16 | * [Usage](#usage)
17 | * [1. Write schema](#1-write-schema)
18 | * [2. Render a form](#2-render-a-form)
19 | * [3. Make it prettier](#3-make-it-prettier)
20 | * [4. Translate labels](#4-translate)
21 | * [Available skins](#available-skins)
22 | * [Roadmap](#roadmap)
23 | * [Documentation](#documentation)
24 | * [Types](#types)
25 | * [Select and radios](#select-and-radios)
26 | * [Validators](#validators)
27 | * [`validate` validator](#the-validate-validator)
28 | * [Other schema fields](#other-schema-fields)
29 | * [Schema onChange](docs/schema-onchange.md)
30 | * [Schema](#schema)
31 | * [`Autoform` component](#autoform-component)
32 | * [Config](#config)
33 | * [Field props override](#field-props-override)
34 | * [Coercers](#coercers)
35 | * [Forcing errors](#forcing-errors)
36 | * [Imperative handling](#imperative-handling)
37 | * [Translation](#translation)
38 | * [`tr()`](#tr)
39 | * [`stringExists()`](#stringexists)
40 | * [Variable substitution](#variable-substitution)
41 | * [Adding strings](#adding-strings)
42 | * [Multilanguage](#multilanguage)
43 | * [Translation utils](#translation-utils)
44 | * [Use your own](#use-your-own-translation-system)
45 | * [Extending](#extending)
46 | * [Extending tutorial](https://github.com/dgonz64/rhfa-playground)
47 | * [With styles](#with-styles)
48 | * [Overriding skin](#overriding-skin)
49 | * [`coerce`](#coerce)
50 | * [`props`](#props)
51 | * [object](#props-is-an-object)
52 | * [function](#props-is-a-function)
53 | * [`onChange`, `onBlur`](#onchange-and-onblur-in-props)
54 | * [Directly](#use-it-directly-recommended)
55 | * [`setValue`](#use-setvalue)
56 | * [Other skin attributes](#other-skin-block-attributes)
57 | * [`skin[type].props`](#skintypeprops)
58 | * [`skin[type].component`](#skintypecomponent-or-skintypepropscomponent)
59 | * [Importing base](#importing-base)
60 |
61 | ## Play with the demos
62 |
63 | * [Emergency styles](https://codesandbox.io/s/rhfa-emergency-6upsj?file=/src/App.js)
64 | * [Bootstrap 4](https://codesandbox.io/s/rhfa-bootstrap-ttbfw?file=/src/App.js)
65 | * [Material-UI](https://codesandbox.io/s/rhfa-material-ui-k9pe9?file=/src/App.js)
66 | * [Blueprint](https://codesandbox.io/s/rhfa-blueprint-l4773?file=/src/App.js)
67 |
68 | ### Full webpack project demos
69 |
70 | * [Play with the bootstrap4 demo](https://dgonz64.github.io/react-hook-form-auto-demo-bootstrap4/demo/). [Project](https://github.com/dgonz64/react-hook-form-auto-demo-bootstrap4).
71 | * [Play with emergency styles demo](https://dgonz64.github.io/react-hook-form-auto-demo/demo). [Project](https://github.com/dgonz64/react-hook-form-auto-demo).
72 | * [Play with Material-UI demo](https://dgonz64.github.io/rhfa-demo-material-ui/demo/). [Project](https://github.com/dgonz64/rhfa-demo-material-ui)
73 | * [React Native](https://github.com/dgonz64/rhfa-demo-react-native)
74 |
75 | ## Installation
76 |
77 | $ npm install react-hook-form react-hook-form-auto --save
78 |
79 | ## Version deprecations
80 |
81 | * 1.3.0 works with react-hook-form 7.
82 | * If you didn't override the skin, it should work out of the box after update.
83 | * If you overrided the skin, then follow [this guide](docs/migrate-1.2-to-1.3.md).
84 | * 1.2.0 works with react-hook-form 6: `npm install react-hook-form@6 react-hook-form-auto@1.2 --save`
85 | * 1.1.0 works with react-hook-form 4 and 5. Older versions of this library (1.0.x) will only work with version 3 of react-hook-form.
86 |
87 | ## Usage
88 |
89 | ### 1. Write schema
90 |
91 | Write a schema for each model you have:
92 |
93 | ```javascript
94 | import { createSchema } from 'react-hook-form-auto'
95 |
96 | export const client = createSchema('client', {
97 | name: {
98 | type: 'string',
99 | required: true,
100 | max: 32
101 | },
102 | age: {
103 | type: 'number'
104 | }
105 | })
106 | ```
107 |
108 | In this example we are stating that a `client` is required to have a `name` and providing its allowed length. Also `client` has `age` and it's a number.
109 |
110 | ### 2. Render a form
111 |
112 | `` React component will generate inputs including translatable label, proper input types and error messages.
113 |
114 | ```javascript
115 | import { Autoform } from 'react-hook-form-auto'
116 | import { client } from './models/client'
117 |
118 | const MyForm = ({ onSubmit }) =>
119 |
123 | ```
124 |
125 | Form will be validated following the rules set by the schema.
126 |
127 | It also allows you to build arrays from other schemas. Simply specify the other schema within brackets `[]`. `Autoform` default skin will allow you to add and remove elements.
128 |
129 | ```javascript
130 | import { createSchema } from 'react-hook-form-auto'
131 | import { client } from './client'
132 |
133 | export const company = createSchema('company', {
134 | clients: {
135 | type: [client],
136 | minChildren: 10,
137 | arrayMode: 'panel' // You can override config
138 | }
139 | })
140 | ```
141 |
142 | You can override form array config for a particular field using `arrayMode`
143 |
144 | ### 3. Make it prettier
145 |
146 | #### 3a. Make it less ugly with some styling
147 |
148 | Install the emergency styles if you don't want to bundle a whole css library.
149 |
150 | $ npm install rhfa-emergency-styles --save
151 |
152 | Then set the `styles` prop of ``:
153 |
154 | ```javascript
155 | import styles from 'rhfa-emergency-styles'
156 |
157 | // With sass...
158 | import 'rhfa-emergency-styles/prefixed.sass'
159 | // ...or without
160 | import 'rhfa-emergency-styles/dist/styles.css'
161 |
162 |
163 | ```
164 |
165 | If you use `sass` you have to make sure you are [not excluding `node_modules`](https://github.com/dgonz64/react-hook-form-auto-demo/commit/94dbe78dc93a4110f915a5809a6880a8c7a55970) in your build process.
166 |
167 | If you use `css-modules` you have [better options](https://github.com/dgonz64/rhfa-emergency-styles).
168 |
169 | #### 3b. Make it pretty with Bootstrap 4
170 |
171 | We can take advantage of the styling strategy and pass bootstrap classes as `styles` props. You can grab them [from here](https://github.com/dgonz64/react-hook-form-auto-demo-bootstrap4/blob/master/src/styles.js) \[[raw](https://raw.githubusercontent.com/dgonz64/react-hook-form-auto-demo-bootstrap4/master/src/styles.js)\]. Then use them:
172 |
173 | ```javascript
174 | import styles from './bsStyles' // copy-pasted styles description
175 |
176 |
177 | ```
178 |
179 | As you have to pass the styles on every `Autoform` render, I recommend [creating a module](https://github.com/dgonz64/react-hook-form-auto-demo/blob/master/src/components/Autoform.jsx) or a HoC.
180 |
181 | Read the [documentation](#documentation) to find out what else you can do.
182 |
183 | #### 4. Translate
184 |
185 | You probably see labels like `models.client.name` instead of the proper ones. That's because the project uses a built-in translation system. You can setup those strings both at once or incrementally.
186 |
187 | Simple call `addTranslations` directly in your module or modules. Example:
188 |
189 | ```javascript
190 | import { addTranslations } from 'react-hook-form-auto'
191 |
192 | addTranslations({
193 | models: {
194 | client: {
195 | name: 'Name',
196 | age: 'Age'
197 | }
198 | }
199 | })
200 | ```
201 |
202 | A simple way to fill this is by replicating the unstranslated string in the object. The dot is a subobject. In the former example you would see a label called `models.client.name`.
203 |
204 | ## Available skins
205 |
206 | Some of them need other imports. See instructions from each.
207 |
208 | ### [Vanilla](https://github.com/dgonz64/react-hook-form-auto) (here)
209 | ### Bootstrap 4 (as instructed in this document)
210 | ### [Material-UI](https://github.com/dgonz64/rhfa-material-ui)
211 | ### [Blueprint](https://github.com/dgonz64/rhfa-blueprint)
212 | ### [React Native](https://github.com/dgonz64/rhfa-react-native)
213 |
214 | ## Rationale
215 |
216 | One of the purposes of the library is to avoid repeating code by not having to write a set of input components for every entity. Also when time is of the essence, writing forms can be exasperating.
217 |
218 | These are some of the advantages of using an automatic form system.
219 |
220 | * More [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)
221 | * More [SSoT](https://en.wikipedia.org/wiki/Single_source_of_truth)
222 | * *...so less bugs*
223 |
224 | Also react-hook-form-auto has some of its own
225 |
226 | * Includes a translation system
227 | * It's easily expandable
228 | * It's simple
229 | * When possible tries to use [convention over configuration](https://en.wikipedia.org/wiki/Convention_over_configuration)
230 |
231 | # Roadmap
232 |
233 | Actually there aren't clearly defined goals. The library already suits my personal needs. Do you need anything that is not there? Feel free to write an issue! Those are the tasks I think may be interesting and will randomly work on them:
234 |
235 | - [x] Automatic form generation
236 | - [x] Able to stylize components
237 | - [x] Datatype coertion
238 | - [x] Provide more and better styling examples
239 | - [x] Styles to make it look like bootstrap4
240 | - [x] _Native_ components for famous ui libraries like bootstrap4
241 | - [ ] Need other? Open issue!
242 | - [x] Actually limit children
243 | - [x] React Native support
244 | - [x] Translated messages from server/async
245 | - [x] Make it compatible with `react-hook-form` 7.
246 |
247 | # Documentation
248 |
249 | ## Types
250 |
251 | Each schema can be regarded as a Rails model or a database table. You can store them anywhere, for example in a module:
252 |
253 | ```javascript
254 | import { createSchema } from 'react-hook-form-auto'
255 |
256 | export const computer = createSchema('computers', { /* ...schema... */ })
257 | ```
258 |
259 | Each first level entry in the schema object represents the fields in the form or the columns in the database analogy. To configure the field you use another object. Example:
260 |
261 | ```javascript
262 | {
263 | name: { type: 'string' }
264 | }
265 | ```
266 |
267 | These are the types a field can be:
268 |
269 | | Type | Valid when... | Input control |
270 | | ---------- | ------------------------ | ----------------------------- |
271 | | string | Value is a string | `` |
272 | | number | Value is a number | `` |
273 | | range | Between two numbers | `` |
274 | | [\] | Each array item is valid | Renders as array |
275 | | \ | Value follows schema | Renders as submodel |
276 | | select | Value belongs to set | `` |
277 | | radios | Value belongs to set | `` |
278 | | boolean | Value is boolean | `` |
280 |
281 | You can specify the type as a constructor. There's not an easily measurable advantage. Example:
282 |
283 | ```javascript
284 | { type: String }
285 | ```
286 |
287 | #### select and radios
288 |
289 | They both allow options as an array that can be one of strings with the options keys that will be feed to translator using `trModel()` or can be objects in the form `{ value, label }`. If an object is provided, label will be used for the HTML content (display) and value for the option's value.
290 |
291 | Options can also be a function. In that case it will be evaluated with the component props as the first argument. The results used as explained above, that is, array of strings or objects.
292 |
293 | Options as object
294 |
295 | Example with keys:
296 |
297 | ```javascript
298 | {
299 | type: 'select',
300 | options: ['red', 'blue']
301 | }
302 | ```
303 |
304 | Example with objects:
305 |
306 | ```javascript
307 | {
308 | type: 'select',
309 | options: [
310 | { value: 'r', key: 'red' },
311 | { value: 'b', key: 'blue' }
312 | ]
313 | }
314 | ```
315 |
316 | You can specify label too. In that case it will be a direct already translated string. If you setup `key` then label will be infered and translated.
317 |
318 | Example with direct labels:
319 |
320 | ```javascript
321 | {
322 | type: 'select',
323 | options: [
324 | { value: 'r', label: 'Some red' },
325 | { value: 'b', label: 'Some blue' }
326 | ]
327 | }
328 | ```
329 |
330 | Example with function. This example assumes `Autoform` component has a color collection in the props:
331 |
332 | ```javascript
333 | {
334 | type: 'select',
335 | options: props => props.colors.map(color => ({
336 | value: color.id,
337 | key: color.name
338 | }))
339 | }
340 | ```
341 |
342 | ### Validators
343 |
344 | | Validation | Type | Meaning | Error string ID |
345 | | ----------- | -------- | -------------------------------------------------- | --- |
346 | | minChildren | number | Minimum amount of children an array field can have | `error.minChildren` |
347 | | maxChildren | number | Maximum amount of children an array field can have | `error.maxChildren` |
348 | | min | number | Minimum value | `error.min` |
349 | | max | number | Maximum value | `error.min` |
350 | | required | boolean | The value can't be empty or undefined | `error.required` |
351 | | validate | function | Function that takes the value and entry and returns validity | |
352 | | pattern | regex | Regex. The value matches the regex | |
353 |
354 | The string returned will be translated. The translation will receive the field's schema as [variables](#variable-substitution).
355 |
356 | ### The `validate` validator
357 |
358 | The function used to validate the field, as any other validator, can return:
359 |
360 | * true to fail (and automatically generate message) or false to pass
361 | * An string that will be the error message
362 |
363 | ### Other schema fields
364 |
365 | There are some other attributes you can pass while defining the field schema:
366 |
367 | | Attribute | Type | Meaning |
368 | | ----------- | -------- | -------------------------------------------------- |
369 | | noAutocomplete | boolean | Inputs (or skin's `component`) will have `autocomplete=off` to help skip browser's autocomplete |
370 | | addWrapperProps | object | Directly passed to wrapper component |
371 | | addInputProps | object | Props merged into input component |
372 | | onChange | function | Allows you to change values on the fly. See [this](docs/schema-onchange.md). |
373 | | helperText | string | Explicit string for the helper text for the field, for example `tr('example')` |
374 |
375 | #### Helper text
376 |
377 | The `helperText` attribute in the schema allows you to set a fixed helper text for the field. The helper text can be changed in other ways and this is the precedence order:
378 |
379 | 1. Text set using `setHelperText` from schema (or `FieldPropsOverride`) `onChange`.
380 | 1. Explicit text set by schema `{ name: 'name', type: 'string', helperText: tr('example') }`
381 | 1. Automatic translation string in the form `models..._helper`
382 |
383 | ### Schema
384 |
385 | The schema establishes the validation and inputs. The instance can be stored anywhere, like your own model object or a module. At the moment it doesn't change over time.
386 |
387 | ### createSchema
388 |
389 | {{#function name="createSchema"~}}
390 | {{>body~}}
391 | {{/function}}
392 |
393 | ### Autoform component
394 |
395 | The `` component accepts the following props
396 |
397 | | Prop | Type | Meaning |
398 | | ----------- | --------------- | ------------------------------------------------------ |
399 | | schema | Schema instance | Schema used to build the form |
400 | | elementProps | object | Props extended in all the inputs |
401 | | initialValues | object | Initial values |
402 | | children | element | Whatever you want to put inside the form |
403 | | onSubmit | function | (optional) Code called when submitting with the coerced doc |
404 | | onChange | function | (optional) Code called after any change with the current coerced doc |
405 | | onErrors | function | (optional) Code called when form has errors |
406 | | forceErrors | object | (optional) Object with the errors or _falsy_ like `null` to not force. Keys will be the field name (example `pets.0.name`) and the value is another object with key `message`. Example: `{ username: { message: 'Taken' } }` |
407 | | styles | object | Styles used by the defaultSkin |
408 | | submitButton | boolean | (optional) Include submit button |
409 | | submitButtonText | element | (optional) Contents for the submit button |
410 | | skin | object | Base skin |
411 | | skinOverride | object | Override some skin components |
412 | | disableCoercing | boolean | Disable all coercing and get values as text |
413 | | noAutocomplete | boolean | Disable all autocompleting by passing `autocomplete=off` to the input or skin's `props` |
414 |
415 | Any other prop will be passed to the skin `props()`.
416 |
417 | ### Config
418 |
419 | The `config` prop is an object that has the following attributes
420 |
421 | | Attribute | Meaning |
422 | | ---------- | ------------------------------------------------------------------- |
423 | | arrayMode | `'table'` or `'panels'` depending on wanted array field format. `panels` uses card/box/panel wrapping for elements. `table` uses tables (might not fit but if it does is perfect for compact models) |
424 |
425 | ### Field props override
426 |
427 | You can override field props individually. You can do this with a component called `FieldPropsOverride`. This is useful when you want to create an special field with some functionality that forces you to provide an event handler. Let's see an example:
428 |
429 | ```javascript
430 | import { Autoform, FieldPropsOverride } from 'react-hook-form-auto'
431 |
432 | const Component = ({ onKeyDown }) =>
433 |
434 |
438 |
439 | ```
440 |
441 | The name can specified without taking into account array ordinals. For example, if a `pet` serves as an schema array for an `owner` and you want to override every pet name from `pets` field (array), you should use `pets.name` as the `name` prop:
442 |
443 | ```javascript
444 |
448 | ```
449 |
450 | It can also be an specific path like `pets[0].name`.
451 |
452 | #### ``'s `onChange`
453 |
454 | In `FieldPropsOverride`, `onChange` prop is automatically chained with both ReactHookForm and schema `onChange`. The callback receives exactly [the same arguments as schema `onChange`](https://github.com/dgonz64/react-hook-form-auto/blob/master/docs/schema-onchange.md). The order of calling is:
455 |
456 | 1. ReactHookForm `onChange`
457 | 1. Schema's `onChange`
458 | 1. `FieldPropsOverride`'s `onChange`
459 |
460 | Example:
461 |
462 | ```javascript
463 | const handleChange = (value, { setValue }) => {
464 | if (value == 'tall')
465 | setValue('name', 'André the Giant')
466 | }
467 |
468 | return (
469 |
470 |
474 |
475 | )
476 | ```
477 |
478 | ### Coercers
479 |
480 | While react-hook-form works with inputs, this library is focused in models. This means:
481 |
482 | * Values from inputs are coerced to the datatype you define in the schema.
483 | * Default values are left untouched if there they are not defined or registered.
484 | * You can manually use setValue from your skin or skinOverride.
485 |
486 | You can also disable coercers with the `Autoform`'s `disableCoercing` prop.
487 |
488 | ### Select
489 |
490 | Select will include an empty option for uninitialized elements. Please, write an issue if you want this to be configurable.
491 |
492 | ### Forcing errors
493 |
494 | You can force an error message for any field, including a nested one. For it you elaborate an object with the field keys in ReactHookForm notation, for example `pets.0.name`. Complete example:
495 |
496 | ```javascript
497 | const LoginContainer = () => {
498 | const [ serverErrors, setServerErrors ] = useState(null)
499 |
500 | const handleLogin = () => {
501 | setServerErrors({
502 | username: { message: 'Already taken' },
503 | password: { message: 'Also, wrong password' }
504 | })
505 | }
506 |
507 | return (
508 |
513 | )
514 | }
515 | ```
516 |
517 | Also take a look at this [demo](https://codesandbox.io/s/rhfa-emergency-example-server-validation-9571b7?file=/src/App.js). After you pass client validation (just fill both names), you can click Send to simulate server validation errors.
518 |
519 | ## Imperative handling
520 |
521 | `Autoform` component sets some functions to be used in referenced component:
522 |
523 | ```javascript
524 | let formRef
525 |
526 | // Example: imperative submit
527 | const doSubmit = () => {
528 | formRef.submit()
529 | }
530 |
531 | const MyForm = ({ onSubmit }) =>
532 | formRef = e}
535 | />
536 | ```
537 |
538 | Functions returned:
539 |
540 | | Attribute | Description |
541 | | --- | --- |
542 | | submit() | Imperative submit. |
543 | | setValue(name, value, options) | Sets a value in any place of the document. You can use a path. As this library coerces values, it's better to use this than the one from react-hook-form to avoid inconsistences. The parameter `options` is passed to ReactHookForm |
544 | | setVisible(name, visible) | Sets the visibility for an element. |
545 | | reset() | Resets every field value to initial's. |
546 | | getValues() | Returns the coerced form values |
547 | | formHook() | Call this in order to get react-hook-form vanilla reference object. |
548 |
549 | ## Translation
550 |
551 | react-hook-form-auto uses internally a simple better-than-nothing built-in translation system. This system and its translation tables can be replaced and even completely overridden.
552 |
553 | The translation strings are hierarchized following some pseudo-semantic rules:
554 |
555 | ```javascript
556 | `models.${model}.${field}.${misc}`
557 | ```
558 |
559 | The meaning of the last `misc` part usually depends on the type of field.
560 |
561 | The dots navigates through children in the string table. Example:
562 |
563 | ```javascript
564 | {
565 | models: {
566 | computer: {
567 | cpu: {
568 | arm: 'ARM',
569 | // ...
570 | },
571 | // ...
572 | }
573 | }
574 | }
575 | ```
576 |
577 | To translate string for your use call `tr()` and pass the string path separated by dots:
578 |
579 | ```javascript
580 | import { tr } from 'react-hook-form-auto
581 |
582 | const message = tr('models.computers.cpu.arm')
583 |
584 | /* message value is 'ARM' */
585 | ```
586 |
587 | This is the usage of the `tr()` function:
588 |
589 | ### tr
590 | {{#function name="tr"~}}
591 | {{>body~}}
592 | {{/function}}
593 |
594 | ### stringExists
595 | {{#function name="stringExists"~}}
596 | {{>body~}}
597 | {{/function}}
598 |
599 | ### Variable substitution
600 |
601 | You can also put variables in the translation strings:
602 |
603 | ```javascript
604 | {
605 | name: {
606 | create: '__name__ created'
607 | }
608 | }
609 | ```
610 |
611 | This allows you to translate and inserting a value in the correct place of the string. Example:
612 |
613 | ```javascript
614 | import { tr } from 'react-hook-form-auto'
615 |
616 | const name = 'Alice'
617 | const message = tr('name.create', { name })
618 |
619 | /* message value is 'Alice created' */
620 | ```
621 |
622 | ### Adding strings
623 |
624 | ```javascript
625 | import { addTranslations } from 'react-hook-form-auto'
626 | ```
627 |
628 | Then you call addTranslations with the object tree:
629 |
630 | ### addTranslations
631 | {{#function name="addTranslations"~}}
632 | {{>body~}}
633 | {{/function}}
634 |
635 | It's ok to overwrite a language with another. The non translated strings from the new language will remain from the former in the table. At the moment it's up to you to take care about language completeness (or, better, use an external translator).
636 |
637 | ### Multilanguage
638 |
639 | If your application has more than one language and it can be changed on the fly, I better recommend to use translation utils over the core translations. Those functions store the languages separately:
640 |
641 | ```javascript
642 | import { setLanguageByName } from 'react-hook-form-auto'
643 |
644 | setLanguageByName('en')
645 | ```
646 |
647 | You can add the translation strings directly to a language:
648 |
649 | ### addLanguageTranslations
650 | {{#function name="addLanguageTranslations"~}}
651 | {{>body~}}
652 | {{/function}}
653 |
654 | ### Translation utils
655 |
656 | There are some functions that deal with semantic organization of the translation strings this library uses. You can take advantage of them if you are building a skin:
657 |
658 | ### trModel
659 | {{#function name="trModel"~}}
660 | {{>body~}}
661 | {{/function}}
662 |
663 | ### trField
664 | {{#function name="trField"~}}
665 | {{>body~}}
666 | {{/function}}
667 |
668 | ### trError
669 | {{#function name="trError"~}}
670 | {{>body~}}
671 | {{/function}}
672 |
673 | ### Use your own translation system
674 |
675 | You can disable the translation system to use your own.
676 |
677 | ### setTranslator
678 | {{#function name="setTranslator"~}}
679 | {{>body~}}
680 | {{/function}}
681 |
682 | Example:
683 |
684 | ```javascript
685 | import { setTranslator } from 'react-hook-form-auto'
686 | import { myTranslationTranslator } from './serious_translator'
687 |
688 | setTranslator((tr, data) => {
689 | /* do something with tr or data */
690 |
691 | return myTranslationTranslator(something, somethingElse)
692 | })
693 | ```
694 |
695 | Or you can drop it directly to `setTranslator()` if it's compatible.
696 |
697 | ## Extending
698 |
699 | If you just need another appearance, you can do it changing styles. If you are adapting an existing UI library (like [Blueprint](https://blueprintjs.com/)) then it's better to extend skin.
700 |
701 | ### With styles
702 |
703 | The default skin from `react-hook-form-auto` uses css classes. You can override them providing your set. Example with css-modules:
704 |
705 | ```javascript
706 | import styles from './my_styles.css'
707 |
708 |
709 | ```
710 |
711 | The whole [rhfa-emergency-styles](https://github.com/dgonz64/rhfa-emergency-styles) does this and can serve as an example.
712 |
713 | ### Overriding skin
714 |
715 | When you need to adapt behaviour, styles might not be enough. To solve this you can override full components.
716 |
717 | The inputs and auxiliary elements are created using a set of components. The mapping can be set all together by overriding the skin, like this:
718 |
719 | ```javascript
720 | import { Autoform as RHFAutoform } from 'react-hook-form-auto'
721 |
722 | import overrides from './skinOverride'
723 |
724 | export const Autoform = (props) =>
725 |
729 | ```
730 |
731 | You can take a look at [defaultSkin.js](https://github.com/dgonz64/react-hook-form-auto/blob/master/src/ui/defaultSkin.jsx) and [`components/index.js`](https://github.com/dgonz64/react-hook-form-auto/tree/master/src/ui/components) from any skin to have a glimpse.
732 |
733 | Also find [here](https://github.com/dgonz64/rhfa-material-ui/blob/master/src/skinOverride.js#L56) a full skin override.
734 |
735 | I made a tutorial adapting Material-UI to react-hook-form-auto. You can find it [here](https://github.com/dgonz64/rhfa-playground).
736 |
737 | ### Extension process
738 |
739 | There's an entry in the skin object for every field type. The value of the entry is an object with two attributes like in this example:
740 |
741 | ```javascript
742 | number: {
743 | coerce: value => parseFloat(value),
744 | props: {
745 | component: 'input',
746 | type: 'number'
747 | }
748 | },
749 | ```
750 |
751 | #### coerce
752 |
753 | Function that converts result to its correct datatype.
754 |
755 | #### props
756 |
757 | Prop transformation for the rendered component.
758 |
759 | The attribute `component` is the React component used to render the input inside the wrappers.
760 |
761 | ```javascript
762 | select: {
763 | props: {
764 | component: Select
765 | }
766 | },
767 | ```
768 |
769 | It can be also a first level attribute, useful if you don't need to change props
770 |
771 | ```javascript
772 | select: {
773 | component: Select
774 | },
775 | ```
776 |
777 | ##### `props` is an object
778 |
779 | Props merged to component's default:
780 |
781 | ```javascript
782 | range: {
783 | coerce: value => parseFloat(value),
784 | props: {
785 | component: 'input',
786 | type: 'range'
787 | }
788 | },
789 | ```
790 |
791 | If `component` is a string, like in this example, it will only receive ``-like props, like `name` and `onChange`.
792 |
793 | ##### `props` is a function
794 |
795 | Function that takes the component's intended props and returns component's final props:
796 |
797 | ```javascript
798 | string: {
799 | props: props => ({
800 | ...props,
801 | component: props.fieldSchema.textarea ? 'textarea' : 'input'
802 | }),
803 | },
804 | ```
805 |
806 | #### `onChange` and `onBlur` in props
807 |
808 | The `component` in the `props` property is also in charge of the field update: For that matter you have two options
809 |
810 | ##### Use it directly (recommended)
811 |
812 | ```javascript
813 | const Input = ({ name, onChange, onBlur }) =>
814 |
815 |
816 |
817 |
818 | const mySkinOverride = {
819 | string: {
820 | component: Input
821 | }
822 | }
823 | ```
824 |
825 | ##### Use `setValue`
826 |
827 | Another way is to use `setValue(name, newValue)` on every change. The component is still uncontrolled but model's value will be updated.
828 |
829 | ```javascript
830 | props: ({ name, register, setValue }) => {
831 | const setValueFromEvent = event => {
832 | setValue(name, event.target.value)
833 | }
834 |
835 | return {
836 | component:
837 | }
838 | }
839 | ```
840 |
841 | ### Other skin block attributes
842 |
843 | ```javascript
844 | range: {
845 | coerce: value => parseFloat(value),
846 | controlled: true,
847 | skipRegister: true,
848 | nameForErrors: name => `${name}__counter`,
849 | props: {
850 | component: 'input',
851 | type: 'range'
852 | }
853 | },
854 | ```
855 |
856 | | Attribute | Means |
857 | | --------- | ----- |
858 | | `controlled` | Will `useController()`. Component will receive also `value` prop |
859 | | `skipRegister` | Will not automatically register this field. Hasn't any meaning if `controlled` is `true` |
860 | | `nameForErrors` | Function that receives `name` and returns a transformed name used to navigate through the `errors` object returned from `react-hook-form`'s `useFormState`. Helps to create errors for non registered field like `minChildren` does. |
861 |
862 | ## Overriding skin
863 |
864 | You can override (or add) specific types by using ``'s `skinOverride` prop.
865 |
866 | ### Avoid `array` and `schema`
867 |
868 | Only override `array` and `schema` types if you know `react-hook-form-auto` internals. For example both need `skipRegister`.
869 |
870 | ### `skin[type].props`
871 |
872 | The rest of the properties a skin block can override:
873 |
874 | | Prop | Type | Use |
875 | | ---------------- | ----------------- | ------------------------------------------ |
876 | | `component` | element | Component used to render the input |
877 | | `wrapper` | function | Override wrapper `skin.defaultWrap` |
878 | | `name` | string | (**Must not be changed**) Complete field name (with hierarchy) |
879 | | `field` | string | (**Shouldn't be changed**) This field name |
880 | | `option` | string | Forces value (used in radios for example ) |
881 | | `inline` | boolean | Goes to the wrapper as `inline` |
882 | | `styles` | object | Style overriding |
883 | | `fieldSchema` | object | Schema specification for the field |
884 | | `schemaTypeName` | string (required) | Name of the schema that contains the field |
885 | | `parent` | string | Name of the containing field |
886 | | `config` | object | Override config for this input |
887 | | `index` | number | Index for arrays |
888 | | `formHook` | object | ReacHookForm's register() returned object |
889 | | `autoformProps` | object | Properties passed to Autoform |
890 | | `skinElement` | object | skin resolver for the type of this field (for example `skin.boolean`) |
891 |
892 | ### `skin[type].component` or `skin[type].props.component`
893 |
894 | The value of this field is used to render the input component. If it's a component, the will receive these props, appart from the rest of the `skin[type].props` block.
895 |
896 | If `component` is specified both from `props` and directly (`skin[type].component`), the one coming from props will have priority.
897 |
898 | | Prop | Type | Use |
899 | | ---------------- | -------- | --------------------------------------------------- |
900 | | `id` | string | Id usable to match label in dom |
901 | | `name` | string | Full path name for the `` |
902 | | `type` | string | `type` as would be passed to `` |
903 | | `defaultValue` | any | As calculated from initialValues, schema and skin |
904 | | `onChange` | function | Should be called back with dom `change` event handler |
905 | | `onBlur` | function | Callback for dom `blur` |
906 | | `className` | function | Calculated classes |
907 | | `field` | function | Relative field name |
908 | | `inputRef` | function | ref for the input element. Comes from `register` and it's used to update fields imperatively |
909 | | `errorText` | string | Validation errors or `''` |
910 | | `fieldSchema` | object | Part of the schema that refers to this field |
911 | | `formHook` | object | As returned from the `useForm()` call |
912 | | `styles` | object | All the styles in case you do class styling |
913 | | `skinElement` | object | Complete skin block specific to this type of field (for example `skin.number`) |
914 | | `setVisible` | function | Changes visibility of fields `setVisible(name, visible)`. If the field is not visible, then it will not appear in submit doc neither. |
915 | | `setHelperText` | function | Changes field helper text `setHelperText(name, text)` |
916 | | `setValue` | function | Changes value of any field `setValue(name, value)` |
917 | | `arrayControl` | object | Some control functions in the case field is inside an array. See [this](doc/schema-onchange.md#arraycontrol) |
918 | | `value` | any | Only for controlled types (`skin[type].controlled == true`) |
919 | | ... | any | ...any other prop added in schema with `addInputProps` |
920 |
921 | ### `InputWrap`
922 |
923 | It's a common component used to create all the input structure, including wrappers. You can use it to avoid boilerplate.
924 |
925 | | Prop | Type | Use |
926 | | ---------------- | -------- | --------------------------------------------------- |
927 | | `children` | element | (optional) You can use InputWrap to... wrap. |
928 | | `id` | string | id for the `for` attribute of the label |
929 | | `name` | string | Full name (with hierarchy) |
930 | | `schemaTypeName` | string | Name of the schema as created |
931 | | `styles` | object | Styles |
932 | | `labelOverride` | string | Label overriding |
933 | | `inline` | boolean | Will omit label |
934 | | `errorText` | string | Validation error |
935 |
936 | The props for the input component are different depending if the `inputComponent` is a string, like `'input'` or a React component.
937 |
938 | #### Native component (ie. `'input'`)
939 |
940 | For a 'lowercase' component like ``, props passed are
941 |
942 | * id
943 | * name
944 | * type
945 | * option (if provided, for the value)
946 | * defaultValue
947 | * onChange
948 | * onBlur
949 | * ref
950 | * className calculated
951 | * ...any fields added to schema's `addInputProps`
952 |
953 | #### Class component (ie. `MySlider`)
954 |
955 | Class components receive all the provided or derived props.
956 |
957 | ### Rendering
958 |
959 | To render the input or inputs, two functions are used:
960 |
961 | ### renderInputs
962 | {{#function name="renderInputs"~}}
963 | {{>body~}}
964 | {{/function}}
965 |
966 | ### renderInput
967 | {{#function name="renderInput"~}}
968 | {{>body~}}
969 | {{/function}}
970 |
971 | ## Importing base
972 |
973 | You can use the base bundle to import `AutoformBase` component. `AutoformBase` is like `Autoform` but doesn't use `react-dom` dependency.
974 |
975 | ```javascript
976 | import { Autoform, ... } from 'react-hook-form-auto/dist/base.js'
977 | ```
978 |
979 | You can also do it in a more permanent way if you use webpack:
980 |
981 | ```javascript
982 | resolve: {
983 | alias: {
984 | 'react-hook-form-auto': 'react-hook-form-auto/dist/base.js'
985 | }
986 | },
987 | ```
988 |
989 | # Help wanted / contribute
990 |
991 | react-hook-form-auto needs your help. Skins must be written!
992 |
993 | Also, it would make my day to see the library working in your project, provided it's public. Please, tell me!
994 |
995 | ### Where to begin
996 |
997 | Make a fork and do your magic followed by a [pull request](https://help.github.com/articles/about-pull-requests/). If it's an implementation, test case and documentation update would be highly appreciated.
998 |
999 | ### Some code pointers
1000 |
1001 | Everything is work in progress until there's a bunch of skins or we are in complete control of the world, whichever comes first.
1002 |
1003 | The most important files:
1004 |
1005 | * `src/ui/components/componentRender.jsx` processes the schema to build the inputs and allows skin overrides
1006 | * `src/ui/Autoform.jsx` The component and the coercing logic
1007 | * `src/ui/AutofieldContainer.jsx` Registers or calls `useController()`
1008 | * `src/ui/Autofield.jsx` Wraps component within wrapper
1009 | * `jsdoc2md/README.hbs` Documentation source
1010 |
1011 | See also the [contributing doc](https://github.com/dgonz64/react-hook-form-auto/blob/master/CONTRIBUTING.md).
1012 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-hook-form-auto",
3 | "version": "1.3.15",
4 | "description": "Generate automatic forms following a schema",
5 | "main": "lib/index.js",
6 | "keywords": [
7 | "react",
8 | "reactjs",
9 | "hooks",
10 | "react-hook-form",
11 | "form",
12 | "validators",
13 | "validation"
14 | ],
15 | "author": "David González ",
16 | "license": "MIT",
17 | "repository": {
18 | "type": "git",
19 | "url": "https://github.com/dgonz64/react-hook-form-auto"
20 | },
21 | "devDependencies": {
22 | "@babel/cli": "^7.24.6",
23 | "@babel/core": "^7.24.6",
24 | "@babel/plugin-transform-class-properties": "^7.24.6",
25 | "@babel/plugin-transform-object-rest-spread": "^7.24.6",
26 | "@babel/preset-env": "^7.24.6",
27 | "@babel/preset-react": "^7.24.6",
28 | "@wojtekmaj/enzyme-adapter-react-17": "^0.8.0",
29 | "babel-loader": "^9.1.3",
30 | "enzyme": "^3.11.0",
31 | "jest": "^27.0.0",
32 | "jsdoc-to-markdown": "^8.0.1",
33 | "jsdom": "19.0.0",
34 | "jsdom-global": "3.0.2",
35 | "prop-types": "^15.8.1",
36 | "react": "^17",
37 | "react-dom": "^17.0.0",
38 | "react-hook-form": "^7.51.5",
39 | "rimraf": "^5.0.7",
40 | "webpack": "^5.91.0",
41 | "webpack-cli": "^5.1.4",
42 | "webpack-merge": "^5.10.0"
43 | },
44 | "peerDependencies": {
45 | "react": "^16.13.1 || ^17 || ^18",
46 | "react-hook-form": "^7"
47 | },
48 | "dependencies": {
49 | "classnames": "^2.5.1"
50 | },
51 | "scripts": {
52 | "clean": "rimraf dist && rimraf lib",
53 | "start": "npm run clean && babel src --out-dir lib --watch --verbose --source-maps",
54 | "build": "npm run clean && npm run build:commonjs && npm run build:umd && npm run build:umd:min && npm run build:base && npm run build:base:min",
55 | "build:commonjs": "babel src --out-dir lib",
56 | "build:umd": "webpack --env mode=production --env minify=false",
57 | "build:umd:min": "webpack --env mode=production --env minify=true",
58 | "build:base": "webpack --env mode=development --env minify=false --env buildtype=base",
59 | "build:base:min": "webpack --env mode=production --env minify=true --env buildtype=base",
60 | "watch:base": "webpack --env mode=development --env minify=false --env buildtype=base --watch",
61 | "docs": "jsdoc2md -t jsdoc2md/README.hbs src/*.js src/**/*.js src/**/*.jsx > README.md; echo ",
62 | "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
63 | "test:short": "jest",
64 | "test": "jest --verbose",
65 | "prepublishOnly": "npm run build"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/autoform_state.js:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState, useEffect } from 'react'
2 | import { deepmerge } from './utils'
3 |
4 | import {
5 | valueOrCreate,
6 | objectTraverse
7 | } from './utils'
8 | import { createCoercers } from './coercing'
9 | import { PubSub } from './pubsub'
10 |
11 | export const useAutoformState = ({
12 | initialValues,
13 | onSubmit,
14 | onChange,
15 | schema,
16 | skin,
17 | formHook,
18 | skipManualReset
19 | }) => {
20 | const stateRef = useRef({
21 | stateControl: new PubSub(),
22 | fields: {}
23 | })
24 |
25 | const { stateControl } = stateRef.current
26 |
27 | const coercersBase = {
28 | initialValues,
29 | stateRef,
30 | skin,
31 | onSubmit,
32 | schema
33 | }
34 |
35 | const coercedSubmit = createCoercers({
36 | ...coercersBase,
37 | notify: onSubmit
38 | })
39 |
40 | let coercedChange
41 | if (onChange) {
42 | const coercedChangeDoc = onChange && createCoercers({
43 | ...coercersBase,
44 | notify: onChange
45 | })
46 |
47 | coercedChange = () => {
48 | const doc = formHook.getValues()
49 | return coercedChangeDoc(doc)
50 | }
51 | } else {
52 | coercedChange = null
53 | }
54 |
55 | const schemaDef = schema.getSchema()
56 | const findOrInitState = (name) => {
57 | return valueOrCreate(stateRef.current.fields, name, () => {
58 | const nameForVisible = `${name}.initiallyVisible`
59 | const initiallyVisible = objectTraverse(schemaDef, nameForVisible, {
60 | returnValue: true
61 | })
62 |
63 | return {
64 | visible: initiallyVisible === null ? true : initiallyVisible,
65 | helperText: null
66 | }
67 | })
68 | }
69 |
70 | const setValue = (name, value, options = {}) => {
71 | const fieldState = findOrInitState(name)
72 |
73 | const newState = {
74 | ...fieldState,
75 | value,
76 | changed: true
77 | }
78 |
79 | if (!options.skipSetInput)
80 | formHook.setValue(name, value, options)
81 | }
82 |
83 | stateControl.findOrInitState = findOrInitState
84 | stateControl.setValue = setValue
85 |
86 | /**
87 | * Sets values in the stateRef. Doesn't trigger.
88 | */
89 | const setValues = (values, { parent = null, field }) => {
90 | const fields = Object.keys(values)
91 | fields.forEach(field => {
92 | const cur = values[field]
93 | if (typeof cur == 'object') {
94 | return setValues(cur, { parent: field, field })
95 | } else {
96 | const name = inputName({ parent, field })
97 | findOrInitState(fieldName)
98 | }
99 | })
100 | }
101 |
102 | const resetState = (values, omit) => {
103 | stateRef.current.fields = {}
104 |
105 | setValues(values || {}, {})
106 |
107 | const currentValues = formHook.getValues()
108 |
109 | if (!skipManualReset) {
110 | // Reset by setting everything to initialValues or null.
111 | function resetValues(obj, initials = {}, path = '', isArray = false) {
112 | const fields = Object.keys(obj)
113 | fields.forEach((field, idx) => {
114 | const value = obj[field]
115 | const elPath = isArray ?
116 | `${path}.${idx}` : (path ? `${path}.${field}` : field)
117 | const initial = initials[field]
118 | if (typeof value == 'object')
119 | resetValues(value, initial, elPath, Array.isArray(value))
120 | else {
121 | const initialOrNull = typeof initial == 'undefined' ? null : initial
122 | formHook.setValue(elPath, initialOrNull)
123 | }
124 | })
125 | }
126 |
127 | resetValues(currentValues, initialValues)
128 | }
129 |
130 | formHook.reset(values, omit)
131 | }
132 |
133 | const changeAndPublish = (name, attr, value) => {
134 | const state = findOrInitState(name)
135 |
136 | stateControl.publish(name, {
137 | ...state,
138 | [attr]: value
139 | })
140 |
141 | state[attr] = value
142 | }
143 |
144 | const setVisible = (name, visible) => {
145 | changeAndPublish(name, 'visible', visible)
146 | }
147 |
148 | const setHelperText = (name, text) => {
149 | changeAndPublish(name, 'helperText', text)
150 | }
151 |
152 | const getValues = () => {
153 | let values = {}
154 |
155 | const coercedGetValues = createCoercers({
156 | ...coercersBase,
157 | notify: (coerced) => {
158 | deepmerge(values, coerced)
159 | }
160 | })
161 |
162 | const doc = formHook.getValues()
163 | coercedGetValues(doc)
164 |
165 | return values
166 | }
167 |
168 | return {
169 | coercedSubmit,
170 | coercedChange,
171 | setValue,
172 | setVisible,
173 | setHelperText,
174 | resetState,
175 | stateControl,
176 | getValues
177 | }
178 | }
179 |
180 | // Subscribes to visible, helperText and potential future additions
181 | export const useAutofieldState = ({ name, stateControl }) => {
182 | const initialState = stateControl.findOrInitState(name)
183 | const [ state, setState ] = useState({ ...initialState })
184 |
185 | useEffect(() => {
186 | stateControl.subscribe(name, setState)
187 | return () => {
188 | stateControl.unsubscribe(name, setState)
189 | }
190 | }, [])
191 |
192 | return state
193 | }
194 |
--------------------------------------------------------------------------------
/src/coercing.js:
--------------------------------------------------------------------------------
1 | import {
2 | deepmerge,
3 | schemaTypeEx,
4 | objectTraverse
5 | } from './utils'
6 |
7 | export function createCoercers({
8 | initialValues,
9 | stateRef,
10 | skin,
11 | notify,
12 | schema
13 | }) {
14 | return function coercedSubmit(doc) {
15 | const coerceObject = ({ object, schemaDef }) => {
16 | const fields = Object.keys(schemaDef)
17 | const result = deepmerge({}, object)
18 |
19 | fields.forEach(fieldName => {
20 | const fieldSchema = schemaDef[fieldName]
21 | const { type } = fieldSchema
22 | const typeKey = schemaTypeEx(type)
23 | const { coerce } = fieldSchema.coerce ?
24 | fieldSchema : skin[typeKey]
25 | const value = object[fieldName]
26 |
27 | if (coerce) {
28 | result[fieldName] = coerce(value, {
29 | coerceObject,
30 | schemaDef,
31 | fieldName
32 | })
33 | }
34 | })
35 |
36 | return result
37 | }
38 |
39 | const coerceWithSchema = ({ doc, schema }) => {
40 | const schemaDef = schema.getSchema()
41 |
42 | return coerceObject({
43 | object: doc,
44 | schemaDef
45 | })
46 | }
47 |
48 | const fields = Object.keys(stateRef.current.fields)
49 | const values = fields.reduce((values, field) => {
50 | const state = stateRef.current.fields[field]
51 |
52 | if (state.visible) {
53 | const [ container, attr ] = objectTraverse(values, field, {
54 | createIfMissing: true
55 | })
56 | const [ docContainer ] = objectTraverse(doc, field)
57 | if (container && attr) {
58 | if (state.changed)
59 | container[attr] = state.value
60 | else if (docContainer)
61 | container[attr] = docContainer[attr]
62 | }
63 | }
64 |
65 | return values
66 | }, {})
67 |
68 | const wholeObj = deepmerge({}, initialValues, values)
69 | const coerced = coerceWithSchema({ doc: wholeObj, schema })
70 |
71 | notify(coerced, doc)
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/createSchema.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Creates a Schema from the specification.
3 | *
4 | * @function
5 | * @param {string} typeName Name of the model being created.
6 | * It can be chosen freely.
7 | * @param {object} schema Schema specification.
8 | */
9 | export function createSchema(typeName, schema) {
10 | return {
11 | _type: 'schema',
12 |
13 | /**
14 | * Returns the schema specification.
15 | *
16 | * @returns {object} Schema specification.
17 | */
18 | getSchema: () => schema,
19 |
20 | /**
21 | * Returns the schema specification.
22 | *
23 | * @returns {object} Schema specification.
24 | */
25 | getFieldSchema: (name) => schema[name],
26 |
27 | /**
28 | * Returns the schema name.
29 | *
30 | * @returns {string} Schema name (also called ``typeName``).
31 | */
32 | getType: () => typeName,
33 |
34 | /**
35 | * Returns the name of the fields.
36 | */
37 | getFieldNames: () => Object.keys(schema)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export * from './index_base'
2 | export { Autoform } from './ui/Autoform'
3 |
4 | export { Panel, Button } from './ui/components'
5 | export { FieldPropsOverride } from './ui/components/FieldPropsOverride'
6 | export { InputWrap } from './ui/components/InputWrap'
7 |
--------------------------------------------------------------------------------
/src/index_base.js:
--------------------------------------------------------------------------------
1 | export {
2 | addTranslations,
3 | setTranslator,
4 | tr,
5 | stringExists,
6 | setTranslateVariableRegex,
7 | setTranslateReferenceRegex,
8 | } from './translate'
9 | export {
10 | processOptions,
11 | objectTraverse
12 | } from './utils'
13 | export {
14 | setLanguageByName,
15 | addLanguageTranslations,
16 | trModel,
17 | trField,
18 | trError,
19 | trPath,
20 | trPathSetBase
21 | } from './translation_utils'
22 | export { createSchema } from './createSchema'
23 | export { AutoformBase } from './ui/AutoformBase'
24 | export * from './ui/componentRender'
25 |
--------------------------------------------------------------------------------
/src/pubsub.js:
--------------------------------------------------------------------------------
1 | import { valueOrCreate } from './utils'
2 |
3 | export class PubSub {
4 | constructor() {
5 | this.handlers = {}
6 | }
7 |
8 | subscribe(name, callback) {
9 | const handlers = valueOrCreate(this.handlers, name, () => [])
10 | const formerIndex = handlers.indexOf(callback)
11 | if (formerIndex == -1)
12 | handlers.push(callback)
13 | }
14 |
15 | unsubscribe(name, callback) {
16 | const handlers = this.handlers[name]
17 | if (handlers) {
18 | const index = handlers.indexOf(callback)
19 | if (index != -1)
20 | handlers.splice(index, 1)
21 | }
22 | }
23 |
24 | publish(name, data) {
25 | const handlers = this.handlers[name]
26 | if (handlers) {
27 | handlers.forEach(handler => {
28 | handler(data)
29 | })
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/translate.js:
--------------------------------------------------------------------------------
1 | import { deepmerge } from './utils.js'
2 |
3 | let translations = {}
4 |
5 | let varRegex = /__(.*?)__/g
6 | let refRegex = /@@(.*?)@@/g
7 |
8 | function findString(id = '') {
9 | const part = id.split('.')
10 |
11 | const lastIndex = part.length - 1
12 | return part.reduce((nodeInfo, cur, index) => {
13 | const { node, found } = nodeInfo
14 | const isLast = index == lastIndex
15 | const isString = typeof node == 'string'
16 |
17 | if (isString) {
18 | return {
19 | found,
20 | node
21 | }
22 | } else {
23 | if (node && node[cur]) {
24 | if (isLast && node[cur]._) {
25 | return {
26 | found: true,
27 | node: node[cur]._
28 | }
29 | } else {
30 | return {
31 | found: isLast,
32 | node: node[cur]
33 | }
34 | }
35 | } else {
36 | return {
37 | found: node && '_' in node,
38 | node: node && node._
39 | }
40 | }
41 | }
42 | }, { node: translations })
43 | }
44 |
45 | function regexReplace(regex, str, callback) {
46 | let match
47 | let result = str
48 |
49 | const re = new RegExp(regex)
50 | while ((match = re.exec(str)) !== null) {
51 | const value = callback(match[1])
52 | if (typeof value != 'undefined')
53 | result = result.replace(match[0], value)
54 | }
55 |
56 | return result
57 | }
58 |
59 | /**
60 | * Translates a string given its id.
61 | *
62 | * @param {string} id Identifier in the form
63 | * `key1.key2.key3`
64 | * @param {object} vars Object with substitution variables. It will
65 | * substitute ocurrences when string contains this expression:
66 | * `__variable__`. For example the string `"My name is __name__"` with
67 | * `vars = { name: 'David' }` will return `"My name is David"`.
68 | *
69 | * Keys will be searched by partitioning the path.
70 | *
71 | * It will get the latest found key if any. For example, given the
72 | * strings `{ "a": { "b": 'Hello' } }` and looking for `'a.b.c'` it will
73 | * return `'a.b'` (`"Hello"`).
74 | * @returns Translated string
75 | */
76 | export function tr(id, vars = {}) {
77 | let { node } = findString(id)
78 | if (node) {
79 | // Find variables
80 | node = regexReplace(varRegex, node, match => vars[match])
81 |
82 | // Find references
83 | node = regexReplace(refRegex, node, match => tr(match, vars))
84 |
85 | return node
86 | } else
87 | return id
88 | }
89 |
90 | /**
91 | * Returns if the string does exist
92 | *
93 | * @param {string} id Identifier
94 | *
95 | * @returns { boolean } true if it exists
96 | */
97 | export function stringExists(id) {
98 | const { found } = findString(id)
99 | return found
100 | }
101 |
102 | /**
103 | * Sets the language.
104 | *
105 | * At the moment this does the same as addTranslations. The
106 | * reason is not to lose translations reference until a better
107 | * way is figured out.
108 | *
109 | * @param {lang} Translations object with the format
110 | * { key: { _: 'Some string', inner: 'Some other string' } }
111 | * Then, we have the following paths
112 | * - key -> 'Some string'
113 | * - key.inner -> 'Some other string'
114 | */
115 | export function setLanguage(lang) {
116 | addTranslations(lang)
117 | }
118 |
119 | /**
120 | * Appends translations to current translation table
121 | *
122 | * @param {object} lang Translations merged into current.
123 | */
124 | export function addTranslations(lang) {
125 | translations = deepmerge(translations, lang)
126 | }
127 |
128 | /**
129 | * Sets the translation engine that responds to tr().
130 | *
131 | * @param {function} translate Function with signature
132 | * translate(id, params).
133 | */
134 | export function setTranslator(translate) {
135 | tr = translate
136 | }
137 |
138 | /**
139 | * Sets the regex for the variables
140 | */
141 | export function setTranslateVariableRegex(newVarRegex) {
142 | varRegex = newVarRegex
143 | }
144 |
145 | /**
146 | * Sets the regex for the substitutions
147 | */
148 | export function setTranslateReferenceRegex(newRefRegex) {
149 | refRegex = newRefRegex
150 | }
151 |
--------------------------------------------------------------------------------
/src/translation_utils.js:
--------------------------------------------------------------------------------
1 | import { en, es } from './translations'
2 | import { deepmerge } from './utils.js'
3 | import {
4 | tr,
5 | setLanguage
6 | } from './translate'
7 |
8 | export { tr, setLanguage }
9 |
10 | const defLangs = { en, es }
11 |
12 | let modelBasePath = 'models'
13 |
14 | /**
15 | * Loads a language from the languages table.
16 | *
17 | * @param {string} name Language code as in `'en'` or `'fr'`.
18 | */
19 | export function setLanguageByName(name) {
20 | if (name in defLangs)
21 | setLanguage(defLangs[name])
22 | }
23 |
24 | /**
25 | * Allows to add a bunch of strings to a language
26 | */
27 | export function addLanguageTranslations(lang, strings) {
28 | defLangs[lang] = deepmerge(defLangs[lang], strings)
29 | }
30 |
31 | /**
32 | * Multipurpose semantic-ish translation.
33 | *
34 | * @param {string} modelName Object name, usually what
35 | * you pass as the first parameter when you create
36 | * the schema.
37 | * @param {string} field Field name
38 | * @param {string} op Thing that varies based on
39 | * the type.
40 | */
41 | export function trModel(modelName, field, op) {
42 | return tr(trPath(modelName, field, op))
43 | }
44 |
45 | /**
46 | * Translate field name
47 | *
48 | * @param {string|object} modelName Object name, usually what
49 | * you pass as the first parameter when you create
50 | * the schema. It can also be an object with component
51 | * props so it will figure out the values
52 | * @param {string} field Field name
53 | */
54 | export function trField(modelName, field) {
55 | if (typeof modelName == 'object') {
56 | field = modelName.field
57 | modelName = modelName.schemaTypeName
58 | }
59 |
60 | return tr(trPath(modelName, field, '_field'))
61 | }
62 |
63 | /**
64 | * Translates error message.
65 | *
66 | * @param {string} error Code of the error (usually the
67 | * validation code-name)
68 | * @param {object} data Field configuration from `createSchema()`.
69 | */
70 | export function trError(error, data) {
71 | return tr(`error.${error}`, data)
72 | }
73 |
74 | /**
75 | * Generates a model translation path.
76 | *
77 | * @param {string} model Name of the model (ie: 'client')
78 | * @param {string} field Name of the field
79 | * @param {string} op Name of the option or any subthing.
80 | *
81 | * @returns {string} id for the translation string
82 | */
83 | export function trPath(model, field, op) {
84 | if (typeof op == 'undefined')
85 | return [modelBasePath, model, field].join('.')
86 | else
87 | return [modelBasePath, model, field, op].join('.')
88 | }
89 |
90 | /**
91 | * Sets the base for the semantich(ish) translation, so
92 | * instead of 'models..' can be
93 | * 'my.base..'
94 | *
95 | * @param {string} newBasePath New path prepended to all
96 | * string paths.
97 | */
98 | export function trPathSetBase(newBasePath) {
99 | modelBasePath = newBasePath
100 | }
101 |
--------------------------------------------------------------------------------
/src/translations/en.js:
--------------------------------------------------------------------------------
1 | export const en = {
2 | add: 'Add',
3 | remove: 'Remove',
4 | error: {
5 | _: 'Error',
6 | type: 'Incorrect value, expecting a __type__',
7 | min: 'Value must be more than __min__',
8 | max: 'Value must be less than __max__',
9 | minLength: 'Value must be more than __minLength__ characters long',
10 | maxLength: 'Value must be less than __maxLength__ characters long',
11 | required: 'Required',
12 | minChildren: 'Expected to have at least __minChildren__',
13 | maxChildren: 'Can only have __maxChildren__',
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/translations/es.js:
--------------------------------------------------------------------------------
1 | export const es = {
2 | add: 'Añadir',
3 | remove: 'Quitar',
4 | error: {
5 | _: 'Error',
6 | type: 'Valor incorrecto, esperando __type__',
7 | min: 'El valor debe ser superior a __min__',
8 | max: 'El valor debe ser inferior a __max__',
9 | minLength: 'El valor debe tener como mínimo __minLength__ caracteres',
10 | maxLength: 'El valor debe tener como máximo __maxLength__ caracteres',
11 | required: 'Requerido',
12 | minChildren: 'Se esperan __minChildren__',
13 | maxChildren: 'Sólo puede haber __maxChildren__',
14 | }
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/src/translations/index.js:
--------------------------------------------------------------------------------
1 | export * from './en'
2 | export * from './es'
3 |
--------------------------------------------------------------------------------
/src/ui/Autofield.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useFormState } from 'react-hook-form'
3 | import { objectTraverse } from '../utils'
4 | import { tr, stringExists } from '../translate'
5 | import { trPath } from '../translation_utils'
6 | import classnames from 'classnames'
7 |
8 | export const Autofield = (props) => {
9 | const {
10 | id,
11 | name,
12 | wrapper = props.skin.defaultWrap,
13 | component,
14 | field,
15 | formHook: { control },
16 | formHook,
17 | defaultValue,
18 | fieldSchema,
19 | helperText,
20 | inputRef,
21 | forceErrors,
22 | type,
23 | option,
24 | inline,
25 | styles,
26 | skinElement,
27 | noRef,
28 | noAutocomplete,
29 | onChange,
30 | onBlur,
31 | ...rest
32 | } = props
33 |
34 | const nameForErrors = skinElement.nameForErrors ?
35 | skinElement.nameForErrors(name) : name
36 |
37 | const { errors } = useFormState({ control, name: nameForErrors })
38 | const fieldErrors = forceErrors && forceErrors[nameForErrors]
39 | || objectTraverse(errors, nameForErrors, { returnValue: true })
40 | const errorText = fieldErrors && fieldErrors.message
41 |
42 | const actualKey = option ? `${name}.${option}` : name
43 | const $wrapper = wrapper
44 | const $component = component
45 | const isComponent = typeof component != 'string'
46 | let componentBaseProps = {
47 | id,
48 | key: actualKey,
49 | name,
50 | type,
51 | defaultValue,
52 | onChange,
53 | onBlur,
54 | className: classnames(styles.input, styles.standard, {
55 | [styles.errored]: fieldErrors
56 | }),
57 | ...fieldSchema.addInputProps
58 | }
59 |
60 | if (option)
61 | componentBaseProps.value = option
62 |
63 | let finalHelperText = helperText || fieldSchema.helperText
64 | if (!finalHelperText) {
65 | const helperId = trPath(props.schemaTypeName, field, '_helper')
66 | if (stringExists(helperId))
67 | finalHelperText = tr(helperId)
68 | }
69 |
70 | let componentProps
71 | if (isComponent) {
72 | componentProps = {
73 | ...rest,
74 | ...componentBaseProps,
75 | field,
76 | forceErrors,
77 | errorText,
78 | fieldSchema,
79 | formHook,
80 | styles,
81 | skinElement,
82 | inputRef,
83 | helperText: finalHelperText
84 | }
85 | } else {
86 | componentProps = {
87 | ...componentBaseProps,
88 | ref: inputRef
89 | }
90 | }
91 |
92 | if (noAutocomplete || fieldSchema.noAutocomplete)
93 | componentProps.autoComplete = 'off'
94 |
95 | return (
96 | <$wrapper
97 | {...rest}
98 | id={id}
99 | key={actualKey}
100 | name={name}
101 | field={field}
102 | styles={styles}
103 | fieldSchema={fieldSchema}
104 | errorText={errorText}
105 | helperText={finalHelperText}
106 | inline={inline}
107 | addWrapperProps={fieldSchema.addWrapperProps}
108 | >
109 | <$component
110 | {...componentProps}
111 | key={componentProps.key}
112 | />
113 | $wrapper>
114 | )
115 | }
116 |
--------------------------------------------------------------------------------
/src/ui/AutofieldContainer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useController } from 'react-hook-form'
3 | import { Autofield } from './Autofield'
4 | import {
5 | objectTraverse,
6 | valueFromEvent,
7 | getPropsTransform
8 | } from '../utils'
9 | import { useAutofieldState } from '../autoform_state'
10 |
11 | export const AutofieldContainer = (props) => {
12 | const {
13 | id,
14 | name,
15 | fieldSchema,
16 | defaultValue,
17 | schemaTypeName,
18 | skinElement,
19 | formHook: { control },
20 | formHook,
21 | register,
22 | rules,
23 | overrides,
24 | skin,
25 | stateControl,
26 | setVisible,
27 | setHelperText,
28 | setValue,
29 | arrayControl
30 | } = props
31 |
32 | const {
33 | visible,
34 | helperText
35 | } = useAutofieldState({ name, stateControl })
36 |
37 | let baseProps = Object.assign({}, props, { helperText })
38 |
39 | const { controlled } = skinElement
40 |
41 | if (controlled) {
42 | const { field } = useController({
43 | name,
44 | control: control,
45 | rules
46 | })
47 |
48 | baseProps.onChange = field.onChange
49 | baseProps.onBlur = field.onBlur
50 | baseProps.value = field.value
51 | } else {
52 | if (!skinElement.skipRegister) {
53 | const registerProps = register(name, rules)
54 | if (registerProps) {
55 | baseProps.onBlur = registerProps.onBlur
56 | baseProps.onChange = registerProps.onChange
57 | baseProps.inputRef = registerProps.ref
58 | }
59 | }
60 | }
61 |
62 | // Allow field schema or overrides onChange
63 | if ('onChange' in fieldSchema || 'onChange' in overrides) {
64 | const baseOnChange = baseProps.onChange
65 | const overrideOnChange = overrides.onChange
66 | if (overrideOnChange)
67 | delete overrides.onChange
68 |
69 | const onChangeArguments = {
70 | name,
71 | setVisible,
72 | setHelperText,
73 | formHook,
74 | setValue,
75 | arrayControl
76 | }
77 |
78 | const fireOnChange = (value) => {
79 | if (fieldSchema.onChange)
80 | fieldSchema.onChange(value, onChangeArguments)
81 | if (overrideOnChange)
82 | overrideOnChange(value, onChangeArguments)
83 | }
84 |
85 | baseProps.onChange = (event) => {
86 | const value = valueFromEvent(event)
87 | baseOnChange(event)
88 | fireOnChange(value)
89 | }
90 |
91 | baseProps.setValue = (name, value) => {
92 | setValue(name, value)
93 | fireOnChange(value)
94 | }
95 | }
96 |
97 | // Allow general onChange, passed to
98 | if (props.onChange) {
99 | const oldOnChange = baseProps.onChange
100 | baseProps.onChange = (event) => {
101 | oldOnChange(event)
102 | props.onChange()
103 | }
104 | }
105 |
106 | const propsTransform = getPropsTransform(skinElement)
107 |
108 | let transformedProps
109 | if (typeof propsTransform == 'function')
110 | transformedProps = propsTransform ? propsTransform(baseProps) : baseProps
111 | else
112 | transformedProps = { ...baseProps, ...propsTransform }
113 | transformedProps = { ...transformedProps, ...overrides }
114 |
115 | const component = transformedProps.component || skinElement.component
116 | if (visible && component) {
117 | return (
118 |
122 | )
123 | } else {
124 | return null
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/ui/Autoform.jsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from 'react'
2 | import defaultSkin from './defaultSkin'
3 | import { AutoformBase } from './AutoformBase'
4 |
5 | /**
6 | * Creates a form using the current skin. The form
7 | * has all the needed fields, styles and validation
8 | * errors in order to work.
9 | */
10 | export let Autoform = (props, ref) => {
11 | const {
12 | skin = defaultSkin,
13 | ...rest
14 | } = props
15 |
16 | return (
17 |
22 | )
23 | }
24 |
25 | Autoform = forwardRef(Autoform)
26 |
--------------------------------------------------------------------------------
/src/ui/AutoformBase.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useImperativeHandle, forwardRef } from 'react'
2 | import {
3 | objectTraverse,
4 | isObject,
5 | deepmerge,
6 | getSkinComponent
7 | } from '../utils'
8 | import { useForm } from 'react-hook-form'
9 | import { getComponents, renderInputs } from './componentRender'
10 | import { useAutoformState } from '../autoform_state'
11 |
12 | import baseSkin from './baseSkin'
13 |
14 | /**
15 | * Creates a form using the provided skin. The form
16 | * has all the needed fields, styles and validation
17 | * errors in order to work.
18 | */
19 | export let AutoformBase = (props, ref) => {
20 | const {
21 | schema,
22 | elementProps,
23 | initialValues = {},
24 | children,
25 | onSubmit,
26 | onErrors,
27 | styles,
28 | submitButton,
29 | submitButtonText,
30 | skin,
31 | skinOverride,
32 | skipManualReset,
33 | ...rest
34 | } = props
35 |
36 | if (!schema) {
37 | throw new Error(' was rendered without schema.')
38 | }
39 |
40 | const formHook = useForm({
41 | mode: 'all',
42 | defaultValues: initialValues
43 | })
44 | const {
45 | control,
46 | formState,
47 | register,
48 | unregister,
49 | handleSubmit,
50 | reset
51 | } = formHook
52 |
53 | const finalSkin = { ...baseSkin, ...skin, ...skinOverride }
54 |
55 | const {
56 | coercedSubmit,
57 | coercedChange,
58 | setValue,
59 | setVisible,
60 | setHelperText,
61 | resetState,
62 | stateControl,
63 | getValues
64 | } = useAutoformState({
65 | initialValues,
66 | onSubmit,
67 | onChange: props.onChange,
68 | schema,
69 | skin: finalSkin,
70 | formHook,
71 | skipManualReset
72 | })
73 |
74 | const submit = handleSubmit(coercedSubmit, onErrors)
75 |
76 | useImperativeHandle(ref, () => ({
77 | submit,
78 | formHook: () => formHook,
79 | setValue,
80 | setVisible,
81 | getValues,
82 | reset: resetState
83 | }))
84 |
85 | const inputProps = {
86 | ...rest,
87 | ...elementProps,
88 | reset,
89 | children,
90 | initialValues,
91 | schema,
92 | register,
93 | unregister,
94 | styles,
95 | skin: finalSkin,
96 | formHook,
97 | autoformProps: props,
98 | stateControl,
99 | setValue,
100 | setVisible,
101 | setHelperText,
102 | onChange: coercedChange
103 | }
104 |
105 | const Button = getSkinComponent(finalSkin.button)
106 | const Form = getSkinComponent(finalSkin.form)
107 |
108 | return (
109 |
123 | )
124 | }
125 |
126 | AutoformBase = forwardRef(AutoformBase)
127 |
--------------------------------------------------------------------------------
/src/ui/baseSkin.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { InputArrayWrap } from './components/InputArrayWrap'
3 | import { Submodel } from './components/Submodel'
4 | import { getSkinComponent } from '../utils'
5 |
6 | function getOtherSchema(schemaDef, fieldName, { isArray }) {
7 | const field = schemaDef[fieldName]
8 | const { type } = field
9 | const other = isArray ? type[0] : type
10 | return other.getSchema()
11 | }
12 |
13 | import { deletedMark } from './deletedMark'
14 |
15 | export default {
16 | array: {
17 | skipRegister: true,
18 | nameForErrors: name => `${name}__count`,
19 | coerce: (arr = [], { coerceObject, schemaDef, fieldName }) => {
20 | const otherSchema = getOtherSchema(schemaDef, fieldName, {
21 | isArray: true
22 | })
23 |
24 | if (Array.isArray(arr)) {
25 | return arr.map(entry => {
26 | if (entry[deletedMark])
27 | return null
28 | else
29 | return coerceObject({ object: entry, schemaDef: otherSchema })
30 | }).filter(entry => entry !== null)
31 | } else {
32 | return []
33 | }
34 | },
35 | props: props => {
36 | const {
37 | config = {},
38 | fieldSchema,
39 | skin,
40 | ...rest
41 | } = props
42 |
43 | const { arrayMode } = config
44 | const finalArrayMode = fieldSchema.arrayMode || arrayMode
45 | const isTable = finalArrayMode == 'table'
46 | const ArrayTable = getSkinComponent(skin.arrayTable)
47 | const ArrayPanel = getSkinComponent(skin.arrayPanel)
48 | const arrayHandler = isTable ? ArrayTable : ArrayPanel
49 |
50 | return {
51 | ...rest,
52 | config,
53 | component: InputArrayWrap,
54 | initiallyEmpty: fieldSchema.initiallyEmpty,
55 | fieldSchema,
56 | arrayHandler,
57 | inline: true,
58 | noRef: true,
59 | isTable,
60 | skin
61 | }
62 | },
63 | },
64 | schema: {
65 | skipRegister: true,
66 | coerce: (obj = {}, { coerceObject, schemaDef, fieldName }) => {
67 | const otherSchema = getOtherSchema(schemaDef, fieldName, { isArray: false })
68 |
69 | return coerceObject({ object: obj, schemaDef: otherSchema })
70 | },
71 | component: Submodel
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/ui/componentRender.jsx:
--------------------------------------------------------------------------------
1 | import React, { Children } from 'react'
2 |
3 | import {
4 | schemaTypeEx,
5 | inputName
6 | } from '../utils'
7 | import { tr } from '../translate'
8 | import { trError } from '../translation_utils'
9 | import { FieldPropsOverride } from './components/FieldPropsOverride'
10 | import { AutofieldContainer } from './AutofieldContainer'
11 |
12 | const validations = {
13 | required: ({ value, message }) => message,
14 | maxLength: 'maxLength',
15 | minLength: 'minLength',
16 | max: 'max',
17 | min: 'min',
18 | pattern: 'pattern',
19 | validate: 'validate'
20 | }
21 |
22 | /**
23 | * Creates validation rules after schema
24 | *
25 | * @param {object} fieldSchema
26 | */
27 | export function validationRules(fieldSchema) {
28 | const validationKeys = Object.keys(validations)
29 | return validationKeys.reduce((result, key) => {
30 | if (key in fieldSchema) {
31 | const validation = fieldSchema[key]
32 | let data
33 | if (typeof validation == 'object') {
34 | if (validation.message && typeof validation.message == 'function')
35 | validation.message = validation.message(fieldSchema)
36 | data = validation
37 | } else if (key == 'validate') {
38 | data = value => {
39 | const erroring = validation(value)
40 | return erroring === false || erroring
41 | }
42 | } else {
43 | data = {
44 | value: fieldSchema[key],
45 | message: trError(key, fieldSchema)
46 | }
47 | }
48 |
49 | result[key] = typeof validations[key] == 'function' ?
50 | validations[key](data) : data
51 | }
52 |
53 | return result
54 | }, {})
55 | }
56 |
57 | /**
58 | * Searches in children to find overrides.
59 | */
60 | function searchForOverrides(parent, name, children = []) {
61 | const childrenArr = Children.map(children, child => child)
62 |
63 | return childrenArr.reduce((override, child) => {
64 | const childName = child.props.name
65 | const dottedChild = childName && childName.replace(/(\[|\]\.)/g, '.')
66 | const isOverride = child.type == FieldPropsOverride
67 | if (isOverride && dottedChild == name) {
68 | const cloned = Object.assign({}, child.props)
69 | delete cloned.name
70 |
71 | return cloned
72 | } else {
73 | return override
74 | }
75 | }, {})
76 | }
77 |
78 | /**
79 | * Renders a single field.
80 | *
81 | * @param {object} params
82 | * @param {string} params.field Name of the field
83 | * @param {object} params.fieldSchema Schema specification
84 | * for the field
85 | * @param {string} params.parent Prefix of the field name
86 | * @param {string} params.schemaTypeName Name of the schema
87 | * (first argument while instantiating a schema)
88 | * @param {object} params.config Form configuration
89 | * @param {...object} params.rest props passed to the component
90 | */
91 | export function renderInput({
92 | field,
93 | fieldSchema,
94 | fieldSchema: {
95 | type,
96 | required,
97 | defaultValue
98 | },
99 | initialValue,
100 | parent,
101 | children,
102 | propOverrides,
103 | schemaTypeName,
104 | config = {},
105 | index,
106 | skin,
107 | styles,
108 | ...rest
109 | }) {
110 | const strType = schemaTypeEx(type)
111 |
112 | function describePlace() {
113 | return `Schema "${schemaTypeName}" has field "${field}"`
114 | }
115 |
116 | if (!strType) {
117 | throw `${describePlace()} that lacks type description.`
118 | }
119 |
120 | const skinElement = skin[strType]
121 |
122 | if (!skinElement) {
123 | throw `${describePlace()} with type "${strType}" `
124 | + 'that doesn\'t exist in skin.'
125 | }
126 |
127 | const rules = validationRules(fieldSchema)
128 | const fullField = inputName({ parent, index, field })
129 | const id = `${schemaTypeName}-${fullField}`
130 |
131 | const overrides = searchForOverrides(parent, fullField, propOverrides)
132 |
133 | defaultValue = typeof initialValue == 'undefined' ?
134 | defaultValue : initialValue
135 |
136 | return (
137 |
155 | )
156 | }
157 |
158 | /**
159 | * Renders the inputs to make the schema work.
160 | *
161 | * @param {object} params
162 | * @param {Schema} params.schema Schema instance
163 | * @param {object} params.config Rendering configuration
164 | * @param {string} params.config.arrayMode 'panels' or 'table'
165 | * @param {...object} params.rest Props passed to every input
166 | *
167 | * @returns {array} React elements with the form and inputs.
168 | */
169 | export function renderInputs({
170 | schema,
171 | config = {},
172 | children,
173 | propOverrides,
174 | initialValues = {},
175 | styles = {},
176 | ...rest
177 | }) {
178 | const schemaDef = schema.getSchema()
179 | const schemaKeys = Object.keys(schemaDef)
180 |
181 | return schemaKeys.map(field =>
182 | renderInput({
183 | ...rest,
184 | field,
185 | config,
186 | propOverrides: propOverrides || children,
187 | fieldSchema: schemaDef[field],
188 | schemaTypeName: schema.getType(),
189 | initialValue: initialValues[field],
190 | styles
191 | })
192 | )
193 | }
194 |
--------------------------------------------------------------------------------
/src/ui/components/Button.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export const Button = ({ styles, text, children, ...rest }) =>
4 |
13 |
--------------------------------------------------------------------------------
/src/ui/components/Checkbox.jsx:
--------------------------------------------------------------------------------
1 | import { trModel } from '../../translation_utils'
2 |
3 | export let Checkbox = ({
4 | id,
5 | schemaTypeName,
6 | name,
7 | onChange,
8 | onBlur,
9 | defaultValue,
10 | inputRef,
11 | styles = {},
12 | field
13 | }) => {
14 | const defaultChecked = defaultValue !== 'false' && defaultValue
15 |
16 | return (
17 |
18 |
29 |
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/src/ui/components/FieldPropsOverride.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | /**
4 | * Allows to specify extra props for a field in runtime.
5 | */
6 | export const FieldPropsOverride = () => null
7 |
--------------------------------------------------------------------------------
/src/ui/components/InputArrayPanel.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { getSkinComponent } from '../../utils'
3 |
4 | const renderItemHeader = ({ styles, closeButton }) => {
5 | return (
6 |