mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] component: allow custom validator functions for props
closes #375
This commit is contained in:
@@ -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)
|
||||||
|
},
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user