How to choose an object from a nested model in a template


#1

I have the following model tree already loaded using a single request to the backend:

- document
  - group_1
    - observation_1
    - observation_2
  - group_2
    - observation_3
    - observation_4

And here are the corresponding models.

app/models/document.js

export default DS.Model.extend({
  code: DS.attr('string'),
  name: DS.attr('string'),
  groups: DS.hasMany('group'),
});

app/models/group.js

export default DS.Model.extend({
  code: DS.attr('string'),
  name: DS.attr('string'),
  document: DS.belongsTo('document'),
  observations: DS.hasMany('observation')
});

app/models/observation.js

export default DS.Model.extend({
  code: DS.attr('string'),
  name: DS.attr('string'),
  value: DS.attr('string'),
  group: DS.belongsTo('group')
});

Having the following test data

- document
  code: 'progress_note'
  name: 'Progress note'
  - group
    code: 'patient_id'
    name: 'Patient data'
    - observation
      code: 'first_name'
      name: 'First name'
      value: 'David'
    - observation
      code: 'last_name'
      name: 'Last name'
      value: 'Bowie'
  - group
    code: 'vital_signs'
    name: 'Vital signs'
    - observation
      code: 'temperature'
      name: 'Temperature'
      value: '36.7'
    - observation
      code: 'heart_rate'
      name: 'Heart rate'
      value: '60'

I can show the data iterating over the model with the following template:

