diff --git a/src/compiler/index.ts b/src/compiler/index.ts index 16683bc1c..6f9eb47d0 100644 --- a/src/compiler/index.ts +++ b/src/compiler/index.ts @@ -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"; @@ -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 = diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 11c9bdb7a..103c17480 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -1,5 +1,6 @@ import { OwlError } from "../common/owl_error"; import { parseXML } from "../common/utils"; +import { userDirectives } from "../runtime/template_set"; // ----------------------------------------------------------------------------- // AST Type definition @@ -197,8 +198,12 @@ export type AST = // Parser // ----------------------------------------------------------------------------- const cache: WeakMap = 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(`${xml}`).firstChild as Element; return _parse(elem); @@ -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) || @@ -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 // ----------------------------------------------------------------------------- diff --git a/src/index.ts b/src/index.ts index 83d84e892..8288b1f0f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,5 +12,6 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate( dev: this.dev, translateFn: this.translateFn, translatableAttributes: this.translatableAttributes, + userDirectives: this.userDirectives, }); }; diff --git a/src/runtime/template_set.ts b/src/runtime/template_set.ts index cdb7e16b4..65bdedf38 100644 --- a/src/runtime/template_set.ts +++ b/src/runtime/template_set.ts @@ -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; getTemplate?: (s: string) => Element | Function | string | void; + userDirectives?: userDirectives; } export class TemplateSet { @@ -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; @@ -42,6 +49,7 @@ export class TemplateSet { } } this.getRawTemplate = config.getTemplate; + this.userDirectives = config.userDirectives || {}; } addTemplate(name: string, template: string | Element) { diff --git a/tests/compiler/__snapshots__/t_user.test.ts.snap b/tests/compiler/__snapshots__/t_user.test.ts.snap new file mode 100644 index 000000000..9a7cf183c --- /dev/null +++ b/tests/compiler/__snapshots__/t_user.test.ts.snap @@ -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(\`
\`); + + 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(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let hdlr1 = [ctx['click'], ctx]; + return block1([hdlr1]); + } +}" +`; diff --git a/tests/compiler/t_user.test.ts b/tests/compiler/t_user.test.ts new file mode 100644 index 000000000..6c3e5b92f --- /dev/null +++ b/tests/compiler/t_user.test.ts @@ -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`
`; + 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(`
`); + 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`
`; + 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(`
`); + fixture.querySelector("div")!.click(); + expect(steps).toEqual(["mouse", "clicked"]); + }); +});