Data Architecture Critique

My app’s layout has a header, main section, and an optional footer.

Here is application.hbs

{{nav-bar}}
<section class="main">
  {{outlet}}
</section>
{{#if displayFooter}}
  {{nav-footer}}
{{/if}}

In the, /company Route, I load a lot of relational data - a company’s employees, a company’s store locations, etc. However, I need to use that data in the header and footer - which being adjacent HTML elements to the company template I can’t easily do. Furthermore, the data needs to be synchronized - if I add or remove a store from the company template, that needs to be reflected in the header and footer too.

I’ve accomplished this by making a navigationService and setting all the queries in the model of /companies Route to the service:

afterModel(resolvedModel, transition){
  this.navigationService.setProperties({
    company: resolvedModel.company,
    employees: resolvedModel.employees,
    stores: resolvedModel.stores,
    ...
  });
}

Then, I use navigationService.company instead of model.company in the company template, and now have access to this data in both the header and footer.

What I’m looking for is a critique of this data architecture. Is there something better I could be doing? Did I make a mistake by putting the header/footer outside the outlet in application.hbs? Am I missing something else? Or is this really the optimal way to do what I’m trying to?

Thanks!

If you need to keep the header and footer in application.hbs, I think a service is an all right solution. As I’m still new to Ember, I’d like to hear what others think and learn from them too.

The project that I work on used to be architected like yours. The footer needed to read and interact with model but couldn’t directly access it. We would end up writing a lot of peekRecord and peekAll, rely on a pub-sub service to refresh data (while making sure to destroy the event listeners when appropriate), and rely on observers to trigger events. We would end up copy-pasting components and tests for these components for each new route, often without understanding if we really needed to.

Recently, I changed the architecture to take advantage of nested routes. My application.hbs now has a header (as it is mostly static) and {{outlet}}. Each route template contains some visualization of the model and a reusable footer. The developer gets to decide what goes into the footer for a particular route, and the footer now has direct access to model. I think my whole team, especially junior developers who are learning Ember (and may not know what is the Ember way and what is our way), was happy with this change.

When coming up with a new architecture, reading https://dockyard.com/blog/2018/11/26/how-to-yield-an-ember-component-in-multiple-places helped me understand what I had to do in order to (1) give developers freedom in customizing reusable components and (2) make the new UI look exactly like the old, so that my team could replace code one at a time.

First question would be, do the header and footer render correctly outside the /company route? That is, are they in a “complete” state apart from the data loaded by /company?

If not, I would nest them under the /company route so they can benefit from the router’s conventions.

If they are, and they only change what they render when the user enters /company, I think what you have is OK, but I’d work on finding a more declarative solution for your rendering logic. (Calling setProperties in afterModel is imperative and only accounts for one logical path through your application; what if someone later adds a feature within the /company route that allows the user to select a different company? This logic might not run and therefore your header would get out of sync with the selected company.)

Instead I like to use components in templates for this sort of thing, since Handlebars by its nature is declarative. I try to look to OSS Addons for precedent – Ember Page Title is an example of an API that allows a local template to mutate an ancestor’s state, using only this single helper:

{{title company.name}}

WIth this helper you now have a guarantee that your page’s title will stay up-to-date with the latest value of company.name property, no matter how company is set.

So, you could mimic this and write a component that would let you drop something like

{{navigation-info company=model.company}}

right into your /company template, thereby getting all the benefits of declarative rendering via HBS. That component would take care of the imperative code behind the scenes, using a service to communicate between the two parts of your application.

If your navigation header and footer have more complex requirements, you could use a project like Ember Elsewhere or the technique from the blog post @ijlee2 mentioned to give your child routes like /company more control over what’s rendered in the destination:

{{#navigation-info as |navInfo|}}
  {{#navInfo.header}}
    {{fa-icon 'building'}} {{model.company.name}}
  {{/navInfo.header}}
  
  {{#navInfo.footer}}
    {{model.employees.count}} employees
  {{/navInfo.footer}}
{{/navigation-info}}
1 Like

Thanks for the suggestions @ijlee2 and @samselikoff. I’m going to start looking into contextual components, ember-page-title and ember-elsewhere as alternatives.

@samselikoff - to answer your question about the header:

The header is rendered on every page. It includes a link to the home page and signin/signout buttons on all pages - very standard so far. There are differences in the header from page to page though. For example on the /company page, the header also includes a dropdown menu with a list of all the company stores for quick navigation. On the /employee page the header also includes a print button. Both of these elements are dependent on data loaded in the route and should only appear on those specific pages. Sidenote: when I was first tackling this issue, my first instinct was to change the design to remove these dynamic elements from the header, but that is not possible - the design’s fixed this way.

The footer is only shown on the /company page and includes a few buttons to take actions on a company.

What I thought I wanted was to keep the HTML document in proper order:

<header></header>
page content for /company, /employee, etc here
<footer></footer>

That is why I made application.hbs the way I did.

I also was hesitant of having to declare the header and footer in each top-level template to keep things DRY.

// app/templates/company.hbs
<Header @dropdownContents=model.stores />
page content for /company
<Footer @model=model>

// app/templates/employee.hbs
<Header printLink=@model.printUrl />
page content for /employee

// app/templates/store.hbs
<Header />
page content for /store

That would have removed my data issue but at the expense of lots of repeated code (there are many more pages in the app - so I’d need to do this about 20 times!)

@andrewcallahan You’re welcome, thanks for asking us (the community).

I think you can still practice DRY by using renderTemplate method in your router. I created a Twiddle to demo: https://ember-twiddle.com/edcdbad601b1d86e14936d354cffb14e.

If you click on Typical route, you will see that Ember uses the template for most-routes instead. If you click on Special route, you will see that the header displays information about your model.

Edit: The end of this documentation page mentions you can use templateName property instead. https://guides.emberjs.com/release/routing/rendering-a-template/

1 Like