Skip to content

Commit

Permalink
[IMP] parser: add support for user-space directives
Browse files Browse the repository at this point in the history
This commit adds the support for user-space directives. To use the
user-space directive, an Object of functions needs to be configured on
the owl APP:
```js
 new App(..., {
    userDirectives: {
     my-user-directive: function (el, value) {
            el.setAttribute("t-on-click", value);
            return el;
       }
   }
  });
```
The functions will be called when a user directive with the name of the
function is found. The original element will be replaced with the one
returned by the function.
This :
```xml
<div t-user-my-user-directive="click" />
```
will be replace by :
```xml
<div t-on-click="value"/>
```

issue : #1650
  • Loading branch information
jpp-odoo committed Nov 12, 2024
1 parent 26c7856 commit 53807d8
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 3 deletions.
5 changes: 3 additions & 2 deletions src/compiler/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { TemplateSet } from "../runtime/template_set";
import type { TemplateSet, userDirectives } from "../runtime/template_set";
import type { BDom } from "../runtime/blockdom";
import { CodeGenerator, Config } from "./code_generator";
import { parse } from "./parser";
Expand All @@ -10,13 +10,14 @@ export type TemplateFunction = (app: TemplateSet, bdom: any, helpers: any) => Te

interface CompileOptions extends Config {
name?: string;
userDirectives?: userDirectives;
}
export function compile(
template: string | Element,
options: CompileOptions = {}
): TemplateFunction {
// parsing
const ast = parse(template);
const ast = parse(template, options.userDirectives);

// some work
const hasSafeContext =
Expand Down
40 changes: 39 additions & 1 deletion src/compiler/parser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { OwlError } from "../common/owl_error";
import { parseXML } from "../common/utils";
import { userDirectives } from "../runtime/template_set";

// -----------------------------------------------------------------------------
// AST Type definition
Expand Down Expand Up @@ -197,8 +198,12 @@ export type AST =
// Parser
// -----------------------------------------------------------------------------
const cache: WeakMap<Element, AST> = new WeakMap();
let userDirectives: userDirectives = {};

export function parse(xml: string | Element): AST {
export function parse(xml: string | Element, userDir?: userDirectives): AST {
if (userDir) {
userDirectives = userDir;
}
if (typeof xml === "string") {
const elem = parseXML(`<t>${xml}</t>`).firstChild as Element;
return _parse(elem);
Expand Down Expand Up @@ -229,6 +234,7 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null {
return parseTextCommentNode(node, ctx);
}
return (
parseTUser(node, ctx) ||
parseTDebugLog(node, ctx) ||
parseTForEach(node, ctx) ||
parseTIf(node, ctx) ||
Expand Down Expand Up @@ -277,6 +283,38 @@ function parseTextCommentNode(node: Node, ctx: ParsingContext): AST | null {
return null;
}

function parseTUser(node: Element, ctx: ParsingContext): AST | null {
if (!Object.keys(userDirectives).length) {
return null;
}
const nodeAttrsNames = node.getAttributeNames();
for (let attr of nodeAttrsNames) {
if (attr === "t-user" || attr === "t-user-") {
throw new OwlError("Missing user directive name with t-user directive");
}
if (attr.startsWith("t-user-")) {
const directiveName = attr.split(".")[0].slice(7)
const userDirective = userDirectives[directiveName];
if (!userDirective) {
throw new OwlError(`User directive "${directiveName}" is not defined`);
}
const value = node.getAttribute(attr)!;
const modifier = attr.split(".").length > 1 ? attr.split(".")[1] : undefined;
node.removeAttribute(attr);
try{
node = userDirective(node, value, modifier);
} catch (error) {
throw new OwlError(`User directive "${directiveName}" throw the following error: ${error}`);
}
if (!(node instanceof Element)) {
throw new OwlError(`The return value of the User directive "${directiveName}" should be an Element`);
}
return parseNode(node, ctx);
}
}
return null;
}

// -----------------------------------------------------------------------------
// debugging
// -----------------------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(
dev: this.dev,
translateFn: this.translateFn,
translatableAttributes: this.translatableAttributes,
userDirectives: this.userDirectives,
});
};
8 changes: 8 additions & 0 deletions src/runtime/template_set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ import { parseXML } from "../common/utils";

const bdom = { text, createBlock, list, multi, html, toggler, comment };

export type userDirectives = Record<
string,
(node: Element, value: string, modifier?: string) => Element
>;

export interface TemplateSetConfig {
dev?: boolean;
translatableAttributes?: string[];
translateFn?: (s: string) => string;
templates?: string | Document | Record<string, string>;
getTemplate?: (s: string) => Element | Function | string | void;
userDirectives?: userDirectives;
}

export class TemplateSet {
Expand All @@ -27,6 +33,7 @@ export class TemplateSet {
translateFn?: (s: string) => string;
translatableAttributes?: string[];
Portal = Portal;
userDirectives: userDirectives;

constructor(config: TemplateSetConfig = {}) {
this.dev = config.dev || false;
Expand All @@ -42,6 +49,7 @@ export class TemplateSet {
}
}
this.getRawTemplate = config.getTemplate;
this.userDirectives = config.userDirectives || {};
}

addTemplate(name: string, template: string | Element) {
Expand Down
29 changes: 29 additions & 0 deletions tests/compiler/__snapshots__/t_user.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`t-user can use t-user directive on a node 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"my-div\\" block-handler-0=\\"click\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['click'], ctx];
return block1([hdlr1]);
}
}"
`;
exports[`t-user can use t-user directive with modifier on a node 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"my-div\\" block-handler-0=\\"click\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['click'], ctx];
return block1([hdlr1]);
}
}"
`;
57 changes: 57 additions & 0 deletions tests/compiler/t_user.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { App, Component, xml } from "../../src";
import { makeTestFixture, snapshotEverything } from "../helpers";

let fixture: HTMLElement;

snapshotEverything();

beforeEach(() => {
fixture = makeTestFixture();
});

describe("t-user", () => {
test("can use t-user directive on a node", async () => {
const steps: string[] = [];
class SomeComponent extends Component {
static template = xml`<div t-user-plop="click" class="my-div"/>`;
click() {
steps.push("clicked");
}
}
const app = new App(SomeComponent, {
userDirectives: {
plop: (node, value) => {
node.setAttribute("t-on-click", value);
return node;
},
},
});
await app.mount(fixture);
expect(fixture.innerHTML).toBe(`<div class="my-div"></div>`);
fixture.querySelector("div")!.click();
expect(steps).toEqual(["clicked"]);
});

test("can use t-user directive with modifier on a node", async () => {
const steps: string[] = [];
class SomeComponent extends Component {
static template = xml`<div t-user-plop.mouse="click" class="my-div"/>`;
click() {
steps.push("clicked");
}
}
const app = new App(SomeComponent, {
userDirectives: {
plop: (node, value, modifier) => {
node.setAttribute("t-on-click", value);
steps.push(modifier || "");
return node;
},
},
});
await app.mount(fixture);
expect(fixture.innerHTML).toBe(`<div class="my-div"></div>`);
fixture.querySelector("div")!.click();
expect(steps).toEqual(["mouse", "clicked"]);
});
});

0 comments on commit 53807d8

Please sign in to comment.