Skip to content

Commit b2d976e

Browse files
nickgrosJuansasa
andauthored
Support If/Then/Else (#2700)
* Added tests Added if then else logic to resolve schemas * Changed resolve method's name * Added $ref tests * Code review changes * Remove only on test, add integration tests from #2466 * Fixed merge order * Update tests method names * Code review changes #2700 Co-authored-by: Juansasa <[email protected]> Co-authored-by: Quang Vu <[email protected]>
1 parent 00d8576 commit b2d976e

File tree

5 files changed

+725
-1
lines changed

5 files changed

+725
-1
lines changed

packages/core/src/utils.js

+40-1
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,7 @@ export function optionsList(schema) {
582582
});
583583
} else {
584584
const altSchemas = schema.oneOf || schema.anyOf;
585-
return altSchemas.map((schema, i) => {
585+
return altSchemas.map(schema => {
586586
const value = toConstant(schema);
587587
const label = schema.title || String(value);
588588
return {
@@ -675,6 +675,40 @@ export function stubExistingAdditionalProperties(
675675
return schema;
676676
}
677677

678+
/**
679+
* Resolves a conditional block (if/else/then) by removing the condition and merging the appropriate conditional branch with the rest of the schema
680+
*/
681+
const resolveCondition = (schema, rootSchema, formData) => {
682+
let {
683+
if: expression,
684+
then,
685+
else: otherwise,
686+
...resolvedSchemaLessConditional
687+
} = schema;
688+
689+
const conditionalSchema = isValid(expression, formData, rootSchema)
690+
? then
691+
: otherwise;
692+
693+
if (conditionalSchema) {
694+
return retrieveSchema(
695+
mergeSchemas(
696+
resolvedSchemaLessConditional,
697+
retrieveSchema(conditionalSchema, rootSchema, formData)
698+
),
699+
rootSchema,
700+
formData
701+
);
702+
} else {
703+
return retrieveSchema(resolvedSchemaLessConditional, rootSchema, formData);
704+
}
705+
};
706+
707+
/**
708+
* Resolves references and dependencies within a schema and its 'allOf' children.
709+
*
710+
* Called internally by retrieveSchema.
711+
*/
678712
export function resolveSchema(schema, rootSchema = {}, formData = {}) {
679713
if (schema.hasOwnProperty("$ref")) {
680714
return resolveReference(schema, rootSchema, formData);
@@ -712,6 +746,11 @@ export function retrieveSchema(schema, rootSchema = {}, formData = {}) {
712746
return {};
713747
}
714748
let resolvedSchema = resolveSchema(schema, rootSchema, formData);
749+
750+
if (schema.hasOwnProperty("if")) {
751+
return resolveCondition(schema, rootSchema, formData);
752+
}
753+
715754
if ("allOf" in schema) {
716755
try {
717756
resolvedSchema = mergeAllOf({

packages/core/test/ifthenelse_test.js

+321
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
import { expect } from "chai";
2+
3+
import { createFormComponent, createSandbox } from "./test_utils";
4+
5+
describe("conditional items", () => {
6+
let sandbox;
7+
8+
beforeEach(() => {
9+
sandbox = createSandbox();
10+
});
11+
12+
afterEach(() => {
13+
sandbox.restore();
14+
});
15+
16+
const schema = {
17+
type: "object",
18+
properties: {
19+
street_address: {
20+
type: "string",
21+
},
22+
country: {
23+
enum: ["United States of America", "Canada"],
24+
},
25+
},
26+
if: {
27+
properties: { country: { const: "United States of America" } },
28+
},
29+
then: {
30+
properties: { zipcode: { type: "string" } },
31+
},
32+
else: {
33+
properties: { postal_code: { type: "string" } },
34+
},
35+
};
36+
37+
const schemaWithRef = {
38+
type: "object",
39+
properties: {
40+
country: {
41+
enum: ["United States of America", "Canada"],
42+
},
43+
},
44+
if: {
45+
properties: {
46+
country: {
47+
const: "United States of America",
48+
},
49+
},
50+
},
51+
then: {
52+
$ref: "#/definitions/us",
53+
},
54+
else: {
55+
$ref: "#/definitions/other",
56+
},
57+
definitions: {
58+
us: {
59+
properties: {
60+
zip_code: {
61+
type: "string",
62+
},
63+
},
64+
},
65+
other: {
66+
properties: {
67+
postal_code: {
68+
type: "string",
69+
},
70+
},
71+
},
72+
},
73+
};
74+
75+
it("should render then when condition is true", () => {
76+
const formData = {
77+
country: "United States of America",
78+
};
79+
80+
const { node } = createFormComponent({
81+
schema,
82+
formData,
83+
});
84+
85+
expect(node.querySelector("input[label=zipcode]")).not.eql(null);
86+
expect(node.querySelector("input[label=postal_code]")).to.eql(null);
87+
});
88+
89+
it("should render else when condition is false", () => {
90+
const formData = {
91+
country: "France",
92+
};
93+
94+
const { node } = createFormComponent({
95+
schema,
96+
formData,
97+
});
98+
99+
expect(node.querySelector("input[label=zipcode]")).to.eql(null);
100+
expect(node.querySelector("input[label=postal_code]")).not.eql(null);
101+
});
102+
103+
it("should render control when data has not been filled in", () => {
104+
const formData = {};
105+
106+
const { node } = createFormComponent({
107+
schema,
108+
formData,
109+
});
110+
111+
// An empty formData will make the conditional evaluate to true because no properties are required in the if statement
112+
// Please see https://github.com/epoberezkin/ajv/issues/913
113+
expect(node.querySelector("input[label=zipcode]")).not.eql(null);
114+
expect(node.querySelector("input[label=postal_code]")).to.eql(null);
115+
});
116+
117+
it("should render then when condition is true with reference", () => {
118+
const formData = {
119+
country: "United States of America",
120+
};
121+
122+
const { node } = createFormComponent({
123+
schema: schemaWithRef,
124+
formData,
125+
});
126+
127+
expect(node.querySelector("input[label=zip_code]")).not.eql(null);
128+
expect(node.querySelector("input[label=postal_code]")).to.eql(null);
129+
});
130+
131+
it("should render else when condition is false with reference", () => {
132+
const formData = {
133+
country: "France",
134+
};
135+
136+
const { node } = createFormComponent({
137+
schema: schemaWithRef,
138+
formData,
139+
});
140+
141+
expect(node.querySelector("input[label=zip_code]")).to.eql(null);
142+
expect(node.querySelector("input[label=postal_code]")).not.eql(null);
143+
});
144+
145+
describe("allOf if then else", () => {
146+
const schemaWithAllOf = {
147+
type: "object",
148+
properties: {
149+
street_address: {
150+
type: "string",
151+
},
152+
country: {
153+
enum: [
154+
"United States of America",
155+
"Canada",
156+
"United Kingdom",
157+
"France",
158+
],
159+
},
160+
},
161+
allOf: [
162+
{
163+
if: {
164+
properties: { country: { const: "United States of America" } },
165+
},
166+
then: {
167+
properties: { zipcode: { type: "string" } },
168+
},
169+
},
170+
{
171+
if: {
172+
properties: { country: { const: "United Kingdom" } },
173+
},
174+
then: {
175+
properties: { postcode: { type: "string" } },
176+
},
177+
},
178+
{
179+
if: {
180+
properties: { country: { const: "France" } },
181+
},
182+
then: {
183+
properties: { telephone: { type: "string" } },
184+
},
185+
},
186+
],
187+
};
188+
189+
it("should render correctly when condition is true in allOf (1)", () => {
190+
const formData = {
191+
country: "United States of America",
192+
};
193+
194+
const { node } = createFormComponent({
195+
schema: schemaWithAllOf,
196+
formData,
197+
});
198+
199+
expect(node.querySelector("input[label=zipcode]")).not.eql(null);
200+
});
201+
202+
it("should render correctly when condition is false in allOf (1)", () => {
203+
const formData = {
204+
country: "",
205+
};
206+
207+
const { node } = createFormComponent({
208+
schema: schemaWithAllOf,
209+
formData,
210+
});
211+
212+
expect(node.querySelector("input[label=zipcode]")).to.eql(null);
213+
});
214+
215+
it("should render correctly when condition is true in allof (2)", () => {
216+
const formData = {
217+
country: "United Kingdom",
218+
};
219+
220+
const { node } = createFormComponent({
221+
schema: schemaWithAllOf,
222+
formData,
223+
});
224+
225+
expect(node.querySelector("input[label=postcode]")).not.eql(null);
226+
expect(node.querySelector("input[label=zipcode]")).to.eql(null);
227+
expect(node.querySelector("input[label=telephone]")).to.eql(null);
228+
});
229+
230+
it("should render correctly when condition is true in allof (3)", () => {
231+
const formData = {
232+
country: "France",
233+
};
234+
235+
const { node } = createFormComponent({
236+
schema: schemaWithAllOf,
237+
formData,
238+
});
239+
240+
expect(node.querySelector("input[label=postcode]")).to.eql(null);
241+
expect(node.querySelector("input[label=zipcode]")).to.eql(null);
242+
expect(node.querySelector("input[label=telephone]")).not.eql(null);
243+
});
244+
245+
const schemaWithAllOfRef = {
246+
type: "object",
247+
properties: {
248+
street_address: {
249+
type: "string",
250+
},
251+
country: {
252+
enum: [
253+
"United States of America",
254+
"Canada",
255+
"United Kingdom",
256+
"France",
257+
],
258+
},
259+
},
260+
definitions: {
261+
unitedkingdom: {
262+
properties: { postcode: { type: "string" } },
263+
},
264+
},
265+
allOf: [
266+
{
267+
if: {
268+
properties: { country: { const: "United Kingdom" } },
269+
},
270+
then: {
271+
$ref: "#/definitions/unitedkingdom",
272+
},
273+
},
274+
],
275+
};
276+
277+
it("should render correctly when condition is true when then contains a reference", () => {
278+
const formData = {
279+
country: "United Kingdom",
280+
};
281+
282+
const { node } = createFormComponent({
283+
schema: schemaWithAllOfRef,
284+
formData,
285+
});
286+
287+
expect(node.querySelector("input[label=postcode]")).not.eql(null);
288+
});
289+
});
290+
291+
it("handles additionalProperties with if then else", () => {
292+
/**
293+
* Ensures that fields defined in "then" or "else" (e.g. zipcode) are handled
294+
* with regular form fields, not as additional properties
295+
*/
296+
297+
const formData = {
298+
country: "United States of America",
299+
zipcode: "12345",
300+
otherKey: "otherValue",
301+
};
302+
const { node } = createFormComponent({
303+
schema: {
304+
...schema,
305+
additionalProperties: true,
306+
},
307+
formData,
308+
});
309+
310+
// The zipcode field exists, but not as an additional property
311+
expect(node.querySelector("input[label=zipcode]")).not.eql(null);
312+
expect(
313+
node.querySelector("div.form-additional input[label=zipcode]")
314+
).to.eql(null);
315+
316+
// The "otherKey" field exists as an additional property
317+
expect(
318+
node.querySelector("div.form-additional input[label=otherKey]")
319+
).not.eql(null);
320+
});
321+
});

0 commit comments

Comments
 (0)