Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for nested attributes in groupBy filter #1276

Merged
merged 1 commit into from
Jul 13, 2020
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
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