Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IMP] compiler: add support for t-for directive #1491

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
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