Skip to content
Merged
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
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,29 @@ The value of the `loop` attribute is not a pure expressions evaluation, and it d

So you don't need to declare all the available variables (in this case, the index is skipped), and the expressions after `in` doesn't need to be a local variable, it can be any expressions.

#### Loop meta

Inside a loop, you have access to a special `loop` object, which contains information about the loop currently being executed:

- `loop.index` - the current iteration of the loop (0 indexed)
- `loop.remaining` - number of iterations until the end (0 indexed)
- `loop.first` - boolean indicating if it's the first iteration
- `loop.last` - boolean indicating if it's the last iteration
- `loop.length` - total number of items

Example:

```html
<each loop='item in [1,2,3]'>
<li>Item value: {{ item }}</li>
<li>Current iteration of the loop: {{ loop.index }}</li>
<li>Number of iterations until the end: {{ loop.remaining }} </li>
<li>This {{ loop.first ? 'is' : 'is not' }} the first iteration</li>
<li>This {{ loop.last ? 'is' : 'is not' }} the last iteration</li>
<li>Total number of items: {{ loop.length }}</li>
</each>
```

### Scopes

You can replace locals inside certain area wrapped in a `<scope>` tag. For example you can use it after [posthtml-include](https://github.com/posthtml/posthtml-include)
Expand Down
25 changes: 25 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,29 @@ function executeScope (scope, locals, node) {
return walk({ locals: scope }, node.content)
}

/**
* @description Returns an object containing loop metadata
*
* @method getLoopMeta
*
* @param {Integer|Object} index Current iteration
* @param {Object} target Object being iterated
*
* @return {Object} Object containing loop metadata
*/
function getLoopMeta (index, target) {
index = Array.isArray(target) ? index : Object.keys(target).indexOf(index)
const arr = Array.isArray(target) ? target : Object.keys(target)

return {
index: index,
remaining: arr.length - index - 1,
first: arr.indexOf(arr[index]) === 0,
last: index + 1 == arr.length,
length: arr.length
}
}

/**
* @author Jeff Escalante Denis (@jescalan),
* Denis Malinochkin (mrmlnc),
Expand Down Expand Up @@ -324,10 +347,12 @@ function walk (opts, nodes) {
// run the loop, different types of loops for arrays and objects
if (Array.isArray(target)) {
for (let index = 0; index < target.length; index++) {
opts.locals.loop = getLoopMeta(index, target)
m.push(executeLoop(keys, target[index], index, opts.locals, treeString))
}
} else {
for (let key in target) {
opts.locals.loop = getLoopMeta(key, target)
m.push(executeLoop(keys, target[key], key, opts.locals, treeString))
}
}
Expand Down
24 changes: 24 additions & 0 deletions test/expect/loop_metadata.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<ul>

<li>Item: 1</li>
<li>Current iteration of the loop: 0</li>
<li>Number of iterations until the end: 2 </li>
<li>This is the first iteration</li>
<li>This is not the last iteration</li>
<li>Total number of items: 3</li>

<li>Item: 2</li>
<li>Current iteration of the loop: 1</li>
<li>Number of iterations until the end: 1 </li>
<li>This is not the first iteration</li>
<li>This is not the last iteration</li>
<li>Total number of items: 3</li>

<li>Item: 3</li>
<li>Current iteration of the loop: 2</li>
<li>Number of iterations until the end: 0 </li>
<li>This is not the first iteration</li>
<li>This is the last iteration</li>
<li>Total number of items: 3</li>

</ul>
47 changes: 47 additions & 0 deletions test/expect/loop_nested_metadata.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<ul>

<li>foo: [1,2]</li>
<li>There are 1 iterations remaining on the foo object</li>

<ul>
<li>Nested item value: 1</li>
<li>Current iteration of the loop: 0</li>
<li>Number of iterations until the end: 1</li>
<li>This is the first iteration</li>
<li>This is not the last iteration</li>
<li>Total number of items: 2</li>
</ul>

<ul>
<li>Nested item value: 2</li>
<li>Current iteration of the loop: 1</li>
<li>Number of iterations until the end: 0</li>
<li>This is not the first iteration</li>
<li>This is the last iteration</li>
<li>Total number of items: 2</li>
</ul>


<li>bar: [3,4]</li>
<li>There are 0 iterations remaining on the bar object</li>

<ul>
<li>Nested item value: 3</li>
<li>Current iteration of the loop: 0</li>
<li>Number of iterations until the end: 1</li>
<li>This is the first iteration</li>
<li>This is not the last iteration</li>
<li>Total number of items: 2</li>
</ul>

<ul>
<li>Nested item value: 4</li>
<li>Current iteration of the loop: 1</li>
<li>Number of iterations until the end: 0</li>
<li>This is not the first iteration</li>
<li>This is the last iteration</li>
<li>Total number of items: 2</li>
</ul>


</ul>
10 changes: 10 additions & 0 deletions test/fixtures/loop_metadata.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<ul>
<each loop='item in items'>
<li>Item: {{ item }}</li>
<li>Current iteration of the loop: {{ loop.index }}</li>
<li>Number of iterations until the end: {{ loop.remaining }} </li>
<li>This {{ loop.first ? 'is' : 'is not' }} the first iteration</li>
<li>This {{ loop.last ? 'is' : 'is not' }} the last iteration</li>
<li>Total number of items: {{ loop.length }}</li>
</each>
</ul>
16 changes: 16 additions & 0 deletions test/fixtures/loop_nested_metadata.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<ul>
<each loop='value, key in items'>
<li>{{ key }}: {{ JSON.stringify(value) }}</li>
<li>There are {{ loop.remaining }} iterations remaining on the {{ key }} object</li>
<each loop='item in value'>
<ul>
<li>Nested item value: {{ item }}</li>
<li>Current iteration of the loop: {{ loop.index }}</li>
<li>Number of iterations until the end: {{ loop.remaining }}</li>
<li>This {{ loop.first ? 'is' : 'is not' }} the first iteration</li>
<li>This {{ loop.last ? 'is' : 'is not' }} the last iteration</li>
<li>Total number of items: {{ loop.length }}</li>
</ul>
</each>
</each>
</ul>
12 changes: 12 additions & 0 deletions test/test-loops.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,15 @@ test('Loops - expression error', (t) => {
t.is(err.message, 'Invalid or unexpected token')
})
})

test('Loops - metadata', (t) => {
return process(t, 'loop_metadata', {
locals: { items: [1, 2, 3] }
})
})

test('Loops - nested metadata', (t) => {
return process(t, 'loop_nested_metadata', {
locals: { items: { foo: [1, 2], bar: [3, 4] } }
})
})