mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] component,qweb: props validation
We re-add the possibility to validate props when dev mode is active. No change in the API right Now. The dev mode is activated via the configure method of App class.
This commit is contained in:
committed by
Aaron Bohy
parent
15e4c856da
commit
c16d8ed6de
+24
-8
@@ -5,13 +5,24 @@ import { TemplateSet } from "./qweb/template_helpers";
|
||||
|
||||
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
|
||||
|
||||
interface Config {
|
||||
dev?: boolean;
|
||||
env?: { [key: string]: any };
|
||||
translatableAttributes?: string[];
|
||||
translateFn?: (s: string) => string;
|
||||
}
|
||||
|
||||
export const DEV_MSG = `Owl is running in 'dev' mode.
|
||||
|
||||
This is not suitable for production use.
|
||||
See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for more information.`;
|
||||
|
||||
export class App<T extends typeof Component = any> extends TemplateSet {
|
||||
Root: T;
|
||||
props: any;
|
||||
env: any = {};
|
||||
scheduler = new Scheduler(window.requestAnimationFrame.bind(window));
|
||||
root: ComponentNode | null = null;
|
||||
dev: boolean = true;
|
||||
|
||||
constructor(Root: T, props?: any) {
|
||||
super();
|
||||
@@ -19,15 +30,20 @@ export class App<T extends typeof Component = any> extends TemplateSet {
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
configure(params: any) {
|
||||
if (params.env) {
|
||||
this.env = params.env;
|
||||
configure(config: Config) {
|
||||
if (config.dev) {
|
||||
this.dev = config.dev;
|
||||
console.info(DEV_MSG);
|
||||
}
|
||||
if (params.translateFn) {
|
||||
this.translateFn = params.translateFn;
|
||||
|
||||
if (config.env) {
|
||||
this.env = config.env;
|
||||
}
|
||||
if (params.translatableAttributes) {
|
||||
this.translatableAttributes = params.translatableAttributes;
|
||||
if (config.translateFn) {
|
||||
this.translateFn = config.translateFn;
|
||||
}
|
||||
if (config.translatableAttributes) {
|
||||
this.translatableAttributes = config.translatableAttributes;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { Component } from "./component";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Prop validation helper
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Validate the component props (or next props) against the (static) props
|
||||
* description. This is potentially an expensive operation: it may needs to
|
||||
* visit recursively the props and all the children to check if they are valid.
|
||||
* This is why it is only done in 'dev' mode.
|
||||
*/
|
||||
|
||||
// message: { type: String, default: "hello" };
|
||||
|
||||
export const validateProps = function (name: string | typeof Component, props: any, parent?: any) {
|
||||
const ComponentClass =
|
||||
typeof name !== "string" ? name : parent.constructor.components[name as string];
|
||||
|
||||
const propsDef = (<any>ComponentClass).props;
|
||||
if (propsDef instanceof Array) {
|
||||
// list of strings (prop names)
|
||||
for (let i = 0, l = propsDef.length; i < l; i++) {
|
||||
const propName = propsDef[i];
|
||||
if (propName[propName.length - 1] === "?") {
|
||||
// optional prop
|
||||
break;
|
||||
}
|
||||
if (!(propName in props)) {
|
||||
throw new Error(`Missing props '${propsDef[i]}' (component '${ComponentClass.name}')`);
|
||||
}
|
||||
}
|
||||
for (let key in props) {
|
||||
if (!propsDef.includes(key) && !propsDef.includes(key + "?")) {
|
||||
throw new Error(`Unknown prop '${key}' given to component '${ComponentClass.name}'`);
|
||||
}
|
||||
}
|
||||
} 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.message = `Invalid prop '${propName}' in component ${ComponentClass.name} (${e.message})`;
|
||||
throw e;
|
||||
}
|
||||
if (!isValid) {
|
||||
throw new Error(`Invalid Prop '${propName}' in component '${ComponentClass.name}'`);
|
||||
}
|
||||
}
|
||||
for (let propName in props) {
|
||||
if (!(propName in propsDef)) {
|
||||
throw new Error(`Unknown prop '${propName}' given to component '${ComponentClass.name}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if an invidual prop value matches its (static) prop definition
|
||||
*/
|
||||
function isValidProp(prop: any, propDef: any): boolean {
|
||||
if (propDef === true) {
|
||||
return true;
|
||||
}
|
||||
if (typeof propDef === "function") {
|
||||
// Check if a value is constructed by some Constructor. Note that there is a
|
||||
// slight abuse of language: we want to consider primitive values as well.
|
||||
//
|
||||
// So, even though 1 is not an instance of Number, we want to consider that
|
||||
// it is valid.
|
||||
if (typeof prop === "object") {
|
||||
return prop instanceof propDef;
|
||||
}
|
||||
return typeof prop === propDef.name.toLowerCase();
|
||||
} else if (propDef instanceof Array) {
|
||||
// If this code is executed, this means that we want to check if a prop
|
||||
// matches at least one of its descriptor.
|
||||
let result = false;
|
||||
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
|
||||
result = result || isValidProp(prop, propDef[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// propsDef is an object
|
||||
if (propDef.optional && prop === undefined) {
|
||||
return true;
|
||||
}
|
||||
let result = propDef.type ? isValidProp(prop, propDef.type) : true;
|
||||
if (propDef.validate) {
|
||||
result = result && propDef.validate(prop);
|
||||
}
|
||||
if (propDef.type === Array && propDef.element) {
|
||||
for (let i = 0, iLen = prop.length; i < iLen; i++) {
|
||||
result = result && isValidProp(prop[i], propDef.element);
|
||||
}
|
||||
}
|
||||
if (propDef.type === Object && propDef.shape) {
|
||||
const shape = propDef.shape;
|
||||
for (let key in shape) {
|
||||
result = result && isValidProp(prop[key], shape[key]);
|
||||
}
|
||||
if (result) {
|
||||
for (let propName in prop) {
|
||||
if (!(propName in shape)) {
|
||||
throw new Error(`unknown prop '${propName}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -32,6 +32,7 @@ export interface CompileOptions {
|
||||
name?: string;
|
||||
translateFn?: (s: string) => string;
|
||||
translatableAttributes?: string[];
|
||||
dev?: boolean;
|
||||
}
|
||||
|
||||
export function compileTemplate(template: string, options?: CompileOptions): TemplateFunction {
|
||||
@@ -177,6 +178,7 @@ export class QWebCompiler {
|
||||
target = new CodeTarget("main");
|
||||
templateName: string;
|
||||
template: string;
|
||||
dev: boolean;
|
||||
translateFn: (s: string) => string;
|
||||
translatableAttributes: string[];
|
||||
ast: AST;
|
||||
@@ -186,6 +188,7 @@ export class QWebCompiler {
|
||||
this.template = template;
|
||||
this.translateFn = options.translateFn || ((s: string) => s);
|
||||
this.translatableAttributes = options.translatableAttributes || TRANSLATABLE_ATTRS;
|
||||
this.dev = options.dev || false;
|
||||
this.ast = parse(template);
|
||||
if (options.name) {
|
||||
this.templateName = options.name;
|
||||
@@ -925,6 +928,14 @@ export class QWebCompiler {
|
||||
} else {
|
||||
expr = `\`${ast.name}\``;
|
||||
}
|
||||
|
||||
if (this.dev) {
|
||||
const propVar = this.generateId("props");
|
||||
this.addLine(`const ${propVar} = ${propString}`);
|
||||
this.addLine(`helpers.validateProps(${expr}, ${propVar}, ctx)`);
|
||||
propString = propVar;
|
||||
}
|
||||
|
||||
let blockArgs = `${expr}, ${propString}, key + \`${key}\`, node, ctx`;
|
||||
|
||||
// slots
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BDom, createBlock, html, list, multi, text, toggler } from "../blockdom";
|
||||
import { compileTemplate, Template } from "./compiler";
|
||||
import { component } from "../component/component_node";
|
||||
import { validateProps } from "../component/props_validation";
|
||||
|
||||
const bdom = { text, createBlock, list, multi, html, toggler, component };
|
||||
|
||||
@@ -94,6 +95,7 @@ export const UTILS = {
|
||||
prepareList,
|
||||
setContextValue,
|
||||
shallowEqual,
|
||||
validateProps,
|
||||
};
|
||||
|
||||
export class TemplateSet {
|
||||
@@ -102,6 +104,7 @@ export class TemplateSet {
|
||||
translateFn?: (s: string) => string;
|
||||
translatableAttributes?: string[];
|
||||
utils: typeof UTILS;
|
||||
dev?: boolean;
|
||||
|
||||
constructor() {
|
||||
const call = (subTemplate: string, ctx: any, parent: any) => {
|
||||
@@ -128,6 +131,7 @@ export class TemplateSet {
|
||||
}
|
||||
const templateFn = compileTemplate(rawTemplate, {
|
||||
name,
|
||||
dev: this.dev,
|
||||
translateFn: this.translateFn,
|
||||
translatableAttributes: this.translatableAttributes,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user