ember: navigating an application

2020-07-17

 | 

~5 min read

 | 

870 words

expo component tree

Trying to understand a little more about ember and how data flows through the app - making as many parallels to a React app as I can.

The first thing I wanted to know is where do these values in the list come from.

So, for example, this card says DoorDash, Lafayette F, etc. - where are those values coming from?

card/template.hbs
<button class={{this.style.detailButton}} {{action showDetail order}} data-order-card
  data-order-card-selected={{this.isSelected}}>
  <div class={{this.cardContentClass}} data-test-card={{order.id}}>
    <div class={{this.style.handoffMode}} data-test-handoff>{{format-order-provider-label order}}</div>
    <div class={{this.style.nameAndCost}}>
      <div class={{this.style.name}} data-test-name>{{customerName}}</div>
      <div class={{this.style.cost}} data-test-total>{{format-currency order.total}}</div>
    </div>
    <div class={{this.style.time}} data-test-time>{{dateLabel}}</div>
    <div class={{this.style.badges}}>{{orders/badges order=order}}</div>
  </div>
</button>

Before going too much further, one interesting thing here is the action:

card/template.hbs
<button {{action showDetail order}} }>
  {{!-- ... --}}
</button>

Unlike React where this would be attached to an showDetail(event, order)} two things are happening:

  1. Because the action is not assigned to an onclick property, it will not receive the event object as its first argument.
  2. showDetail in this case is a reducer to update the Redux state and which takes an order as its argument.

So, moving along - how do we get the values that populate the template? For example:

card/template.hbs
{{!-- ... --}}
    <div class={{this.style.nameAndCost}}>
        <div class={{this.style.name}} data-test-name>{{customerName}}</div>
        <div class={{this.style.cost}} data-test-total>{{format-currency order.total}}</div>
    </div>
{{!-- ... --}}

First customerName is a derived value based on the properties that are passed to the card in the order object. Specifically:

card/component.ts
export default class Card extends Component {
  order: Order
  //...
  @computed("order")
  get customerName() {
    return (
      formatTitleCase([this.order.customer.firstName]) +
      " " +
      formatFirstChar([this.order.customer.lastName])
    )
  }
}

Here, we’re generating a new property, customerName that’s a composite of two fields that are on the order. This is very similar in practice to React’s useEffect where the @computed('order') is the dependency array - every time it changes, the component will reevaluate. In this case, it could have been more specific - instead of watching the entire object, it could have been @computed('order.customer.firstName','order.customer.lastName').

The second piece, the order total, is a bit of magic. The {{format-currency order.total}} is actually a call to formatCurrency with the argument of order.total. But how is it made available? Ember magic!

When content is passed in mustaches {{}}, Ember will first try to find a helper or component with that name.

But how does Ember know what’s a helper? Well, we can use a full Helper class, but in this case, it’s unnecessary and we just use helper, a lighter-weight approach, that creates a pure-function accessible by Ember:

formatCurrency is defined as:

helpers/format-currency.ts
import { helper } from "@ember/component/helper"
import { isNone } from "@ember/utils"

export function formatCurrency(params) {
  if (isNone(params[0]) || isNaN(params[0]) || params[0].length === 0) {
    return "$0.00"
  }
  return `$${Number(params[0]).toFixed(2)}`
}

export default helper(formatCurrency)

Okay - so that’s some of the painting done on this component, but where does the order come from that is the source of this information?

Well, from the component tree, we see that the card is wrapped by a list:

list/template.ts
<ul id="order-summary-list" class={{this.ordersStyle}}>
  {{#if (gt orders.length 0)}}
    {{#vertical-collection orders
      estimateHeight=173
      staticHeight=false
      bufferSize=30
      as |value|}}
      <li>
        {{orders/card showDetail=showDetail order=value selectedOrderId=selectedOrder.id}}
      </li>
    {{/vertical-collection}}
  {{/if}}
</ul>

Great - here we can clearly see that the card is receiving order which is the value of the vertical-collection that takes orders. But wait, where did showDetail and selectedOrder come from?

Working our way up the tree some more, the list is a child of Orders:

orders/template.hbs
<div class={{style.container}}>
  <!-- ... -->
  {{#if hasOrders}}
    {{orders/list
      orders=orders
      selectedOrder=selectedOrder
      isFilterStarted=isFilterStarted
      showDetail=showDetail}}
    {{#if showFooter}}
      {{orders/footer orders=orders printPosOrders=printPosOrders isReadOnly=isReadOnly}}
    {{/if}}
  <!-- ... -->
  {{/if}}
</div>

Okay, so making progress, but again the orders already has the selectedOrder and showDetail (as well as isFilterStarted).

Another level up and we get to the wrapper. This appears to be mostly a styling container and doesn’t address our concerns. In fact, it’s very focused:

orders-wrapper/template.hbs
<div class={{wrapperClass}}>
  {{yield (hash
    ordersListClass=this.style.ordersList
    orderDetailClass=this.style.orderDetails
  )}}
</div>

Okay, so one more level up and we have the definitive Orders:

orders/template.hbs
{{yield (hash
    orders=orders
    selectedOrder=selectedOrder
    isFilterStarted=isFilterStarted
    showDetail=(action 'showDetail')
    <!-- ... -->
    )}}

Okay! We’re back on track - we have all of the properties we care about… but where are they coming from? By looking at the component.ts associated with orders, we get our answer:

orders/component.ts
//...
export default connect(stateToComputed, dispatchToActions)(OrdersContainer)

These details are coming from Redux and this is the level at which we’ve connected to the store! Woot! We’ve solved that part of the mystery. So, now there’s only one piece left — how are the details making their way down to the card?

So far we’ve seen what each component yields ot its children, but how is that all organized? For that, we need to look at the template for the page itself:

templates/orders.hbs
{{#app-layout/page as |page|}}
  <!-- ... -->
  {{#page.body}}
    {{#orders as |ordersHash|}}
      {{#orders-wrapper selectedOrder=ordersHash.selectedOrder as |wrapHash|}}
        <div class={{wrapHash.ordersListClass}} id="tourOrderlist">
          {{orders/orders
            orders=ordersHash.orders
            selectedOrder=ordersHash.selectedOrder
            isFilterStarted=ordersHash.isFilterStarted
            showDetail=ordersHash.showDetail
            <!-- ... -->
          }}
        </div>
        <!-- ... -->
    {{/orders}}
  {{/page.body}}
{{/app-layout/page}}

And suddenly, it becomes very clear how the wrapper doesn’t stop the propagation of all of those necessary values! They’re passed along as part of the orderHash!

This was a little walk through of how to navigate an Ember application. Hope you found it helpful.



Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!