Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

If/Then/Else - Code Review changes from #2506 and tests from #2466 #2700

Merged
merged 11 commits into from
Feb 18, 2022
41 changes: 40 additions & 1 deletion packages/core/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ export function optionsList(schema) {
});
} else {
const altSchemas = schema.oneOf || schema.anyOf;
return altSchemas.map((schema, i) => {
return altSchemas.map(schema => {
const value = toConstant(schema);
const label = schema.title || String(value);
return {
Expand Down Expand Up @@ -665,6 +665,40 @@ export function stubExistingAdditionalProperties(
return schema;
}

/**
* Resolves a conditional block (if/else/then) by removing the condition and merging the appropriate conditional branch with the rest of the schema
*/
const resolveCondition = (schema, rootSchema, formData) => {
let {
if: expression,
then,
else: otherwise,
...resolvedSchemaLessConditional
} = schema;

const conditionalSchema = isValid(expression, formData, rootSchema)
? then
: otherwise;

if (conditionalSchema) {
return retrieveSchema(
mergeSchemas(
resolvedSchemaLessConditional,
retrieveSchema(conditionalSchema, rootSchema, formData)
),
rootSchema,
formData
);
} else {
return retrieveSchema(resolvedSchemaLessConditional, rootSchema, formData);
}
};

/**
* Resolves references and dependencies within a schema and its 'allOf' children.
*
* Called internally by retrieveSchema.
*/
export function resolveSchema(schema, rootSchema = {}, formData = {}) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document this

// Resolves references and dependencies and references within branches of allOf
// Called internally by retrieveSchema

if (schema.hasOwnProperty("$ref")) {
return resolveReference(schema, rootSchema, formData);
Expand Down Expand Up @@ -702,6 +736,11 @@ export function retrieveSchema(schema, rootSchema = {}, formData = {}) {
return {};
}
let resolvedSchema = resolveSchema(schema, rootSchema, formData);

if (schema.hasOwnProperty("if")) {
return resolveCondition(schema, rootSchema, formData);
}

if ("allOf" in schema) {
try {
resolvedSchema = mergeAllOf({
Expand Down
321 changes: 321 additions & 0 deletions packages/core/test/ifthenelse_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
import { expect } from "chai";

import { createFormComponent, createSandbox } from "./test_utils";

describe("conditional items", () => {
let sandbox;

beforeEach(() => {
sandbox = createSandbox();
});

afterEach(() => {
sandbox.restore();
});

const schema = {
type: "object",
properties: {
street_address: {
type: "string",
},
country: {
enum: ["United States of America", "Canada"],
},
},
if: {
properties: { country: { const: "United States of America" } },
},
then: {
properties: { zipcode: { type: "string" } },
},
else: {
properties: { postal_code: { type: "string" } },
},
};

const schemaWithRef = {
type: "object",
properties: {
country: {
enum: ["United States of America", "Canada"],
},
},
if: {
properties: {
country: {
const: "United States of America",
},
},
},
then: {
$ref: "#/definitions/us",
},
else: {
$ref: "#/definitions/other",
},
definitions: {
us: {
properties: {
zip_code: {
type: "string",
},
},
},
other: {
properties: {
postal_code: {
type: "string",
},
},
},
},
};

it("should render then when condition is true", () => {
const formData = {
country: "United States of America",
};

const { node } = createFormComponent({
schema,
formData,
});

expect(node.querySelector("input[label=zipcode]")).not.eql(null);
expect(node.querySelector("input[label=postal_code]")).to.eql(null);
});

it("should render else when condition is false", () => {
const formData = {
country: "France",
};

const { node } = createFormComponent({
schema,
formData,
});

expect(node.querySelector("input[label=zipcode]")).to.eql(null);
expect(node.querySelector("input[label=postal_code]")).not.eql(null);
});

it("should render control when data has not been filled in", () => {
const formData = {};

const { node } = createFormComponent({
schema,
formData,
});

// An empty formData will make the conditional evaluate to true because no properties are required in the if statement
// Please see https://github.com/epoberezkin/ajv/issues/913
expect(node.querySelector("input[label=zipcode]")).not.eql(null);
expect(node.querySelector("input[label=postal_code]")).to.eql(null);
});

it("should render then when condition is true with reference", () => {
const formData = {
country: "United States of America",
};

const { node } = createFormComponent({
schema: schemaWithRef,
formData,
});

expect(node.querySelector("input[label=zip_code]")).not.eql(null);
expect(node.querySelector("input[label=postal_code]")).to.eql(null);
});

it("should render else when condition is false with reference", () => {
const formData = {
country: "France",
};

const { node } = createFormComponent({
schema: schemaWithRef,
formData,
});

expect(node.querySelector("input[label=zip_code]")).to.eql(null);
expect(node.querySelector("input[label=postal_code]")).not.eql(null);
});

describe("allOf if then else", () => {
const schemaWithAllOf = {
type: "object",
properties: {
street_address: {
type: "string",
},
country: {
enum: [
"United States of America",
"Canada",
"United Kingdom",
"France",
],
},
},
allOf: [
{
if: {
properties: { country: { const: "United States of America" } },
},
then: {
properties: { zipcode: { type: "string" } },
},
},
{
if: {
properties: { country: { const: "United Kingdom" } },
},
then: {
properties: { postcode: { type: "string" } },
},
},
{
if: {
properties: { country: { const: "France" } },
},
then: {
properties: { telephone: { type: "string" } },
},
},
],
};

it("should render correctly when condition is true in allOf (1)", () => {
const formData = {
country: "United States of America",
};

const { node } = createFormComponent({
schema: schemaWithAllOf,
formData,
});

expect(node.querySelector("input[label=zipcode]")).not.eql(null);
});

it("should render correctly when condition is false in allOf (1)", () => {
const formData = {
country: "",
};

const { node } = createFormComponent({
schema: schemaWithAllOf,
formData,
});

expect(node.querySelector("input[label=zipcode]")).to.eql(null);
});

it("should render correctly when condition is true in allof (2)", () => {
const formData = {
country: "United Kingdom",
};

const { node } = createFormComponent({
schema: schemaWithAllOf,
formData,
});

expect(node.querySelector("input[label=postcode]")).not.eql(null);
expect(node.querySelector("input[label=zipcode]")).to.eql(null);
expect(node.querySelector("input[label=telephone]")).to.eql(null);
});

it("should render correctly when condition is true in allof (3)", () => {
const formData = {
country: "France",
};

const { node } = createFormComponent({
schema: schemaWithAllOf,
formData,
});

expect(node.querySelector("input[label=postcode]")).to.eql(null);
expect(node.querySelector("input[label=zipcode]")).to.eql(null);
expect(node.querySelector("input[label=telephone]")).not.eql(null);
});

const schemaWithAllOfRef = {
type: "object",
properties: {
street_address: {
type: "string",
},
country: {
enum: [
"United States of America",
"Canada",
"United Kingdom",
"France",
],
},
},
definitions: {
unitedkingdom: {
properties: { postcode: { type: "string" } },
},
},
allOf: [
{
if: {
properties: { country: { const: "United Kingdom" } },
},
then: {
$ref: "#/definitions/unitedkingdom",
},
},
],
};

it("should render correctly when condition is true when then contains a reference", () => {
const formData = {
country: "United Kingdom",
};

const { node } = createFormComponent({
schema: schemaWithAllOfRef,
formData,
});

expect(node.querySelector("input[label=postcode]")).not.eql(null);
});
});

it("handles additionalProperties with if then else", () => {
/**
* Ensures that fields defined in "then" or "else" (e.g. zipcode) are handled
* with regular form fields, not as additional properties
*/

const formData = {
country: "United States of America",
zipcode: "12345",
otherKey: "otherValue",
};
const { node } = createFormComponent({
schema: {
...schema,
additionalProperties: true,
},
formData,
});

// The zipcode field exists, but not as an additional property
expect(node.querySelector("input[label=zipcode]")).not.eql(null);
expect(
node.querySelector("div.form-additional input[label=zipcode]")
).to.eql(null);

// The "otherKey" field exists as an additional property
expect(
node.querySelector("div.form-additional input[label=otherKey]")
).not.eql(null);
});
});
Loading