mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 04:10:40 -07:00
docs: add tutorials for custom filters and tags
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
---
|
||||
title: Access Scope in Filters
|
||||
---
|
||||
|
||||
As covered in [Register Filters/Tags][register-filters], we can access filter arguments directly in filter function like:
|
||||
|
||||
```javascript
|
||||
// Usage: {{ 1 | add: 2, 3 }}
|
||||
// Output: 6
|
||||
engine.registerFilter('add', (initial, arg1, arg2) => initial + arg1 + arg2)
|
||||
```
|
||||
|
||||
When it comes to stateful filters, for example transform a URL path to full URL, we'll need to access a `origin` in current scope:
|
||||
|
||||
```javascript
|
||||
// Usage: {{ '/index.html' | fullURL }}
|
||||
// Scope: { origin: "https://liquidjs.com" }
|
||||
// Output: https://liquidjs.com/index.html
|
||||
|
||||
engine.registerFilter('fullURL', function (path) {
|
||||
const origin = this.context.get(['origin'])
|
||||
return new URL(path, origin).toString()
|
||||
})
|
||||
```
|
||||
|
||||
See this JSFiddle: <http://jsfiddle.net/ctj364up/1/>
|
||||
|
||||
{% note warn Arrow Functions %}
|
||||
<code>this</code> in arrow functions is bound to current JavaScript context, you'll need to use <code>function(){}</code> instead of <code>()=>{}</code> syntax to access <code>this.context</code> correctly.
|
||||
{% endnote %}
|
||||
|
||||
[register-filters]: /tutorials/register-filters-tags.html
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: Parse Parameters
|
||||
---
|
||||
|
||||
## Access Raw Parameters
|
||||
|
||||
As covered in [Register Filters/Tags][register-tags], tag parameters is available on `tagToken.args` as a raw string. For example:
|
||||
|
||||
```javascript
|
||||
// Usage: {% random foo bar coo %}
|
||||
// Output: "foo", "bar" or "coo"
|
||||
engine.registerTag('random', {
|
||||
parse(tagToken) {
|
||||
// tagToken.args === "foo bar coo"
|
||||
this.items = tagToken.args.split(' ')
|
||||
},
|
||||
render(context, emitter) {
|
||||
// get a random index
|
||||
const index = Math.floor(this.items.length * Math.random())
|
||||
// output that item
|
||||
emitter.write(this.items[index])
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Here's a JSFiddle version: <http://jsfiddle.net/ctj364up/2/>
|
||||
|
||||
## Parse Parameters as Values
|
||||
|
||||
Sometimes we need more dynamic tags and want to pass values to the custom tag instead of static strings. Variables in LiquidJS can be literal (string, number, etc.) or a variable from current context scope.
|
||||
|
||||
The following modified template also contains 3 values to random from, but they're values instead of static strings. The first one is string literal, second one is an identifier, third one is a property access sequence containing two identifiers.
|
||||
|
||||
```liquid
|
||||
{% random "foo" bar obj.coo %}
|
||||
```
|
||||
|
||||
It can be tricky to parse all these cases manually, but there's a [Tokenizer][Tokenizer] class in LiquidJS you can make use of.
|
||||
|
||||
```javascript
|
||||
const { Liquid, Tokenizer, evalToken } = require('liquidjs')
|
||||
engine.registerTag('random', {
|
||||
parse(tagToken) {
|
||||
const tokenizer = new Tokenizer(tagToken.args)
|
||||
this.items = []
|
||||
while (!tokenizer.end()) {
|
||||
// here readValue() returns a LiteralToken or PropertyAccessToken
|
||||
this.items.push(tokenizer.readValue())
|
||||
}
|
||||
},
|
||||
* render(context, emitter) {
|
||||
const index = Math.floor(this.items.length * Math.random())
|
||||
const token = this.items[index]
|
||||
// in LiquidJS, we use yield to wait for async call
|
||||
const value = yield evalToken(token, context)
|
||||
emitter.write(value)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Calling this tag in scope `{ bar: "bar", obj: { coo: "coo" } }` yields exactly the same result as the first example. See this JSFiddle: <http://jsfiddle.net/ctj364up/3/>
|
||||
|
||||
{% note info Async ans Promises %}
|
||||
Async calls in LiquidJS are implemented by generators directly, for we can call generators in synchronous manner so this tag implementation is also valid for `renderSync()`, `parseAndRenderSync()`, `renderFileSync()`. If you need to await a promise in tag implementation, simply replace `await somePromise` with `yield somePromise` and keep `* render()` instead of `async render()` will do the trick. See <a href="/tutorials/sync-and-async.html">Sync and Async</a> for more details.
|
||||
{% endnote %}
|
||||
|
||||
## Parse Key-Value Pairs as Named Parameters
|
||||
|
||||
Named parameters become very handy when there're optional parameters or lots of parameters, in which case the order of parameters is not important. This is exactly what [Hash][Hash] class is invented for.
|
||||
|
||||
```liquid
|
||||
{% random from:2, to:max %}
|
||||
```
|
||||
|
||||
In the above example, we're trying to generate a random number in the range [2, max]. We'll use `Hash` to parse `from` and `to` parameters.
|
||||
|
||||
```javascript
|
||||
const { Liquid, Hash } = require('liquidjs')
|
||||
|
||||
engine.registerTag('random', {
|
||||
parse(tagToken) {
|
||||
// parse the parameters structure into `this.args`
|
||||
this.args = new Hash(tagToken.args)
|
||||
},
|
||||
* render(context, emitter) {
|
||||
// evaluate the parameters in `context`
|
||||
const {from, to} = yield this.args.render(context)
|
||||
const length = to - from + 1
|
||||
const value = from + Math.floor(length * Math.random())
|
||||
emitter.write(value)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Rendering `{% random from:2, to:max %}` in scope `{ max: 10 }` will generate a random number in the range [2, 10]. See this JSFiddle: <http://jsfiddle.net/ctj364up/4/>
|
||||
|
||||
|
||||
[register-tags]: /tutorials/register-filters-tags.html
|
||||
[Tokenizer]: /api/classes/parser_tokenizer_.tokenizer.html
|
||||
[Hash]: /api/classes/template_tag_hash_.hash.html
|
||||
@@ -61,4 +61,4 @@ function disabledFilter(name) {
|
||||
}
|
||||
}
|
||||
engine.registerFilter('plus', disabledFilter('plus'));
|
||||
```
|
||||
```
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: Sync and Async
|
||||
---
|
||||
|
||||
LiquidJS supports both sync and async evaluate, and can be used with Promises. To reuse the same set of tag/filter implementations in both sync and async, LiquidJS tags are implemented as generators.
|
||||
|
||||
## Sync and Async API
|
||||
|
||||
All major methods on [Liquid][Liquid] supports both sync and async. These methods return Promises:
|
||||
|
||||
- `render()`
|
||||
- `renderFile()`
|
||||
- `parseFile()`
|
||||
- `parseAndRender()`
|
||||
- `evalValue()`
|
||||
|
||||
The synchronous version of methods contains a `Sync` suffix:
|
||||
|
||||
- `renderSync()`
|
||||
- `renderFileSync()`
|
||||
- `parseFileSync()`
|
||||
- `parseAndRenderSync()`
|
||||
- `evalValueSync()`
|
||||
|
||||
## Implement Sync-Compatible Tags
|
||||
|
||||
### Requirements
|
||||
|
||||
All builtin tags are *sync-compatible* and safe to use for both sync and async APIs. To make your custom tag *sync-compatible*, you'll need to avoid return a `Promise`. That means the `render(context, emitter)`:
|
||||
|
||||
- Should not directly `return <Promise>`, and
|
||||
- Should not be declared as `async`.
|
||||
|
||||
{% note info Non Sync-Compatible Tags %}
|
||||
Non <em>sync-compatible</em> tags are also valid tags, will work just fine for asynchronous API calls. When called synchronously, tags that return a <code>Promise</code> will be rendered as <code>[object Promise]</code>.
|
||||
{% endnote %}
|
||||
|
||||
### Await Promises
|
||||
But LiquidJS is Promise-friendly, right? You can still call Promise-based functions and wait for that Promise within tag implementations. Just replace `await` with `yield` and keep `* render()` instead of `async render()`. e.g.
|
||||
|
||||
```typescript
|
||||
import { TagToken, Context, Emitter, TopLevelToken } from 'liquidjs'
|
||||
|
||||
// Usage: {% upper "alice" %}
|
||||
// Output: ALICE
|
||||
engine.registerTag('upper', {
|
||||
parse: function(tagToken: TagToken, remainTokens: TopLevelToken[]) {
|
||||
this.str = tagToken.args
|
||||
},
|
||||
* render: function(ctx: Context) {
|
||||
// _evalValue will behave synchronously when called by synchronous API
|
||||
// in which case `ctx.sync == true`
|
||||
var str = yield this.liquid._evalValue(this.str, ctx)
|
||||
return str.toUpperCase()
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
See this JSFiddle: <http://jsfiddle.net/ctj364up/6/>.
|
||||
|
||||
## Async-only Tags
|
||||
|
||||
For tags that intended to be used only by async API, or those cannot be implemented synchronously, there's no difference between using generator-base syntax or async syntax. I'll call them *async-only tags*.
|
||||
|
||||
For example, if the above `this.liquid._evalValue()` doesn't respect `ctx.sync` and always returns a Promise, even if the tag is implemented using `* render()` and `yield this.liquid._evalValue()`, it will be rendered as `<object Promise>` anyway.
|
||||
|
||||
For *async-only tags*, you can use async syntax at will. Be careful some APIs in LiquidJS return Promises and others return Generators. You'll need [toPromise][toPromise] API to convert a Generator to a Promise, for example:
|
||||
|
||||
```typescript
|
||||
import { TagToken, Context, Emitter, TopLevelToken, toPromise } from 'liquidjs'
|
||||
|
||||
// Usage: {% upper "alice" %}
|
||||
// Output: ALICE
|
||||
engine.registerTag('upper', {
|
||||
parse: function(tagToken: TagToken, remainTokens: TopLevelToken[]) {
|
||||
this.str = tagToken.args; // name
|
||||
},
|
||||
render: async function(ctx: Context) {
|
||||
var str = await toPromise(this.liquid._evalValue(this.str, ctx));
|
||||
// Or use the alternate API that returns a Promise
|
||||
// var str = await this.liquid.evalValue(this.str, ctx);
|
||||
return str.toUpperCase()
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
See this JSFiddle: <http://jsfiddle.net/ctj364up/5/>.
|
||||
|
||||
[Liquid]: /api/classes/liquid_.liquid.html
|
||||
[toPromise]: /api/modules/liquid_.html#toPromise
|
||||
Reference in New Issue
Block a user