47 | export class FieldRenderer extends React.PureComponent
{}
48 |
49 | export type KeyValue = { [key: string]: any }
50 |
51 | export function basePropTypes() {
52 | return {
53 | id: PropTypes.string.isRequired,
54 | className: PropTypes.string,
55 | data: PropTypes.any,
56 | errors: PropTypes.arrayOf(PropTypes.string),
57 | actions: PropTypes.objectOf(PropTypes.func),
58 | config: PropTypes.shape({
59 | type: PropTypes.string,
60 | }),
61 | disabled: PropTypes.bool,
62 | dirty: PropTypes.bool,
63 | };
64 | }
65 |
--------------------------------------------------------------------------------
/packages/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@react-ui-generator/core",
3 | "version": "0.7.5",
4 | "description": "Core module for react-ui-generator",
5 | "main": "./out/core.js",
6 | "types": "./out/index.d.ts",
7 | "repository": "https://github.com/react-ui-generator/react-ui-generator/packages/core",
8 | "scripts": {
9 | "build": "webpack --config config/webpack.config.js",
10 | "test": "jest -c config/tests/jest.config.js"
11 | },
12 | "author": "Alexei Zaviruha ",
13 | "license": "MIT",
14 | "dependencies": {
15 | "lodash-es": "^4.17.15",
16 | "prop-types": "^15.6.1"
17 | },
18 | "devDependencies": {
19 | "@babel/core": "^7.5.5",
20 | "@babel/plugin-proposal-class-properties": "^7.0.0",
21 | "@babel/plugin-proposal-json-strings": "^7.0.0",
22 | "@babel/plugin-syntax-dynamic-import": "^7.0.0",
23 | "@babel/plugin-syntax-import-meta": "^7.0.0",
24 | "@babel/preset-env": "^7.0.0",
25 | "@babel/preset-react": "^7.0.0",
26 | "@testing-library/jest-dom": "^4.0.0",
27 | "@testing-library/react": "^8.0.7",
28 | "@types/jest": "^24.0.15",
29 | "@types/lodash-es": "^4.17.3",
30 | "@types/prop-types": "^15.5.3",
31 | "@types/react": "^16.0.29",
32 | "@types/sinon": "^5.0.1",
33 | "awesome-typescript-loader": "^5.2.1",
34 | "babel-jest": "^24.8.0",
35 | "babel-loader": "^8.0.6",
36 | "babel-upgrade": "^0.0.24",
37 | "clean-webpack-plugin": "^3.0.0",
38 | "jest": "^24.8.0",
39 | "regenerator": "^0.14.2",
40 | "sinon": "^5.1.0",
41 | "source-map-loader": "^0.2.4",
42 | "ts-jest": "^24.0.2",
43 | "typescript": "^3.5.3",
44 | "webpack": "^4.36.1",
45 | "webpack-cleanup-plugin": "^0.5.1",
46 | "webpack-cli": "^3.3.6",
47 | "webpack-merge": "^4.1.2"
48 | },
49 | "peerDependencies": {
50 | "react": "^16.3.0"
51 | },
52 | "gitHead": "f34904c44b092961eef4fa3e5fc9b62b18628838"
53 | }
54 |
--------------------------------------------------------------------------------
/packages/antd/src/renderers/Input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { ChangeEvent } from 'react';
3 | import Input from 'antd/lib/input';
4 | import PropTypes from 'prop-types'
5 |
6 | import {
7 | FieldRendererProps,
8 | FieldRenderer,
9 | basePropTypes
10 | } from '@react-ui-generator/core';
11 |
12 | import { FieldWrapper } from './FieldWrapper';
13 |
14 | export interface InputProps extends FieldRendererProps {
15 | type?: string;
16 | }
17 |
18 | const value: string = '';
19 |
20 | export class _Input extends FieldRenderer {
21 | static propTypes = {
22 | ...basePropTypes(),
23 | type: PropTypes.string,
24 | config: PropTypes.shape({
25 | label: PropTypes.string,
26 | placeholder: PropTypes.string,
27 | showAsterix: PropTypes.bool
28 | })
29 | };
30 |
31 | static defaultProps = {
32 | type: 'text',
33 | className: '',
34 | disabled: false,
35 | dirty: false,
36 | config: {
37 | label: '',
38 | placeholder: '',
39 | showAsterix: false
40 | },
41 | data: value
42 | };
43 |
44 | handleChange(event: ChangeEvent): void {
45 | this.props.onChange(event.target.value);
46 | }
47 |
48 | render() {
49 | const {
50 | type,
51 | id,
52 | data,
53 | className,
54 | onChange,
55 | config: { label, showAsterix, placeholder },
56 | disabled,
57 | ...rest
58 | } = this.props;
59 | const value: string = String(data);
60 |
61 | return (
62 |
63 | {
70 | this.handleChange(event);
71 | }}
72 | disabled={disabled}
73 | />
74 |
75 | );
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/doc/ru/antd renderers API.md:
--------------------------------------------------------------------------------
1 | # API отрисовщиков на базе библиотеки Ant Design
2 |
3 | ## Тип отрисовщика "text"
4 |
5 | Возможные настройки для данного типа отрисовщика:
6 |
7 | ```js
8 | {
9 | "type": "text",
10 | "config": {
11 | "label": "...",
12 | "placeholder": "...",
13 | "showAsterix": true|false,
14 | }
15 | }
16 | ```
17 |
18 | ## Тип отрисовщика "textarea"
19 |
20 | Возможные настройки для данного типа отрисовщика:
21 |
22 | ```js
23 | {
24 | "type": "textarea",
25 | "config": {
26 | "label": "...",
27 | "placeholder": "...",
28 | "showAsterix": true|false,
29 | "rows": number
30 | }
31 | }
32 | ```
33 |
34 | ## Тип отрисовщика "checkbox"
35 |
36 | Возможные настройки для данного типа отрисовщика:
37 |
38 | ```js
39 | {
40 | "type": "checkbox",
41 | "config": {
42 | "label": "...",
43 | "title": "...",
44 | "showAsterix": true|false,
45 | }
46 | }
47 | ```
48 |
49 | ## Тип отрисовщика "radiogroup"
50 |
51 | Возможные настройки для данного типа отрисовщика:
52 |
53 | ```js
54 | {
55 | "type": "radiogroup",
56 | "config": {
57 | "label": "...",
58 | "showAsterix": true|false,
59 | "options": [
60 | { "id": ..., "title": "..." },
61 | ...
62 | ]
63 | }
64 | }
65 | ```
66 |
67 | ## Тип отрисовщика "select"
68 |
69 | Возможные настройки для данного типа отрисовщика:
70 |
71 | ```js
72 | {
73 | "type": "select",
74 | "config": {
75 | "label": "...",
76 | "title": "...",
77 | "showAsterix": true|false,
78 | "options": [
79 | { "id": ..., "title": "..." },
80 | ...
81 | ]
82 | }
83 | }
84 | ```
85 |
86 | ## Тип отрисовщика "button"
87 |
88 | Возможные настройки для данного типа отрисовщика:
89 |
90 | ```js
91 | {
92 | "type": "button",
93 | "config": {
94 | "title": "...",
95 | "color": "primary|ghost|dashed|danger",
96 | "outline": true|false
97 | }
98 | }
99 | ```
100 |
--------------------------------------------------------------------------------
/packages/antd/src/renderers/RadioGroup.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import PropTypes from 'prop-types'
3 | import Radio from 'antd/lib/radio';
4 | import { FieldRenderer, basePropTypes } from '@react-ui-generator/core';
5 | import { FieldWrapper } from './FieldWrapper';
6 |
7 | const RadioGroup = Radio.Group;
8 |
9 | export interface RadiogroupItem {
10 | id: string | number;
11 | title: string;
12 | }
13 |
14 | const value: string | number = null;
15 | const options: RadiogroupItem[] = [];
16 |
17 | export class _RadioGroup extends FieldRenderer {
18 | static propTypes = {
19 | ...basePropTypes(),
20 | config: PropTypes.shape({
21 | label: PropTypes.string,
22 | showAsterix: PropTypes.bool,
23 | options: PropTypes.arrayOf(
24 | PropTypes.shape({
25 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
26 | title: PropTypes.string
27 | })
28 | )
29 | })
30 | };
31 |
32 | static defaultProps = {
33 | className: '',
34 | disabled: false,
35 | dirty: false,
36 | config: {
37 | label: '',
38 | showAsterix: false,
39 | options
40 | },
41 | data: value
42 | };
43 |
44 | handleChange(value: string): void {
45 | this.props.onChange(value);
46 | }
47 |
48 | render() {
49 | const {
50 | id,
51 | data,
52 | className,
53 | onChange,
54 | config: { label, showAsterix, options },
55 | disabled,
56 | ...rest
57 | } = this.props;
58 |
59 | return (
60 |
61 | {
65 | this.handleChange(event.target.value);
66 | }}
67 | >
68 | {options.map((item: RadiogroupItem, idx: number) => (
69 |
70 | {item.title}
71 |
72 | ))}
73 |
74 |
75 | );
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/packages/demo/src/reducers.js:
--------------------------------------------------------------------------------
1 | import deepmerge from 'deepmerge';
2 |
3 | import {
4 | withDefaults,
5 | findFieldMetaById,
6 | } from '@react-ui-generator/core';
7 |
8 | import {
9 | UPDATE_FORM,
10 | CLEAR_FORM,
11 | FORM_SENDING_START,
12 | FORM_SENDING_FINISH,
13 | ADD_RELATIVE,
14 | REMOVE_RELATIVE
15 | } from '@actions';
16 |
17 | const meta = require('@meta/complete');
18 | const initialState = {
19 | meta,
20 | data: withDefaults({}, meta.fields),
21 | errors: {},
22 | dirtiness: {},
23 | isValid: false,
24 | };
25 |
26 | function reducer(state = initialState, { type: actionType, payload }) {
27 | switch (actionType) {
28 | case UPDATE_FORM: {
29 | return merge(state, payload);
30 | }
31 |
32 | case CLEAR_FORM: {
33 | return { ...initialState };
34 | }
35 |
36 | case FORM_SENDING_START: {
37 | return { ...state, isFetching: true };
38 | }
39 |
40 | case FORM_SENDING_FINISH: {
41 | return { ...state, isFetching: false };
42 | }
43 |
44 | case ADD_RELATIVE: {
45 | const subFormMeta = findFieldMetaById('relatives', state.meta.fields);
46 |
47 | const newState = merge(state, {
48 | data: {
49 | relatives: [
50 | ...state.data.relatives,
51 | withDefaults({}, subFormMeta.renderer.config.fields)
52 | ]
53 | }
54 | });
55 |
56 | return newState;
57 | }
58 |
59 | case REMOVE_RELATIVE: {
60 | const idx = payload;
61 | const subFormMeta = findFieldMetaById('relatives', state.meta.fields);
62 | const newValue = [ ...state.data.relatives ];
63 |
64 | newValue.splice(idx, 1);
65 |
66 | const newState = merge(state, {
67 | data: {
68 | relatives: newValue
69 | }
70 | });
71 |
72 | return newState;
73 | }
74 |
75 | default:
76 | return state;
77 | }
78 | }
79 |
80 | export default reducer;
81 |
82 |
83 | function overwriteMerge(destArray, sourceArray, options) {
84 | return sourceArray;
85 | }
86 |
87 | function merge(dest, source) {
88 | return deepmerge(dest, source, { arrayMerge: overwriteMerge });
89 | }
--------------------------------------------------------------------------------
/packages/antd/src/renderers/Upload.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import get from 'lodash-es/get'
3 | import PropTypes from 'prop-types'
4 |
5 | import Upload, { UploadChangeParam } from 'antd/lib/upload';
6 | import Button from 'antd/lib/button';
7 | import Icon from 'antd/lib/icon';
8 | import { FieldRenderer, basePropTypes } from '@react-ui-generator/core';
9 | import { FieldWrapper } from './FieldWrapper';
10 |
11 | const value: string = null;
12 |
13 | export class _Upload extends FieldRenderer {
14 | static propTypes = {
15 | ...basePropTypes(),
16 | config: PropTypes.shape({
17 | label: PropTypes.string,
18 | buttonLabel: PropTypes.string,
19 | showAsterix: PropTypes.bool,
20 | url: PropTypes.string,
21 | responsePath: PropTypes.string
22 | })
23 | };
24 |
25 | static defaultProps = {
26 | className: '',
27 | disabled: false,
28 | dirty: false,
29 | config: {
30 | label: '',
31 | buttonLabel: '',
32 | showAsterix: false,
33 | url: '',
34 | responsePath: ''
35 | },
36 | data: value
37 | };
38 |
39 | handleChange = (info: UploadChangeParam): void => {
40 | const { config, onChange } = this.props;
41 | const uploaded = info.fileList
42 | .filter(item => item.response)
43 | .map(item => get(item, `response.${config.responsePath}`))
44 | .map(id => id.toString());
45 |
46 | onChange(uploaded);
47 | };
48 |
49 | render() {
50 | const {
51 | id,
52 | data,
53 | className,
54 | onChange,
55 | config: { label, showAsterix, url, responsePath, buttonLabel },
56 | disabled,
57 | ...rest
58 | } = this.props;
59 | const value: string = String(data);
60 |
61 | return (
62 |
63 |
69 |
73 |
74 |
75 | );
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/packages/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@react-ui-generator/demo",
3 | "private": true,
4 | "version": "0.7.5",
5 | "description": "Interactive examples for react-ui-generator",
6 | "main": "out/index.js",
7 | "repository": "https://github.com/react-ui-generator/react-ui-generator/packages/demo",
8 | "scripts": {
9 | "test": "echo \"Error: no test specified\" && exit 1",
10 | "build": "yarn run webpack-cli --config ./config/webpack.common.js --mode development",
11 | "build:prod": "yarn run webpack-cli --config ./config/webpack.prod.js",
12 | "start": "yarn run webpack-dev-server --config ./config/webpack.dev.js"
13 | },
14 | "author": "Alexei Zaviruha ",
15 | "license": "MIT",
16 | "devDependencies": {
17 | "@babel/core": "^7.5.5",
18 | "@babel/preset-env": "^7.0.0",
19 | "@babel/preset-react": "^7.0.0",
20 | "@types/jest": "^24.0.15",
21 | "@types/lodash-es": "^4.17.3",
22 | "babel-jest": "^24.8.0",
23 | "babel-loader": "^8.0.6",
24 | "babel-upgrade": "^0.0.24",
25 | "css-loader": "^3.2.0",
26 | "html-webpack-plugin": "^3.2.0",
27 | "jest": "^24.8.0",
28 | "progress-bar-webpack-plugin": "^1.11.0",
29 | "react-hot-loader": "^3.1.3",
30 | "source-map-explorer": "^1.5.0",
31 | "source-map-loader": "^0.2.3",
32 | "style-loader": "^1.0.0",
33 | "ts-jest": "^24.0.2",
34 | "typescript": "^3.5.3",
35 | "uglifyjs-webpack-plugin": "^1.2.5",
36 | "webpack": "^4.36.1",
37 | "webpack-bundle-analyzer": "^3.6.0",
38 | "webpack-cleanup-plugin": "^0.5.1",
39 | "webpack-cli": "^3.3.6",
40 | "webpack-dev-server": "^3.7.2",
41 | "webpack-merge": "^4.1.2",
42 | "webpack-visualizer-plugin": "^0.1.11"
43 | },
44 | "dependencies": {
45 | "@react-ui-generator/antd": "^0.7.5",
46 | "@react-ui-generator/bootstrap": "^0.7.5",
47 | "@react-ui-generator/core": "^0.7.5",
48 | "@react-ui-generator/serializers": "^0.5.3",
49 | "@react-ui-generator/validators": "^0.7.5",
50 | "ajv": "^6.0.0",
51 | "antd": "^3.24.2",
52 | "deepmerge": "^2.0.1",
53 | "history": "^4.7.2",
54 | "lodash-es": "^4.17.15",
55 | "moment": "^2.22.1",
56 | "react": "^16.3.2",
57 | "react-dom": "^16.3.2",
58 | "react-redux": "^5.0.6",
59 | "redux": "^3.7.2",
60 | "redux-thunk": "^2.2.0"
61 | },
62 | "gitHead": "f34904c44b092961eef4fa3e5fc9b62b18628838"
63 | }
64 |
--------------------------------------------------------------------------------
/packages/antd/src/renderers/Select.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import PropTypes from 'prop-types'
3 | import Select, { SelectValue } from 'antd/lib/select';
4 | import { FieldRenderer, basePropTypes } from '@react-ui-generator/core';
5 |
6 | import { FieldWrapper } from './FieldWrapper';
7 |
8 | const { Option } = Select;
9 |
10 | export interface SelectItemProps {
11 | id: string | number;
12 | title: string;
13 | }
14 |
15 | const value: SelectValue = undefined;
16 | const options: SelectItemProps[] = [];
17 |
18 | export class _Select extends FieldRenderer {
19 | static propTypes = {
20 | ...basePropTypes(),
21 | config: PropTypes.shape({
22 | label: PropTypes.string,
23 | placeholder: PropTypes.string,
24 | showAsterix: PropTypes.bool,
25 | options: PropTypes.arrayOf(
26 | PropTypes.shape({
27 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
28 | title: PropTypes.string
29 | })
30 | )
31 | })
32 | };
33 |
34 | static defaultProps = {
35 | className: '',
36 | disabled: false,
37 | dirty: false,
38 | config: {
39 | label: '',
40 | placeholder: '',
41 | showAsterix: false,
42 | options
43 | },
44 | data: value
45 | };
46 |
47 | handleChange = (value: SelectValue): void => {
48 | this.props.onChange(value);
49 | };
50 |
51 | render() {
52 | const {
53 | id,
54 | config: { label, placeholder, showAsterix, title, options, allowClear },
55 | data,
56 | disabled,
57 | className,
58 | onChange,
59 | ...rest
60 | } = this.props;
61 |
62 | let value: SelectValue;
63 |
64 | if (typeof data === 'boolean') {
65 | value = Number(data);
66 | } else if (data === null) {
67 | value = undefined;
68 | } else {
69 | value = data;
70 | }
71 |
72 | return (
73 |
74 |
89 |
90 | );
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/packages/bootstrap/src/renderers/Select.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { ChangeEvent } from 'react';
3 | import makeClass from 'classnames';
4 | import { Input } from 'reactstrap';
5 | import { FieldRendererProps, KeyValue } from '@react-ui-generator/core';
6 | import { ValidatableField } from './ValidatableField';
7 |
8 | export interface SelectProps extends FieldRendererProps {
9 | title: string;
10 | caret?: boolean;
11 | isOpen?: boolean;
12 | }
13 |
14 | export interface SelectState {
15 | valueTypes: KeyValue
16 | }
17 |
18 | interface SelectItemProps {
19 | id: any;
20 | title: string;
21 | }
22 |
23 | const Types: KeyValue = {
24 | 'number': Number,
25 | 'boolean': Boolean
26 | }
27 |
28 | export class Select extends React.PureComponent {
29 | constructor(props: SelectProps) {
30 | super(props);
31 | this.state = { valueTypes: this.getValueTypes(props) };
32 | }
33 |
34 | componentWillReceiveProps(nextProps: SelectProps) {
35 | this.setState({ valueTypes: this.getValueTypes(nextProps) });
36 | }
37 |
38 | getValueTypes(props: SelectProps): KeyValue {
39 | return props.config.options.reduce(
40 | (acc: KeyValue, option: SelectItemProps) => ({
41 | ...acc,
42 | [option.id]: typeof option.id
43 | }),
44 | {}
45 | );
46 | }
47 |
48 | handleChange(event: ChangeEvent): void {
49 | const rawValue = event.target.value;
50 | const valueType = this.state.valueTypes[rawValue];
51 | const Type = Types[valueType] || String;
52 | const value = Type(rawValue);
53 |
54 | this.props.onChange(value);
55 | }
56 |
57 | render() {
58 | const {
59 | id,
60 | actions: { onToggle },
61 | config: { title, options },
62 | data,
63 | disabled,
64 | className,
65 | onChange,
66 | errors
67 | } = this.props;
68 |
69 | const value: string = String(data);
70 |
71 | return (
72 |
73 | this.handleChange(event)} value={value}>
74 | >
77 | {options.map((item: SelectItemProps) => {
78 | const { id, title } = item;
79 | return (
80 |
83 | );
84 | })}
85 |
86 |
87 | );
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/TODO:
--------------------------------------------------------------------------------
1 |
2 | Roadmap to v1.0:
3 | ✔ [core] Implement basic form generation.
4 | ✔ [core] Implement bootstrap renderers.
5 | ✔ [core] Implement nested forms (SubForm and List).
6 | ✔ [core] Implement basic validation. @done(17-02-19)
7 | ✔ [validators] Implement JSON Schema validator. (Ajv) @done(17-02-19)
8 | ✔ [bootstrap] Fix implicit type convertion for Select options. @done(18-02-19)
9 | ✔ [core] Implement validation for nested forms. @done(18-02-19)
10 | ✔ [bootstrap] Implement password option for Bootstrap Text renderer. @done(18-02-19 21:39)
11 | ✔ [bootstrap] Fix validation rendering for Bootstrap RadioGroup renderer. @done(18-02-19 21:58)
12 | ✔ Implement basic serializer. @done(18-02-19 22:39)
13 | ✔ Implement serializer for nested forms. @done(18-02-19 22:39)
14 | ✔ [core] Implement isValid argument for form's onChange callback. @done(18-02-19 22:46)
15 | ✔ [general] Split core to few more packages (validators, serializers). @done(18-02-23 09:38)
16 | ✔ [core] Consider moving `errors` into `data`. @done(18-03-01 09:15) -- rejected. Simple props are easy to undestand and manipulate.
17 | ✔ [core] Make `errors` and `validator` optional. @done(18-03-06 09:27)
18 | ✔ [general] Minimize bundle size @done(18-05-15 15:01)
19 | ✔ [antd] Implement AntD renderers. @done(18-05-27 17:44)
20 | ✔ [core] Add checking for `null` in serializer's `if (typeof value === 'object')`. @done(18-05-29 16:43)
21 | ✔ [core] Add `propTypes` and `defaultProps` for renderers @done(18-05-30 16:33)
22 | ✔ [core] Add unit-test for `@react-ui-generator/core` @done(18-06-10)
23 | ✔ [antd] Add support for `config.showAsterix` property for all fields, that can be marked as required. @done(18-08-14 14:34)
24 | ✔ [metaphor] Add initial test suite. @done(19-10-21 17:14)
25 | ✔ [core, metaphor] Replace Enzyme with React Testing Library @done(19-10-21 17:14)
26 | ☐ [core] Change default value of radiogroup field to `false` (instead of "")
27 | ☐ [antd] Add `config.icon` and `config.iconPosition = right | left` to the Button renderer
28 | ☐ [core] Consider adding of the children propagation to the `Field` renderer
29 | ☐ [bootstrap] Add `propTypes` and `defaultProps` for renderers
30 | ☐ Implement dirtify helper.
31 | ✔ Add basic russian documentation. @done(18-09-20)
32 | ☐ Add basic english documentation.
33 | ☐ Add typedocs
34 | ☐ Implement AntD validator.
35 | ☐ Implement LIVR validator.
36 | ☐ Implement async, non-blocking validation.
37 | ☐ Add complete russian documentation.
38 | ☐ Add complete english documentation.
39 | ☐ Add simple JSX-to-JSON convertor, for layout serialization.
40 |
--------------------------------------------------------------------------------
/packages/antd/src/renderers/MultipleSelect.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Select, { SelectValue } from 'antd/lib/select';
3 | import PropTypes from 'prop-types'
4 | import {
5 | FieldRenderer,
6 | basePropTypes
7 | } from '@react-ui-generator/core';
8 |
9 | import { FieldWrapper } from './FieldWrapper';
10 |
11 | const { Option } = Select;
12 |
13 | export type MultipleSelectValue = string[] | number[];
14 |
15 | export interface SelectItemProps {
16 | id: string | number;
17 | title: string;
18 | }
19 |
20 | const value: SelectValue = undefined;
21 | const options: SelectItemProps[] = [];
22 |
23 | export class MultipleSelect extends FieldRenderer {
24 | static propTypes = {
25 | ...basePropTypes(),
26 | config: PropTypes.shape({
27 | label: PropTypes.string,
28 | placeholder: PropTypes.string,
29 | showAsterix: PropTypes.bool,
30 | options: PropTypes.arrayOf(
31 | PropTypes.shape({
32 | id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
33 | title: PropTypes.string
34 | })
35 | )
36 | })
37 | };
38 |
39 | static defaultProps = {
40 | className: '',
41 | disabled: false,
42 | dirty: false,
43 | config: {
44 | label: '',
45 | placeholder: '',
46 | showAsterix: false,
47 | options
48 | },
49 | data: value,
50 | };
51 |
52 | handleChange = (value: MultipleSelectValue): void => {
53 | this.props.onChange(value);
54 | };
55 |
56 | render() {
57 | const {
58 | id,
59 | config: { label, placeholder, showAsterix, options, allowClear },
60 | data,
61 | disabled,
62 | className,
63 | onChange,
64 | ...rest
65 | } = this.props;
66 |
67 | let value: SelectValue;
68 |
69 | if (typeof data === 'boolean') {
70 | value = Number(data);
71 | } else if (Array.isArray(data) && (data.length === 0)) {
72 | value = undefined;
73 | } else {
74 | value = data;
75 | }
76 |
77 | return (
78 |
79 |
95 |
96 | );
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/packages/core/src/components/renderers/ListForm.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Renderer "ListForm" allows you to render a list of nested subforms.
3 | * It is useful when you need to build a form with a list of the same sections,
4 | * where each section consists of more than one fields
5 | * (e.g. section "Linked accounts" in user profile form,
6 | * with "username", "avatar", "unlink button" fields,).
7 | */
8 |
9 | import * as React from 'react';
10 | import { GeneratedForm } from '../GeneratedForm';
11 | import {
12 | FieldRendererComponent,
13 | FieldRendererProps,
14 | FieldRenderer,
15 | KeyValue,
16 | FieldMetaDescription,
17 | } from '../../interfaces';
18 |
19 | import { enhanceFieldMeta } from '../../utils';
20 |
21 | export interface ListFormProps extends FieldRendererProps {
22 | dirtiness: KeyValue[],
23 | renderers: { [key: string]: FieldRendererComponent };
24 | validator(data: KeyValue): KeyValue;
25 | }
26 |
27 | export class ListForm extends FieldRenderer {
28 | static defaultProps = {
29 | data: [] as KeyValue[],
30 | dirtiness: [] as KeyValue[]
31 | };
32 |
33 | handleOnChange(idx: number, itemData: any, itemDirtiness: any) {
34 | const newValue = [...this.props.data];
35 | const newDirtines = [...this.props.dirtiness];
36 |
37 | newValue[idx] = itemData;
38 | newDirtines[idx] = itemDirtiness;
39 | this.props.onChange(newValue, newDirtines);
40 | }
41 |
42 | render() {
43 | const {
44 | id,
45 | className,
46 | onChange,
47 | disabled,
48 | config,
49 | data,
50 | errors,
51 | dirtiness,
52 | renderers,
53 | validator,
54 | actions
55 | } = this.props;
56 |
57 | const enhancedFieldsMeta: FieldMetaDescription[] = [];
58 |
59 | for (let meta of config.fields) {
60 | const newMeta = enhanceFieldMeta(meta);
61 |
62 | newMeta.renderer.config.disabled = disabled;
63 | enhancedFieldsMeta.push(newMeta);
64 | }
65 |
66 | const values = data || [];
67 | const result = [];
68 |
69 | for (let idx = 0; idx < values.length; idx++) {
70 | const itemData = values[idx];
71 | const keyId = itemData.id || idx
72 | const indexedActions = Object.keys(actions).reduce((acc: KeyValue, key: string) => {
73 | acc[key] = (...args: any[]) => actions[key](idx, ...args);
74 | return acc;
75 | }, {});
76 |
77 | result.push(
78 | this.handleOnChange(idx, data, dirtiness)}
89 | isSubForm
90 | >
91 | {this.props.children}
92 |
93 | );
94 | }
95 |
96 | return result;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/packages/core/__tests__/serializer.test.ts:
--------------------------------------------------------------------------------
1 | import { serializeToObject } from '../src/serializer';
2 |
3 |
4 | describe('serializeToObject()', () => {
5 | test('should extract value from object in `GeneratedForm` data\'s format', () => {
6 | const data = {
7 | field1: { value: 'foo' },
8 | field2: { value: 'bar' },
9 | field3: { value: 'baz' },
10 | }
11 |
12 | const serialized = {
13 | field1: 'foo',
14 | field2: 'bar',
15 | field3: 'baz',
16 | }
17 |
18 | expect(serializeToObject(data)).toEqual(serialized);
19 | });
20 |
21 | test('should correctly process subform data ("list" subform type)', () => {
22 | const data = {
23 | field1: { value: 'foo' },
24 | field2: {
25 | value: [
26 | { subField: { value: 'bar1' } },
27 | { subField: { value: 'bar2' } },
28 | { subField: { value: 'bar3' } },
29 | ],
30 | },
31 | field3: { value: 'baz' },
32 | }
33 |
34 | const serialized = {
35 | field1: 'foo',
36 | field2: [
37 | { subField: 'bar1' },
38 | { subField: 'bar2' },
39 | { subField: 'bar3' },
40 | ],
41 | field3: 'baz',
42 | }
43 |
44 | expect(serializeToObject(data)).toEqual(serialized);
45 | });
46 |
47 | test('should correctly process subform data ("form" subform type)', () => {
48 | const data = {
49 | field1: { value: 'foo' },
50 | field2: {
51 | value: {
52 | subField1: { value: 'bar1' },
53 | subField2: { value: 'bar2' },
54 | subField3: { value: 'bar3' },
55 | },
56 | },
57 | field3: { value: 'baz' },
58 | }
59 |
60 | const serialized = {
61 | field1: 'foo',
62 | field2: {
63 | subField1: 'bar1',
64 | subField2: 'bar2',
65 | subField3: 'bar3',
66 | },
67 | field3: 'baz',
68 | }
69 |
70 | expect(serializeToObject(data)).toEqual(serialized);
71 | });
72 |
73 |
74 | test('should correctly process deeply nested subform data', () => {
75 | const data = {
76 | field1: { value: 'foo' },
77 |
78 | field2: {
79 | value: [
80 | {
81 | subField1: {
82 | value: [
83 | { subField1_1: { value: 'bar1_1'} },
84 | { subField1_1: { value: 'bar1_1'} },
85 | { subField1_1: { value: 'bar1_1'} },
86 | ]
87 | },
88 | subField2: { value: 'bar2' }
89 | },
90 | ],
91 | },
92 |
93 | field3: {
94 | value: {
95 | subField1: { value: 'baz1' },
96 | subField2: {
97 | value: {
98 | subField2_1: { value: 'baz2_1' },
99 | subField2_2: { value: 'baz2_2' },
100 | }
101 | },
102 | }
103 | },
104 | }
105 |
106 | const serialized = {
107 | field1: 'foo',
108 | field2: [
109 | {
110 | subField1: [
111 | { subField1_1: 'bar1_1' },
112 | { subField1_1: 'bar1_1' },
113 | { subField1_1: 'bar1_1' }
114 | ],
115 |
116 | subField2: 'bar2',
117 | },
118 | ],
119 |
120 | field3: {
121 | subField1: 'baz1',
122 | subField2: {
123 | subField2_1: 'baz2_1',
124 | subField2_2: 'baz2_2',
125 | }
126 | }
127 | }
128 |
129 | expect(serializeToObject(data)).toEqual(serialized);
130 | });
131 | });
132 |
--------------------------------------------------------------------------------
/packages/demo/src/containers/GeneratedFormExample.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import Ajv from 'ajv';
4 | import { Form, Row, Col } from 'antd';
5 |
6 | import {
7 | GeneratedForm,
8 | Field,
9 | Fields,
10 | } from '@react-ui-generator/core';
11 |
12 | import 'antd/dist/antd.css'; // or 'antd/dist/antd.less'
13 |
14 |
15 | import { buildAjvValidator } from '@react-ui-generator/validators';
16 | import { Renderers, Layouts } from '@react-ui-generator/antd';
17 |
18 | import {
19 | toggleSex,
20 | updateForm,
21 | sendForm,
22 | clearForm,
23 | addRelative,
24 | removeRelative
25 | } from '@actions';
26 |
27 | import CloseButton from '../components/CloseButton';
28 | import validationSchema from '../validation/jsonSchema.json';
29 |
30 | /**
31 | * You can add custon renderers here.
32 | */
33 | const renderers = {
34 | ...Renderers,
35 | closeButton: CloseButton,
36 | };
37 |
38 | const { FormLayout, FieldLayout } = Layouts;
39 |
40 | class GeneratedFormExample extends React.PureComponent {
41 | render() {
42 | const { meta, data, errors, dirtiness, updateForm, ...actions } = this.props;
43 |
44 | return (
45 | updateForm({ data, errors, isValid, dirtiness })}
54 | validator={buildAjvValidator(Ajv, validationSchema)}
55 | >
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | );
104 | }
105 | }
106 |
107 | export default connect(
108 | state => state,
109 | dispatch => ({
110 | updateForm: payload => dispatch(updateForm(payload)),
111 | toggleSex: fieldId => dispatch(toggleSex(fieldId)),
112 | sendForm: () => dispatch(sendForm()),
113 | clearForm: () => dispatch(clearForm()),
114 | addRelative: () => dispatch(addRelative()),
115 | removeRelative: (...payload) => dispatch(removeRelative(...payload))
116 | })
117 | )(GeneratedFormExample);
118 |
--------------------------------------------------------------------------------
/packages/core/src/utils.ts:
--------------------------------------------------------------------------------
1 | import {
2 | RawMetaDescription,
3 | FormMetaDescription,
4 | RawFieldMetaDescription,
5 | FieldMetaDescription,
6 | KeyValue
7 | } from './interfaces';
8 |
9 | function enhanceFormMeta(meta: RawMetaDescription): FormMetaDescription {
10 | const result: FormMetaDescription = {
11 | fields: []
12 | };
13 |
14 | for (let field of meta.fields) {
15 | result.fields.push(enhanceFieldMeta(field));
16 | }
17 |
18 | return result;
19 | }
20 |
21 | function enhanceFieldMeta(meta: RawFieldMetaDescription): FieldMetaDescription {
22 | return {
23 | id: meta.id,
24 | renderer: computeFieldRenderer(meta),
25 | actions: meta.actions ? { ...meta.actions } : {},
26 | hidden: meta.hidden || false,
27 | disabled: meta.disabled || false
28 | };
29 | }
30 |
31 | function computeFieldRenderer(meta: RawFieldMetaDescription) {
32 | const _renderer = { type: 'text', config: {} };
33 |
34 | if (meta.renderer && typeof meta.renderer === 'object') {
35 | return { ...meta.renderer };
36 | } else if (typeof meta.renderer === 'string') {
37 | _renderer.type = meta.renderer;
38 | }
39 |
40 | return _renderer;
41 | }
42 |
43 | function extractFieldActions(formActions: KeyValue, fieldActions: KeyValue) {
44 | let actions: KeyValue = {};
45 |
46 | for (let callbackName of Object.keys(fieldActions)) {
47 | const actionName = fieldActions[callbackName];
48 | const fn = formActions[actionName];
49 |
50 | if (fn) {
51 | actions[callbackName] = fn;
52 | }
53 | }
54 |
55 | return actions;
56 | }
57 |
58 | /**
59 | * Completes `data` object with default values for known types of renderers.
60 | * Default values for custom renderers can be provided with `defaults` argument.
61 | */
62 | function withDefaults(
63 | data: KeyValue = {},
64 | fieldsMeta: FieldMetaDescription[] = [],
65 | defaults: KeyValue = {}
66 | ): KeyValue {
67 | const _defaults: KeyValue = {
68 | form: {},
69 | list: [],
70 |
71 | checkbox: false,
72 | radiogroup: '',
73 | select: null,
74 | multiple: [],
75 | text: '',
76 | textarea: '',
77 | date: null,
78 | upload: null,
79 |
80 | ...defaults
81 | };
82 |
83 | const resultData = { ...data };
84 |
85 | for (let fieldMeta of fieldsMeta) {
86 | const { id, renderer: { type } } = fieldMeta;
87 | const dataValue = data[id];
88 | let defaultValue;
89 |
90 | if (dataValue === undefined) {
91 | defaultValue = _defaults[type];
92 |
93 | if (defaultValue !== undefined) {
94 | const value =
95 | type === 'list'
96 | ? [withDefaults({}, fieldMeta.renderer.config.fields)] // and what about "form"?
97 | : defaultValue;
98 |
99 | resultData[id] = value;
100 | }
101 | }
102 | }
103 |
104 | return resultData;
105 | }
106 |
107 | function findFieldIdx(fieldId: string, fields: JSX.Element[]) {
108 | return fields.findIndex(({ props }) => props.id === fieldId);
109 | }
110 |
111 | function findFieldMetaById(
112 | fieldId: string,
113 | fieldsMeta: FieldMetaDescription[] = []
114 | ): FieldMetaDescription {
115 | return fieldsMeta.find(meta => meta.id === fieldId);
116 | }
117 |
118 | function makeDirty (data: KeyValue | KeyValue[]): KeyValue {
119 | if (Array.isArray(data)) {
120 | return data.map(makeDirty)
121 | }
122 |
123 | return Object.entries(data).reduce((acc, [key, value]) => {
124 | return { ...acc, [key]: (value && typeof value === 'object') ? makeDirty(value) : true }
125 | }, {})
126 | }
127 |
128 | export {
129 | enhanceFormMeta,
130 | enhanceFieldMeta,
131 | extractFieldActions,
132 | withDefaults,
133 | findFieldIdx,
134 | findFieldMetaById,
135 | makeDirty,
136 | };
137 |
--------------------------------------------------------------------------------
/packages/demo/src/meta/complete_bootstrap.json:
--------------------------------------------------------------------------------
1 | {
2 | "fields": [
3 | {
4 | "id": "email",
5 | "renderer": {
6 | "type": "text",
7 | "config": {
8 | "label": "E-mail",
9 | "placeholder": "Enter your e-mail"
10 | }
11 | },
12 | "serializer": "auth.email",
13 | "hidden": false
14 | },
15 |
16 | {
17 | "id": "password",
18 | "renderer": {
19 | "type": "text",
20 | "config": {
21 | "label": "Password",
22 | "placeholder": "Enter your password",
23 | "isPassword": true
24 | }
25 | },
26 | "serializer": "auth.password"
27 | },
28 |
29 | {
30 | "id": "confirmation",
31 | "renderer": {
32 | "type": "text",
33 | "config": {
34 | "label": "Confirmation",
35 | "placeholder": "Enter your password again",
36 | "isPassword": true
37 | }
38 | },
39 | "serializer": null
40 | },
41 |
42 |
43 | {
44 | "id": "sex",
45 | "renderer": {
46 | "type": "select",
47 | "config": {
48 | "label": "Gender",
49 | "title": "Choose your gender",
50 | "options": [
51 | { "id": "male", "title": "Female" },
52 | { "id": "female", "title": "Male" },
53 | { "id": "dontknow", "title": "I don't know" }
54 | ]
55 | }
56 | }
57 | },
58 |
59 | {
60 | "id": "isMarried",
61 | "renderer": {
62 | "type": "checkbox",
63 | "config": {
64 | "label": "Are you married?",
65 | "title": "Married"
66 | }
67 | }
68 | },
69 |
70 | {
71 | "id": "aboutMe",
72 | "renderer": {
73 | "type": "textarea",
74 | "config": {
75 | "label": "Short bio",
76 | "placeholder": "Write your short biography here...",
77 | "rows": 2
78 | }
79 | },
80 | "hidden": false
81 | },
82 |
83 | {
84 | "id": "answer",
85 | "renderer": {
86 | "type": "radiogroup",
87 | "config": {
88 | "label": "What is the only correct answer from this list?",
89 | "options": [
90 | { "id": 42, "title": "42" },
91 | { "id": 43, "title": "43" }
92 | ]
93 | }
94 | }
95 | },
96 |
97 | {
98 | "id": "relatives",
99 | "serializer": "additional.relatives",
100 | "renderer": {
101 | "type": "list",
102 | "config": {
103 | "label": "Add information about your relatives",
104 | "fields": [
105 | {
106 | "id": "firstName",
107 | "renderer": {
108 | "type": "text",
109 | "config": {
110 | "label": "First name",
111 | "placeholder": "First name"
112 | }
113 | }
114 | },
115 |
116 | {
117 | "id": "lastName",
118 | "renderer": {
119 | "type": "text",
120 | "config": {
121 | "label": "Last name",
122 | "placeholder": "Last name"
123 | }
124 | }
125 | },
126 |
127 | {
128 | "id": "btnRemoveRelative",
129 | "renderer": "closeButton",
130 | "actions": {
131 | "onClick": "removeRelative"
132 | }
133 | }
134 | ]
135 | }
136 | }
137 | },
138 |
139 | {
140 | "id": "btnAddRelative",
141 | "renderer": {
142 | "type": "button",
143 | "config": {
144 | "title": "Add relatives",
145 | "color": "primary",
146 | "outline": true
147 | }
148 | },
149 | "actions": {
150 | "onClick": "addRelative"
151 | }
152 | },
153 |
154 | {
155 | "id": "btnSend",
156 | "renderer": {
157 | "type": "button",
158 | "config": {
159 | "title": "Send",
160 | "color": "primary"
161 | }
162 | },
163 | "actions": {
164 | "onClick": "sendForm"
165 | }
166 | },
167 |
168 | {
169 | "id": "btnClear",
170 | "renderer": {
171 | "type": "button",
172 | "config": {
173 | "title": "Clear",
174 | "color": "primary",
175 | "outline": true
176 | }
177 | },
178 | "actions": {
179 | "onClick": "clearForm"
180 | }
181 | }
182 | ]
183 | }
184 |
--------------------------------------------------------------------------------
/packages/core/src/components/GeneratedForm.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import get from 'lodash-es/get';
3 |
4 | import {
5 | RawMetaDescription,
6 | KeyValue,
7 | FieldRendererComponent,
8 | FormMetaDescription,
9 | } from '../interfaces';
10 |
11 | import { extractFieldActions, enhanceFormMeta } from '../utils';
12 |
13 | import { Layout } from './Layout';
14 | import { SubForm } from './renderers/SubForm';
15 | import { ListForm } from './renderers/ListForm';
16 | import { Fields } from './Fields';
17 |
18 | export interface GeneratedFormProps {
19 | className?: string;
20 | meta: RawMetaDescription;
21 | data: KeyValue;
22 | errors: any;
23 | dirtiness: KeyValue;
24 | renderers: { [key: string]: FieldRendererComponent };
25 | actions: KeyValue;
26 | isSubForm?: boolean;
27 | validator(formData: KeyValue): KeyValue;
28 | onChange(data: KeyValue, errors: KeyValue, isValid: boolean, dirtiness: KeyValue): void;
29 | }
30 |
31 | export interface GeneratedFormState {
32 | meta: FormMetaDescription;
33 | }
34 |
35 | const meta: FormMetaDescription = { fields: [] };
36 |
37 | export class GeneratedForm extends React.PureComponent<
38 | GeneratedFormProps,
39 | GeneratedFormState
40 | > {
41 | state = { meta };
42 |
43 | static getDerivedStateFromProps(props: GeneratedFormProps) {
44 | return {
45 | meta: enhanceFormMeta(props.meta)
46 | };
47 | }
48 |
49 | handleChange(fieldId: string, newValue: any, newDirtines: any): void {
50 | const nextData = { ...this.props.data };
51 | const nextDirtiness = { ...this.props.dirtiness };
52 |
53 | nextData[fieldId] = newValue;
54 | nextDirtiness[fieldId] = newDirtines;
55 |
56 | const { validator, isSubForm } = this.props;
57 |
58 | /**
59 | * Skip validation if current form is subform,
60 | * because validation is processed on the top level.
61 | */
62 | const isValidatable = validator && !isSubForm;
63 | const nextErrors = isValidatable
64 | ? validator(nextData)
65 | : { isValid: true, errors: {} };
66 |
67 | this.props.onChange(nextData, nextErrors.errors, nextErrors.isValid, nextDirtiness);
68 | }
69 |
70 | render() {
71 | const fields: JSX.Element[] = [];
72 |
73 | const {
74 | className = '',
75 | data,
76 | errors,
77 | dirtiness,
78 | actions: formActions,
79 | renderers,
80 | validator,
81 | children
82 | } = this.props;
83 |
84 | for (let field of this.state.meta.fields) {
85 | const {
86 | id,
87 | renderer: { type, config },
88 | actions: fieldActions,
89 | hidden,
90 | disabled,
91 | } = field;
92 |
93 | /**
94 | * Skip rendering of hidden fields.
95 | * It does not influence field's state, only presentation.
96 | */
97 | if (hidden) {
98 | continue;
99 | }
100 |
101 | const Renderer: FieldRendererComponent =
102 | type === 'form' ? SubForm : (type === 'list' ? ListForm : renderers[type]);
103 |
104 | if (!Renderer) {
105 | throw new Error(`Could not find renderer of type "${type}" (field "${id}") in form renderers.`)
106 | }
107 |
108 | const actions: { [key: string]: any } = extractFieldActions(
109 | formActions,
110 | fieldActions
111 | );
112 |
113 | const subFormAdditionalProps =
114 | type === 'form' || type === 'list'
115 | ? {
116 | isSubForm: true,
117 | data: data[id],
118 | actions: formActions,
119 | errors: get(errors, id, type === 'list' ? [] : {}),
120 | dirtiness: get(dirtiness, id, type === 'list' ? [] : {}),
121 | renderers,
122 | validator
123 | }
124 | : {};
125 |
126 | fields.push(
127 | {
135 | this.handleChange(id, newValue, newDirtines);
136 | }}
137 | disabled={disabled}
138 | dirty={dirtiness[id] || false}
139 | isSubForm={false}
140 | dirtiness={undefined}
141 | renderers={undefined}
142 | validator={undefined}
143 | {...subFormAdditionalProps}
144 | />
145 | );
146 | }
147 |
148 | return (
149 |
150 | {children || }
151 |
152 | );
153 | }
154 |
155 | onChange(id: string, event: any): void {}
156 | }
157 |
--------------------------------------------------------------------------------
/doc/ru/metadata format.md:
--------------------------------------------------------------------------------
1 | # Формат метаданных
2 |
3 | Описание формы представляет собою объект, в корне которого всегда находится поле `fields`.
4 |
5 | ```js
6 | {
7 | "fields": [ ]
8 | }
9 | ```
10 |
11 | ## Мета-описание отдельного поля
12 |
13 | Каждый элемент массива `field` содержит мета-описание одного поля формы. В своей минимальной конфигурации такое мета-описание выглядит следующим образом:
14 |
15 | ```js
16 | {
17 | "id": "%id поля%",
18 | }
19 | ```
20 |
21 | Это эквивалентно следующему мета-описанию:
22 |
23 | ```js
24 | {
25 | "id": "%id поля%",
26 | "renderer": "text"
27 | }
28 | ```
29 |
30 | где `renderer` - это тип [контрола](https://en.wikipedia.org/wiki/Widget_(GUI)), который будет использован для отрисовки данного поля на форме.
31 |
32 |
33 | ### Простейшая форма описания отрисовщика
34 |
35 | Поле `renderer` может содержать любое тип отрисовщика (*renderer*), однако такой тип должен быть определён в наборе отрисовщиков, подключенных к компоненту ``. Изначально `react-ui-generator` поддерживат два набора отрисовщиков: один на базе компонентов Reactstrap, и второй - на базе компонентов Ant Design. Для Ant Design доступны следующие типы отрисовщиков:
36 |
37 | ```js
38 | {
39 | "id": "%id поля%",
40 | "renderer": "text|textarea|checkbox|radiogroup|select|button|datepicker|upload"
41 | }
42 | ```
43 |
44 | ### Расширенная форма описания отрисовщика
45 |
46 | Приведенный выше вариант мета-описания поля подходит только для самых простых случаев. Гораздо чаще отрисовщик нуждается в дополнительных настройках (напр. "цвет кнопки", "формат даты" etc). Для такого случая используется следующий формат мета-описания поля:
47 |
48 | ```js
49 | {
50 | "id": "%id поля%",
51 | "renderer": {
52 | "type": "%тип визуального представления поля%",
53 | "config": {
54 | "option1": "value1",
55 | ...
56 | "optionN": "valueN"
57 | }
58 | }
59 | }
60 | ```
61 |
62 | Где содержимое секции `config` не является строго регламентированным и может содержать любые настройки, которые понимает/поддерживает данный отрисовщик. Конкретный набор настроек для того или иного отрисовщика содержится в описании API соответствующего набора компонентов.
63 |
64 | ### Видимость поля
65 |
66 | Любое поле формы может быть скрыто, независимо от выбранного типа отрисовщика. Решение об отображении/скрытии поля принимается до вызова отрисовщика поля, и в случае скрытия - такого вызова просто не происходит. Управляется видимость поля с помощью настройки `hidden`:
67 |
68 | ```js
69 | {
70 | "id": "%id поля%",
71 | "renderer": "" | { ... },
72 | ...
73 | "hidden": true|false
74 | }
75 | ```
76 |
77 | ### Выключенность поля
78 |
79 | Любое поле может быть выключено (*disabled*), независимо от выбранного типа отрисовщика. Здесь имеется ввиду, что никакие взаимодействия с данными полем (*actions*) не будут обрабатьываться компонентом ``. Однако, т.к. визуализация поля в выключенном состоянии - это ответственность отрисовщика данного типа поля, то нет возможности гарантировать на уровне компонента `` что такая визуализация будет всегда корректной. Например, если автор отрисовщика забудет предусмотреть визуализацию состояния `disabled`, то поле будет выглядеть как активное.
80 |
81 | Управляется состояние выключенности с помощью настройки `disabled`:
82 |
83 | ```js
84 | {
85 | "id": "%id поля%",
86 | "renderer": "" | { ... },
87 | ...
88 | "disabled": true|false
89 | }
90 | ```
91 |
92 | ### Настройка взаимодействия с полем (actions)
93 |
94 | Мета-описание формы в `react-ui-generator` является декларативным. Таким образом, не предполагается возможности описания логики полей прямо в метаданных. Вместо этого есть возможность указать, какие функции из коллекции функций, подключенных к `` должны вызываться при срабатывании того или иного события компонента:
95 |
96 | ```js
97 | {
98 | "id": "%id поля%",
99 | "renderer": "button",
100 | ...
101 | "actions": {
102 | "onClick": "sendForm"
103 | }
104 | }
105 | ```
106 |
107 | При этом, к компоненту `` должна быть подключена коллекция функций, содержащая функцию с именем "sendForm":
108 |
109 | ```js
110 | const actions = {
111 | sendForm: (event) => { ... },
112 | ...
113 | };
114 |
115 | ...
116 |
117 |
118 | ```
119 |
120 | ### Полный пример
121 |
122 | Итого, метаописание всей формы будет иметь слелующую структуру:
123 |
124 | ```js
125 | {
126 | "fields": [
127 | {
128 | "id": "%id поля%",
129 | "renderer": {
130 | "type": "%тип визуального представления поля%",
131 | "config": {
132 | "option1": "value1",
133 | ...
134 | "optionN": "valueN"
135 | }
136 | }
137 | },
138 | "hidden": true|false,
139 | "disabled": true|false,
140 | "serializer": "some.nested.field",
141 | "actions": {
142 | "onClick": "...",
143 | "onKeyUp": "...",
144 | ...
145 | }
146 | ]
147 | }
148 | ```
149 |
--------------------------------------------------------------------------------
/packages/core/__tests__/components/GeneratedForm.test.tsx:
--------------------------------------------------------------------------------
1 | import 'regenerator-runtime/runtime';
2 |
3 | import React from 'react';
4 | import { render, cleanup } from '@testing-library/react';
5 | import { prettyDOM } from '@testing-library/dom';
6 | import '@testing-library/jest-dom/extend-expect';
7 |
8 | import cloneDeep from 'lodash-es/cloneDeep';
9 |
10 | import { GeneratedForm } from '../../src/components/GeneratedForm';
11 | import { Layout } from '../../src/components/Layout';
12 | import { Fields } from '../../src/components/Fields';
13 | import { FieldRenderer } from '../../src/interfaces';
14 |
15 | import metaMinimal from '../../examples/meta/minimal';
16 |
17 | class Text extends FieldRenderer {
18 | render() {
19 | const { id, disabled } = this.props;
20 |
21 | return (
22 |
23 | {`text-renderer${disabled ? '-disabled' : ''}: ${id}`}
24 |
25 | );
26 | }
27 | }
28 |
29 | class CustomRenderer extends FieldRenderer {
30 | render() {
31 | return custom-renderer
;
32 | }
33 | }
34 |
35 | class DefaultConfig extends FieldRenderer {
36 | render() {
37 | const { config } = this.props;
38 | return {config ? 'has config' : 'no config'}
;
39 | }
40 | }
41 |
42 | class CustomConfig extends FieldRenderer {
43 | render() {
44 | return {this.props.config.test}
;
45 | }
46 | }
47 |
48 | class CustomOnChange extends FieldRenderer {
49 | render() {
50 | return (
51 | this.props.onChange('test', [])} />
52 | );
53 | }
54 | }
55 |
56 | const mockRenderers: { [key: string]: typeof FieldRenderer } = {
57 | text: Text,
58 | customRenderer: CustomRenderer,
59 | defaultConfig: DefaultConfig,
60 | customConfig: CustomConfig,
61 | customOnChange: CustomOnChange
62 | };
63 |
64 | describe('
', () => {
65 | const getFormInstace = ({ meta, onChange = () => {} }) => (
66 |
77 | );
78 |
79 | afterEach(cleanup);
80 |
81 | describe('Metadata completion and propagation', () => {
82 | test('should render "text" renderer if renderer not specified', () => {
83 | const { getAllByText } = render(getFormInstace({ meta: metaMinimal }));
84 |
85 | expect(getAllByText(/text-renderer/).length).toEqual(3);
86 | });
87 |
88 | test('should propagate `id` property to field renderers', () => {
89 | const { getAllByText } = render(getFormInstace({ meta: metaMinimal }));
90 |
91 | expect(getAllByText(/foo/).length).toBe(1);
92 | expect(getAllByText(/bar/).length).toBe(1);
93 | expect(getAllByText(/baz/).length).toBe(1);
94 | });
95 |
96 | test('should propagate `disabled` property to field renderers', () => {
97 | const meta: any = cloneDeep(metaMinimal);
98 | meta.fields[0].disabled = true;
99 | meta.fields[1].disabled = true;
100 | const { getAllByText } = render(getFormInstace({ meta }));
101 |
102 | expect(getAllByText(/text-renderer:/).length).toEqual(1);
103 | expect(getAllByText(/text-renderer-disabled/).length).toEqual(2);
104 | });
105 |
106 | test('should propagate empty object as `config` property to field renderers, if it not specified', () => {
107 | const meta: any = cloneDeep(metaMinimal);
108 | meta.fields[1].renderer = 'defaultConfig';
109 | const { getAllByText } = render(getFormInstace({ meta }));
110 |
111 | expect(getAllByText('has config').length).toBe(1);
112 | });
113 |
114 | test('should propagate `config` property to field renderers', () => {
115 | const meta: any = cloneDeep(metaMinimal);
116 | const testText = 'config test';
117 |
118 | meta.fields[2].renderer = {
119 | type: 'customConfig',
120 | config: { test: testText }
121 | };
122 |
123 | const { getAllByText } = render(getFormInstace({ meta }));
124 |
125 | expect(getAllByText(testText).length).toBe(1);
126 | });
127 | });
128 |
129 | describe('Rendering', () => {
130 | test('should not render hidden fields', () => {
131 | const meta: any = cloneDeep(metaMinimal);
132 | meta.fields[1].hidden = true;
133 | const { getAllByText } = render(getFormInstace({ meta }));
134 |
135 | expect(getAllByText(/text-renderer/).length).toBe(2);
136 | });
137 |
138 | test('should choose field renderer by the value of `renderer` field', () => {
139 | const meta: any = cloneDeep(metaMinimal);
140 | meta.fields[1].renderer = 'customRenderer';
141 | const { getAllByText } = render(getFormInstace({ meta }));
142 |
143 | expect(getAllByText(/text-renderer/).length).toBe(2);
144 | expect(getAllByText(/custom-renderer/).length).toBe(1);
145 | });
146 |
147 | // test('')
148 | });
149 | });
150 |
--------------------------------------------------------------------------------
/packages/metaphor/src/lib/Metaphor.ts:
--------------------------------------------------------------------------------
1 | import cloneDeep from 'lodash-es/cloneDeep'
2 | import get from 'lodash-es/get'
3 | import set from 'lodash-es/set'
4 | import invariant from 'invariant'
5 |
6 | import {
7 | RawMetaDescription,
8 | FormMetaDescription,
9 | FieldMetaDescription,
10 | enhanceFormMeta,
11 | enhanceFieldMeta,
12 | findFieldMetaById,
13 | } from '@react-ui-generator/core'
14 |
15 | import { FieldPart } from '../'
16 |
17 | export interface IdsToProcess {
18 | [key: string]: boolean
19 | }
20 |
21 | export type FieldBooleanProps = 'hidden' | 'disabled'
22 |
23 | class Metaphor {
24 | private meta: FormMetaDescription
25 |
26 | constructor(baseMeta: RawMetaDescription) {
27 | this.meta = enhanceFormMeta(baseMeta)
28 | }
29 |
30 | togglePropByFieldIds(
31 | propName: FieldBooleanProps,
32 | value: boolean,
33 | reverseIfNotMatch?: boolean,
34 | ids?: string[] | string
35 | ): Metaphor {
36 | const idsArray = typeof ids === 'string' ? [ids] : ids
37 | const fieldsToProcess: IdsToProcess = idsArray
38 | ? idsArray.reduce((acc, id) => ({ ...acc, [id]: true }), {})
39 | : {}
40 |
41 | for (let fieldMeta of this.meta.fields) {
42 | const fieldId = fieldMeta.id
43 |
44 | if (ids === undefined || fieldsToProcess[fieldId]) {
45 | fieldMeta[propName] = value
46 | } else if (reverseIfNotMatch) {
47 | fieldMeta[propName] = !value
48 | }
49 | }
50 |
51 | return this
52 | }
53 |
54 | value(): FormMetaDescription {
55 | return cloneDeep(this.meta)
56 | }
57 |
58 | get(path: string) {
59 | const [fieldId, ...tokens] = path.split('.')
60 | const fieldMeta = findFieldMetaById(fieldId, this.meta.fields)
61 | return get(fieldMeta, tokens)
62 | }
63 |
64 | set(path: string, value: any) {
65 | const [fieldId, ...tokens] = path.split('.')
66 | const fieldMeta = findFieldMetaById(fieldId, this.meta.fields)
67 | set(fieldMeta, tokens, value)
68 |
69 | return this
70 | }
71 |
72 | show(fieldsToShow?: string[] | string, hideNotMatched?: boolean): Metaphor {
73 | return this.togglePropByFieldIds(
74 | 'hidden',
75 | false,
76 | hideNotMatched,
77 | fieldsToShow
78 | )
79 | }
80 |
81 | showAll(): Metaphor {
82 | return this.show()
83 | }
84 |
85 | hide(fieldsToHide?: string[] | string, showNotMatched?: boolean): Metaphor {
86 | return this.togglePropByFieldIds(
87 | 'hidden',
88 | true,
89 | showNotMatched,
90 | fieldsToHide
91 | )
92 | }
93 |
94 | hideAll(): Metaphor {
95 | return this.hide()
96 | }
97 |
98 | enable(
99 | fieldsToEnable?: string[] | string,
100 | disableNotMatched?: boolean
101 | ): Metaphor {
102 | return this.togglePropByFieldIds(
103 | 'disabled',
104 | false,
105 | disableNotMatched,
106 | fieldsToEnable
107 | )
108 | }
109 |
110 | enableAll(): Metaphor {
111 | return this.enable()
112 | }
113 |
114 | disable(
115 | fieldsToDisable?: string[] | string,
116 | enableNotMatched?: boolean
117 | ): Metaphor {
118 | return this.togglePropByFieldIds(
119 | 'disabled',
120 | true,
121 | enableNotMatched,
122 | fieldsToDisable
123 | )
124 | }
125 |
126 | disableAll(): Metaphor {
127 | return this.disable()
128 | }
129 |
130 | /**
131 | * Clones an existing field. Allows to append a cloned field
132 | * just after the original, or to the end of form.
133 | */
134 | clone(
135 | idSrc: string,
136 | idTarget: string,
137 | appendToSrc: boolean = true
138 | ): Metaphor {
139 | const { fields } = this.meta
140 | const idxSrc = getFieldIdx(idSrc, fields)
141 |
142 | invariantFieldUnique(idTarget, fields)
143 |
144 | const fieldMeta = cloneDeep(findFieldMetaById(idSrc, fields))
145 | fieldMeta.id = idTarget
146 |
147 | if (appendToSrc) {
148 | fields.splice(idxSrc + 1, 0, fieldMeta)
149 | } else {
150 | fields.push(fieldMeta)
151 | }
152 |
153 | return this
154 | }
155 |
156 | /**
157 | * Creates a new field. Allows to append it under one of existing fields
158 | * or to the end of form.
159 | */
160 | add(fieldId: string, type: string, underId?: string): Metaphor {
161 | const { fields } = this.meta
162 |
163 | invariantFieldUnique(fieldId, fields)
164 |
165 | const rawMeta = { id: fieldId, renderer: type }
166 | const completeMeta = enhanceFieldMeta(rawMeta)
167 |
168 | if (underId) {
169 | const idx = getFieldIdx(underId, fields)
170 | fields.splice(idx + 1, 0, completeMeta)
171 | } else {
172 | fields.push(completeMeta)
173 | }
174 |
175 | return this
176 | }
177 |
178 | remove(fieldId: string): Metaphor {
179 | const { fields } = this.meta
180 | const idx = getFieldIdx(fieldId, fields)
181 |
182 | fields.splice(idx, 1)
183 | return this
184 | }
185 |
186 | get config(): FieldPart {
187 | return new FieldPart(this, 'renderer.config')
188 | }
189 |
190 | get actions(): FieldPart {
191 | return new FieldPart(this, 'actions')
192 | }
193 | }
194 |
195 | export function notFoundMessage(id: string): string {
196 | return `Field with id "${id}" is not found.`
197 | }
198 |
199 | export function inUseMessage(id: string): string {
200 | return `Id "${id}" is already in use.`
201 | }
202 |
203 | export function invariantFieldUnique(fieldId: string, fields: FieldMetaDescription[]) {
204 | invariant(
205 | fields.every(item => item.id !== fieldId),
206 | inUseMessage(fieldId)
207 | )
208 | }
209 |
210 | export function getFieldIdx(fieldId: string, fields: FieldMetaDescription[]): number {
211 | const idx = fields.findIndex(item => item.id === fieldId)
212 |
213 | invariant(idx !== -1, notFoundMessage(fieldId))
214 | return idx
215 | }
216 |
217 | export default Metaphor
218 |
--------------------------------------------------------------------------------
/packages/core/__tests__/utils.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | findFieldIdx,
5 | enhanceFormMeta,
6 | extractFieldActions,
7 | withDefaults
8 | } from '../src/utils';
9 |
10 | import metaMinimal from '../examples/meta/minimal';
11 | import metaFieldActions from '../examples/meta/field-action';
12 | import allRenderers from '../examples/meta/all-renderers';
13 |
14 |
15 | describe('findFieldIdx()', () => {
16 | const testId = 'testId';
17 |
18 | test('should return `-1` for empty `fields` array', () => {
19 | expect(findFieldIdx(testId, [])).toBe(-1);
20 | });
21 |
22 | test('should return `-1` if `fields` array does not contain field with test id', () => {
23 | const fields: JSX.Element[] = [
24 |
,
25 |
26 | ];
27 |
28 | expect(findFieldIdx(testId, fields)).toBe(-1);
29 | });
30 |
31 | test('should return index if `fields` array contains field with test id', () => {
32 | const fields: JSX.Element[] = [
33 |
,
34 |
,
35 |
36 | ];
37 |
38 | expect(findFieldIdx(testId, fields)).toBe(2);
39 | });
40 | });
41 |
42 |
43 | describe('enhanceFormMeta()', () => {
44 | describe('Minimal metadata config', () => {
45 | const minimalFieldMeta = metaMinimal.fields[0];
46 | const completedMeta = enhanceFormMeta(metaMinimal);
47 | const fieldMeta = completedMeta.fields[0];
48 |
49 | test('should not change size of "fields" array in meta data', () => {
50 | expect(completedMeta.fields.length).toBe(metaMinimal.fields.length);
51 | });
52 |
53 | test('should not change "id" property of field meta', () => {
54 | expect(minimalFieldMeta.id).toBeDefined();
55 | expect(fieldMeta.id).toBeDefined();
56 | expect(fieldMeta.id).toBe(minimalFieldMeta.id);
57 | });
58 |
59 | test('should add "renderer" object property to field meta', () => {
60 | expect(minimalFieldMeta['renderer']).toBeUndefined();
61 | expect(fieldMeta.renderer.type).toBe("text");
62 | expect(fieldMeta.renderer.config).toBeDefined();
63 | });
64 |
65 | test('should add "hidden" property to field meta with default value `false`', () => {
66 | expect(minimalFieldMeta['hidden']).toBeUndefined();
67 | expect(fieldMeta.hidden).toBe(false);
68 | });
69 |
70 | test('should add "disabled" property to field meta with default value `false`', () => {
71 | expect(minimalFieldMeta['disabled']).toBeUndefined();
72 | expect(fieldMeta.disabled).toBe(false);
73 | });
74 |
75 | test('should add "actions" object property to field meta', () => {
76 | expect(minimalFieldMeta['actions']).toBeUndefined();
77 | expect(fieldMeta.actions).toBeDefined();
78 | });
79 | });
80 | });
81 |
82 |
83 | describe('extractFieldActions()', () => {
84 | function foo () {}
85 | function bar () {}
86 | function baz () {}
87 |
88 | const formActions = {
89 | 'foo': foo,
90 | 'bar': bar,
91 | 'baz': baz
92 | };
93 |
94 | test('should join callback names with action handlers by action name', () => {
95 | const fieldActions = metaFieldActions.fields[0].actions;
96 | const result = extractFieldActions(formActions, fieldActions);
97 |
98 | expect(result['onClick']).toBe(foo);
99 | expect(result['onDoubleClick']).toBe(bar);
100 | expect(Object.keys(result)).toEqual(['onClick', 'onDoubleClick']);
101 | });
102 |
103 | test('should skip callback names that refers to absent action handlers', () => {
104 | const fieldActions = metaFieldActions.fields[1].actions;
105 | const result = extractFieldActions(formActions, fieldActions);
106 |
107 | expect(result['onClick']).toBe(foo);
108 | expect(result['onDoubleClick']).toBeUndefined();
109 | expect(Object.keys(result)).toEqual(['onClick']);
110 | });
111 | });
112 |
113 |
114 | describe('withDefaults()', () => {
115 | test('should not add fields to `data` for empty `fieldsMeta`', () => {
116 | const data = { foo: '42' };
117 | const result = withDefaults(data, []);
118 |
119 | expect(Object.keys(result)).toEqual(Object.keys(data));
120 | });
121 |
122 | test('should return shallow copy of `data` (not original object)', () => {
123 | const data = { foo: '42' };
124 | const result = withDefaults(data, []);
125 |
126 | expect(result).not.toBe(data);
127 | });
128 |
129 | test('should return default values for known renderers', () => {
130 | const data = { foo: '42' };
131 | const completedMeta = enhanceFormMeta(allRenderers);
132 | const result = withDefaults(data, completedMeta.fields);
133 |
134 | const expectedResult = {
135 | foo: '42',
136 | 'field-text': '',
137 | 'field-textarea': '',
138 | 'field-date': null,
139 | 'field-select': null,
140 | 'field-multiple': [],
141 | 'field-checkbox': false,
142 | 'field-radiogroup': '',
143 | 'field-upload': null,
144 | 'subform-list': [{ field1: '', field2: '' }],
145 | }
146 |
147 | expect(result).toEqual(expectedResult);
148 | });
149 |
150 | test('should use defaults from function arguments if exits', () => {
151 | const data = { foo: '42' };
152 | const completedMeta = enhanceFormMeta(allRenderers);
153 |
154 | const customDefaults = {
155 | 'text': 'this is default text renderer message',
156 | 'select': null,
157 | }
158 |
159 | const result = withDefaults(data, completedMeta.fields, customDefaults);
160 |
161 | const expectedResult = {
162 | foo: '42',
163 | 'field-text': customDefaults.text,
164 | 'field-textarea': '',
165 | 'field-date': null,
166 | 'field-select': customDefaults.select,
167 | 'field-multiple': [],
168 | 'field-checkbox': false,
169 | 'field-radiogroup': '',
170 | 'field-upload': null,
171 | 'subform-list': [{ field1: '', field2: '' }],
172 | }
173 |
174 | expect(result).toEqual(expectedResult);
175 | });
176 | });
177 |
--------------------------------------------------------------------------------
/packages/demo/src/meta/complete.json:
--------------------------------------------------------------------------------
1 | {
2 | "fields": [
3 | {
4 | "id": "email",
5 | "renderer": {
6 | "type": "text",
7 | "config": {
8 | "label": "E-mail",
9 | "placeholder": "Enter your e-mail",
10 | "showAsterix": true
11 | }
12 | },
13 | "serializer": "auth.email",
14 | "hidden": false
15 | },
16 |
17 | {
18 | "id": "password",
19 | "renderer": {
20 | "type": "password",
21 | "config": {
22 | "label": "Password",
23 | "placeholder": "Enter your password",
24 | "showAsterix": true
25 | }
26 | },
27 | "serializer": "auth.password"
28 | },
29 |
30 | {
31 | "id": "confirmation",
32 | "renderer": {
33 | "type": "password",
34 | "config": {
35 | "label": "Confirmation",
36 | "placeholder": "Enter your password again",
37 | "showAsterix": true
38 | }
39 | },
40 | "serializer": null
41 | },
42 |
43 | {
44 | "id": "birthdate",
45 | "renderer": {
46 | "type": "date",
47 | "config": {
48 | "label": "Birthday",
49 | "placeholder": "Enter your birthday",
50 | "showAsterix": true,
51 | "format": "DD.MM.YYYY"
52 | }
53 | }
54 | },
55 |
56 | {
57 | "id": "sex",
58 | "renderer": {
59 | "type": "select",
60 | "config": {
61 | "label": "Gender",
62 | "placeholder": "Choose your gender",
63 | "allowClear": true,
64 | "showAsterix": true,
65 | "options": [
66 | { "id": "female", "title": "Female" },
67 | { "id": "male", "title": "Male" },
68 | { "id": "dontknow", "title": "I don't know" }
69 | ]
70 | }
71 | }
72 | },
73 |
74 | {
75 | "id": "pl",
76 | "renderer": {
77 | "type": "multiple",
78 | "config": {
79 | "label": "Favorite programming language",
80 | "placeholder": "Choose your favorite programming language",
81 | "allowClear": true,
82 | "showAsterix": true,
83 | "options": [
84 | { "id": "elm", "title": "Elm" },
85 | { "id": "javascript", "title": "JavaScript" },
86 | { "id": "purescript", "title": "PureScript" },
87 | { "id": "reasonml", "title": "ReasonML" },
88 | { "id": "typescript", "title": "TypeScript" },
89 | { "id": "other", "title": "other" }
90 | ]
91 | }
92 | }
93 | },
94 |
95 | {
96 | "id": "isMarried",
97 | "renderer": {
98 | "type": "checkbox",
99 | "config": {
100 | "label": "Are you married?",
101 | "title": "Married",
102 | "showAsterix": true
103 | }
104 | }
105 | },
106 |
107 | {
108 | "id": "aboutMe",
109 | "renderer": {
110 | "type": "textarea",
111 | "config": {
112 | "label": "Short bio",
113 | "placeholder": "Write your short biography here...",
114 | "showAsterix": true,
115 | "rows": 2
116 | }
117 | },
118 | "hidden": false
119 | },
120 |
121 | {
122 | "id": "answer",
123 | "renderer": {
124 | "type": "radiogroup",
125 | "config": {
126 | "label": "What is the only correct answer from this list?",
127 | "showAsterix": true,
128 | "options": [
129 | { "id": 42, "title": "42" },
130 | { "id": 43, "title": "43" }
131 | ]
132 | }
133 | }
134 | },
135 |
136 | {
137 | "id": "cv",
138 | "renderer": {
139 | "type": "upload",
140 | "config": {
141 | "label": "Upload your CV",
142 | "url": "//jsonplaceholder.typicode.com/posts/",
143 | "responsePath": "id",
144 | "showAsterix": true
145 | }
146 | }
147 | },
148 |
149 | {
150 | "id": "relatives",
151 | "serializer": "additional.relatives",
152 | "renderer": {
153 | "type": "list",
154 | "config": {
155 | "label": "Add information about your relatives",
156 | "fields": [
157 | {
158 | "id": "firstName",
159 | "renderer": {
160 | "type": "text",
161 | "config": {
162 | "label": "First name",
163 | "placeholder": "First name",
164 | "showAsterix": true
165 | }
166 | }
167 | },
168 |
169 | {
170 | "id": "lastName",
171 | "renderer": {
172 | "type": "text",
173 | "config": {
174 | "label": "Last name",
175 | "placeholder": "Last name",
176 | "showAsterix": true
177 | }
178 | }
179 | },
180 |
181 | {
182 | "id": "btnRemoveRelative",
183 | "renderer": "closeButton",
184 | "actions": {
185 | "onClick": "removeRelative"
186 | }
187 | }
188 | ]
189 | }
190 | }
191 | },
192 |
193 | {
194 | "id": "btnAddRelative",
195 | "renderer": {
196 | "type": "button",
197 | "config": {
198 | "title": "Add relatives",
199 | "color": "primary",
200 | "outline": true
201 | }
202 | },
203 | "actions": {
204 | "onClick": "addRelative"
205 | }
206 | },
207 |
208 | {
209 | "id": "btnSend",
210 | "renderer": {
211 | "type": "button",
212 | "config": {
213 | "title": "Send",
214 | "color": "primary"
215 | }
216 | },
217 | "actions": {
218 | "onClick": "sendForm"
219 | }
220 | },
221 |
222 | {
223 | "id": "btnClear",
224 | "renderer": {
225 | "type": "button",
226 | "config": {
227 | "title": "Clear",
228 | "color": "primary",
229 | "outline": true
230 | }
231 | },
232 | "actions": {
233 | "onClick": "clearForm"
234 | }
235 | }
236 | ]
237 | }
238 |
--------------------------------------------------------------------------------
/packages/metaphor/docs/assets/js/search.js:
--------------------------------------------------------------------------------
1 | var typedoc = typedoc || {};
2 | typedoc.search = typedoc.search || {};
3 | typedoc.search.data = {"kinds":{"1":"External module","64":"Function","128":"Class","256":"Interface","512":"Constructor","1024":"Property","2048":"Method","262144":"Accessor","4194304":"Type alias"},"rows":[{"id":0,"kind":1,"name":"\"lib/Metaphor\"","url":"modules/_lib_metaphor_.html","classes":"tsd-kind-external-module"},{"id":1,"kind":256,"name":"IdsToProcess","url":"interfaces/_lib_metaphor_.idstoprocess.html","classes":"tsd-kind-interface tsd-parent-kind-external-module","parent":"\"lib/Metaphor\""},{"id":2,"kind":128,"name":"Metaphor","url":"classes/_lib_metaphor_.metaphor.html","classes":"tsd-kind-class tsd-parent-kind-external-module","parent":"\"lib/Metaphor\""},{"id":3,"kind":1024,"name":"meta","url":"classes/_lib_metaphor_.metaphor.html#meta","classes":"tsd-kind-property tsd-parent-kind-class tsd-is-private","parent":"\"lib/Metaphor\".Metaphor"},{"id":4,"kind":512,"name":"constructor","url":"classes/_lib_metaphor_.metaphor.html#constructor","classes":"tsd-kind-constructor tsd-parent-kind-class","parent":"\"lib/Metaphor\".Metaphor"},{"id":5,"kind":2048,"name":"togglePropByFieldIds","url":"classes/_lib_metaphor_.metaphor.html#togglepropbyfieldids","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"lib/Metaphor\".Metaphor"},{"id":6,"kind":2048,"name":"value","url":"classes/_lib_metaphor_.metaphor.html#value","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"lib/Metaphor\".Metaphor"},{"id":7,"kind":2048,"name":"get","url":"classes/_lib_metaphor_.metaphor.html#get","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"lib/Metaphor\".Metaphor"},{"id":8,"kind":2048,"name":"set","url":"classes/_lib_metaphor_.metaphor.html#set","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"lib/Metaphor\".Metaphor"},{"id":9,"kind":2048,"name":"show","url":"classes/_lib_metaphor_.metaphor.html#show","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"lib/Metaphor\".Metaphor"},{"id":10,"kind":2048,"name":"showAll","url":"classes/_lib_metaphor_.metaphor.html#showall","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"lib/Metaphor\".Metaphor"},{"id":11,"kind":2048,"name":"hide","url":"classes/_lib_metaphor_.metaphor.html#hide","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"lib/Metaphor\".Metaphor"},{"id":12,"kind":2048,"name":"hideAll","url":"classes/_lib_metaphor_.metaphor.html#hideall","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"lib/Metaphor\".Metaphor"},{"id":13,"kind":2048,"name":"enable","url":"classes/_lib_metaphor_.metaphor.html#enable","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"lib/Metaphor\".Metaphor"},{"id":14,"kind":2048,"name":"enableAll","url":"classes/_lib_metaphor_.metaphor.html#enableall","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"lib/Metaphor\".Metaphor"},{"id":15,"kind":2048,"name":"disable","url":"classes/_lib_metaphor_.metaphor.html#disable","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"lib/Metaphor\".Metaphor"},{"id":16,"kind":2048,"name":"disableAll","url":"classes/_lib_metaphor_.metaphor.html#disableall","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"lib/Metaphor\".Metaphor"},{"id":17,"kind":2048,"name":"clone","url":"classes/_lib_metaphor_.metaphor.html#clone","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"lib/Metaphor\".Metaphor"},{"id":18,"kind":2048,"name":"add","url":"classes/_lib_metaphor_.metaphor.html#add","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"lib/Metaphor\".Metaphor"},{"id":19,"kind":2048,"name":"remove","url":"classes/_lib_metaphor_.metaphor.html#remove","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"lib/Metaphor\".Metaphor"},{"id":20,"kind":262144,"name":"config","url":"classes/_lib_metaphor_.metaphor.html#config","classes":"tsd-kind-get-signature tsd-parent-kind-class","parent":"\"lib/Metaphor\".Metaphor"},{"id":21,"kind":262144,"name":"actions","url":"classes/_lib_metaphor_.metaphor.html#actions","classes":"tsd-kind-get-signature tsd-parent-kind-class","parent":"\"lib/Metaphor\".Metaphor"},{"id":22,"kind":4194304,"name":"FieldBooleanProps","url":"modules/_lib_metaphor_.html#fieldbooleanprops","classes":"tsd-kind-type-alias tsd-parent-kind-external-module","parent":"\"lib/Metaphor\""},{"id":23,"kind":64,"name":"notFoundMessage","url":"modules/_lib_metaphor_.html#notfoundmessage","classes":"tsd-kind-function tsd-parent-kind-external-module","parent":"\"lib/Metaphor\""},{"id":24,"kind":64,"name":"inUseMessage","url":"modules/_lib_metaphor_.html#inusemessage","classes":"tsd-kind-function tsd-parent-kind-external-module","parent":"\"lib/Metaphor\""},{"id":25,"kind":64,"name":"invariantFieldUnique","url":"modules/_lib_metaphor_.html#invariantfieldunique","classes":"tsd-kind-function tsd-parent-kind-external-module","parent":"\"lib/Metaphor\""},{"id":26,"kind":64,"name":"getFieldIdx","url":"modules/_lib_metaphor_.html#getfieldidx","classes":"tsd-kind-function tsd-parent-kind-external-module","parent":"\"lib/Metaphor\""},{"id":27,"kind":1,"name":"\"lib/FieldPart\"","url":"modules/_lib_fieldpart_.html","classes":"tsd-kind-external-module"},{"id":28,"kind":256,"name":"FieldPart","url":"interfaces/_lib_fieldpart_.fieldpart.html","classes":"tsd-kind-interface tsd-parent-kind-external-module","parent":"\"lib/FieldPart\""},{"id":29,"kind":128,"name":"FieldPartGateway","url":"classes/_lib_fieldpart_.fieldpartgateway.html","classes":"tsd-kind-class tsd-parent-kind-external-module","parent":"\"lib/FieldPart\""},{"id":30,"kind":1024,"name":"form","url":"classes/_lib_fieldpart_.fieldpartgateway.html#form","classes":"tsd-kind-property tsd-parent-kind-class tsd-is-private","parent":"\"lib/FieldPart\".FieldPartGateway"},{"id":31,"kind":1024,"name":"path","url":"classes/_lib_fieldpart_.fieldpartgateway.html#path","classes":"tsd-kind-property tsd-parent-kind-class tsd-is-private","parent":"\"lib/FieldPart\".FieldPartGateway"},{"id":32,"kind":512,"name":"constructor","url":"classes/_lib_fieldpart_.fieldpartgateway.html#constructor","classes":"tsd-kind-constructor tsd-parent-kind-class","parent":"\"lib/FieldPart\".FieldPartGateway"},{"id":33,"kind":2048,"name":"set","url":"classes/_lib_fieldpart_.fieldpartgateway.html#set","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"lib/FieldPart\".FieldPartGateway"},{"id":34,"kind":2048,"name":"up","url":"classes/_lib_fieldpart_.fieldpartgateway.html#up","classes":"tsd-kind-method tsd-parent-kind-class","parent":"\"lib/FieldPart\".FieldPartGateway"},{"id":35,"kind":1,"name":"\"index\"","url":"modules/_index_.html","classes":"tsd-kind-external-module"}]};
--------------------------------------------------------------------------------
/packages/metaphor/docs/modules/_index_.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
"index" | @react-ui-generator/metaphor
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | - Preparing search index...
23 | - The search index is not available
24 |
25 |
@react-ui-generator/metaphor
26 |
27 |
49 |
50 |
51 |
52 |
53 |
54 |
62 |
External module "index"
63 |
64 |
65 |
66 |
94 |
154 |
155 |
Generated using TypeDoc
156 |
157 |
158 |
159 |
160 |
161 |
--------------------------------------------------------------------------------
/packages/metaphor/docs/globals.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
@react-ui-generator/metaphor
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | - Preparing search index...
23 | - The search index is not available
24 |
25 |
@react-ui-generator/metaphor
26 |
27 |
49 |
50 |
51 |
52 |
53 |
54 |
59 |
@react-ui-generator/metaphor
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | Index
68 |
69 |
70 |
71 | External modules
72 |
77 |
78 |
79 |
80 |
81 |
82 |
104 |
105 |
106 |
166 |
167 |
Generated using TypeDoc
168 |
169 |
170 |
171 |
172 |
173 |
--------------------------------------------------------------------------------
/packages/metaphor/docs/interfaces/_lib_fieldpart_.fieldpart.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
FieldPart | @react-ui-generator/metaphor
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | - Preparing search index...
23 | - The search index is not available
24 |
25 |
@react-ui-generator/metaphor
26 |
27 |
49 |
50 |
51 |
52 |
53 |
54 |
65 |
Interface FieldPart
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | Hierarchy
74 |
75 | -
76 | FieldPart
77 |
78 |
79 |
80 |
81 | Indexable
82 | [key: string]: any
83 |
84 |
85 |
117 |
118 |
119 |
179 |
180 |
Generated using TypeDoc
181 |
182 |
183 |
184 |
185 |
186 |
--------------------------------------------------------------------------------
/packages/metaphor/docs/modules/_lib_fieldpart_.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
"lib/FieldPart" | @react-ui-generator/metaphor
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | - Preparing search index...
23 | - The search index is not available
24 |
25 |
@react-ui-generator/metaphor
26 |
27 |
49 |
50 |
51 |
52 |
53 |
54 |
62 |
External module "lib/FieldPart"
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | Index
71 |
72 |
73 |
79 |
80 | Interfaces
81 |
84 |
85 |
86 |
87 |
88 |
89 |
117 |
118 |
119 |
179 |
180 |
Generated using TypeDoc
181 |
182 |
183 |
184 |
185 |
186 |
--------------------------------------------------------------------------------
/packages/metaphor/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
@react-ui-generator/metaphor
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | - Preparing search index...
23 | - The search index is not available
24 |
25 |
@react-ui-generator/metaphor
26 |
27 |
49 |
50 |
51 |
52 |
53 |
54 |
59 |
@react-ui-generator/metaphor
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | Metaphor
69 |
70 |
DSL for easing baking of metadata for the react-ui-generator.
71 |
72 | Example
73 |
74 |
import Metaphor from '@react-ui-generator/metaphor';
75 | import BaseMeta from './meta.json';
76 |
77 | const formToView = new Metaphor(BaseMeta)
78 | .showAll()
79 | .disableAll()
80 | .value();
81 |
82 | const formToEdit = new Metaphor(BaseMeta)
83 | .enableAll()
84 | .disable(['id', 'createdAt', 'author'])
85 | .config
86 | .set('email', { showAsterix: true })
87 | .set(['birthDate', 'employmentDate'], {
88 | showAsterix: true,
89 | format: 'DD.MM.YYYY',
90 | })
91 | .up()
92 | .actions
93 | .set(['btnSave'], { onClick: 'sendFormToServer' })
94 | .set('btnCancel', { onClick: isFormChanged() ? 'confirmEditCancellation' : 'closeForm' })
95 | .up()
96 | .hide(calcSecretFieldsByUserRole(), true)
97 | .value();
98 |
99 |
100 |
122 |
123 |
124 |
184 |
185 |
Generated using TypeDoc
186 |
187 |
188 |
189 |
190 |
191 |
--------------------------------------------------------------------------------