Is it OK to use computed properties on the model for template display logic?

A colleague and I have been thinking on a topic today and I’d love to get the feedback of other ember developers.

I have a webform that is rendered with a model. The model will either be saved or destroyed and has some hidden fields that render if certain inputs are valid. For example if ‘Other’ is selected in a down menu, a free text field will appear on the form.

This if logic is managed by an {{#if property}} block and the question we were pondering today is where it is valid to have that property.

By default you would probably create the property on the controller. However, I realized that this form would be using two different controllers based on whether it was being rendered on a /new or /edit route and I’d have to create them twice.

So instead I added the property via a computed property within the model being worked on by the form.

This has a few benefits. It means that the model is always carrying the information it needs to render the form correctly as per the data within, whether it is used in /new',/edit` or anywhere else. It also means that the properties don’t have to be duplicated in multiple controllers. It works very well.

But it does feel a bit wrong. It feels like it’s a level of state management that possibly shouldn’t be in the model as it’s working on a pure template concern. However, the properties are very specific to the model and will only be needed in cases where the model is being worked on in some respect.

You could argue that it isn’t that much different from the fullname example in the Guide docs - a computed property not returned to the API that is used to dictate some display behavior. Although the return of fullname is a piece of data, technically so is the return of the show_other_field as it’s just a boolean.

So although it works (super well) it feels like I’m probably violating one of Embers core principles with this kind of implementation. I’d love to know what experienced Ember devs think and where the best location for these kinds of properties in.

This is a good question and I think there’s a ton of nuance (and is something I’ve gotten wrong many many times).

The first question I try to ask before I put a computed property on the model is something like — is this truly a model concern? (It starts to get really fuzzy when you consider something like a computed property based on an async relationship!)

If I’m unsure, I ask am I reaching for this solution only because it’s convenient? If true, then it might be good to consider some other patterns.

The first pattern I might reach for in this case would be to create a component for the form, that way you can use it in each of the routes that you have. That component can “decorate” the model with the visual-specific properties. For example, that may look something like this:

templates/routes/new.hbs

<MyModelForm @model={{this.model}} />

templates/routes/edit.hbs

<MyModelForm @model={{this.model}} />

components/MyModelForm.js

export default MyModelForm extends Component {
  get showOtherField() {
    return this.args.model.foo === 'other';
  }
}

templates/components/MyModelForm.hbs

{{#if this.showOtherField}}
  <select></select>
{{/if}}

If your question is much larger than something that’d be solved by a single component and you want your model to have a bunch of computed properties that are available everywhere, you still might be better served to keep them out of the model and reach for some sort of wrapper class for your model (Ember has the ObjectProxy which you could use for this — though, it has some gotchas related to ES Getters/Setters).

Given how well native classes are now supported in Ember, too, you could create your own class that defines a number of computed properties and gives access to the underlying model.

routes/new.js & routes/edit.js

import DecoratedModel from '../utils/DecoratedModel.js';

export default class NewRoute extends Route {
  async model() {
    const model = this.store.find('my-model', 1);

    return new DecoratedModel(model);
  }
}

utils/DecoratedModel.js

export default class DecoratedModel {
  @tracked model;

  constructor(model) {
    this.model = model;
  }

  get showOtherField() {
    return this.model.foo === 'other';
  }
}
3 Likes

Thank you for taking the time to give such a thorough reply!

It would never have occurred to me to create a new class and allow the model to inherit the properties from them. It seems like a great solution. I do have one question though, namely whether it could be considered to be a layer of abstraction that isn’t needed?

The outcome it seems is the same - computed properties that are observing model properties and being shipped alongside the model to be used for later. However, by creating another class I might be putting that code somewhere it might not be obvious to look for it.

Putting it in the model makes it obvious - the CPs watch model properties and they are right there alongside the properties they observe. Extrapolating out to a decorator means that another developer is going to have to go and find it.

Of course if you are using the same properties on multiple models it becomes a no brainer to use a decorator. I’d be interested in hearing your thoughts on the potential abstraction it introduces

Yea! That’s a very fair point — it being clear where to look is an important consideration (that’s part of the reason why I would prefer to the Component-based approach).

There’s a few things that come to mind, too:

  • You may end up using the same model in multiple places where in some scenarios, those “decorations” matter and another where they don’t. This strategy affords you the flexibility to decide when they apply and when they don’t.
  • Ember Changeset does something like this for the specialized concern of buffering changes to validate them.
  • A long-long-long time ago, this was kind-of a core feature of ember that has since been moved away from (looking at you itemController).
  • If properties are just added to the model, you run the risk of it becoming an append-only cluster of confusing functionality that’s nearly impossible to test (I know this…from…uhh… “experience” :stuck_out_tongue: )

When I first started with Ember my apps were small and I would graft presentation logic onto the model with computed properties. As these apps grew that quickly became a problem as the models would get bigger and bigger to the point where a new feature would take a very long time because once I opened the model my eyes would glaze over with too much information happening and no organization. The meaning of a computed property was quickly lost because it was decoupled from it use case (the presentation).

About a year ago I would address this dilemma using proxy objects. Specifically ObjectProxy. Which my component would use to wrap the model. This way the presentation was separated from the model.

I’ve since realized that there are only a small set of use cases for this concept because it does obfuscate the real implementation with a facade. Originally thought to be a good thing it really only works if you are providing a clear API (like ember-data does) but fails in many case because there are a very few people who actually understand Proxies.

I’ve found the best balance to be provider components (also called contextual components). They separate presentation logic from business logic and are explicit in their use.

First I break down the role the presentation side needs. At a high level I ask is this a business logic concern or a presentation concern. I use the litmus test of is this property something that represents actual data I would want to perform calculations on or is this something only the end user will see. I also look at the cost (complexity) to implement.

There is a great deal of fuzziness in the borders here.
  Only used in code Only shown to user
Complex Lib Object/Service Provider Component
Simple Model CP Model CP/Component CP/Helper

In the case of say fullName (computed from firstName and lastName) I would classify that as Only shown to user and Simple. I’d ask myself is this a general concept that spans multiple different models types (Helper)? No. Will this value only ever be used in this one component ever (Component CP)? No. Then this is best as a Model CP.

Provider Component Examples

In the case where I feel comfortable that a provider component is most appropriate here is how I implement them:

Presenter Example

Usage

<FileStatusPresentor @status={{this.model.status}} as |status|>
  <div class="file-status {{status.statusClass}}">
    <i class="fa fa-{{status.iconClass}}"></i>
    {{status.label}}
  </div>
</FileStatusPresentor>

JavaScript

// app/components/file-status-provider.js
import Component from '@ember/component';
import { FILE_STATUSES as FS } from '../models/file';

export const FILE_STATUS_CLASSES = Object.freeze({
  [FS.READONLY]: 'read-only',
  [FS.WRITABLE]: 'writable',
  [FS.HIDDEN]: 'hidden'
});

export const FILE_STATUS_ICONS = Object.freeze({
  [FS.READONLY]: 'lock',
  [FS.WRITABLE]: 'pencil',
  [FS.HIDDEN]: 'ghost'
});

export const FILE_STATUS_LABELS = Object.freeze({
  [FS.READONLY]: 'Read Only',
  [FS.WRITABLE]: 'Can Write',
  [FS.HIDDEN]: 'Hidden'
});

export default Component.extend({
  tagName: '',
  statusClasses: FILE_STATUS_CLASSES,
  icons: FILE_STATUS_ICONS,
  labels: FILE_STATUS_LABELS
});

Template

{{! app/templates/components/file-status-provider.hbs }}
{{yield (hash
  statusClass=(get this.statusClasses @status)
  iconClass=(get this.icons @status)
  label=(get this.labels @status)
)}}

Selector Example

I also expand this idea to arrays as well.

Usage

<FileSelector as |selector|>
  {{@each this.files as |file|}}
    <div class="file {{if (get selector.check file.id) 'selected'}}">
      {{file.name}}
      {{#if (get selector.checkID file.id)}}
        <button {{action selector.deselect file}}>Deselect</button>
      {{else}}
        <button {{action selector.select file}}>Select</button>
      {{/if}}
    </div>
  {{/each}}
  <button {{action "saveSelection" selector.selected}}>Save</button>
</FileSelector>

JavaScript

// app/components/file-selector.js
import Component from '@ember/component';
import { computed } from '@ember/object';

export default Component.extend({
  tagName: '',

  selectedIDLookup: computed('selected.[]', function() {
    let lookup = {};
    for (let file of this.selected) {
      lookup[file.id]: true;
    }
    return lookup;
  }),

  init() {
    this._super(...arguments);
    this.set('selected', []);
  },

  actions: {
    addFile(file) {
      if (this.selectedIDLookup[file.id]) return;
      this.selected.pushObject(file);
    },

    removeFile(file) {
      this.selected.removeObject(file);
    }
  }
});

Template

{{! app/templates/components/file-selector.hbs }}
{{yield (hash
  selected=this.selected
  checkID=this.selectedIDLookup
  select=(action "addFile")
  deselect=(action "removeFile")
)}}
2 Likes

Thanks for providing such a detailed insight into your process. It’s definitely a mindset I should be adopting when I consider these cases.

I do agree with you and @Spencer_Price that contextual components are useful for this kind of behaviour.

In all honesty the reason I looked for another solution to defer to (ie, the model) was because the form is being created from an existing ember-bootstrap {{#bs-form}} component and I am very unfamiliar with extending out from components in existing libraries.

I guess in this instance I would create a component like my-very-own-form as a wrapper for a {{#bs-form}} and have the properties handling the presentation concerns on the wrapper.

It’s funny how often in Ember I can end up with a solution that ‘works’ but still leaves me with a feeling that I’m invoking some kind of anti-pattern or not realising some key piece of functionality that is available to me.

Actually in this case I would use the provider component I demonstrate above. That way your customized computed properties would be separate from both the model and the form itself.

Because you used {{#bs-form}} above I will revert my example to legacy curly brace syntax this time.

{{#my-very-own-presenter model=this.model as |presenter|}}
  {{#bs-form}}
    do something with {{presentor.myValue}}
  {{/bs-form}}
{{/my-very-own-presenter}}

Hi Spencer

I was having a go at implementing your Decorator solution today and ran into some issues with your use of Octane-like syntax.

I’ve updated my app to the recently released 3.10.1 but run into problems when I try to use @tracked type syntax.

After struggling to find any real documentation on the subject beyond this post - is it an Octane feature that hasn’t made into releases yet?

Ahh yes @sorvah — it’d require using the Octane blueprint: https://octane-guides-preview.emberjs.com/release/getting-started/quick-start/#toc_create-a-new-application