Files
owl/src/runtime/template_helpers.ts
T
Samuel Degueldre 610ed02373 [FIX] runtime: properly support t-foreach on strings
Previously, support for iterables was added to t-foreach. The idea was
that anything that you can spread or on which you can use for..of would
be supported. Due to an implementation mistakes, strings, which are
iterable were not supported because we checked that the typeof the
iterable was 'object'.

To fix this, we coerce the iterable to an object and check whether that
coerced value has a Symbol.iterator property, which is what happens
behind the scenes when using for..of or spreading a primitive.

Closes: odoo/owl#1503
2023-08-25 09:24:04 +02:00

256 lines
6.8 KiB
TypeScript

import { BDom, multi, text, toggler, createCatcher } from "./blockdom";
import { Markup } from "./utils";
import { html } from "./blockdom/index";
import { isOptional, validateSchema } from "./validation";
import type { ComponentConstructor } from "./component";
import { markRaw } from "./reactivity";
import { OwlError } from "../common/owl_error";
import type { ComponentNode } from "./component_node";
const ObjectCreate = Object.create;
/**
* This file contains utility functions that will be injected in each template,
* to perform various useful tasks in the compiled code.
*/
function withDefault(value: any, defaultValue: any): any {
return value === undefined || value === null || value === false ? defaultValue : value;
}
function callSlot(
ctx: any,
parent: any,
key: string,
name: string,
dynamic: boolean,
extra: any,
defaultContent?: (ctx: any, node: any, key: string) => BDom
): BDom {
key = key + "__slot_" + name;
const slots = ctx.props.slots || {};
const { __render, __ctx, __scope } = slots[name] || {};
const slotScope = ObjectCreate(__ctx || {});
if (__scope) {
slotScope[__scope] = extra;
}
const slotBDom = __render ? __render(slotScope, parent, key) : null;
if (defaultContent) {
let child1: BDom | undefined = undefined;
let child2: BDom | undefined = undefined;
if (slotBDom) {
child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
} else {
child2 = defaultContent(ctx, parent, key);
}
return multi([child1, child2]);
}
return slotBDom || text("");
}
function capture(ctx: any): any {
const result = ObjectCreate(ctx);
for (let k in ctx) {
result[k] = ctx[k];
}
return result;
}
function withKey(elem: any, k: string) {
elem.key = k;
return elem;
}
function prepareList(collection: unknown): [unknown[], unknown[], number, undefined[]] {
let keys: unknown[];
let values: unknown[];
if (Array.isArray(collection)) {
keys = collection;
values = collection;
} else if (collection instanceof Map) {
keys = [...collection.keys()];
values = [...collection.values()];
} else if (Symbol.iterator in Object(collection)) {
keys = [...(<Iterable<unknown>>collection)];
values = keys;
} else if (collection && typeof collection === "object") {
values = Object.values(collection);
keys = Object.keys(collection);
} else {
throw new OwlError(`Invalid loop expression: "${collection}" is not iterable`);
}
const n = values.length;
return [keys, values, n, new Array(n)];
}
const isBoundary = Symbol("isBoundary");
function setContextValue(ctx: { [key: string]: any }, key: string, value: any): void {
const ctx0 = ctx;
while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {
const newCtx = ctx.__proto__;
if (!newCtx) {
ctx = ctx0;
break;
}
ctx = newCtx;
}
ctx[key] = value;
}
function toNumber(val: string): number | string {
const n = parseFloat(val);
return isNaN(n) ? val : n;
}
function shallowEqual(l1: any[], l2: any[]): boolean {
for (let i = 0, l = l1.length; i < l; i++) {
if (l1[i] !== l2[i]) {
return false;
}
}
return true;
}
class LazyValue {
fn: any;
ctx: any;
component: any;
node: any;
key: any;
constructor(fn: any, ctx: any, component: any, node: any, key: any) {
this.fn = fn;
this.ctx = capture(ctx);
this.component = component;
this.node = node;
this.key = key;
}
evaluate(): any {
return this.fn.call(this.component, this.ctx, this.node, this.key);
}
toString() {
return this.evaluate().toString();
}
}
/*
* Safely outputs `value` as a block depending on the nature of `value`
*/
export function safeOutput(value: any, defaultValue?: any): ReturnType<typeof toggler> {
if (value === undefined || value === null) {
return defaultValue ? toggler("default", defaultValue) : toggler("undefined", text(""));
}
let safeKey;
let block;
switch (typeof value) {
case "object":
if (value instanceof Markup) {
safeKey = `string_safe`;
block = html(value as string);
} else if (value instanceof LazyValue) {
safeKey = `lazy_value`;
block = value.evaluate();
} else if (value instanceof String) {
safeKey = "string_unsafe";
block = text(value);
} else {
// Assuming it is a block
safeKey = "block_safe";
block = value;
}
break;
case "string":
safeKey = "string_unsafe";
block = text(value);
break;
default:
safeKey = "string_unsafe";
block = text(String(value));
}
return toggler(safeKey, block);
}
/**
* 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.
*/
export function validateProps<P>(name: string | ComponentConstructor<P>, props: P, comp?: any) {
const ComponentClass =
typeof name !== "string"
? name
: (comp.constructor.components[name] as ComponentConstructor<P> | undefined);
if (!ComponentClass) {
// this is an error, wrong component. We silently return here instead so the
// error is triggered by the usual path ('component' function)
return;
}
const schema = ComponentClass.props;
if (!schema) {
if (comp.__owl__.app.warnIfNoStaticProps) {
console.warn(`Component '${ComponentClass.name}' does not have a static props description`);
}
return;
}
const defaultProps = ComponentClass.defaultProps;
if (defaultProps) {
let isMandatory = (name: string) =>
Array.isArray(schema)
? schema.includes(name)
: name in schema && !("*" in schema) && !isOptional(schema[name]);
for (let p in defaultProps) {
if (isMandatory(p)) {
throw new OwlError(
`A default value cannot be defined for a mandatory prop (name: '${p}', component: ${ComponentClass.name})`
);
}
}
}
const errors = validateSchema(props, schema);
if (errors.length) {
throw new OwlError(
`Invalid props for component '${ComponentClass.name}': ` + errors.join(", ")
);
}
}
function makeRefWrapper(node: ComponentNode) {
let refNames: Set<String> = new Set();
return (name: string, fn: Function) => {
if (refNames.has(name)) {
throw new OwlError(
`Cannot set the same ref more than once in the same component, ref "${name}" was set multiple times in ${node.name}`
);
}
refNames.add(name);
return fn;
};
}
export const helpers = {
withDefault,
zero: Symbol("zero"),
isBoundary,
callSlot,
capture,
withKey,
prepareList,
setContextValue,
shallowEqual,
toNumber,
validateProps,
LazyValue,
safeOutput,
createCatcher,
markRaw,
OwlError,
makeRefWrapper,
};