[IMP] qweb: introduce t-model directive

supported modifiers: lazy, trim, number
This commit is contained in:
Bruno Boi
2021-11-02 11:29:23 +01:00
committed by Aaron Bohy
parent 348b505e5f
commit 219923d752
49 changed files with 1627 additions and 852 deletions
+3 -3
View File
@@ -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";
}
+2 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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,
};
}
+6
View File
@@ -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]) {