Skip to content

Commit 514e032

Browse files
committed
feat(lib): introduce conditionals !
1 parent 2b9868e commit 514e032

4 files changed

Lines changed: 219 additions & 22 deletions

File tree

lib/forms/component.tsx

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
} from './renderers';
2323
import { saveButtonRenderer } from './renderers';
2424
import {
25+
applyConditionsToSchema,
2526
cleanData,
2627
getSchemaFromConfig,
2728
isMutationConfig,
@@ -50,7 +51,7 @@ export type ApolloFormProps<T> = {
5051
data: any;
5152
title?: string;
5253
subTitle?: string;
53-
config: ApolloFormConfig & { mutation: { name: T } };
54+
config: ApolloFormConfig & { mutation?: { name: T } };
5455
onSave?: (data: object) => void;
5556
onCancel?: () => void;
5657
ui?: UiSchema & ApolloFormUi;
@@ -63,6 +64,7 @@ export interface ApolloFormState {
6364
isSaved: boolean;
6465
hasError: boolean;
6566
schema: JSONSchema6;
67+
schemaWithConditionals: JSONSchema6;
6668
// tslint:disable-next-line:no-any
6769
data: any;
6870
}
@@ -145,13 +147,20 @@ export function configure<MutationNamesType = {}>(opts: ApolloFormConfigureOptio
145147
isSaved: false,
146148
hasError: false,
147149
schema: {},
150+
schemaWithConditionals: {},
148151
data: {}
149152
};
150153

151154
componentDidMount() {
155+
const schema = getSchemaFromConfig(jsonSchema, this.props.config, this.props.title);
152156
this.setState(() => ({
153-
schema: getSchemaFromConfig(jsonSchema, this.props.config, this.props.title),
154-
data: this.props.data
157+
schema,
158+
data: this.props.data,
159+
schemaWithConditionals: applyConditionsToSchema(
160+
schema,
161+
this.props.ui,
162+
this.state.data
163+
)
155164
}));
156165
}
157166

@@ -163,14 +172,32 @@ export function configure<MutationNamesType = {}>(opts: ApolloFormConfigureOptio
163172
const currentMutationName = config.mutation.name;
164173
const previousMutationName = prevConfig.mutation.name;
165174
if (currentMutationName !== previousMutationName) {
166-
this.setState({
167-
schema: getSchemaFromConfig(jsonSchema, config, this.props.title)
168-
});
175+
this.setState(
176+
{
177+
schema: getSchemaFromConfig(jsonSchema, config, this.props.title)
178+
},
179+
() => this.setState({
180+
schemaWithConditionals: applyConditionsToSchema(
181+
this.state.schema,
182+
this.props.ui,
183+
this.state.data
184+
)
185+
})
186+
);
169187
}
170188
} else {
171-
this.setState({
172-
schema: getSchemaFromConfig(jsonSchema, config, this.props.title)
173-
});
189+
this.setState(
190+
{
191+
schema: getSchemaFromConfig(jsonSchema, config, this.props.title)
192+
},
193+
() => this.setState({
194+
schemaWithConditionals: applyConditionsToSchema(
195+
this.state.schema,
196+
this.props.ui,
197+
this.state.data
198+
)
199+
})
200+
);
174201
}
175202
}
176203
}
@@ -209,9 +236,15 @@ export function configure<MutationNamesType = {}>(opts: ApolloFormConfigureOptio
209236
}
210237

