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
Samuel Degueldre
parent
0f8c859d5f
commit
107200fd94
@@ -19,6 +19,14 @@ export function applyDefaultProps(props: { [key: string]: any }, ComponentClass:
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Prop validation helper
|
// 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
|
* 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);
|
applyDefaultProps(props, ComponentClass);
|
||||||
|
|
||||||
const propsDef = ComponentClass.props;
|
let propsDef = getPropDescription(ComponentClass.props);
|
||||||
if (propsDef instanceof Array) {
|
const allowAdditionalProps = "*" in propsDef;
|
||||||
// list of strings (prop names)
|
|
||||||
for (const propName of propsDef) {
|
for (let propName in propsDef) {
|
||||||
if (propName[propName.length - 1] === "?") {
|
if (propName === "*") {
|
||||||
// optional prop
|
continue;
|
||||||
break;
|
}
|
||||||
}
|
if (props[propName] === undefined) {
|
||||||
if (!(propName in props)) {
|
if (propsDef[propName] && !propsDef[propName].optional) {
|
||||||
throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
|
throw new Error(`Missing props '${propName}' (component '${ComponentClass.name}')`);
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let key in props) {
|
let isValid;
|
||||||
if (!propsDef.includes(key) && !propsDef.includes(key + "?")) {
|
try {
|
||||||
throw new Error(`Unknown prop '${key}' given to component '${ComponentClass.name}'`);
|
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) {
|
if (!isValid) {
|
||||||
// propsDef is an object now
|
throw new Error(`Invalid Prop '${propName}' in component '${ComponentClass.name}'`);
|
||||||
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 (!allowAdditionalProps) {
|
||||||
for (let propName in props) {
|
for (let propName in props) {
|
||||||
if (!(propName in propsDef)) {
|
if (!(propName in propsDef)) {
|
||||||
throw new Error(`Unknown prop '${propName}' given to component '${ComponentClass.name}'`);
|
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`] = `
|
exports[`props validation can validate a prop with multiple types 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -723,6 +723,36 @@ describe("props validation", () => {
|
|||||||
expect(error!).toBeDefined();
|
expect(error!).toBeDefined();
|
||||||
expect(error!.message).toBe("Missing props 'mandatory' (component 'Child')");
|
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.
|
// "compileOnSave": false, // Signals to the IDE to generate all files for a given tsconfig.json upon saving.
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Main options
|
// 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'.
|
"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.
|
// "lib": ["esnext", "dom"], // Specify library files to be included in the compilation.
|
||||||
// "allowJs": false, // Allow javascript files to be compiled.
|
// "allowJs": false, // Allow javascript files to be compiled.
|
||||||
|
|||||||
Reference in New Issue
Block a user