Issue with nested promises for a group-by helper


#1

I am creating a group-by helper (addon when ready) which supports nested async paths (relationships). The helper works for an array with nested objects but for some reason it is not displaying correctly for nested model relationships. I believe the issue is related to promises resolving and the helper recomputing/updating.

I created an ember-twiddle to demonstrate the issue.

group-by helper:

/* Reference
 * https://gist.github.com/Asherlc/cc438c9dc13912618b8b
 * https://github.com/DockYard/ember-composable-helpers/blob/master/addon/helpers/group-by.js
 * */
import Ember from 'ember';
import DS from 'ember-data';

const {
  A,
  isArray,
  Helper,
  RSVP,
  computed,
  computed: { mapBy },
  defineProperty,
  get,
  observer,
  set,
  isEmpty,
} = Ember;

const {
  PromiseObject,
} = DS;

const groupBy = function () {
  const groups = Ember.Object.create({});
  const array = get(this, 'array');
  const byPath = get(this, 'byPath');
  const paths = byPath.split('.');

  const prom = RSVP.resolve(array).then((items) => {
    return items.map((itemPromise) => {
      return RSVP.resolve(itemPromise).then((item) => {
        const groupNamePromise = paths.reduce(
          (previousItemPromise, path) => RSVP.resolve(previousItemPromise)
            .then(previousItem => get(previousItem, path))
          , item);

        return RSVP.resolve(groupNamePromise).then((groupName) => {
          let group = get(groups, groupName);

          if (!isArray(group)) {
            group = A();
            groups[`${groupName}`] = group;
          }

          group.pushObject(item);
        });
      });
    });
  });

  return PromiseObject.create({
    promise: RSVP.resolve(prom).then(() => groups),
  });
};

export default Helper.extend({
  compute([byPath, array]) {
    set(this, 'array', array);
    set(this, 'byPath', byPath);

    return get(this, 'content');
  },

  update() {
    const byPath = get(this, 'byPath');
    if (!isEmpty(byPath)) {
      if (byPath.includes('.')) { // nested path: item.pathA.pathB.pathX
        // Dependent keys containing @each only work one level deep.
        byPath.split('.').forEach((path, index, paths) => {
          if (index === 0) {
            defineProperty(this, `_nested${index}`, mapBy('array', path));
          } else if (index + 1 === paths.length) {
            defineProperty(this, 'content', computed(`_nested${index - 1}.@each.{${path}}`,
              groupBy));
          } else {
            defineProperty(this, `_nested${index}`, mapBy(`_nested${index - 1}`, path));
          }
        });
      } else { // single path: item.pathA
        defineProperty(this, 'content', computed(`array.@each.${byPath}`, groupBy));
      }
    } else {
      defineProperty(this, 'content', null);
    }
  },

  paramsDidChanged: observer('byPath', 'array.[]', function () {
    console.debug('paramsDidChanged');
    this.update();
    this.recompute();
  }),

  contentDidChange: observer('content.[]', function () {
    console.debug('contentDidChange');
    this.recompute();
  }),
});

Sample data:

let nestedData = [
  { skill: { type: { label: 'language' }, name: 'Python' } },
  { skill: { type: { label: 'language' }, name: 'JavaScript' } },
  { skill: { type: { label: 'web' }, name: 'Django' } },
  { skill: { type: { label: 'web' }, name: 'Flask' } },
  { skill: { type: { label: 'database' }, name: 'MySQL' } },
];

Usage:

{{#each-in (group-by "skill.type.label" nestedData) as |label stacks|}}
  <h4>{{label}}</h4>
  <ul>
    {{#each stacks as |stack|}}
      <li>{{stack.skill.name}}</li>
    {{/each}}
  </ul>
{{/each-in}}

#2

Solution below with help from EmberJS fulfill multiple nested promises:

  const promises = RSVP.resolve(array).then((items) => {
    return RSVP.all(items.map((item) => {
      const itemGroup = paths.reduce(
        (previous, path) => {
          const previousItem = RSVP.resolve(previous);
          return previousItem.then(nestedItem => get(nestedItem, path));
        }, item);

      return RSVP.resolve(itemGroup).then((groupName) => {
        let group = get(groups, `${groupName}`);

        if (!isArray(group)) {
          group = A();
          groups[`${groupName}`] = group;
        }

        group.pushObject(item);
      });
    }));
  });

  return PromiseObject.create({
    promise: promises.then(() => groups),
  });