211238
onChange = (data: IChangeEvent) => {
239+
const newSchema = applyConditionsToSchema(
240+
this.state.schema,
241+
this.props.ui,
242+
data.formData
243+
);
212244
this.setState(() => ({
213245
isDirty: true,
214-
data: data.formData,
246+
data: cleanData(data.formData, newSchema.properties),
247+
schemaWithConditionals: newSchema,
215248
hasError: data.errors.length > 0,
216249
isSaved: false
217250
}));
@@ -275,7 +308,7 @@ export function configure<MutationNamesType = {}>(opts: ApolloFormConfigureOptio
275308
config={this.props.config}
276309
ui={this.props.ui}
277310
liveValidate={this.props.liveValidate}
278-
schema={this.state.schema}
311+
schema={this.state.schemaWithConditionals}
279312
data={this.state.data}
280313
subTitle={this.props.subTitle}
281314
isDirty={this.state.isDirty}

lib/forms/utils.ts

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,28 @@
11
// tslint:disable:no-any
2-
import { get, transform, unset, merge, map, set, has, isPlainObject, last, take, cloneDeep, uniq, isUndefined } from 'lodash';
3-
import { ApolloFormBuilder } from './definitions';
2+
import { PureQueryOptions } from 'apollo-client';
3+
import { every } from 'async';
44
import { DocumentNode } from 'graphql';
55
import { JSONSchema6 } from 'json-schema';
6-
import { retrieveSchema } from 'react-jsonschema-form/lib/utils';
7-
import { PureQueryOptions } from 'apollo-client';
6+
import {
7+
cloneDeep, filter, get,
8+
has, isPlainObject, isUndefined,
9+
last,
10+
map,
11+
merge,
12+
set,
13+
take,
14+
transform,
15+
uniq,
16+
unset,
17+
Dictionary,
18+
MemoVoidDictionaryIterator
19+
} from 'lodash';
820
import { RefetchQueriesProviderFn } from 'react-apollo';
21+
import { UiSchema } from 'react-jsonschema-form';
22+
import { retrieveSchema } from 'react-jsonschema-form/lib/utils';
23+
import { isObject } from 'util';
24+
import { ApolloFormUi } from './component';
25+
import { ApolloFormBuilder } from './definitions';
926

1027
// ApolloForm options object is composed of 2 modes : "mutation" or "manual"
1128
export type ApolloFormConfigBase = {
@@ -31,7 +48,7 @@ export interface ApolloFormConfigMutation extends ApolloFormConfigBase {
3148
}
3249

3350
export interface ApolloFormConfigManual extends ApolloFormConfigBase {
34-
schema: object;
51+
schema: JSONSchema6;
3552
saveData: (formData: any) => any;
3653
}
3754

@@ -58,12 +75,68 @@ export const flattenSchemaProperties = (schema: any): any => {
5875
);
5976
};
6077

78+
const applyConditionsReducer =
79+
(ui: UiSchema & ApolloFormUi, data: object) =>
80+
(acc: JSONSchema6, curr: JSONSchema6, key: string) => {
81+
const propUi: (UiSchema & ApolloFormUi) | undefined = get(ui, key);
82+
const prop = last(key.split('.'));
83+
if (propUi && propUi['ui:if']) {
84+
if (
85+
filter(propUi['ui:if'], (predicate, k) => {
86+
const value = get(data, k);
87+
return predicate && predicate !== value;
88+
}).length === 0
89+
) {
90+
Object.assign(acc, curr);
91+
}
92+
} else if (has(curr, 'properties')) {
93+
Object.assign(
94+
acc,
95+
{
96+
[prop]: {
97+
type: 'object',
98+
properties: {},
99+
...(curr.required ? { required: curr.required } : {})
100+
}
101+
}
102+
);
103+
map(curr.properties, (v, k) => {
104+
(acc as any)[prop].properties[k] =
105+
applyConditionsReducer(ui, data)({}, v as JSONSchema6, `${key}.${k}`);
106+
});
107+
} else {
108+
Object.assign(acc, curr);
109+
}
110+
return acc;
111+
};
112+
113+
export const applyConditionsToSchema =
114+
(jsonSchema: JSONSchema6, ui: UiSchema & ApolloFormUi, data: object): JSONSchema6 => {
115+
const schema = cloneDeep(jsonSchema);
116+
return schema.properties ?
117+
Object.assign(
118+
{},
119+
schema,
120+
{
121+
properties: transform(
122+
schema.properties,
123+
applyConditionsReducer(ui, data),
124+
{}
125+
)
126+
}
127+
) :
128+
schema;
129+
};
130+
61131
// Given a config, return a valid JSON Schema
62132
export const getSchemaFromConfig = (jsonSchema: JSONSchema6, config: ApolloFormConfig, title?: string): JSONSchema6 => {
63133
let schema: any;
64134
// generated schema given mode: "manual" or "mutation"
65135
if (!isMutationConfig(config)) {
66-
schema = config.schema;
136+
schema = ApolloFormBuilder.getSchema(
137+
jsonSchema,
138+
config.schema.properties || {}
139+
);
67140
} else {
68141
const mutationConfig = ApolloFormBuilder.getMutationConfig(jsonSchema, config.mutation.name);
69142
schema = ApolloFormBuilder.getSchema(

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"awesome-typescript-loader": "^5.0.0",
4141
"babel-core": "^6.26.3",
4242
"css-loader": "^0.28.11",
43+
"functional-json-schema": "0.0.2-3",
4344
"graphql-tag": "^2.9.2",
4445
"graphql-tools": "^3.0.2",
4546
"jest": "^23.1.0",

stories/index.tsx

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// tslint:disable-next-line:no-unused-variable
22
import { storiesOf } from '@storybook/react';
3+
import { schema, types } from 'functional-json-schema';
34
import { graphqlSync, introspectionQuery, DocumentNode, IntrospectionQuery } from 'graphql';
45
import { fromIntrospectionQuery } from 'graphql-2-json-schema';
56
import gql from 'graphql-tag';
@@ -8,12 +9,13 @@ import { keys } from 'lodash';
89
import * as React from 'react';
910
import { ApolloConsumer, Mutation } from 'react-apollo';
1011
import { FieldProps } from 'react-jsonschema-form';
11-
import { schema } from '../graphql-mock';
12-
import { configure, ApolloFormConfigureTheme } from '../lib/forms/component';
13-
const { Button, Input, Checkbox, Header, Form } = require('semantic-ui-react');
12+
import { schema as mockSchema } from '../graphql-mock';
13+
import { configure, ApolloFormConfigureTheme, ErrorListComponent } from '../lib/forms/component';
14+
import { ApolloFormBuilder } from '../lib/forms/definitions';
15+
const { Button, Input, Checkbox, Header, Form, Message } = require('semantic-ui-react');
1416
const { withKnobs, select, boolean: bool } = require('@storybook/addon-knobs/react');
1517

16-
const introspection = graphqlSync(schema, introspectionQuery).data as IntrospectionQuery;
18+
const introspection = graphqlSync(mockSchema, introspectionQuery).data as IntrospectionQuery;
1719
const jsonSchema = fromIntrospectionQuery(introspection);
1820
const document = gql`
1921
mutation createTodo($todo: TodoInputType!) {
@@ -23,6 +25,15 @@ const document = gql`
2325
}
2426
`;
2527

28+
const ErrorList: ErrorListComponent = p => (
29+
<Message
30+
error={true}
31+
visible={true}
32+
header="There was some errors"
33+
list={p.errors.map(e => e.message)}
34+
/>
35+
);
36+
2637
const theme: ApolloFormConfigureTheme = {
2738
templates: {
2839
FieldTemplate: props => {
@@ -51,7 +62,10 @@ const theme: ApolloFormConfigureTheme = {
5162
),
5263
BooleanField: (p: FieldProps) => (
5364
<Checkbox label={p.title} checked={p.formData} onChange={
54-
(e: React.SyntheticEvent<HTMLInputElement>) => p.onChange(e.currentTarget.value)
65+
(e: React.SyntheticEvent<HTMLInputElement>, data: object) => {
66+
// tslint:disable-next-line:no-any
67+
p.onChange((data as any).checked);
68+
}
5569
} />
5670
)
5771
},
@@ -79,6 +93,8 @@ storiesOf('ApolloForm', module)
7993
<ApolloConsumer>
8094
{client => {
8195
const withTheme = bool('withTheme', true);
96+
const liveValidate = bool('liveValidate', false);
97+
const showErrorsList = bool('showErrorsList', true);
8298
const ApplicationForm = configure({
8399
client,
84100
jsonSchema,
@@ -89,6 +105,7 @@ storiesOf('ApolloForm', module)
89105
return (
90106
<ApplicationForm
91107
title={'Todo Form'}
108+
liveValidate={liveValidate}
92109
config={{
93110
mutation: {
94111
name: mutationName,
@@ -97,6 +114,8 @@ storiesOf('ApolloForm', module)
97114
}}
98115
data={{}}
99116
ui={{
117+
showErrorsList: showErrorsList,
118+
errorListComponent: ErrorList,
100119
todo: {
101120
name: {
102121
'ui:label': 'Task name'
@@ -114,6 +133,77 @@ storiesOf('ApolloForm', module)
114133
{form.header()}
115134
{form.form()}
116135
{form.buttons()}
136+
{JSON.stringify(form.data)}
137+
</div>
138+
</Form>
139+
)
140+
}
141+
</ApplicationForm>
142+
);
143+
}}
144+
</ApolloConsumer>
145+
);
146+
}).add('with conditionals', () => {
147+
return (
148+
<ApolloConsumer>
149+
{client => {
150+
const withTheme = bool('withTheme', true);
151+
const liveValidate = bool('liveValidate', false);
152+
const showErrorsList = bool('showErrorsList', true);
153+
const ApplicationForm = configure({
154+
client,
155+
jsonSchema,
156+
theme: withTheme ? theme : undefined
157+
});
158+
return (
159+
<ApplicationForm
160+
title={'Todo Form'}
161+
liveValidate={liveValidate}
162+
config={{
163+
name: 'todo',
164+
schema: {
165+
type: 'object',
166+
properties: {
167+
shipping: {
168+
type: 'object',
169+
properties: {
170+
billingSameAsDelivery: { type: 'boolean' },
171+
billing: {
172+
type: 'object',
173+
properties: {
174+
address: { type: 'string' }
175+
}
176+
}
177+
}
178+
}
179+
}
180+
} as JSONSchema6,
181+
saveData: data => {
182+
// tslint:disable-next-line:no-console
183+
console.log('save !', data);
184+
}
185+
}}
186+
data={{}}
187+
ui={{
188+
showErrorsList: showErrorsList,
189+
errorListComponent: ErrorList,
190+
shipping: {
191+
billing: {
192+
'ui:if': {
193+
'shipping.billingSameAsDelivery': true
194+
},
195+
}
196+
}
197+
}}
198+
>
199+
{
200+
form => (
201+
<Form>
202+
<div style={{ padding: '20px' }}>
203+
{form.header()}
204+
{form.form()}
205+
{form.buttons()}
206+
{JSON.stringify(form.data)}
117207
</div>
118208
</Form>
119209
)

0 commit comments

Comments
 (0)