Skip to content

Commit

Permalink
[IMP] compiler: add support for t-for directive
Browse files Browse the repository at this point in the history
This commit adds support for the `t-for` whose syntax and usage is
similar to the syntax of the for..of loop in JS (and in fact it compiles
to a for..of loop). This looping construct supports looping on arbitrary
iterables and destructuring assignments, which would be difficult to
support in a backward compatible manner on the existing t-foreach
directive.
  • Loading branch information
sdegueldre committed Jul 24, 2023
1 parent 9d99f89 commit fb97955
Show file tree
Hide file tree
Showing 10 changed files with 2,172 additions and 23 deletions.
2 changes: 1 addition & 1 deletion doc/reference/app.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ const { loadFile, mount } = owl;
Dev mode activates some additional checks and developer amenities:

- [Props validation](./props.md#props-validation) is performed
- [t-foreach](./templates.md#loops) loops check for key unicity
- [t-for and t-foreach](./templates.md#loops) loops check for key unicity
- Lifecycle hooks are wrapped to report their errors in a more developer-friendly way
- onWillStart and onWillUpdateProps will emit a warning in the console when they
take longer than 3 seconds in an effort to ease debugging the presence of deadlocks
51 changes: 31 additions & 20 deletions doc/reference/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ For reference, here is a list of all standard QWeb directives:
| `t-out` | [Outputting value, possibly without escaping](#outputting-data) |
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
| `t-foreach`, `t-as` | [Loops](#loops) |
| `t-for/t-of`, `t-foreach/t-as` | [Loops](#loops) |
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
| `t-call` | [Rendering sub templates](#sub-templates) |
| `t-debug`, `t-log` | [Debugging](#debugging) |
Expand Down Expand Up @@ -372,6 +372,24 @@ Like conditions, `t-foreach` applies to the element bearing the directive’s at

is equivalent to the previous example.

Owl also has another pair of directives that can be used for looping that allow
destructuring the contents of the loop item: `t-for` and `t-of`, which behaves
much like `for..of` in javascript:

```xml
<t t-for="[left, right]" t-of="[['a', 1], ['b', 2], ['c', 3]]" t-key="left">
<p><t t-esc="left"/>: <t t-esc="right"/></p>
</t>
```

will be rendered as:

```xml
<p>a: 1</p>
<p>b: 2</p>
<p>c: 3</p>
```

An important difference should be made with the usual `QWeb` behaviour: Owl
requires the presence of a `t-key` directive, to be able to properly reconcile
renderings.
Expand All @@ -380,8 +398,9 @@ renderings.
and maps, it will expose the key of the current iteration as the contents of the
`t-as`, and the corresponding value with the same name and the suffix `_value`.

In addition to the name passed via t-as, `t-foreach` provides a few other useful
variables (note: `$as` will be replaced with the name passed to `t-as`):
In addition to the name passed via t-as, `t-foreach` (but not `t-for`) provides
a few other useful variables (note: `$as` will be replaced with the name passed
to `t-as`):

- `$as_value`: the current iteration value, identical to `$as` for arrays and
other iterables, but for objects and maps, it provides the value (where `$as`
Expand All @@ -393,10 +412,9 @@ variables (note: `$as` will be replaced with the name passed to `t-as`):
(equivalent to `$as_index + 1 == $as_size`), requires the iteratee’s size be
available

These extra variables provided and all new variables created into the `t-foreach`
are only available in the scope of the `t-foreach`. If the variable exists outside
the context of the `t-foreach`, the value is copied at the end of the foreach
into the global context.
These variables and all new variables created inside`t-foreach` and `t-for` are
only available inside of the loop. If a variable existed outside the context of
the loop, the assignment will affect the outer variable.

```xml
<t t-set="existing_variable" t-value="false"/>
Expand All @@ -408,7 +426,7 @@ into the global context.
<!-- existing_variable and new_variable now true -->
</p>

<!-- existing_variable always true -->
<!-- existing_variable still true -->
<!-- new_variable undefined -->
```

Expand Down Expand Up @@ -471,18 +489,11 @@ are all equivalent:
</t>
```

If there is no `t-key` directive, Owl will use the index as a default key.

Note: the `t-foreach` directive only accepts arrays (lists) or objects. It does
not work with other iterables, such as `Set`. However, it is only a matter of
using the `...` javascript operator. For example:

```xml
<t t-foreach="[...items]" t-as="item">...</t>
```

The `...` operator will convert the `Set` (or any other iterables) into a list,
which will work with Owl QWeb.
The `t-key` directive is mandatory, and as mentioned should represent the object's
identity. You may be tempted to use the loop index as a key, but keep in mind that
this is only correct if items in the loop cannot be reordered. If this is not the
case, using the index as the key can lead to bugs that are difficult to find, so
use the index as the key only if you are sure items cannot be reordered.

### Sub Templates

Expand Down
45 changes: 44 additions & 1 deletion src/compiler/code_generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
ASTTCallBlock,
ASTTEsc,
ASTText,
ASTTFor,
ASTTForEach,
ASTTif,
ASTTKey,
Expand Down Expand Up @@ -471,6 +472,8 @@ export class CodeGenerator {
return this.compileTIf(ast, ctx);
case ASTType.TForEach:
return this.compileTForeach(ast, ctx);
case ASTType.TFor:
return this.compileTFor(ast, ctx);
case ASTType.TKey:
return this.compileTKey(ast, ctx);
case ASTType.Multi:
Expand Down Expand Up @@ -907,7 +910,7 @@ export class CodeGenerator {
if (!ast.hasNoValue) {
this.addLine(`ctx[\`${ast.elem}_value\`] = ${keys}[${loopVar}];`);
}
this.define(`key${this.target.loopLevel}`, ast.key ? compileExpr(ast.key) : loopVar);
this.define(`key${this.target.loopLevel}`, compileExpr(ast.key));
if (this.dev) {
// Throw error on duplicate keys in dev mode
this.helpers.add("OwlError");
Expand Down Expand Up @@ -954,6 +957,46 @@ export class CodeGenerator {
return block.varName;
}

compileTFor(ast: ASTTFor, ctx: Context): string {
let { block } = ctx;
if (block) {
this.insertAnchor(block);
}
block = this.createBlock(block, "list", ctx);
this.target.loopLevel++;
this.addLine(`ctx = Object.create(ctx);`);
// Throw errors on duplicate keys in dev mode
if (this.dev) {
this.define(`keys${block.id}`, `new Set()`);
}
this.define(`c_block${block.id}`, "[]");
const index = `i${this.target.loopLevel}`;
this.addLine(`let ${index} = ${0};`);
const binding = compileExpr(ast.binding);
this.addLine(`for (${binding} of ${compileExpr(ast.iterable)}) {`);
this.target.indentLevel++;
this.define(`key${this.target.loopLevel}`, compileExpr(ast.key));
if (this.dev) {
// Throw error on duplicate keys in dev mode
this.helpers.add("OwlError");
this.addLine(
`if (keys${block.id}.has(String(key${this.target.loopLevel}))) { throw new OwlError(\`Got duplicate key in t-for: \${key${this.target.loopLevel}}\`)}`
);
this.addLine(`keys${block.id}.add(String(key${this.target.loopLevel}));`);
}
const subCtx = createContext(ctx, { block, index });
this.compileAST(ast.body, subCtx);
this.addLine(`${index}++;`);
this.target.indentLevel--;
this.target.loopLevel--;
this.addLine(`}`);
if (!ctx.isLast) {
this.addLine(`ctx = ctx.__proto__;`);
}
this.insertBlock("l", block, ctx);
return block.varName;
}

compileTKey(ast: ASTTKey, ctx: Context): string | null {
const tKeyExpr = generateId("tKey_");
this.define(tKeyExpr, compileExpr(ast.expr));
Expand Down
43 changes: 42 additions & 1 deletion src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const enum ASTType {
TCallBlock,
TTranslation,
TPortal,
TFor,
}

export interface ASTText {
Expand Down Expand Up @@ -104,7 +105,15 @@ export interface ASTTForEach {
hasNoLast: boolean;
hasNoIndex: boolean;
hasNoValue: boolean;
key: string | null;
key: string;
}

export interface ASTTFor {
type: ASTType.TFor;
iterable: string;
binding: string;
body: AST;
key: string;
}

export interface ASTTKey {
Expand Down Expand Up @@ -183,6 +192,7 @@ export type AST =
| ASTTCall
| ASTTOut
| ASTTForEach
| ASTTFor
| ASTTKey
| ASTComponent
| ASTSlot
Expand Down Expand Up @@ -230,6 +240,7 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null {
return (
parseTDebugLog(node, ctx) ||
parseTForEach(node, ctx) ||
parseTFor(node, ctx) ||
parseTIf(node, ctx) ||
parseTPortal(node, ctx) ||
parseTCall(node, ctx) ||
Expand Down Expand Up @@ -531,6 +542,36 @@ function parseTForEach(node: Element, ctx: ParsingContext): AST | null {
};
}

function parseTFor(node: Element, ctx: ParsingContext): AST | null {
if (!node.hasAttribute("t-for")) {
return null;
}
const binding = node.getAttribute("t-for")!;
node.removeAttribute("t-for");
const iterable = node.getAttribute("t-of") || "";
node.removeAttribute("t-of");
const key = node.getAttribute("t-key");
if (!key) {
throw new OwlError(
`"Directive t-for should always be used with a t-key!" (expression: t-for="${binding}" t-of="${iterable}")`
);
}
node.removeAttribute("t-key");
const body = parseNode(node, ctx);

if (!body) {
return null;
}

return {
type: ASTType.TFor,
iterable,
binding,
body,
key,
};
}

function parseTKey(node: Element, ctx: ParsingContext): AST | null {
if (!node.hasAttribute("t-key")) {
return null;
Expand Down
Loading

0 comments on commit fb97955

Please sign in to comment.