[IMP] props validation: cannot set default value on mandatory props

This commit is contained in:
Géry Debongnie
2022-02-04 09:36:54 +01:00
committed by aab-odoo
parent 2afa1f4ace
commit a402115227
5 changed files with 63 additions and 8 deletions
+2
View File
@@ -30,6 +30,8 @@ All changes are documented here in no particular order.
- breaking: `catchError` method is replaced by `onError` hook ([details](#36-catcherror-method-is-replaced-by-onerror-hook)) - breaking: `catchError` method is replaced by `onError` hook ([details](#36-catcherror-method-is-replaced-by-onerror-hook))
- breaking: Support for inline css (`css` tag and static `style`) has been removed ([details](#37-support-for-inline-css-css-tag-and-static-style-has-been-removed)) - breaking: Support for inline css (`css` tag and static `style`) has been removed ([details](#37-support-for-inline-css-css-tag-and-static-style-has-been-removed))
- new: prop validation system can now describe that additional props are allowed (with `*`) ([doc](doc/reference/props.md#props-validation)) - new: prop validation system can now describe that additional props are allowed (with `*`) ([doc](doc/reference/props.md#props-validation))
- breaking: prop validation system does not allow default prop on a mandatory (not optional) prop ([doc](doc/reference/props.md#props-validation))
**Templates** **Templates**
+6 -1
View File
@@ -168,11 +168,15 @@ For each key, a `prop` definition is either a boolean, a constructor, a list of
- `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, - `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 - `validate`: this is a function which should return a boolean to determine if
the value is valid or not. Useful for custom validation logic. the value is valid or not. Useful for custom validation logic.
- `optional`: if true, the prop is not mandatory
There is a special `*` prop that means that additional prop are allowed. This is There is a special `*` prop that means that additional prop are allowed. This is
sometimes useful for generic components that will propagate some or all their sometimes useful for generic components that will propagate some or all their
props to their child components. props to their child components.
Note that default values cannot be defined for a mandatory props. Doing so will
result in a prop validation error.
Examples: Examples:
```js ```js
@@ -190,7 +194,8 @@ class ComponentB extends owl.Component {
element: {type: Object, shape: {id: Boolean, text: String } element: {type: Object, shape: {id: Boolean, text: String }
}, },
date: Date, date: Date,
combinedVal: [Number, Boolean] combinedVal: [Number, Boolean],
optionalProp: { type: Number, optional: true }
}; };
... ...
+13 -2
View File
@@ -47,6 +47,7 @@ export function validateProps<P>(name: string | ComponentConstructor<P>, props:
} }
applyDefaultProps(props, ComponentClass); applyDefaultProps(props, ComponentClass);
const defaultProps = ComponentClass.defaultProps || {};
let propsDef = getPropDescription(ComponentClass.props); let propsDef = getPropDescription(ComponentClass.props);
const allowAdditionalProps = "*" in propsDef; const allowAdditionalProps = "*" in propsDef;
@@ -54,8 +55,18 @@ export function validateProps<P>(name: string | ComponentConstructor<P>, props:
if (propName === "*") { if (propName === "*") {
continue; continue;
} }
const propDef = propsDef[propName];
let isMandatory = !!propDef;
if (typeof propDef === "object" && "optional" in propDef) {
isMandatory = !propDef.optional;
}
if (isMandatory && propName in defaultProps) {
throw new Error(
`A default value cannot be defined for a mandatory prop (name: '${propName}', component: ${ComponentClass.name}`
);
}
if ((props as any)[propName] === undefined) { if ((props as any)[propName] === undefined) {
if (propsDef[propName] && !propsDef[propName].optional) { if (isMandatory) {
throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`); throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
} else { } else {
continue; continue;
@@ -63,7 +74,7 @@ export function validateProps<P>(name: string | ComponentConstructor<P>, props:
} }
let isValid; let isValid;
try { try {
isValid = isValidProp((props as any)[propName], propsDef[propName]); isValid = isValidProp((props as any)[propName], propDef);
} catch (e) { } catch (e) {
(e as Error).message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${ (e as Error).message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${
(e as Error).message (e as Error).message
@@ -1,6 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`default props can set default required boolean values 1`] = ` exports[`default props a default prop cannot be defined on a mandatory prop 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const props1 = {};
helpers.validateProps(\`Child\`, props1, ctx);
return component(\`Child\`, props1, key + \`__1\`, node, ctx);
}
}"
`;
exports[`default props can set default boolean values 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
@@ -16,7 +29,7 @@ exports[`default props can set default required boolean values 1`] = `
}" }"
`; `;
exports[`default props can set default required boolean values 2`] = ` exports[`default props can set default boolean values 2`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
+27 -3
View File
@@ -684,7 +684,7 @@ describe("props validation", () => {
test("default values are applied before validating props at update", async () => { test("default values are applied before validating props at update", async () => {
// need to do something about errors catched in render // need to do something about errors catched in render
class SubComp extends Component { class SubComp extends Component {
static props = { p: { type: Number } }; static props = { p: { type: Number, optional: true } };
static template = xml`<div><t t-esc="props.p"/></div>`; static template = xml`<div><t t-esc="props.p"/></div>`;
static defaultProps = { p: 4 }; static defaultProps = { p: 4 };
} }
@@ -791,9 +791,9 @@ describe("default props", () => {
expect(fixture.innerHTML).toBe("<div><div>4</div></div>"); expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
}); });
test("can set default required boolean values", async () => { test("can set default boolean values", async () => {
class SubComp extends Component { class SubComp extends Component {
static props = ["p", "q"]; static props = ["p?", "q?"];
static defaultProps = { p: true, q: false }; static defaultProps = { p: true, q: false };
static template = xml`<span><t t-if="props.p">hey</t><t t-if="!props.q">hey</t></span>`; static template = xml`<span><t t-if="props.p">hey</t><t t-if="!props.q">hey</t></span>`;
} }
@@ -804,4 +804,28 @@ describe("default props", () => {
await mount(Parent, fixture, { dev: true }); await mount(Parent, fixture, { dev: true });
expect(fixture.innerHTML).toBe("<div><span>heyhey</span></div>"); expect(fixture.innerHTML).toBe("<div><span>heyhey</span></div>");
}); });
test("a default prop cannot be defined on a mandatory prop", async () => {
class Child extends Component {
static props = {
mandatory: Number,
};
static defaultProps = { mandatory: 3 };
static template = xml` <div><t t-esc="props.mandatory"/></div>`;
}
class Parent extends Component {
static components = { Child };
static template = xml`<Child/>`;
}
let error: Error;
try {
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
expect(error!).toBeDefined();
expect(error!.message).toBe(
"A default value cannot be defined for a mandatory prop (name: 'mandatory', component: Child"
);
});
}); });