[FIX] qweb/component: better handling of class attribute

This changes requires using the class module of vdom.

closes #192
This commit is contained in:
Géry Debongnie
2019-06-25 14:24:03 +02:00
parent dbfc7e4acd
commit 1b12cf9b91
7 changed files with 522 additions and 230 deletions
+13
View File
@@ -53,6 +53,7 @@ export interface Meta<T extends Env, Props> {
observer?: Observer;
render?: CompiledTemplate;
mountedHandlers: { [key: number]: Function };
classObj?: { [key: string]: boolean };
}
// If a component does not define explicitely a template
@@ -435,6 +436,12 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
const __owl__ = this.__owl__;
__owl__.renderPromise = null;
const target = __owl__.vnode || document.createElement(vnode.sel!);
if (this.__owl__.classObj) {
(<any>vnode).data.class = Object.assign(
(<any>vnode).data.class || {},
this.__owl__.classObj
);
}
__owl__.vnode = patch(target, vnode);
}
@@ -528,6 +535,12 @@ export class Component<T extends Env, Props extends {}, State extends {}> {
*/
__mount(vnode: VNode, elm: HTMLElement): VNode {
const __owl__ = this.__owl__;
if (__owl__.classObj) {
(<any>vnode).data.class = Object.assign(
(<any>vnode).data.class || {},
__owl__.classObj
);
}
__owl__.vnode = patch(elm, vnode);
if (__owl__.parent!.__owl__.isMounted && !__owl__.isMounted) {
this.__callMounted();
+63 -38
View File
@@ -88,21 +88,27 @@ const NODE_HOOKS_PARAMS = {
interface Utils {
h: typeof h;
objectToAttrString(obj: Object): string;
toObj(expr: any): Object;
shallowEqual(p1: Object, p2: Object): boolean;
[key: string]: any;
}
export const UTILS: Utils = {
h: h,
objectToAttrString(obj: Object): string {
let classes: string[] = [];
for (let k in obj) {
if (obj[k]) {
classes.push(k);
toObj(expr) {
if (typeof expr === "string") {
expr = expr.trim();
if (!expr) {
return {};
}
let words = expr.split(/\s+/);
let result = {};
for (let i = 0; i < words.length; i++) {
result[words[i]] = true;
}
return result;
}
return classes.join(" ");
return expr;
},
shallowEqual(p1, p2) {
for (let k in p1) {
@@ -513,6 +519,7 @@ export class QWeb extends EventBus {
props.push(`${key}: _${val}`);
}
}
let classObj = "";
for (let i = 0; i < attributes.length; i++) {
let name = attributes[i].name;
@@ -524,13 +531,23 @@ export class QWeb extends EventBus {
!(<Element>node).getAttribute("t-attf-" + name)
) {
const attID = ctx.generateID();
ctx.addLine(`var _${attID} = '${value}';`);
if (!name.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
name = '"' + name + '"';
if (name === "class") {
let classDef = value
.trim()
.split(/\s+/)
.map(a => `'${a}':true`)
.join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
} else {
ctx.addLine(`var _${attID} = '${value}';`);
if (!name.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
name = '"' + name + '"';
}
attrs.push(`${name}: _${attID}`);
handleBooleanProps(name, attID);
}
attrs.push(`${name}: _${attID}`);
handleBooleanProps(name, attID);
}
// dynamic attributes
@@ -538,32 +555,37 @@ export class QWeb extends EventBus {
let attName = name.slice(6);
const v = ctx.getValue(value);
let formattedValue = v.id || ctx.formatExpression(v);
if (
formattedValue[0] === "{" &&
formattedValue[formattedValue.length - 1] === "}"
) {
formattedValue = `this.utils.objectToAttrString(${formattedValue})`;
if (attName === "class") {
formattedValue = `this.utils.toObj(${formattedValue})`;
if (classObj) {
ctx.addLine(`Object.assign(${classObj}, ${formattedValue})`);
} else {
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = ${formattedValue};`);
}
} else {
const attID = ctx.generateID();
if (!attName.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
attName = '"' + attName + '"';
}
// we need to combine dynamic with non dynamic attributes:
// class="a" t-att-class="'yop'" should be rendered as class="a yop"
const attValue = (<Element>node).getAttribute(attName);
if (attValue) {
const attValueID = ctx.generateID();
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att =>
att.startsWith(attName + ":")
);
attrs.splice(attrIndex, 1);
}
ctx.addLine(`var _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID);
}
const attID = ctx.generateID();
if (!attName.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
attName = '"' + attName + '"';
}
// we need to combine dynamic with non dynamic attributes:
// class="a" t-att-class="'yop'" should be rendered as class="a yop"
const attValue = (<Element>node).getAttribute(attName);
if (attValue) {
const attValueID = ctx.generateID();
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
const attrIndex = attrs.findIndex(att =>
att.startsWith(attName + ":")
);
attrs.splice(attrIndex, 1);
}
ctx.addLine(`var _${attID} = ${formattedValue};`);
attrs.push(`${attName}: _${attID}`);
handleBooleanProps(attName, attID);
}
if (name.startsWith("t-attf-")) {
@@ -604,6 +626,9 @@ export class QWeb extends EventBus {
if (props.length > 0) {
parts.push(`props:{${props.join(",")}}`);
}
if (classObj) {
parts.push(`class:${classObj}`);
}
if (withHandlers) {
parts.push(`on:{}`);
}
+25 -19
View File
@@ -469,26 +469,28 @@ QWeb.addDirective({
ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattStyle)};`);
tattStyle = attVar;
}
let updateClassCode = "";
let classObj = "";
if (classAttr || tattClass || styleAttr || tattStyle || events.length) {
let classCode = "";
if (classAttr) {
classCode =
classAttr
.trim()
.split(/\s+/)
.map(c => `vn.elm.classList.add('${c}')`)
.join(";") + ";";
let classDef = classAttr
.trim()
.split(/\s+/)
.map(a => `'${a}':true`)
.join(",");
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = {${classDef}};`);
}
if (tattClass) {
const attVar = `_${ctx.generateID()}`;
ctx.addLine(`const ${attVar} = ${ctx.formatExpression(tattClass)};`);
classCode = `for (let k in ${attVar}) {
if (${attVar}[k]) {
vn.elm.classList.add(k);
}
}`;
updateClassCode = `let cl=w${componentID}.el.classList;for (let k in ${attVar}) {if (${attVar}[k]) {cl.add(k)} else {cl.remove(k)}}`;
let tattExpr = ctx.formatExpression(tattClass);
if (tattExpr[0] !== "{" || tattExpr[tattExpr.length - 1] !== "}") {
tattExpr = `this.utils.toObj(${tattExpr})`;
}
if (classAttr) {
ctx.addLine(`Object.assign(${classObj}, ${tattExpr})`);
} else {
classObj = `_${ctx.generateID()}`;
ctx.addLine(`let ${classObj} = ${tattExpr};`);
}
}
let eventsCode = events
.map(function([eventName, mods, handlerName, extraArgs]) {
@@ -524,7 +526,7 @@ QWeb.addDirective({
.join("");
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : "";
createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}${eventsCode}}};`;
createHook = `vnode.data.hook = {create(_, vn){${styleCode}${eventsCode}}};`;
}
ctx.addLine(
@@ -630,12 +632,16 @@ QWeb.addDirective({
ctx.addLine(
`def${defID} = def${defID}.then(()=>{if (w${componentID}.__owl__.isDestroyed) {return};${
tattStyle ? `w${componentID}.el.style=${tattStyle};` : ""
}${updateClassCode}let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}c${
}let pvnode=w${componentID}.__owl__.pvnode;${keepAliveCode}c${
ctx.parentNode
}[_${dummyID}_index]=pvnode;});`
);
ctx.closeIf();
if (classObj) {
ctx.addLine(`w${componentID}.__owl__.classObj=${classObj};`);
}
if (async) {
ctx.addLine(
`def${defID}.then(w${componentID}.__applyPatchQueue.bind(w${componentID}, patchQueue${componentID}));`
@@ -706,7 +712,7 @@ QWeb.addDirective({
ctx.addLine(
`const slot${slotKey} = this.slots[context.__owl__.slotId + '_' + '${value}'];`
);
ctx.addIf(`slot${slotKey}`)
ctx.addIf(`slot${slotKey}`);
ctx.addLine(
`slot${slotKey}(context.__owl__.parent, Object.assign({}, extra, {parentNode: c${
ctx.parentNode
+42 -2
View File
@@ -59,7 +59,7 @@ function vnode(
elm: Element | Text | undefined
): VNode {
let key = data === undefined ? undefined : data.key;
return {sel, data, children, text, elm, key};
return { sel, data, children, text, elm, key };
}
//------------------------------------------------------------------------------
@@ -938,4 +938,44 @@ export const attrsModule = {
update: updateAttrs
} as Module;
export const patch = init([eventListenersModule, attrsModule, propsModule]);
//------------------------------------------------------------------------------
// class.ts
//------------------------------------------------------------------------------
function updateClass(oldVnode: VNode, vnode: VNode): void {
var cur: any,
name: string,
elm: Element,
oldClass = (oldVnode.data as VNodeData).class,
klass = (vnode.data as VNodeData).class;
if (!oldClass && !klass) return;
if (oldClass === klass) return;
oldClass = oldClass || {};
klass = klass || {};
elm = vnode.elm as Element;
for (name in oldClass) {
if (!klass[name]) {
elm.classList.remove(name);
}
}
for (name in klass) {
cur = klass[name];
if (cur !== oldClass[name]) {
(elm.classList as any)[cur ? "add" : "remove"](name);
}
}
}
const classModule = { create: updateClass, update: updateClass } as Module;
//------------------------------------------------------------------------------
// patch
//------------------------------------------------------------------------------
export const patch = init([
eventListenersModule,
attrsModule,
propsModule,
classModule
]);