mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] qweb: introduce t-model directive
supported modifiers: lazy, trim, number
This commit is contained in:
@@ -144,10 +144,10 @@ export function isProp(tag: string, key: string): boolean {
|
||||
case "option":
|
||||
return key === "selected" || key === "disabled";
|
||||
case "textarea":
|
||||
return key === "readonly" || key === "disabled";
|
||||
break;
|
||||
case "button":
|
||||
return key === "value" || key === "readonly" || key === "disabled";
|
||||
case "select":
|
||||
return key === "value" || key === "disabled";
|
||||
case "button":
|
||||
case "optgroup":
|
||||
return key === "disabled";
|
||||
}
|
||||
|
||||
@@ -18,8 +18,9 @@ export function createEventHandler(rawEvent: string): EventHandlerCreator {
|
||||
}
|
||||
|
||||
// Native listener
|
||||
let nextNativeEventId = 1;
|
||||
function createElementHandler(evName: string, capture: boolean = false): EventHandlerCreator {
|
||||
let eventKey = `__event__${evName}`;
|
||||
let eventKey = `__event__${evName}_${nextNativeEventId++}`;
|
||||
if (capture) {
|
||||
eventKey = `${eventKey}_capture`;
|
||||
}
|
||||
|
||||
+36
-1
@@ -277,7 +277,7 @@ export class QWebCompiler {
|
||||
// define blocks and utility functions
|
||||
this.addLine(`let { text, createBlock, list, multi, html, toggler, component } = bdom;`);
|
||||
this.addLine(
|
||||
`let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;`
|
||||
`let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers;`
|
||||
);
|
||||
if (this.shouldDefineAssign) {
|
||||
this.addLine(`let assign = Object.assign;`);
|
||||
@@ -554,6 +554,41 @@ export class QWebCompiler {
|
||||
}
|
||||
}
|
||||
|
||||
// t-model
|
||||
if (ast.model) {
|
||||
const {
|
||||
baseExpr,
|
||||
expr,
|
||||
eventType,
|
||||
shouldNumberize,
|
||||
shouldTrim,
|
||||
targetAttr,
|
||||
specialInitTargetAttr,
|
||||
} = ast.model;
|
||||
|
||||
const baseExpression = compileExpr(baseExpr);
|
||||
const id = this.generateId();
|
||||
this.addLine(`const bExpr${id} = ${baseExpression};`);
|
||||
|
||||
const expression = compileExpr(expr);
|
||||
|
||||
let idx: number;
|
||||
if (specialInitTargetAttr) {
|
||||
idx = block!.insertData(`${baseExpression}[${expression}] === '${attrs[targetAttr]}'`);
|
||||
attrs[`block-attribute-${idx}`] = specialInitTargetAttr;
|
||||
} else {
|
||||
idx = block!.insertData(`${baseExpression}[${expression}]`);
|
||||
attrs[`block-attribute-${idx}`] = targetAttr;
|
||||
}
|
||||
let valueCode = `ev.target.${targetAttr}`;
|
||||
valueCode = shouldTrim ? `${valueCode}.trim()` : valueCode;
|
||||
valueCode = shouldNumberize ? `toNumber(${valueCode})` : valueCode;
|
||||
|
||||
const handler = `[(ev) => { bExpr${id}[${expression}] = ${valueCode}; }]`;
|
||||
idx = block!.insertData(handler);
|
||||
attrs[`block-handler-${idx}`] = eventType;
|
||||
}
|
||||
|
||||
const dom = xmlDoc.createElement(ast.tag);
|
||||
for (const [attr, val] of Object.entries(attrs)) {
|
||||
if (!(attr === "class" && val === "")) {
|
||||
|
||||
+60
-4
@@ -39,6 +39,15 @@ export interface ASTDomNode {
|
||||
content: AST[];
|
||||
ref: string | null;
|
||||
on: { [key: string]: string };
|
||||
model: {
|
||||
baseExpr: string;
|
||||
expr: string;
|
||||
targetAttr: string;
|
||||
specialInitTargetAttr: string | null;
|
||||
eventType: "change" | "click" | "input";
|
||||
shouldTrim: boolean;
|
||||
shouldNumberize: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface ASTMulti {
|
||||
@@ -275,13 +284,16 @@ function parseTDebugLog(node: Element, ctx: ParsingContext): AST | null {
|
||||
// -----------------------------------------------------------------------------
|
||||
// Regular dom node
|
||||
// -----------------------------------------------------------------------------
|
||||
const hasDotAtTheEnd = /\.[\w_]+\s*$/;
|
||||
const hasBracketsAtTheEnd = /\[[^\[]+\]\s*$/;
|
||||
|
||||
function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (node.tagName === "t") {
|
||||
const { tagName } = node;
|
||||
if (tagName === "t") {
|
||||
return null;
|
||||
}
|
||||
const children: AST[] = [];
|
||||
if (node.tagName === "pre") {
|
||||
if (tagName === "pre") {
|
||||
ctx = { inPreTag: true };
|
||||
}
|
||||
let ref = null;
|
||||
@@ -297,16 +309,57 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
}
|
||||
}
|
||||
|
||||
const nodeAttrsNames = node.getAttributeNames();
|
||||
const attrs: ASTDomNode["attrs"] = {};
|
||||
const on: ASTDomNode["on"] = {};
|
||||
let model: ASTDomNode["model"] = null;
|
||||
|
||||
for (let attr of node.getAttributeNames()) {
|
||||
for (let attr of nodeAttrsNames) {
|
||||
const value = node.getAttribute(attr)!;
|
||||
if (attr.startsWith("t-on")) {
|
||||
if (attr === "t-on") {
|
||||
throw new Error("Missing event name with t-on directive");
|
||||
}
|
||||
on[attr.slice(5)] = value;
|
||||
} else if (attr.startsWith("t-model")) {
|
||||
if (!["input", "select", "textarea"].includes(tagName)) {
|
||||
throw new Error("The t-model directive only works with <input>, <textarea> and <select>");
|
||||
}
|
||||
|
||||
let baseExpr, expr;
|
||||
if (hasDotAtTheEnd.test(value)) {
|
||||
const index = value.lastIndexOf(".");
|
||||
baseExpr = value.slice(0, index);
|
||||
expr = `'${value.slice(index + 1)}'`;
|
||||
} else if (hasBracketsAtTheEnd.test(value)) {
|
||||
const index = value.lastIndexOf("[");
|
||||
baseExpr = value.slice(0, index);
|
||||
expr = value.slice(index + 1, -1);
|
||||
} else {
|
||||
throw new Error(`Invalid t-model expression: "${value}" (it should be assignable)`);
|
||||
}
|
||||
|
||||
const typeAttr = node.getAttribute("type");
|
||||
const isInput = tagName === "input";
|
||||
const isSelect = tagName === "select";
|
||||
const isTextarea = tagName === "textarea";
|
||||
const isCheckboxInput = isInput && typeAttr === "checkbox";
|
||||
const isRadioInput = isInput && typeAttr === "radio";
|
||||
const isOtherInput = isInput && !isCheckboxInput && !isRadioInput;
|
||||
const hasLazyMod = attr.includes(".lazy");
|
||||
const hasNumberMod = attr.includes(".number");
|
||||
const hasTrimMod = attr.includes(".trim");
|
||||
const eventType = isRadioInput ? "click" : isSelect || hasLazyMod ? "change" : "input";
|
||||
|
||||
model = {
|
||||
baseExpr,
|
||||
expr,
|
||||
targetAttr: isCheckboxInput ? "checked" : "value",
|
||||
specialInitTargetAttr: isRadioInput ? "checked" : null,
|
||||
eventType,
|
||||
shouldTrim: hasTrimMod && (isOtherInput || isTextarea),
|
||||
shouldNumberize: hasNumberMod && (isOtherInput || isTextarea),
|
||||
};
|
||||
} else {
|
||||
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
|
||||
throw new Error(`Unknown QWeb directive: '${attr}'`);
|
||||
@@ -319,11 +372,12 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
}
|
||||
return {
|
||||
type: ASTType.DomNode,
|
||||
tag: node.tagName,
|
||||
tag: tagName,
|
||||
attrs,
|
||||
on,
|
||||
ref,
|
||||
content: children,
|
||||
model,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -356,6 +410,7 @@ function parseTEscNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
on: ast.on,
|
||||
ref,
|
||||
content: [tesc],
|
||||
model: ast.model,
|
||||
};
|
||||
}
|
||||
if (ast && ast.type === ASTType.TComponent) {
|
||||
@@ -394,6 +449,7 @@ function parseTRawNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
on: ast.on,
|
||||
ref,
|
||||
content: [tRaw],
|
||||
model: ast.model,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ export const UTILS = {
|
||||
prepareList,
|
||||
setContextValue,
|
||||
shallowEqual,
|
||||
toNumber,
|
||||
validateProps,
|
||||
};
|
||||
|
||||
@@ -146,6 +147,11 @@ export class TemplateSet {
|
||||
}
|
||||
}
|
||||
|
||||
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]) {
|
||||
|
||||
Reference in New Issue
Block a user