diff --git a/src/component/props_validation.ts b/src/component/props_validation.ts index 5aebdd62..bae01d94 100644 --- a/src/component/props_validation.ts +++ b/src/component/props_validation.ts @@ -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}'`); diff --git a/tests/components/__snapshots__/props_validation.test.ts.snap b/tests/components/__snapshots__/props_validation.test.ts.snap index 82dfe1ec..75ef0ec8 100644 --- a/tests/components/__snapshots__/props_validation.test.ts.snap +++ b/tests/components/__snapshots__/props_validation.test.ts.snap @@ -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(\`
hey
\`); + + 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(\`
hey
\`); + + return function template(ctx, node, key = \\"\\") { + return block1(); + } +}" +`; + exports[`props validation can validate a prop with multiple types 1`] = ` "function anonymous(bdom, helpers ) { diff --git a/tests/components/props_validation.test.ts b/tests/components/props_validation.test.ts index b21a7ac8..40300138 100644 --- a/tests/components/props_validation.test.ts +++ b/tests/components/props_validation.test.ts @@ -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`
hey
`; + } + class Parent extends Component { + static template = xml``; + 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`
hey
`; + } + class Parent extends Component { + static template = xml``; + static components = { Child }; + } + + // we just check that it doesn't throw + await expect(mount(Parent, fixture, { dev: true })).resolves.toEqual(expect.anything()); + }); }); //------------------------------------------------------------------------------ diff --git a/tsconfig.json b/tsconfig.json index a7a29b1f..be2ede4a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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.