[IMP] component: allow custom validator functions for props

closes #375
This commit is contained in:
Géry Debongnie
2019-11-22 15:30:32 +01:00
committed by aab-odoo
parent 14db513f3b
commit 3aa586db43
3 changed files with 91 additions and 15 deletions
+12 -3
View File
@@ -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 - 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 type. For example, `id: [Number, String]` means that `id` can be either a string
or a number. 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 - `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), - `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. It is optional (not set means that we only validate the object, 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: Examples:
@@ -924,6 +926,13 @@ Examples:
someFlag: Boolean, // a boolean, mandatory (even if `false`) someFlag: Boolean, // a boolean, mandatory (even if `false`)
someVal: [Boolean, Date], // either a boolean or a date someVal: [Boolean, Date], // either a boolean or a date
otherValue: true, // indicates that it is a prop 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)
},
}; };
``` ```
+5 -2
View File
@@ -48,7 +48,7 @@ QWeb.utils.validateProps = function(Widget, props: Object) {
throw e; throw e;
} }
if (!isValid) { 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) { for (let propName in props) {
@@ -89,7 +89,10 @@ function isValidProp(prop, propDef): boolean {
if (propDef.optional && prop === undefined) { if (propDef.optional && prop === undefined) {
return true; 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) { if (propDef.type === Array && propDef.element) {
for (let i = 0, iLen = prop.length; i < iLen; i++) { for (let i = 0, iLen = prop.length; i < iLen; i++) {
result = result && isValidProp(prop[i], propDef.element); result = result && isValidProp(prop[i], propDef.element);
+74 -10
View File
@@ -138,7 +138,7 @@ describe("props validation", () => {
error = e; error = e;
} }
expect(error).toBeDefined(); 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; error = e;
} }
expect(error).toBeDefined(); 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; error = e;
} }
expect(error).toBeDefined(); 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 () => { test("can validate an optional props", async () => {
@@ -284,7 +284,7 @@ describe("props validation", () => {
error = e; error = e;
} }
expect(error).toBeDefined(); 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 () => { test("can validate an array with given primitive type", async () => {
@@ -389,7 +389,7 @@ describe("props validation", () => {
error = e; error = e;
} }
expect(error).toBeDefined(); 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 () => { test("can validate an object with simple shape", async () => {
@@ -436,7 +436,7 @@ describe("props validation", () => {
error = e; error = e;
} }
expect(error).toBeDefined(); 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; error = undefined;
try { try {
@@ -447,7 +447,7 @@ describe("props validation", () => {
error = e; error = e;
} }
expect(error).toBeDefined(); 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 () => { test("can validate recursively complicated prop def", async () => {
@@ -499,7 +499,7 @@ describe("props validation", () => {
error = e; error = e;
} }
expect(error).toBeDefined(); 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", () => { 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<any, any> {
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<any, any> {
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 () => { test("props are validated in dev mode (code snapshot)", async () => {
env.qweb.addTemplates(` env.qweb.addTemplates(`
<templates> <templates>
@@ -591,7 +655,7 @@ describe("props validation", () => {
}).not.toThrow(); }).not.toThrow();
expect(() => { expect(() => {
QWeb.utils.validateProps(TestWidget, { myprop: 1 }); 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 () => { test("props with type object, and no shape", async () => {
@@ -604,7 +668,7 @@ describe("props validation", () => {
}).not.toThrow(); }).not.toThrow();
expect(() => { expect(() => {
QWeb.utils.validateProps(TestWidget, { myprop: false }); 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 () => { test("props: extra props cause an error", async () => {