[IMP] components: improve props validation

to be able to specify that additional props are allowed
This commit is contained in:
Géry Debongnie
2021-12-20 15:23:06 +01:00
committed by Samuel Degueldre
parent 0f8c859d5f
commit 107200fd94
4 changed files with 114 additions and 36 deletions
+31 -35
View File
@@ -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
) {
+30
View File
@@ -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
View File
@@ -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.