[IMP] component: defaultProps application

We re-add the application of defaultProps. Note that the application
is done twice in dev mode.
This commit is contained in:
Mathieu Duckerts-Antoine
2021-10-22 11:56:56 +02:00
committed by Géry Debongnie
parent 6e0834ce5e
commit c3695ec8db
4 changed files with 131 additions and 44 deletions
+3
View File
@@ -9,6 +9,7 @@ import {
RootFiber, RootFiber,
__internal__destroyed, __internal__destroyed,
} from "./fibers"; } from "./fibers";
import { applyDefaultProps } from "./props_validation";
import { STATUS } from "./status"; import { STATUS } from "./status";
export function component( export function component(
@@ -85,6 +86,7 @@ export class ComponentNode<T extends typeof Component = any> implements VNode<Co
constructor(C: T, props: any, app: App) { constructor(C: T, props: any, app: App) {
currentNode = this; currentNode = this;
this.app = app; this.app = app;
applyDefaultProps(props, C);
this.component = new C(props, app.env, this) as any; this.component = new C(props, app.env, this) as any;
this.renderFn = app.getTemplate(C.template).bind(null, this.component, this); this.renderFn = app.getTemplate(C.template).bind(null, this.component, this);
this.component.setup(); this.component.setup();
@@ -185,6 +187,7 @@ export class ComponentNode<T extends typeof Component = any> implements VNode<Co
parentFiber.root.patched.push(fiber); parentFiber.root.patched.push(fiber);
} }
const component = this.component; const component = this.component;
applyDefaultProps(props, component.constructor as any);
const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props))); const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
await prom; await prom;
if (fiber !== this.fiber) { if (fiber !== this.fiber) {
+19
View File
@@ -1,5 +1,22 @@
import { Component } from "./component"; import { Component } from "./component";
/**
* Apply default props (only top level).
*
* Note that this method does modify in place the props
*/
export function applyDefaultProps(props: { [key: string]: any }, ComponentClass: typeof Component) {
const defaultProps = (ComponentClass as any).defaultProps
if (defaultProps) {
for (let propName in defaultProps) {
if (props![propName] === undefined) {
props![propName] = defaultProps[propName];
}
}
}
}
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Prop validation helper // Prop validation helper
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -17,6 +34,8 @@ export const validateProps = function (name: string | typeof Component, props: a
const ComponentClass = const ComponentClass =
typeof name !== "string" ? name : parent.constructor.components[name as string]; typeof name !== "string" ? name : parent.constructor.components[name as string];
applyDefaultProps(props, ComponentClass);
const propsDef = (<any>ComponentClass).props; const propsDef = (<any>ComponentClass).props;
if (propsDef instanceof Array) { if (propsDef instanceof Array) {
// list of strings (prop names) // list of strings (prop names)
@@ -1,5 +1,56 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`default props can set default required boolean values 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props2 = {}
helpers.validateProps(\`SubComp\`, props2, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
`;
exports[`default props can set default values 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props2 = {}
helpers.validateProps(\`SubComp\`, props2, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
`;
exports[`default props default values are also set whenever component is updated 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['state'].p}
helpers.validateProps(\`SubComp\`, props2, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
`;
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
) { ) {
@@ -204,6 +255,23 @@ exports[`props validation can validate recursively complicated prop def 2`] = `
}" }"
`; `;
exports[`props validation default values are applied before validating props at update 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
const props2 = {p: ctx['state'].p}
helpers.validateProps(\`SubComp\`, props2, ctx)
let b2 = component(\`SubComp\`, props2, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
`;
exports[`props validation props are validated in dev mode (code snapshot) 1`] = ` exports[`props validation props are validated in dev mode (code snapshot) 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
+41 -44
View File
@@ -1,5 +1,5 @@
import { makeTestFixture, nextTick, snapshotApp } from "../helpers"; import { makeTestFixture, snapshotApp } from "../helpers";
import { Component, useState, xml } from "../../src"; import { Component, xml } from "../../src";
import { App, DEV_MSG } from "../../src/app"; import { App, DEV_MSG } from "../../src/app";
import { validateProps } from "../../src/component/props_validation"; import { validateProps } from "../../src/component/props_validation";
@@ -680,7 +680,7 @@ describe("props validation", () => {
expect(error.message).toBe("Missing props 'p' (component 'SubComp')"); expect(error.message).toBe("Missing props 'p' (component 'SubComp')");
}); });
test.skip("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 } };
@@ -690,13 +690,13 @@ describe("props validation", () => {
class Parent extends Component { class Parent extends Component {
static template = xml`<div><SubComp p="state.p"/></div>`; static template = xml`<div><SubComp p="state.p"/></div>`;
static components = { SubComp }; static components = { SubComp };
state: any = useState({ p: 1 }); state: any = { p: 1 };
} }
const app = await mountApp(Parent); const app = await mountApp(Parent);
expect(fixture.innerHTML).toBe("<div><div>1</div></div>"); expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
(app as any).root.component.state.p = undefined; (app as any).root.component.state.p = undefined;
await nextTick(); await (app as any).root.component.render();
expect(fixture.innerHTML).toBe("<div><div>4</div></div>"); expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
}); });
@@ -727,51 +727,48 @@ describe("props validation", () => {
// Default props // Default props
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
describe.skip("default props", () => { describe("default props", () => {
test("can set default values", async () => { test("can set default values", async () => {
// class SubComp extends Component { class SubComp extends Component {
// static defaultProps = { p: 4 }; static defaultProps = { p: 4 };
// static template = xml`<div><t t-esc="props.p"/></div>`; static template = xml`<div><t t-esc="props.p"/></div>`;
// } }
// class Parent extends Component { class Parent extends Component {
// static template = xml`<div><SubComp /></div>`; static template = xml`<div><SubComp /></div>`;
// static components = { SubComp }; static components = { SubComp };
// } }
// const w = new Parent(); await mountApp(Parent);
// await w.mount(fixture); expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
// expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
}); });
test("default values are also set whenever component is updated", async () => { test("default values are also set whenever component is updated", async () => {
// class SubComp extends Component { class SubComp extends Component {
// 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 };
// } }
// class Parent extends Component { class Parent extends Component {
// static template = xml`<div><SubComp p="state.p"/></div>`; static template = xml`<div><SubComp p="state.p"/></div>`;
// static components = { SubComp }; static components = { SubComp };
// state: any = useState({ p: 1 }); state: any = { p: 1 };
// } }
// const w = new Parent(); const app = await mountApp(Parent);
// await w.mount(fixture); expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
// expect(fixture.innerHTML).toBe("<div><div>1</div></div>"); (app as any).root.component.state.p = undefined;
// w.state.p = undefined; await (app as any).root.component.render();
// await nextTick(); 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 required 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>`;
// } }
// class App extends Component { class Parent extends Component {
// static template = xml`<div><SubComp/></div>`; static template = xml`<div><SubComp/></div>`;
// static components = { SubComp }; static components = { SubComp };
// } }
// const w = new App(undefined, {}); await mountApp(Parent);
// await w.mount(fixture); expect(fixture.innerHTML).toBe("<div><span>heyhey</span></div>");
// expect(fixture.innerHTML).toBe("<div><span>heyhey</span></div>");
}); });
}); });