Unless helper dilemma


#1

Several times I’ve worked with Ember and Ember data I faced the following dilemma with the unless helper in templates:

{{#unless model.someAsyncRelationship.someValue}}
    // do something
{{/unless}}

The problem is, the code above will be executed if the model is not loaded yet (undefined or null value), but the code block should only be executed/shown when the model is loaded and the value is falsy.

I can easily work around this problem by checking if model.someAsyncRelationship.isFulfilled, but checking this property does not always trigger the load of the async relationship.

Other solution is to define a property on the model itself which will check for both isFullfilled and someValue.

My question is, does someone know a nice generalized solution for this dilemma.


#2

One option would be to use a combination of ember-truth-helpers. Maybe something like:

{{#if (and (not model.someAsyncRelationship.someValue) model.someAsyncRelationship.isFullfilled)}}
    // do something
{{/if}}

But that’s kind of ugly.


Another option, if you find you have this pattern a lot, is to build your own custom helper. For Example:

// app/helpers/async-not.js
import Ember from 'ember';
const { get } = Ember;

export default Ember.Helper.extend(function([isFulfilled, value], hash) {
  return isFulfilled && !value;
});

Using it:

{{#if (async-not model.someAsyncRelationship.isFulfilled model.someAsyncRelationship.someValue)}}
    // do something
{{/if}}

That still kind of leaves something to be desired though. That’s quite verbose.


Okay I think perhaps I blew the scope now, but here is a helper that tries to slim down the surface of it’s API.

// app/helpers/async-value.js
import Ember from 'ember';
const { 
  get, 
  addObserver, 
  removeObserver 
} = Ember;

export default Ember.Helper.extend({
  installObserver(relationship, key) {
    if(this._installObsRelationship !== relationship || this._installObsKey !== key) {
      this.uninstallObserver();
      this._installObsRelationship = relationship;
      this._installObsKey = key;
      addObserver(this._installObsRelationship, this._installObsKey, this, this.recompute);
      addObserver(this._installObsRelationship, 'isFulfilled', this, this.recompute);
    }
  },
  uninstallObserver() {
    if(this._installObsRelationship && this._installObsKey) {
      removeObserver(this._installObsRelationship, this._installObsKey, this, this.recompute);
      removeObserver(this._installObsRelationship, 'isFulfilled', this, this.recompute);
      this._installObsRelationship = null;
      this._installObsKey = null;
    }
  },
  willDestroy() {
    this.uninstallObserver();
  },
  compute([asyncRelationship, key], hash) {
    this.installObserver(asyncRelationship, key);
    return asyncRelationship && !get(asyncRelationship, key) && get(asyncRelationship, 'isFulfilled');
  }
});

Using it:

{{#if (async-not model.someAsyncRelationship 'someValue')}}
    // do something
{{/if}}

In this case, we have to manually manage some observers for dynamic values. This is something that HTMLBars / Ember do for us automatically when we use full paths (model.asyncRelationship.someValue), however since we want to reduce the redundancy of providing two full paths, we have to manually manage two observers. Seeing that should make you really appreciate Ember.

Using observers in this way might not be the “recommended solution”. But if you have this pattern a lot in your app, it’s what I would do.

I made an ember-twiddle to help demonstrate this: https://ember-twiddle.com/9caf25254d6d35c436c2


#3

The helper looks great. Was also thinking of creating a custom helper, but not experimented with it yet.

I use this pattern at several places yes, so that’s why I was looking for a general way to handle it. The given helper is a nice solution for this!

Thanks for your time to think of a possible solution.


#4

After some testing, I have a little improvement to the above.

It seems there is a fraction of time when isFulfilled is set by ember data and when the key value is set on the model. So in the above code, isFulfilled will be true and key will a fraction of time still be undefined resulting in returning a wrong result.

To solve this, we can check on an undefined value in the final result:

return asyncRelationship && typeof get(asyncRelationship, key) !== 'undefined' && !get(asyncRelationship, key) && get(asyncRelationship, 'isFulfilled');

But I think we can also simplify the observers by removing the isFulfilled observers. It that case it will only look for changes on the key, which will change when isFulfilled is already true.