diff --git a/doc/reference/component.md b/doc/reference/component.md index a4bf7562..858cfff0 100644 --- a/doc/reference/component.md +++ b/doc/reference/component.md @@ -891,10 +891,12 @@ For each key, a `prop` definition is either a boolean, a constructor, a list of - a list of constructors. In that case, this means that we allow more than one type. For example, `id: [Number, String]` means that `id` can be either a string or a number. -- an object. This makes it possible to have more expressive definition. The following sub keys are then allowed: +- an object. This makes it possible to have more expressive definition. The following sub keys are then allowed (but not mandatory): - `type`: the main type of the prop being validated - - `element`: if the type was `Array`, then the `element` key describes the type of each element in the array. It is optional (not set means that we only validate the array, not its elements), - - `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. It is optional (not set means that we only validate the object, not its elements) + - `element`: if the type was `Array`, then the `element` key describes the type of each element in the array. If it is not set, then we only validate the array, not its elements, + - `shape`: if the type was `Object`, then the `shape` key describes the interface of the object. If it is not set, then we only validate the object, not its elements, + - `validate`: this is a function which should return a boolean to determine if + the value is valid or not. Useful for custom validation logic. Examples: @@ -924,6 +926,13 @@ Examples: someFlag: Boolean, // a boolean, mandatory (even if `false`) someVal: [Boolean, Date], // either a boolean or a date otherValue: true, // indicates that it is a prop + kindofsmallnumber: { + type: Number, + validate: n => (0 <= n && n <= 10) + }, + size: { + validate: e => ["small", "medium", "large"].includes(e) + }, }; ``` diff --git a/src/component/props_validation.ts b/src/component/props_validation.ts index 55d1136d..058ed30d 100644 --- a/src/component/props_validation.ts +++ b/src/component/props_validation.ts @@ -48,7 +48,7 @@ QWeb.utils.validateProps = function(Widget, props: Object) { throw e; } if (!isValid) { - throw new Error(`Props '${propName}' of invalid type in component '${Widget.name}'`); + throw new Error(`Invalid Prop '${propName}' in component '${Widget.name}'`); } } for (let propName in props) { @@ -89,7 +89,10 @@ function isValidProp(prop, propDef): boolean { if (propDef.optional && prop === undefined) { return true; } - let result = isValidProp(prop, propDef.type); + let result = propDef.type ? isValidProp(prop, propDef.type) : true; + if (propDef.validate) { + result = result && propDef.validate(prop); + } if (propDef.type === Array && propDef.element) { for (let i = 0, iLen = prop.length; i < iLen; i++) { result = result && isValidProp(prop[i], propDef.element); diff --git a/tests/component/props_validation.test.ts b/tests/component/props_validation.test.ts index 22645bac..b23a6b7c 100644 --- a/tests/component/props_validation.test.ts +++ b/tests/component/props_validation.test.ts @@ -138,7 +138,7 @@ describe("props validation", () => { error = e; } expect(error).toBeDefined(); - expect(error.message).toBe(`Props 'p' of invalid type in component '_a'`); + expect(error.message).toBe("Invalid Prop 'p' in component '_a'"); } }); @@ -195,7 +195,7 @@ describe("props validation", () => { error = e; } expect(error).toBeDefined(); - expect(error.message).toBe(`Props 'p' of invalid type in component '_a'`); + expect(error.message).toBe("Invalid Prop 'p' in component '_a'"); } }); @@ -240,7 +240,7 @@ describe("props validation", () => { error = e; } expect(error).toBeDefined(); - expect(error.message).toBe("Props 'p' of invalid type in component 'TestWidget'"); + expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'"); }); test("can validate an optional props", async () => { @@ -284,7 +284,7 @@ describe("props validation", () => { error = e; } expect(error).toBeDefined(); - expect(error.message).toBe(`Props 'p' of invalid type in component 'TestWidget'`); + expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'"); }); test("can validate an array with given primitive type", async () => { @@ -389,7 +389,7 @@ describe("props validation", () => { error = e; } expect(error).toBeDefined(); - expect(error.message).toBe(`Props 'p' of invalid type in component 'TestWidget'`); + expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'"); }); test("can validate an object with simple shape", async () => { @@ -436,7 +436,7 @@ describe("props validation", () => { error = e; } expect(error).toBeDefined(); - expect(error.message).toBe(`Props 'p' of invalid type in component 'TestWidget'`); + expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'"); error = undefined; try { @@ -447,7 +447,7 @@ describe("props validation", () => { error = e; } expect(error).toBeDefined(); - expect(error.message).toBe(`Props 'p' of invalid type in component 'TestWidget'`); + expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'"); }); test("can validate recursively complicated prop def", async () => { @@ -499,7 +499,7 @@ describe("props validation", () => { error = e; } expect(error).toBeDefined(); - expect(error.message).toBe(`Props 'p' of invalid type in component 'TestWidget'`); + expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'"); }); test("can validate optional attributes in nested sub props", () => { @@ -535,6 +535,70 @@ describe("props validation", () => { ); }); + test("can validate with a custom validator", () => { + class TestComponent extends Component { + static props = { + size: { + validate: e => ["small", "medium", "large"].includes(e) + } + }; + } + let error; + try { + QWeb.utils.validateProps(TestComponent, { size: "small" }); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); + + try { + QWeb.utils.validateProps(TestComponent, { size: "abcdef" }); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe("Invalid Prop 'size' in component 'TestComponent'"); + }); + + test("can validate with a custom validator, and a type", () => { + const validator = jest.fn(n => 0 <= n && n <= 10); + class TestComponent extends Component { + static props = { + n: { + type: Number, + validate: validator + } + }; + } + let error; + try { + QWeb.utils.validateProps(TestComponent, { n: 3 }); + } catch (e) { + error = e; + } + expect(error).toBeUndefined(); + expect(validator).toBeCalledTimes(1); + + try { + QWeb.utils.validateProps(TestComponent, { n: "str" }); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe("Invalid Prop 'n' in component 'TestComponent'"); + expect(validator).toBeCalledTimes(1); + + error = null; + try { + QWeb.utils.validateProps(TestComponent, { n: 100 }); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toBe("Invalid Prop 'n' in component 'TestComponent'"); + expect(validator).toBeCalledTimes(2); + }); + test("props are validated in dev mode (code snapshot)", async () => { env.qweb.addTemplates(` @@ -591,7 +655,7 @@ describe("props validation", () => { }).not.toThrow(); expect(() => { QWeb.utils.validateProps(TestWidget, { myprop: 1 }); - }).toThrow(`Props 'myprop' of invalid type in component 'TestWidget'`); + }).toThrow(`Invalid Prop 'myprop' in component 'TestWidget'`); }); test("props with type object, and no shape", async () => { @@ -604,7 +668,7 @@ describe("props validation", () => { }).not.toThrow(); expect(() => { QWeb.utils.validateProps(TestWidget, { myprop: false }); - }).toThrow(`Props 'myprop' of invalid type in component 'TestWidget'`); + }).toThrow(`Invalid Prop 'myprop' in component 'TestWidget'`); }); test("props: extra props cause an error", async () => {