mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] components: improve props validation
to be able to specify that additional props are allowed
This commit is contained in:
committed by
Aaron Bohy
parent
cc1eea0945
commit
7e40fa300a
@@ -19,6 +19,14 @@ export function applyDefaultProps(props: { [key: string]: any }, ComponentClass:
|
||||
//------------------------------------------------------------------------------
|
||||
// Prop validation helper
|
||||
//------------------------------------------------------------------------------
|
||||
function getPropDescription(staticProps: any) {
|
||||
if (staticProps instanceof Array) {
|
||||
return Object.fromEntries(
|
||||
staticProps.map((p) => (p.endsWith("?") ? [p.slice(0, -1), false] : [p, true]))
|
||||
);
|
||||
}
|
||||
return staticProps || { "*": true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the component props (or next props) against the (static) props
|
||||
@@ -33,46 +41,34 @@ export const validateProps = function (name: string | typeof Component, props: a
|
||||
|
||||
applyDefaultProps(props, ComponentClass);
|
||||
|
||||
const propsDef = ComponentClass.props;
|
||||
if (propsDef instanceof Array) {
|
||||
// list of strings (prop names)
|
||||
for (const propName of propsDef) {
|
||||
if (propName[propName.length - 1] === "?") {
|
||||
// optional prop
|
||||
break;
|
||||
}
|
||||
if (!(propName in props)) {
|
||||
let propsDef = getPropDescription(ComponentClass.props);
|
||||
const allowAdditionalProps = "*" in propsDef;
|
||||
|
||||
for (let propName in propsDef) {
|
||||
if (propName === "*") {
|
||||
continue;
|
||||
}
|
||||
if (props[propName] === undefined) {
|
||||
if (propsDef[propName] && !propsDef[propName].optional) {
|
||||
throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
for (let key in props) {
|
||||
if (!propsDef.includes(key) && !propsDef.includes(key + "?")) {
|
||||
throw new Error(`Unknown prop '${key}' given to component '${ComponentClass.name}'`);
|
||||
}
|
||||
let isValid;
|
||||
try {
|
||||
isValid = isValidProp(props[propName], propsDef[propName]);
|
||||
} catch (e) {
|
||||
(e as Error).message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${
|
||||
(e as Error).message
|
||||
})`;
|
||||
throw e;
|
||||
}
|
||||
} else if (propsDef) {
|
||||
// propsDef is an object now
|
||||
for (let propName in propsDef) {
|
||||
if (props[propName] === undefined) {
|
||||
if (propsDef[propName] && !propsDef[propName].optional) {
|
||||
throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let isValid;
|
||||
try {
|
||||
isValid = isValidProp(props[propName], propsDef[propName]);
|
||||
} catch (e) {
|
||||
(e as Error).message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${
|
||||
(e as Error).message
|
||||
})`;
|
||||
throw e;
|
||||
}
|
||||
if (!isValid) {
|
||||
throw new Error(`Invalid Prop '${propName}' in component '${ComponentClass.name}'`);
|
||||
}
|
||||
if (!isValid) {
|
||||
throw new Error(`Invalid Prop '${propName}' in component '${ComponentClass.name}'`);
|
||||
}
|
||||
}
|
||||
if (!allowAdditionalProps) {
|
||||
for (let propName in props) {
|
||||
if (!(propName in propsDef)) {
|
||||
throw new Error(`Unknown prop '${propName}' given to component '${ComponentClass.name}'`);
|
||||
|
||||
@@ -96,6 +96,58 @@ exports[`default props default values are also set whenever component is updated
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`props validation can specify that additional props are allowed (array) 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const props1 = {message: 'm',otherProp: 'o'}
|
||||
helpers.validateProps(\`Child\`, props1, ctx)
|
||||
return component(\`Child\`, props1, key + \`__2\`, node, ctx);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`props validation can specify that additional props are allowed (array) 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>hey</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`props validation can specify that additional props are allowed (object) 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const props1 = {message: 'm',otherProp: 'o'}
|
||||
helpers.validateProps(\`Child\`, props1, ctx)
|
||||
return component(\`Child\`, props1, key + \`__2\`, node, ctx);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`props validation can specify that additional props are allowed (object) 2`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div>hey</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`props validation can validate a prop with multiple types 1`] = `
|
||||
"function anonymous(bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -723,6 +723,36 @@ describe("props validation", () => {
|
||||
expect(error!).toBeDefined();
|
||||
expect(error!.message).toBe("Missing props 'mandatory' (component 'Child')");
|
||||
});
|
||||
|
||||
test("can specify that additional props are allowed (array)", async () => {
|
||||
class Child extends Component {
|
||||
static props = ["message", "*"];
|
||||
static template = xml`<div>hey</div>`;
|
||||
}
|
||||
class Parent extends Component {
|
||||
static template = xml`<Child message="'m'" otherProp="'o'"/>`;
|
||||
static components = { Child };
|
||||
}
|
||||
|
||||
await expect(mount(Parent, fixture, { dev: true })).resolves.toEqual(expect.anything());
|
||||
});
|
||||
|
||||
test("can specify that additional props are allowed (object)", async () => {
|
||||
class Child extends Component {
|
||||
static props = {
|
||||
message: { type: String },
|
||||
"*": true,
|
||||
};
|
||||
static template = xml`<div>hey</div>`;
|
||||
}
|
||||
class Parent extends Component {
|
||||
static template = xml`<Child message="'m'" otherProp="'o'"/>`;
|
||||
static components = { Child };
|
||||
}
|
||||
|
||||
// we just check that it doesn't throw
|
||||
await expect(mount(Parent, fixture, { dev: true })).resolves.toEqual(expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
// "compileOnSave": false, // Signals to the IDE to generate all files for a given tsconfig.json upon saving.
|
||||
"compilerOptions": {
|
||||
// Main options
|
||||
"target": "es2017", // Specify ECMAScript target version: 'es3' (default), 'es5', 'es2015', 'es2016', 'es2017','es2018' or 'esnext'.
|
||||
"target": "es2019", // Specify ECMAScript target version: 'es3' (default), 'es5', 'es2015', 'es2016', 'es2017','es2018' or 'esnext'.
|
||||
"module": "es6", // Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.
|
||||
// "lib": ["esnext", "dom"], // Specify library files to be included in the compilation.
|
||||
// "allowJs": false, // Allow javascript files to be compiled.
|
||||
|
||||
Reference in New Issue
Block a user