diff --git a/README.md b/README.md index 35fd1e2..a3c21a8 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,9 @@ const result = m("{{^isAdmin}}You are not Admin{{/isAdmin}}", data); // Output: 'You are not Admin' ``` -### Partials (added in v0.3.0) +### Partials + +> This feature was added in `v0.3.0` Partials allow you to include separate templates within your main template. Use the greater than symbol `>` followed by the partial name inside double curly braces `{{> partialName }}`. @@ -87,7 +89,9 @@ const result = m("{{> hello}}", data, {partials}); // Output: 'Hello Bob!' ``` -#### Custom context in partials (added in v0.3.1) +#### Custom context in partials + +> This feature was added in `v0.3.1`. You can provide a custom context for the partial by specifying a field of the data: `{{> partialName dataField}}`. @@ -106,7 +110,9 @@ const result = m("User: {{> user currentUser}}", data, {partials}); // Output: 'User: John Doe ' ``` -### Built-in helpers (added in v0.4.0) +### Built-in helpers + +> Added in `v0.4.0`. Helpers allows you to execute special functions within blocks or sections of your template. Mikel currently supports the following built-in helpers: @@ -126,6 +132,16 @@ const data = { console.log(m("{{#each users}}{{.}}, {{/each}}", data)); // --> 'John, Alice, Bob, ' ``` +When looping throug arrays, you can use the variable `@index` to access to the current index of the item in the array: + +```javascript +const data = { + users: ["John", "Alice", "Bob"], +}; + +console.log(m("{{#each users}}{{@index}}: {{.}}, {{/each}}", data)); // --> '0: John, 1: Alice, 2: Bob, ' +``` + The `each` helper can also iterate over objects: ```javascript @@ -135,7 +151,20 @@ const data = { }, }; -console.log(m("{{#each values}}{{@key}}: {{.}}{{/each}}", data)); // --> 'foo: bar' +console.log(m("{{#each values}}{{.}}{{/each}}", data)); // --> 'bar' +``` + +When looping throug objects, you can use the variable `@key` to access to the current key in the object, and the variable `@value` to access to the corresponding value: + +```javascript +const data = { + values: { + foo: "0", + bar: "1", + }, +}; + +console.log(m("{{#each values}}{{@key}}: {{@value}}, {{/each}}", data)); // --> 'foo: 0, bar: 1, ' ``` #### if @@ -148,7 +177,7 @@ Example: ```javascript const data = { - isAdmin: true; + isAdmin: true, }; console.log(m("{{#if isAdmin}}Hello admin{{/if}}", data)); // --> 'Hello admin' @@ -164,12 +193,45 @@ Example: ```javascript const data = { - isAdmin: false + isAdmin: false, }; console.log(m("{{#unless isAdmin}}Hello guest{{/unless}}", data)); // --> 'Hello guest' ``` +### At-Variables + +> Added in `v0.4.0`. + +At-Variables in Mikel provide convenient access to special values within your templates. These variables, denoted by the `@` symbol, allow users to interact with specific data contexts or values. + +#### @root + +The `@root` variable grants access to the root data context provided to the template. It is always defined and enables users to retrieve values from the top-level data object. + +Example: + +```javascript +const data = { + name: "World", +}; + +console.log(m("Hello, {{@root.name}}!", data)); // -> 'Hello, World!' +``` + +#### @index + +The `@index` variable facilitates access to the current index of the item when iterating over an array using the `#each` helper. It aids in dynamic rendering and indexing within loops. + +#### @key + +The `@key` variable allows users to retrieve the current key of the object entry when looping through an object using the `#each` helper. It provides access to object keys for dynamic rendering and customization. + +#### @value + +The `@value` variable allows users to retrieve the current value of the object entry when iterating over an object using the `#each` helper. It simplifies access to object values for dynamic rendering and data manipulation. + + ## API ### `m(template, data[, options])` diff --git a/index.js b/index.js index cb4bc0c..5ab357b 100644 --- a/index.js +++ b/index.js @@ -12,7 +12,7 @@ const escape = str => { }; const get = (ctx, path) => { - return (path === "." ? ctx : path.split(".").reduce((p, k) => p?.[k], ctx)) || ""; + return (path === "." ? ctx : path.split(".").reduce((p, k) => p?.[k], ctx)) ?? ""; }; const helpers = new Map(Object.entries({ @@ -32,7 +32,7 @@ const compile = (tokens, output, context, opt, index = 0, section = "", vars = { output.push(tokens[i]); } else if (tokens[i].startsWith("@")) { - output.push(vars?.[tokens[i].slice(1).trim() || "_"] ?? ""); + output.push(get(vars || {}, tokens[i].slice(1).trim() ?? "_") ?? ""); } else if (tokens[i].startsWith("!")) { output.push(get(context, tokens[i].slice(1).trim())); @@ -44,13 +44,13 @@ const compile = (tokens, output, context, opt, index = 0, section = "", vars = { context: context, globalOptions: opt, fn: (blockContext = {}, blockVars = {}, blockOutput = []) => { - i = compile(tokens, blockOutput, blockContext, opt, j, t, blockVars); + i = compile(tokens, blockOutput, blockContext, opt, j, t, {root: vars.root, ...blockVars}); return blockOutput.join(""); }, })); // Make sure that this block has been executed if (i + 1 === j) { - i = compile(tokens, [], {}, opt, j, t); + i = compile(tokens, [], {}, opt, j, t, vars); } } else if (tokens[i].startsWith("#") || tokens[i].startsWith("^")) { @@ -60,18 +60,18 @@ const compile = (tokens, output, context, opt, index = 0, section = "", vars = { if (!negate && value && Array.isArray(value)) { const j = i + 1; (value.length > 0 ? value : [""]).forEach(item => { - i = compile(tokens, value.length > 0 ? output : [], item, opt, j, t); + i = compile(tokens, value.length > 0 ? output : [], item, opt, j, t, vars); }); } else { const includeOutput = (!negate && !!value) || (negate && !!!value); - i = compile(tokens, includeOutput ? output : [], context, opt, i + 1, t); + i = compile(tokens, includeOutput ? output : [], context, opt, i + 1, t, vars); } } else if (tokens[i].startsWith(">")) { const [t, v] = tokens[i].slice(1).trim().split(" "); if (typeof opt?.partials?.[t] === "string") { - compile(opt.partials[t].split(tags), output, v ? get(context, v) : context, opt, 0, ""); + compile(opt.partials[t].split(tags), output, v ? get(context, v) : context, opt, 0, "", vars); } } else if (tokens[i].startsWith("/")) { @@ -89,6 +89,6 @@ const compile = (tokens, output, context, opt, index = 0, section = "", vars = { }; export default (str, context = {}, opt = {}, output = []) => { - compile(str.split(tags), output, context, opt, 0, ""); + compile(str.split(tags), output, context, opt, 0, "", {root: context}); return output.join(""); }; diff --git a/test.js b/test.js index 221604d..7b0d246 100644 --- a/test.js +++ b/test.js @@ -168,3 +168,27 @@ describe("[helpers] {{#unless }}", () => { assert.equal(m("_{{#unless value}}Yes!{{/unless}}_", {value: false}), "_Yes!_"); }); }); + +describe("[at-variable] {{@root}}", () => { + it("should reference the global context", () => { + assert.equal(m("{{#each values}}{{@root.key}}{{/each}}", {values: ["a", "b"], key: "c"}), "cc"); + }); +}); + +describe("[at-variable] {{@index}}", () => { + it("sould reference current index in the array", () => { + assert.equal(m("{{#each values}}{{@index}}{{/each}}", {values: ["a", "b", "c"]}), "012"); + }); +}); + +describe("[at-variable] {{@key}}", () => { + it("sshould reference current key when looping throug an object", () => { + assert.equal(m("{{#each values}}{{@key}},{{/each}}", {values: {foo: 1, bar: 2}}), "foo,bar,"); + }); +}); + +describe("[at-variable] {{@value}}", () => { + it("sshould reference current value when looping throug an object", () => { + assert.equal(m("{{#each values}}{{@value}},{{/each}}", {values: {foo: 1, bar: 2}}), "1,2,"); + }); +});