<p>{{document.name}} ({{document.code}})</p>
<ul>
  {{#each document.groups as |group|}}
    <li>{{group.name}}</li>
    <ul>
    {{#each group.observations as |observation|}}
      <li><label>observation.name</label>{{input type="text" value=observation.value}}</li>
    {{/each}}
    </ul>
  {{/each}}
</ul>

However I want to write a template in which the fields appear in a predefined order. As shown in the following (non working) code:

<ul>
  <li>{{getGroup('vital_signs').name}}<li>
  <ul>
    <li>{{input type="text" value=getObservation('vital_signs', 'heart_rate').value}}</li>
    <li>{{input type="text" value=getObservation('vital_signs', 'temperature').value}}</li>
  </ul>
  <li>{{getGroup('patient_id').name}}</li>
  <ul>
    <li>{{input type="text" value=getObservation('patient_id', 'first_name').value}}</li>
    <li>{{input type="text" value=getObservation('patient_id', 'last_name').value}}</li>
  </ul>
</ul>

Any hints in how to implement the getGroup and getObservation functions?


#2

There are three approaches that jump to mind, at least to me… After typing this all out I think I prefer #3:

1. Use computed properties

on the controller/component OR on the models. This would work best for a finite and small set of predetermined/preordered groups. It also assumes there is only one group of each ‘code’ per document:

vitalSignsGroup: Ember.computed.filterBy('model.document.groups', 'code', 'vital_signs'),
patientIdGroup: Ember.computed.filterBy('model.document.groups', 'code', 'patient_id'),
//... etc. 

Then reference them in your template like:

<ul>
  <li>{{vitalSignsGroup.name}}<li>
  <ul>
    <li>{{input type="text" value=vitalSignsGroup.heart_rate}}</li>
    <li>{{input type="text" value=vitalSignsGroup.temperature}}</li>
  </ul>
...
</ul>

(You may need to do vitalSignsGroup.firstObject, or instead of using filterBy in the controller/component you could use a computed property to do the same thing but instead of returning an array just return the value, like return this.get('model.document.groups').findBy('code', 'vital_signs');, etc.)

Also you’d need to extrapolate for observations since you have multiple levels of the same basic problem, aka you need to reference the children by a code.

2. Write a custom helper

to use in your template, with an interface similar to what you wrote above. Actually because you’ll need block structure you may want to use components. This could get kinda verbose and messy though.

3. Use computed properties along with the “with” helper:

I’d actually put this computed property on your document model, and put a similar one on the group model (which collects the observations instead of the groups):

/* this would return an object of all your groups, keyed by the code, e.g.:
{
  vital_signs: <DS.Model Group>,
  patient_id: <DS.model Group>,
}
*/
codedGroups: Ember.computed('groups.@each.code', function(){
  return this.get('groups').reduce(function(collector, item){
    collector[item.get('code')] = item; 
    return collector;
  }, {});
}),

Then in your template:

{{#with model.document.codedGroups.vital_signs as |vitals|}}
  <li>{{vitals.name}}<li>
  <ul>
    <li>{{input type="text" value=vitals.codedObservations.heart_rate.value}}</li>
    ...
  </ul>
{{/with}}

#3

Dan,

Thank you very much for your quick and detailed response. I’ll do some experiments and post here about the results.

Regards, Hector


#4

Since I am just learning Ember I used the first approach.

Here are some code snippets of the working code, sorry for using some property names in Spanish.

nota_evolucion_soap.js

import Ember from 'ember';
import MedicalRecordDocument from './medical-record-document';

export default MedicalRecordDocument.extend({
  fichaIdentificacionGroup: Ember.computed('document.groups', function(){
    return this.get('document.groups').findBy('code', 'ficha_identificacion');
  }),

  apellidoPaterno: Ember.computed('fichaIdentificacionGroup.observations', function() {
    return this.get('fichaIdentificacionGroup.observations').findBy('code', 'apellido_paterno');
  }),

  apellidoMaterno: Ember.computed('fichaIdentificacionGroup.observations', function() {
    return this.get('fichaIdentificacionGroup.observations').findBy('code', 'apellido_materno');
  }),

  signosVitalesGroup: Ember.computed('document.groups', function(){
    return this.get('document.groups').findBy('code', 'signos_vitales');
  }),

  sistolica: Ember.computed('signosVitalesGroup.observations', function() {
    return this.get('signosVitalesGroup.observations').findBy('code', 'sistolica');
  }),

  diastolica: Ember.computed('signosVitalesGroup.observations', function() {
    return this.get('signosVitalesGroup.observations').findBy('code', 'diastolica');
  }),

  notaEvolucionGroup: Ember.computed('document.groups', function(){
    return this.get('document.groups').findBy('code', 'nota_evolucion');
  }),

  subjetivo: Ember.computed('notaEvolucionGroup.observations', function() {
    return this.get('notaEvolucionGroup.observations').findBy('code', 'subjetivo');
  }),

  objetivo: Ember.computed('notaEvolucionGroup.observations', function() {
    return this.get('notaEvolucionGroup.observations').findBy('code', 'objetivo');
  }),

});

nota_evolucion_soap.hbs

<h4>{{document.name}}</h4>

<div class="panel panel-default">
  <div class="panel-heading">
    <h3 class="panel-title">{{fichaIdentificacionGroup.name}}</h3>
  </div>
  <div class="panel-body">
    {{string-field name=apellidoPaterno.name string=apellidoPaterno.value on-leave-field=(action 'updateObservation' apellidoPaterno)}}
    {{string-field name=apellidoMaterno.name string=apellidoMaterno.value on-leave-field=(action 'updateObservation' apellidoMaterno)}}
  </div>
</div>

<div class="panel panel-default">
  <div class="panel-heading">
    <h3 class="panel-title">Subjetivo / Objetivo</h3>
  </div>
  <div class="panel-body">
    {{string-field name=subjetivo.name string=subjetivo.value on-leave-field=(action 'updateObservation' subjetivo)}}
    {{string-field name=objetivo.name string=objetivo.value on-leave-field=(action 'updateObservation' objetivo)}}
  </div>
</div>

<div class="panel panel-default">
  <div class="panel-heading">
    <h3 class="panel-title">{{signosVitalesGroup.name}}</h3>
  </div>
  <div class="panel-body">
    {{string-field name=sistolica.name string=sistolica.value on-leave-field=(action 'updateObservation' sistolica)}}
    {{string-field name=diastolica.name string=diastolica.value on-leave-field=(action 'updateObservation' diastolica)}}
  </div>
</div>

<div class="panel panel-default">
  <div class="panel-heading">
    <h3 class="panel-title">Análisis / Plan</h3>
  </div>
  <div class="panel-body">
    Panel content
  </div>
</div>

This is a screenshot of the resulting page.

I’ll keep studying to make this code less verbose.

Any comments are welcome.


#5

Awesome! For “just learning Ember” it looks like you’re doing really well. I like to think that we’re all “just learning Ember” :grin:

Also it’s great when people share their findings here for others.

Good luck!