2020-07-17
|~5 min read
|870 words
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?
<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:
<button {{action showDetail order}} }>
{{!-- ... --}}
</button>
Unlike React where this would be attached to an showDetail(event, order)}
two things are happening:
onclick
property, it will not receive the event
object as its first argument.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:
{{!-- ... --}}
<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:
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:
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
:
<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
:
<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:
<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
:
{{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:
//...
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:
{{#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!