Skip to content

Latest commit

 

History

History
249 lines (194 loc) · 6.21 KB

0418-deprecate-route-render-methods.md

File metadata and controls

249 lines (194 loc) · 6.21 KB
  • Start Date: 2018-19-12
  • RFC PR: #418
  • Ember Issue: (leave this empty)

Deprecate Route render APIs

Summary

This RFC proposses the deprecation of Route#render, Route#renderTemplate and named {{outlet}} APIs.

Motivation

These APIs are largely holdovers from a time where components where not as prominent in your typical Ember application. While they are still documented, these APIs created an interesting coupling between the Route and the template. These APIs are also prone to breaking conventional naming conventions, which can lead to confusion for developers. Another issue is that it is unclear how something like this works with route based tree shaking, as there are no strong conventions or direct imports as to what is actually being used.

Transition Path

The migration plan here is going to be somewhat situational based on the UI that was being constructed. For cases where named outlets were being used it is likely that they should just be moved to components. For cases where you were escaping the existing DOM hierarchy to render a template somewhere else in the DOM, one should use the built-in {{in-element}} helper or an addon like ember-elsewhere. Below are some example of how a migration would look.

Migrating Named Outlets

Given:

Ember.Route.extend({
  // ...

  renderTemplate() {
    this.render('cart', {
      into: 'checkout',
      outlet: 'cart',
      controller: 'cart'
    })
  }
})
{{! checkout.hbs}}
<section id="items">
  {{outlet}}
</section>
<aside>
  {{outlet "cart"}}
</aside>

This would tell Ember to render cart.hbs into checkout.hbs at the {{outlet "cart"}} and use the cart controller to back the cart.hbs template. This is pretty confusing pattern and creates this implicit coupling that is spread between the Route and template.

Luckily, we can express this entire concept with components.

{{! checkout.hbs}}
<section id="items">
  {{outlet}}
</section>
<aside>
  <Cart />
</aside>

In the event you were using model to derive what to render, you can us the {{component}} helper to dynamically render a component.

Migrating Hiearchy Escaping

Given:

{{! app/templates/authenticated.hbs}}

<nav>
  <h1>ACME Corp.</h1>
  <section>
    {{outlet "account"}}
  </section>
</nav>

<section id="content">
  {{outlet}}
</section>
{{! app/templates/account.hbs }}
{{#link-to 'account'}}
  <img src="{{this.img}}" alt="{{this.name}} />
{{/link-to}}
// app/routes/authenticated.js
import Route from '@ember/route';
import { inject as service } from '@ember/service';

export default Route.extend({
  // ...
  user: service('user'),
  renderTemplate() {
    if (this.user.isLoggedIn) {
      this.render('account', {
        into: 'applcation',
        outlet: 'account',
        controller: 'account'
      });
    } else {
      this.transitionTo('login')
    }
  }
});

One way this could be migrated is like the following:

{{! app/templates/authenticated.hbs}}

<nav>
  <h1>ACME Corp.</h1>
  <section id="account-placeholder"></section>
</nav>

<section id="content">
  {{outlet}}
</section>
{{! app/templates/authenticated/campaigns.hbs }}

{{outlet}}

{{#in-element this.accountPlaceholder}}
  {{#link-to 'account'}}
    <img src="{{this.account.img}}" alt="{{this.account.name}} />
  {{/link-to}}
{{/in-element}}
// app/routes/authenticated.js
import Route from '@ember/route';
import { inject as service } from '@ember/service';

export default Route.extend({
  //...
  user: service('user'),
  beforeModel() {
    if (!this.user.isLoggedIn) {
      this.transitionTo('login')
    }
  }
});
// app/controller/authenticated/campaigns.js
import Route from '@ember/route';
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { inject as controller } from '@ember/controller';

export default Controller.extend({
  //...
  account: controller('account')
  init() {
    this._super(...arguments);
    this.accountPlaceholder = document.getElementById('account-placeholder');
  }
});

If you want to do this with components you could do the same thing as the following:

{{! app/templates/authenticated.hbs}}

<nav>
  <h1>ACME Corp.</h1>
  <section id="account-placeholder"></section>
</nav>

<section id="content">
  {{outlet}}
</section>
{{! app/templates/authenticated/campaigns.hbs }}
{{outlet}}

<UserAccount />
{{! app/templates/components/user-account.hbs }}
{{#in-element this.accountPlaceholder}}
  {{#link-to 'account'}}
    <img src="{{this.account.img}}" alt="{{this.account.name}} />
  {{/link-to}}
{{/in-element}}
// app/routes/authenticated.js
import Route from '@ember/route';
import { inject as service } from '@ember/service';

export default Route.extend({
  //...
  user: service('user'),
  beforeModel() {
    if (!this.user.isLoggedIn) {
      this.transitionTo('login')
    }
  }
});
// app/components/user-account.js
import Component from '@ember/route';
import { inject as controller } from '@ember/controller';

export default Component.extend({
  // ...
  account: controller('account'),
  init() {
    this._super(...arguments);
    this.accountPlaceholder = document.getElementById('account-placeholder');
  }
});

How We Teach This

This has not been a mainline API for quite some time now. The guides do not mention this functionality at all. It is likely that the majority of Ember applications do not use these APIs.

Drawbacks

The drawback of this is that it is churn for applications that are relying heavily of these imperative APIs to construct their UI. They will need to encapsulate and use the existing declarative APIs.

Alternatives

No real alternatives as we want to move away from these style of imperative APIs in favor of declarative ones.

Unresolved questions

Optional, but suggested for first drafts. What parts of the design are still TBD?