Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions doc/reference/template_syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,33 @@ Note that it can be combined with normal class attribute:
<div class="a" t-att-class="{'b': true}"/> <!-- result: <div class="a b"></div> -->
```

### Dynamic style attribute

Similarly, `t-att-style` supports an object notation, with keys being CSS property
names (in either kebab-case or camelCase) and values being the corresponding CSS
values:

```xml
<div t-att-style="{'color': 'red', 'font-size': '20px'}"/>
<!-- result: <div style="color: red; font-size: 20px;"></div> -->

<div t-att-style="{ fontSize: '20px' }"/>
<!-- result: <div style="font-size: 20px;"></div> -->
```

It can also be combined with a normal static style attribute:

```xml
<div style="color: red;" t-att-style="{ fontSize: '20px' }"/>
<!-- result: <div style="color: red; font-size: 20px;"></div> -->
```

String values are supported as well:

```xml
<div t-att-style="'color: red; font-size: 20px;'"/>
```

### Dynamic tag names

When writing generic components or templates, the specific concrete tag for an
Expand Down
64 changes: 34 additions & 30 deletions src/runtime/blockdom/attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,33 +150,6 @@ function toClassObj(expr: string | number | { [c: string]: any }) {
}
}

export function setClass(this: HTMLElement, val: any) {
val = val === "" ? {} : toClassObj(val);
// add classes
const cl = this.classList;
for (let c in val) {
tokenListAdd.call(cl, c);
}
}

export function updateClass(this: HTMLElement, val: any, oldVal: any) {
oldVal = oldVal === "" ? {} : toClassObj(oldVal);
val = val === "" ? {} : toClassObj(val);
const cl = this.classList;
// remove classes
for (let c in oldVal) {
if (!(c in val)) {
tokenListRemove.call(cl, c);
}
}
// add classes
for (let c in val) {
if (!(c in oldVal)) {
tokenListAdd.call(cl, c);
}
}
}

// ---------------------------------------------------------------------------
// Style
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -212,7 +185,7 @@ function toStyleObj(expr: string | { [prop: string]: any }): { [prop: string]: s
}
const prop = trim.call(part.slice(0, colonIdx));
const value = trim.call(part.slice(colonIdx + 1));
if (prop && value) {
if (prop && value && value !== "undefined") {
result[prop] = value;
}
}
Expand All @@ -231,6 +204,36 @@ function toStyleObj(expr: string | { [prop: string]: any }): { [prop: string]: s
}
}

// ---------------------------------------------------------------------------
// Class
// ---------------------------------------------------------------------------

export function setClass(this: HTMLElement, val: any) {
val = val === "" ? {} : toClassObj(val);
for (let k in val) {
tokenListAdd.call(this.classList, k);
}
}

export function updateClass(this: HTMLElement, val: any, oldVal: any) {
oldVal = oldVal === "" ? {} : toClassObj(oldVal);
val = val === "" ? {} : toClassObj(val);
for (let k in oldVal) {
if (!(k in val)) {
tokenListRemove.call(this.classList, k);
}
}
for (let k in val) {
if (val[k] !== oldVal[k]) {
tokenListAdd.call(this.classList, k);
}
}
}

// ---------------------------------------------------------------------------
// Style setters
// ---------------------------------------------------------------------------

export function setStyle(this: HTMLElement, val: any) {
val = val === "" ? {} : toStyleObj(val);
const style = this.style;
Expand All @@ -243,16 +246,17 @@ export function updateStyle(this: HTMLElement, val: any, oldVal: any) {
oldVal = oldVal === "" ? {} : toStyleObj(oldVal);
val = val === "" ? {} : toStyleObj(val);
const style = this.style;
// remove old styles
for (let prop in oldVal) {
if (!(prop in val)) {
style.removeProperty(prop);
}
}
// set new/changed styles
for (let prop in val) {
if (val[prop] !== oldVal[prop]) {
style.setProperty(prop, val[prop]);
}
}
if (!style.cssText) {
removeAttribute.call(this, "style");
}
}
28 changes: 28 additions & 0 deletions tests/components/__snapshots__/style_class.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,34 @@ exports[`style and class handling t-att-class is properly added/removed on widge
}"
`;

exports[`style and class handling t-att-style is cleared when interpolated prop becomes undefined 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;

let block1 = createBlock(\`<span block-attribute-0="style">text</span>\`);

return function template(ctx, node, key = "") {
let attr1 = ctx['this'].getStyle();
return block1([attr1]);
}
}"
`;

exports[`style and class handling t-att-style is cleared when value becomes undefined 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;

let block1 = createBlock(\`<div block-attribute-0="style"/>\`);

return function template(ctx, node, key = "") {
let attr1 = ctx['this'].state.style;
return block1([attr1]);
}
}"
`;

exports[`style and class handling t-att-style with object value 1`] = `
"function anonymous(app, bdom, helpers
) {
Expand Down
32 changes: 32 additions & 0 deletions tests/components/style_class.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,38 @@ describe("style and class handling", () => {
expect(div.style.fontWeight).toBe("");
});

test("t-att-style is cleared when value becomes undefined", async () => {
class App extends Component {
static template = xml`<div t-att-style="this.state.style" />`;
state = proxy({ style: "color: orange;" as any });
}
const widget = await mount(App, fixture);
const div = fixture.querySelector("div")!;
expect(div.style.color).toBe("orange");

widget.state.style = undefined;
await nextTick();
expect(div.style.color).toBe("");
expect(div.hasAttribute("style")).toBe(false);
});

test("t-att-style is cleared when interpolated prop becomes undefined", async () => {
class App extends Component {
static template = xml`<span t-att-style="this.getStyle()">text</span>`;
state = proxy({ color: "orange" as any });
getStyle() {
return `color: ${this.state.color}`;
}
}
const widget = await mount(App, fixture);
const span = fixture.querySelector("span")!;
expect(span.style.color).toBe("orange");

widget.state.color = undefined;
await nextTick();
expect(span.style.color).toBe("");
});

// TODO: does this test need to be moved? (class now a standard prop)
test("error in subcomponent with class", async () => {
class Child extends Component {
Expand Down
Loading