Skip to content

Commit

Permalink
Add support for nested attributes in groupBy filter. Fixes #1198
Browse files Browse the repository at this point in the history
  • Loading branch information
ogonkov authored and fdintino committed Jul 12, 2020
1 parent 7087fa9 commit 1e29863
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 5 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ Changelog
[ogonkov](https://github.com/ogonkovv)!
* Fix precompile binary script `TypeError: name.replace is not a function`.
Fixes [#1295](https://github.com/mozilla/nunjucks/issues/1295).
* Add support for nested attributes on
[`groupby` filter](https://mozilla.github.io/nunjucks/templating.html#groupby);
respect `throwOnUndefined` option, if the groupby attribute is undefined.
Merge of [#1276](https://github.com/mozilla/nunjucks/pull/1276); fixes
[#1198](https://github.com/mozilla/nunjucks/issues/1198). Thanks
[ogonkov](https://github.com/ogonkovv)!

3.2.1 (Mar 17 2020)
-------------------
Expand Down
42 changes: 42 additions & 0 deletions docs/templating.md
Original file line number Diff line number Diff line change
Expand Up @@ -1160,6 +1160,48 @@ green : james jessie
blue : john jim
```

Attribute can use dot notation to use nested attribute, like `date.year`.

**Input**

```jinja
{% set posts = [
{
date: {
year: 2019
},
title: 'Post 1'
},
{
date: {
year: 2018
},
title: 'Post 2'
},
{
date: {
year: 2019
},
title: 'Post 3'
}
]
%}
{% for year, posts in posts | groupby("date.year") %}
:{{ year }}:
{% for post in posts %}
{{ post.title }}
{% endfor %}
{% endfor %}
```

**Output**
:2018:
Post 2
:2019:
Post 1
Post 3

### indent

Indent a string using spaces.
Expand Down
2 changes: 1 addition & 1 deletion nunjucks/src/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ function forceescape(str) {
exports.forceescape = forceescape;

function groupby(arr, attr) {
return lib.groupBy(arr, attr);
return lib.groupBy(arr, attr, this.env.opts.throwOnUndefined);
}

exports.groupby = groupby;
Expand Down
50 changes: 48 additions & 2 deletions nunjucks/src/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,58 @@ function isObject(obj) {

exports.isObject = isObject;

function groupBy(obj, val) {
/**
* @param {string|number} attr
* @returns {(string|number)[]}
* @private
*/
function _prepareAttributeParts(attr) {
if (!attr) {
return [];
}

if (typeof attr === 'string') {
return attr.split('.');
}

return [attr];
}

/**
* @param {string} attribute Attribute value. Dots allowed.
* @returns {function(Object): *}
*/
function getAttrGetter(attribute) {
const parts = _prepareAttributeParts(attribute);

return function attrGetter(item) {
let _item = item;

for (let i = 0; i < parts.length; i++) {
const part = parts[i];

// If item is not an object, and we still got parts to handle, it means
// that something goes wrong. Just roll out to undefined in that case.
if (hasOwnProp(_item, part)) {
_item = _item[part];
} else {
return undefined;
}
}

return _item;
};
}

function groupBy(obj, val, throwOnUndefined) {
const result = {};
const iterator = isFunction(val) ? val : (o) => o[val];
const iterator = isFunction(val) ? val : getAttrGetter(val);
for (let i = 0; i < obj.length; i++) {
const value = obj[i];
const key = iterator(value, i);
if (key === undefined && throwOnUndefined === true) {
throw new TypeError(`groupby: attribute "${val}" resolved to undefined`);
}
(result[key] || (result[key] = [])).push(value);
}
return result;
Expand Down
129 changes: 127 additions & 2 deletions tests/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,34 @@
});

it('groupby', function(done) {
const namesContext = {
items: [{
name: 'james',
type: 'green'
},
{
name: 'john',
type: 'blue'
},
{
name: 'jim',
type: 'blue'
},
{
name: 'jessie',
type: 'green'
}]
};
equal(
'{% for type, items in items | groupby("type") %}' +
':{{ type }}:' +
'{% for item in items %}' +
'{{ item.name }}' +
'{% endfor %}' +
'{% endfor %}',
namesContext,
':green:jamesjessie:blue:johnjim');

equal(
'{% for type, items in items | groupby("type") %}' +
':{{ type }}:' +
Expand All @@ -270,10 +298,107 @@
},
{
name: 'jessie',
type: 'green'
color: 'green'
}]
},
':green:jamesjessie:blue:johnjim');
':green:james:blue:johnjim:undefined:jessie');

equal(
'{% for year, posts in posts | groupby("date.year") %}' +
':{{ year }}:' +
'{% for post in posts %}' +
'{{ post.title }}' +
'{% endfor %}' +
'{% endfor %}',
{
posts: [
{
date: {
year: 2019
},
title: 'Post 1'
},
{
date: {
year: 2018
},
title: 'Post 2'
},
{
date: {
year: 2019
},
title: 'Post 3'
}
]
},
':2018:Post 2:2019:Post 1Post 3');

equal(
'{% for year, posts in posts | groupby("date.year") %}' +
':{{ year }}:' +
'{% for post in posts %}' +
'{{ post.title }}' +
'{% endfor %}' +
'{% endfor %}',
{
posts: [
{
date: {
year: 2019
},
title: 'Post 1'
},
{
date: {
year: 2018
},
title: 'Post 2'
},
{
meta: {
month: 2
},
title: 'Post 3'
}
]
},
':2018:Post 2:2019:Post 1:undefined:Post 3');

equal(
'{% for type, items in items | groupby({}) %}' +
':{{ type }}:' +
'{% for item in items %}' +
'{{ item.name }}' +
'{% endfor %}' +
'{% endfor %}',
namesContext,
':undefined:jamesjohnjimjessie'
);

const undefinedTemplate = (
'{% for type, items in items | groupby("a.b.c") %}' +
':{{ type }}:' +
'{% for item in items %}' +
'{{ item.name }}' +
'{% endfor %}' +
'{% endfor %}'
);
equal(
undefinedTemplate,
namesContext,
':undefined:jamesjohnjimjessie'
);

expect(function() {
render(
undefinedTemplate,
namesContext,
{
throwOnUndefined: true
}
);
}).to.throwError(/groupby: attribute "a\.b\.c" resolved to undefined/);

finish(done);
});
Expand Down

0 comments on commit 1e29863

Please sign in to comment.