Ember RFCs

Many changes, including bug fixes and documentation improvements can be implemented and reviewed via the normal GitHub pull request workflow.

Some changes though are "substantial", and we ask that these be put through a bit of a design process and produce a consensus among the Ember core teams.

The "RFC" (request for comments) process is intended to provide a consistent and controlled path for new features to enter the framework.

Active RFC List

When you need to follow this process

You need to follow this process if you intend to make "substantial" changes to Ember, Ember Data, Ember CLI, their documentation, or any other projects under the purview of the Ember core teams. What constitutes a "substantial" change is evolving based on community norms, but may include the following:

  • A new feature that creates new API surface area, and would require a feature flag if introduced.
  • The removal of features that already shipped as part of the release channel.
  • The introduction of new idiomatic usage or conventions, even if they do not include code changes to Ember itself.

Some changes do not require an RFC:

  • Rephrasing, reorganizing or refactoring
  • Addition or removal of warnings
  • Additions that strictly improve objective, numerical quality criteria (speedup, better browser support)
  • Additions only likely to be noticed by other implementors-of-Ember, invisible to users-of-Ember.

If you submit a pull request to implement a new feature without going through the RFC process, it may be closed with a polite request to submit an RFC first.

Gathering feedback before submitting

It's often helpful to get feedback on your concept before diving into the level of API design detail required for an RFC. You may open an issue on this repo to start a high-level discussion, with the goal of eventually formulating an RFC pull request with the specific implementation design. We also highly recommend sharing drafts of RFCs in #dev-rfc on the Ember Discord for early feedback.

The process

In short, to get a major feature added to Ember, one must first get the RFC merged into the RFC repo as a markdown file. At that point the RFC is 'active' and may be implemented with the goal of eventual inclusion into Ember.

  • Fork the RFC repo http://github.com/emberjs/rfcs
  • Copy the appropriate template. For most RFCs, this is 0000-template.md, for deprecation RFCs it is deprecation-template.md. Copy the template file to text/0000-my-feature.md, where 'my-feature' is descriptive. Don't assign an RFC number yet.
  • Fill in the RFC. Put care into the details: RFCs that do not present convincing motivation, demonstrate understanding of the impact of the design, or are disingenuous about the drawbacks or alternatives tend to be poorly-received.
  • Fill in the relevant core teams. Use the table below to map from projects to teams.
  • Submit a pull request. As a pull request the RFC will receive design feedback from the larger community, and the author should be prepared to revise it in response.
  • Find a champion on the relevant core team. The champion is responsible for shepherding the RFC through the RFC process and representing it in core team meetings.
  • Update the pull request to add the number of the PR to the filename and add a link to the PR in the header of the RFC.
  • Build consensus and integrate feedback. RFCs that have broad support are much more likely to make progress than those that don't receive any comments.
  • Eventually, the [core teams] will decide whether the RFC is a candidate for inclusion in Ember.
  • RFCs that are candidates for inclusion in Ember will enter a "final comment period" lasting 7 days. The beginning of this period will be signaled with a comment and tag on the RFC's pull request. Furthermore, Ember's official Twitter account will post a tweet about the RFC to attract the community's attention.
  • An RFC can be modified based upon feedback from the [core teams] and community. Significant modifications may trigger a new final comment period.
  • An RFC may be rejected by the [core teams] after public discussion has settled and comments have been made summarizing the rationale for rejection. The RFC will enter a "final comment period to close" lasting 7 days. At the end of the "FCP to close" period, the PR will be closed.
  • An RFC may also be closed by the core teams if it is superseded by a merged RFC. In this case, a link to the new RFC should be added in a comment.
  • An RFC author may withdraw their own RFC by closing it themselves.
  • An RFC may be accepted at the close of its final comment period. A core team member will merge the RFC's associated pull request, at which point the RFC will become 'active'.

Relevant Teams

The RFC template requires indicating the relevant core teams. The following table offers a reference of teams responsible for each project. Please reach out for further guidance.

Core TeamProject/Topics
Ember.jsEmber.js
Ember DataEmber Data
Ember CLIEmber CLI
LearningDocumentation, Website, learning experiences
SteeringGovernance

Finding a champion

The RFC Process requires finding a champion from the relevant core teams. The champion is responsible for representing the RFC in team meetings, and for shepherding its progress. Read more about the Champion's job

  • Discord The dev-rfc channel on the Ember Discord is reserved for the discussion of RFCs. We highly recommend circulating early drafts of your RFC in this channel to both receive early feedback and to find a champion.

  • Request on an issue in the RFC repo or on the RFC We monitor the RFC repository. We will circulate requests for champions but highly recommend discussing the RFC in Discord.

The RFC life-cycle

Once an RFC becomes active the relevant teams will plan the feature and create issues in the relevant repositories. Becoming 'active' is not a rubber stamp, and in particular still does not mean the feature will ultimately be merged; it does mean that the core team has agreed to it in principle and are amenable to merging it.

Furthermore, the fact that a given RFC has been accepted and is 'active' implies nothing about what priority is assigned to its implementation, nor whether anybody is currently working on it.

Modifications to active RFC's can be done in followup PR's. We strive to write each RFC in a manner that it will reflect the final design of the feature; but the nature of the process means that we cannot expect every merged RFC to actually reflect what the end result will be at the time of the next major release; therefore we try to keep each RFC document somewhat in sync with the feature as planned, tracking such changes via followup pull requests to the document.

Implementing an RFC

The author of an RFC is not obligated to implement it. Of course, the RFC author (like any other developer) is welcome to post an implementation for review after the RFC has been accepted.

If you are interested in working on the implementation for an 'active' RFC, but cannot determine if someone else is already working on it, feel free to ask (e.g. by leaving a comment on the associated issue).

For Core Team Members

Reviewing RFCs

Each core team is responsible for reviewing open RFCs. The team must ensure that if an RFC is relevant to their team's responsibilities the team is correctly specified in the 'Relevant Team(s)' section of the RFC front-matter. The team must also ensure that each RFC addresses any consequences, changes, or work required in the team's area of responsibility.

As it is with the wider community, the RFC process is the time for teams and team members to push back on, encourage, refine, or otherwise comment on proposals.

Referencing RFCs

  • When mentioning RFCs that have been merged, link to the merged version, not to the pull-request.

Champion Responsibilities

  • achieving consensus from the team(s) to move the RFC through the stages of the RFC process.
  • ensuring the RFC follows the RFC process.
  • shepherding the planning and implementation of the RFC. Before the RFC is accepted, the champion may remove themselves. The champion may find a replacement champion at any time.

Helpful checklists for Champions

Becoming champion of an RFC

  • Assign the RFC to yourself

Moving to FCP to Merge

  • Achieve consensus to move to "FCP to Merge" from relevant core teams
  • Comment in the RFC to address any outstanding issues and to proclaim the start of the FCP period
  • Tweet from @emberjs about the FCP
  • Ensure the RFC has had the filename and header updated with the PR number

Move to FCP to Close

  • Achieve consensus to move to "FCP to Close" from relevant core teams
  • Comment in the RFC to explain the decision

Closing an RFC

  • Comment about the end of the FCP period with no new info
  • Close the PR

Merging an RFC

  • Achieve consensus to merge from relevant core teams
  • Ensure the RFC has had the filename and header updated with the PR number
  • Create a tracking card for the RFC implementation at {projects}
  • Update the RFC header with a link to the tracking
  • Merge
  • Update the RFC PR with a link to the merged RFC (The Rendered links often go stale when the branch or fork is deleted)
  • Ensure relevant teams plan out what is necessary to implement
  • Put relevant issues on the tracking

Ember's RFC process owes its inspiration to the Rust RFC process

  • Start Date: 2019-07-27
  • Relevant Team(s): Ember Data
  • RFC PR: https://github.com/emberjs/rfcs/pull/522
  • Tracking:

Deprecate default Adapter and Serializer fallbacks

Summary

As part of Project Trim, https://github.com/emberjs/data/issues/6166, this deprecates the fallback and default adapter and serializer across ember-data including:

  1. deprecate -default serializer fallback in Store.serializerFor
  2. deprecate adapter.serializer and adapter.defaultSerializer instance property fallbacks (which currently default to -json-api).
  3. deprecate store.defaultAdapter instance property (which defaults to -json-api) and the -json-api adapter fallback behavior in adapterFor.
  4. deprecate record.toJSON instance method since this relies on the -json serializer.

Motivation

The adapter and serializer packages provide reference implementations and base classes that are not required for applications that implement their own following the required interfaces for adapters and serializers as defined in their respective base classes. Deprecating them allows us to simplify the lookup pattern and remove automatic injection and registration of potentially unused classes.

In addition to removing use of initializer injection, this takes a significant step toward simplifying the mental model for how to determine what adapter/serializer is in use. Removing the defaults forces app developers to be more cognizant about the type of application level concerns vs model-specific concerns; they will now need to explicitly define and use specific adapters/serializers. After this deprecation RFC lands, apps will always use an adapter/serializer explicitly put into your application and the rule will be "the adapter matching modelName falling back to application".

Detailed design

The injection of -default and -json-api serializers will be removed in the next major version (4.0). Since this changes some core assumptions we will deprecate the reliance on the existence of the defaults. All deprecation warnings will only be shown in Dev mode.

deprecate -default serializer fallback in store.serializerFor

A deprecation warning will be shown when no model, application or adapter serializer has specified and the default must be used. We will recommend implementing an application serializer.

deprecate adapter.serializer and adapter.defaultSerializer fallbacks

A deprecation warning will be shown when accessing the adapter's defaultSerializer. This will be distinct from the warning about using the application level default. We will recommend implementing an application serializer.

deprecate store.defaultAdapter (-json-api) and the -json-api adapter fallback behavior

A deprecation warning will be shown when the defaultAdapter is accessed. We will recommend implementing an application adapter.

deprecate record.toJSON

A deprecation warning will be shown when toJSON is called since it uses a serializer to create a JSON representation of the model. Users may call record.serialize() or implement their own toJSON instead.

How we teach this

Today we have extensive documentation about creating custom serializers, but we will need to update the guides to specify the desired serializer in app/serializers/application.js

The deprecation guide app will be updated with examples showing how to migrate away from relying on the defaults.

Drawbacks

The drawback to making this change is that apps relying on the default serializer need to add some boilerplate to explicitly set the serializer.

Alternatives

We could not do this.

  • Start Date: 2014-08-14
  • RFC PR: https://github.com/emberjs/rfcs/pull/1
  • Ember Issue: https://github.com/emberjs/data/pull/4086

Summary

For Ember Data. Pass through attribute meta data, which includes parentType, options, name, etc., to the transform associated with that attribute. This will allow provide the following function signiture updates to DS.Transform:

  • transform.serialize(deserialized, attributeMeta)
  • transform.deserialize(serialized, attributeMeta)

Motivation

The main use case is to be able to configure the transform on a per-model basis making more DRY code. So the transform can be aware of type and options on DS.attr can be useful to configure the transform for DRY use.

Detailed design

Implementing

The change will most likely start in eachTransformedAttribute, which gets the attributes for that instance via get(this, 'attributes'). In the forEach the name will be used to get the specific attribute, e.g.

var attributeMeta = attributes.get(name);
callback.call(binding, name, type, attributeMeta);

The next change will be in applyTransforms, where the attributeMeta parameter is added and passed to transform.deserialize as the second argument.

You also have to handle the serialization part in serializeAttribute, where you pass through the attribute parameter to transform.serialize.

Using

A convoluted example:

// Example based on https://github.com/chjj/marked library
App.PostModel = DS.Model.extend({
  title: DS.attr('string'),
  markdown: DS.attr('markdown', {
    markdown: {
      gfm: false,
      sanitize: true
    }
  })
});

App.TechnicalPostModel = DS.Model.extend({
  title: DS.attr('string'),
  gistUrl: DS.attr('string'),
  markdown: DS.attr('markdown', {
    markdown: {
      gfm: true,
      tables: true,
      sanitize: false
    }
  })
});

App.MarkdownTransform = DS.Transform.extend({
  serialize: function (deserialized, attributeMeta) {
    return deserialized.raw;
  },
  
  deserialize: function (serialized, attributeMeta) {
    var options = attributeMeta.options.markdown || {};
    
    return marked(serialized, options);
  }
});

Drawbacks

Extra API surface area, although not much. This could also potentially introduce tight coupling between models and transforms if used improperly, e.g. not returning a default value if using type checking.

Alternatives

  1. Passing the information from the server, which is a poor solution.
  2. Writing a new transform for each model/attribute that needs a variation. Although this might be a good solution sometimes if you extend a base transform.

Unresolved questions

Does the whole meta object need to be passed, or do we selectively pass in only the useful properties? Like options and parentType and name..

  • Start Date: 2014-08-18
  • RFC PR: https://github.com/emberjs/rfcs/pull/3
  • Issues:
    • Ember Stream support: emberjs/ember.js#5522
    • Handlebars parser support: wycats/handlebars.js#906
    • HTMLBars compiler support: tildeio/htmlbars#147

Summary

Introduce block parameters to the Handlebars language to standardize context-preserving helpers, for example:

{{#each people as |person|}}
  {{person.name}}
{{/each}}

Motivation

The Problem

There is no idiomatic way to write a helper that preserves context and yields values to its template. This is particularly painful for components which have strict context-preserving semantics.

Current workarounds

  • Don't write components that need to yield a value.
    • Problem: This may not be an option.
  • Invent a non-standard per-helper syntax (like {{#with foo as bar}} or {{#each item in items}}) that hook into the undocumented keywords to inject variables.
    • Problems: Custom syntaxes are not in the spirit of the Handlebars language and require the consumer to know the special incantation. Component authors must an non-trivial understanding of how keywords work.

New possibilities

{{#for-each obj as |key val|}}
  {{key}}: {{val}}
{{/for-each}}
{{#form-for post as |f|}}
  {{f.input "title"}}
  {{f.textarea "body"}}
  {{f.submit}}
{{/form-for}}

Detailed design

  • Phase 1: Add block params to the Handlebars language
  • Phase 2: Rewrite Ember's helpers to accept streams
  • Phase 3: Add block param support to {{each}} and {{with}}

Phase 1: Add block params to the Handlebars language

The proposed syntax is {{#x-foo a b w=x y=z as |param1 param2 ... paramN|}} and is only available for block helpers.

The names of the block parameters are compiled into the inner template, but are not known to the helper (x-foo in the example above). To call a template and populate its block params we use the arguments option:

var template = compile('{{person.name}}', {
  blockParams: [ 'person' ]
});

template({}, ..., [ personModel ]);

More commonly, block params will be defined inside of the template.

{{#with currentPost.author as |a|}}
  {{a.name}} <em>{{a.email}}</em>
{{/with}}
registerHelper('with', function(object, options) {
  return options.fn(this, ..., [ object ]);
});

For compatibility reasons, the number of block params are passed to the helper so that the pre-block-params behaviour of the helper can be preserved. Example:

function eachHelper(..., options) {
  if (options.blockParamsLength > 0) { /* do new behaviour */ }
  else { /* do old behaviour */ }
}

Phase 2: Rewrite Ember's helpers to accept streams

In the with example above, if the currentPost changes the a block param should update. This means it's not sufficient to pass only the initial value of the author in the arguments. Instead, we pass a stream which emits values whenever the observed property changes.

In Handlebars, a block param can appear anywhere that an identifier can, for example {{log a.name}}. This means that all helpers would need to be modified to understand streams.

Phase 3: Add block param support to {{each}} and {{with}}

Deprecate context-changing and ad-hoc keyword flavors of {{each}} and {{with}} in favor of block params.

Drawbacks

  • Handlebars already has a similar notion of with data which can lead to confusion.

Alternatives

To my knowledge, no other designs have been considered. Not implementing this feature would mean that components would continue to be difficult to compose.

Unresolved questions

The associated HTML syntax for HTMLBars needs to be finalized.

  • Start Date: 2015-01-10
  • Relevant Team(s): Ember CLI
  • RFC PR: #3

Summary

We need a way to run diagnostics on Ember CLI based projects to let developers know about potential system level incompatibilities. Developers should also be able to get a bill of health for their project for things like outdated dependencies. This bill of health should also be extensible. Output from running this command should be as consise and only ever log things that don't seem healthy.

Motivation

The motivation behind this is 2 pronged:

  1. Allows developers to submit system level information in pull requests, so that bugs can be filed and potentially replicated.
  2. Gives developers the ability to know about the health of their project and to potentially help with stagnation.

Detailed design

The design for this is rather simple. We would first introduce a command called ember doctor that would run some default checks. The default checks would do the following:

  • Run ember v --verbose and complain loudly for incompatible versions
  • Run npm outdated --depth 0 to check on outdated modules
  • Run bower list and display out of date bower components
  • Run check to grab OS information

These are what is considered default checks.

In your project developers can setup their own Doctor checks that get merged in with the default checks. To allow for this Ember CLI will have ember generate doctor check:service-health.

This command will generate the following directory structure in the root of the project:

doctor/
  checks/
    service-health.js
  index.js

When ember doctor is ran we simply will do a merge of the default checks and the ones provided by the application.

There should also be a way of excluding checks to be ran. Developers should be able to simply pass flags for things they do not care to run e.g. ember doctor --skip=npm,os.

Addon Design

Much like the project addons can add their own diagnostics as projects. In the addons main entry point there will be a hook much like includedCommands that allows Ember CLI to look up the diagnostics and role them into the consuming project.

var checks = require('./checks');
...
includedChecks: function() {
  return checks;
}
...

Expected Output

Output of running the doctor command should be as concise as possible. Unless there are any issues with the project that is being analyzed, the output should be something like the following:

Success: All diagnostics checked out fine.

In the event that there is an issue with the project that is being analyzed the output will look something like the following:

Warning: NPM modules out of date. Below are the out of date modules.
╔══════╤═══════╤═════════╗
║ Name │ Yours │ Current ║
╟──────┼───────┼─────────╢
║ glob │ 1.1.2 │ 1.2.3   ║
╚══════╧═══════╧═════════╝

Drawbacks

This adds "yet another thing" to the Ember CLI API surface. Doctor will be bound to a network connection such as checking outdated dependencies.

Alternatives

There have been other other attempts to put checking for system level checking in various places. The BDFL's would like to consolidate this into an ember doctor command.

Unresolved questions

  • Start Date: 2014-10-24
  • RFC PR: https://github.com/emberjs/rfcs/pull/10
  • Ember Issue: https://github.com/emberjs/ember.js/pull/12685

Summary

Engines allow multiple logical applications to be composed together into a single application from the user's perspective.

Motivation

Large companies are increasingly adopting Ember.js to power their entire product lines. Often this means separate teams (sometimes distributed around the world) working on the same app. Typically, responsibility is shared by dividing the application into one or more "sections". How this division is actually implemented varies from team to team.

Sometimes, each "section" will be a completely separate Ember app, with a shared navigation bar allowing users to switch between each app. This allows teams to work quickly without stepping on each others' toes, but switching apps feels slow (especially compared to the normally speedy route transitions in Ember) because the entire page must be thrown out, then an entirely new set of the same assets downloaded and parsed. Additionally, code sharing is largely accomplished via copy-and-paste.

Other times, the separation is enforced socially, with each team claiming a section of the same app in the same repository. Unfortunately, this approach leads to frequent conflicts around shared resources, and feedback from tests gets slower and slower as test suites grow in size.

A more modular approach is to break off elements of a single application into separate addons. Addons are essentially mixins for ember-cli applications. In other words, the elements of an addon are merged with those of the application that includes them. While addons allow for distributed development, testing, and packaging, they do not provide the logical run-time separation required for developing completely independent "sections" of an application. Addons must function within the namespace, registry, and router of the application in which they are included.

Engines provide an alternative to these approaches that allows for distributed development, testing, and packaging, as well as logical run-time separation. Because engines are derived from applications, they can be just as full-featured. Each has its own namespace and registry. Even though engines are isolated from the applications that contain them, the boundaries between them allow for controlled sharing of resources.

Engines can be either "routable" or "route-less":

  • Routable engines provide a routing map which can be integrated with the routing maps of parent applications or engines. Routing maps are always eager loaded, which allows for deep linking into an engine's routes regardless of whether the engine itself has been instantiated.

  • Route-less engines can isolate complex functionality that is not related to routing (e.g. a chat engine in a sidebar). Route-less engines can be rendered into outlets ad hoc as routes are loaded.

The potential scope of engines is large enough that this feature merits development and delivery in multiple phases. A minimum viable version could be released sooner, which could be augmented with more advanced features later.

An initial release of engines could provide the following benefits:

  • Distributed development - Engines can be developed and tested in isolation within their own Ember CLI projects and included by applications or other engines. Engines can be packaged and released as addons themselves.

  • Integrated routing - Support for mounting routable engines in the routing maps of applications or other engines.

  • Ad hoc embedding - Support for embedding route-less engines in outlets as needed.

  • Clean boundaries - An engine can cooperate with its parents through a few explicit interfaces. Beyond these interfaces, engines and applications are isolated.

Subsequent releases of engines could allow for the following:

  • Lazy loading - An engine could allow its parent to boot with only its routing map loaded. The rest of the engine could be loaded only as required (i.e. when a route in an engine is visited). This would allow applications to boot faster and limit their memory consumption.

  • Namespaced access to engine resources from applications - This could open up the potential for applications to use, and extend, an engine's resources much like resources in other addons, but without the possibility of namespace collisions.

Detailed design

Engines are very similar to regular applications: they can be developed in isolation in Ember CLI, include addons, and contain all the same elements, including routes, components, initializers, etc. The primary differences are that an engine does not boot itself and an engine does not control the router.

Engine internals

New Engine and EngineInstance classes will be introduced.

Applications and engines will share ancestry. It remains TBD whether applications will subclass engines, or whether a common ancestor will be introduced.

Engines and applications will share the same pattern for registry / container ownership and encapsulation. Both will also have initializers and instance initializers.

Engine instances will have access to their parent instances. An engine's parent could be either an application or engine.

Routable vs. route-less engines

Routable engines will define their routes in a new Ember.Routes class. This class will encapsulate the functionality provided by Router#map, and will be used internally by Ember.Router as well (with no public interface changes of course).

Route-less engines do not define routing maps nor can they contain routes.

Developing engines

Engines can be developed in isolation as Ember CLI addon projects or as part of a parent application.

Engines as addons

Engines can be created as separate addon projects with:

ember engine <engine-name>

This will create a special form of an ember addon. The file structure will match that of a standard addon, but will have an engine directory instead of an addon directory.

Engines can be unit tested and can also be integration tested within a dummy app, just like standard addons.

In-repo engines

An engine can be created within an existing application's project using a special in-repo-engine generator (similar to the in-repo-addon generator):

ember g in-repo-engine <engine-name>

In-repo engines can be unit tested in isolation or integration testing with the main application (instead of a dummy application).

Note: In-repo addons currently are created in the /lib directory (e.g. /lib/my-addon). Unit tests and integration tests are currently co-mingled with tests for the main application. It's recommended that in-repo engines provide better test separation than is provided for regular addons, and perhaps the whole in-repo addon directory structure should be re-examined at the same time in-repo engines are introduced.

Engine directory structure

An engine's directory will contain a file structure identical to the app directory in a standard ember-cli application, with the following exceptions:

  • engine.js instead of app.js - defines the Engine class and loads its initializers.

  • routes.js instead of router.js - defines an engine's routing map in a Routes class. This file should be deleted entirely for route-less engines.

Installing engines

Engines developed as addons can be installed in an application just like any other addon:

ember install <engine-name>

During development, you can use npm link to make your engine available in another parent engine or application.

Mounting routable engines

The new mount() router DSL method is used to mount an engine at a particular "mount-point" in a route map.

For example, the following route map mounts the discourse engine at the /forum path:

Router.map(function() {
  this.mount('discourse', {path: '/forum'});
});

Note: If unspecified, path will match the name of the engine.

Calls to mount can be nested within routes. An engine can be mounted at multiple routes, and each will represent a new instance of the engine to be created.

Mounting route-less engines

A mount() DSL will also be added to routes, which will enable embedding of route-less engines in outlets. This can be called from renderTemplate (or renderComponents once routable components are introduced).

mount has a similar signature to render, although it is obviously engine-specific instead of template-specific. mount can be used to specify a target template and outlet as follows:

renderTemplate: function() {
  // Mount the chat engine in the sidebar
  this.mount('chat', {
    into: 'main',
    outlet: 'sidebar'
  });
}

As a result, the engine's application template will be rendered into the sidebar outlet in the application's main template.

Loading phases

Engines can exist in several phases:

  • Booted - an engine that's been installed in a parent application will have its dependencies loaded and its (non-instance) initializers invoked when the parent application boots.

  • Mounted - Routable and route-less engines have slightly different concepts of "mounting". A routable engine is considered mounted when it has been included by a router at one or more mount-points. A route-less engine is considered mounted as soon as a route's mount call resolves.

  • Instantiated - When an engine is instantiated, an EngineInstance is created and an engine's instance initializers are invoked. A routable engine is instantiated when a route is visited at or beyond its mount-point. A route-less engine is instantiated as soon as it is mounted.

Special before and after hooks could be added to application instance initializers that allow them to be ordered relative to engine instance initializers.

Engine boundaries

Besides its routing map, an engine does not share any other resources with its parent by default. Engines maintain their own registries and containers, which ensure that they stay isolated. However, some explicit sharing of resources between engines and parents is allowed.

Engine / parent dependencies

Dependencies between engines and parents can be defined imperatively or declaratively.

Imperative dependencies can be defined in an engine's instance initializers. When an engine is instantiated, the parent property on its EngineInstance is set to its parent instance (either an ApplicationInstance or EngineInstance). Since the engine instance is available in the instance initializer, this parent property can also be accessed. This allows an engine instance to interrogate its parent, specifically through its RegistryProxy and ContainerProxy interfaces.

Alternatively, declarative dependencies can be defined on a limited basis. The initial API will be limited: an engine can define an array of services that it requires from its parent.

For example, the following engine expects its parent to provide store and session services:

import Ember from 'ember';

var Engine = Ember.Engine.extend({
  dependencies: {
    services: [
      'store',
      'session'
    ]
  }
});

export default Engine;

The parent application can provide a re-mapping of services from its namespace to that of the engine via an engines declaration.

In the following example, the application shares its store service directly with the checkout engine. It also shares its current-user service as the session service requested by the engine.

import Ember from 'ember';

var App = Ember.Application.extend({
  engines: {
    checkout: {
      dependencies: {
        services: [
          'store',
          {session: 'current-user'}
        ]
      }
    }
  }
});

export default App;

When engines are instantiated, the listed dependencies will be looked up on the parent and made accessible within the engine.

Note that the engines declaration provides further space to define characteristics about an engine, such as whether it should be eager or lazy-loaded, URLs for manifest files, etc.

Drawbacks

This RFC introduces the new concept of engines, which increases the learning curve of the framework. However, I believe this issue is mitigated by the fact that engines are an opt-in packaging around existing concepts.

In the end, I believe that "engines" are just a small API for composing existing concepts. And they can be introduced at the top of the conceptual ladder, once users are comfortable with the basics of Ember, and only for those working on large teams or distributing addons.

Alternatives

Several incomplete alternatives are discussed in the Motivations section above.

I know of no alternatives being discussed in the Ember community that meet the same needs as engines; namely, for development and run-time isolation.

Unresolved questions

Non-CLI Users

This RFC assumes Ember CLI. I would prefer to prove this out in Ember CLI before locking down the public APIs/hooks the router exposes for locating and mounting engines. Once this is done, however, we should expose and document those hooks so users who cannot use Ember CLI for whatever reason can still take advantage of composability.

Declarative dependencies

The initial scope of declarative dependency sharing is limited in scope to services. Should other types of dependencies be declaratively shareable? Should addons be the recommended path to share all other dependencies?

Async mounting of route-less engines

Route#renderTemplate is called synchronously, although Route#mount should surely be async. How async mounting is represented in the route lifecycle is TBD. A solution isn't proposed here because the problem is shared by routable and async components, and a common solution should be reached.

Lazy loading manifests

In order to facilitate lazy loading of engines, we will need to determine a structure for manifest files that contain an engine's assets. Furthermore, an application will need to be configurable with URLs for these manifests.

It's likely that an engine's routing map will always be needed at the time of application deployment. Allowing lazy loading of routing maps would prevent the formation of any links from a parent application into an engine's routes.

When developed in isolation as addons, engines will have their own sets of dependencies. These dependencies will be treated like any other addons when engines are deployed together with an application. However, in order to support lazy loading, it would be ideal to dedupe dependencies in order to create a lean and conflict-free asset manifest.

Reference: deduping strategy discussed by @wycats in this Google doc.

Namespaced access to engine resources

The concept of namespaced access to engine resources is mentioned above as a potential goal of a future release of engines. This will require further discussion to decide how it should work both technically and semantically, and how it applies to lazy-loaded engines.

If these problems can be resolved, this feature would allow for more flexibility in parent / engine interactions. Instead of just allowing engines to look up resources in a parent, the inverse could also be allowed.

For example, if the authentication engine contains engines/authentication/models/user.js, a parent application could look up this same model through a namespace. Perhaps as follows:

container.lookup('authentication@model:user');

Other APIs in Ember would need to be extended to support namespaces to take full advantage of this feature. For example, components that ship with an engine might be accessed from the primary application like this:

{{authentication@login-form obscure-password=true}}
  • Start Date: 2014-09-30
  • RFC PR: https://github.com/emberjs/rfcs/pull/11
  • Ember Issue: https://github.com/emberjs/ember.js/pull/9527

Summary

Improve computed property syntax

Motivation

Today, the setter variant of CP's is both confusing, and looks scary as sin. (Too many concepts must be taught and it is too easy to screw it up.)

Detailed design

today:

fullName: Ember.computed('firstName', 'lastName', function(key, value) {
  if (arguments.length > 1) {
    var names = value.split(' ');
    this.setProperties({
      firstName: names[0],
      lastName: names[1]
    });
    return value;
  }

  return this.get('firstName') + ' ' + this.get('lastName');
});

Tomorrow:

fullName: Ember.computed('firstName', 'lastName', {
  get: function(keyName) {
    return this.get('firstName') + ' ' + this.get('lastName');
  },

  set: function(keyName, fullName, oldValue) {
   var names = fullName.split(' ');

   this.setProperties({
     firstName: names[0],
     lastName: names[1]
   });

   return fullName;
  }
});

Notes:

  • we should keep Ember.computed(fn); as shorthand for getter only
  • get xor set variants would also be possible.
  • { get() { } } is es6 syntax for { get: function() { } )

Migration

  • 1.x support both, detect new behaviour by testing if the last arg is not null and typeof object
  • 1.x+1 deprecate if last arg is a function and its arity is greater than 1

Drawbacks

N/A

Alternatives

N/A

Unresolved questions

None

  • Start Date: 2015-05-16
  • Relevant Team(s): Ember CLI
  • RFC PR: #12

Summary

This has come up in #3699 and EmberTown/ember-hearth/#66.

In short, it would be nice for tools that depend on Ember-CLI to be able to read the help output as JSON (for example ember g --help --json).

Motivation

In our specific use case in Ember Hearth we would like to be able to render a dynamic GUI for some tasks, like generating blueprints. This way we could also include any blueprints added by addons. This will also apply to any other tools interfacing with Ember-CLI.

Detailed design

We should probably make the internal help-functions (like printBasicHelp and printDetailedHelp) use JSON internally, and parse to human readable before printing (unless --json is specified).

I'm imagining the json output would be something like this:

{
  "name":"generate",
  "description":"Generates new code from blueprints.",
  "aliases":["g"],
  "flags":[
    {
      "flag":"--verbose",
      "aliases":["-v"],
      "description":"Verbose output"
    }, {…}],
  "commands":[
    {
      "command":"template",
      "description":"Generates a template.",
      "arguments":["name"]
    },
    {
      "command":"model",
      "description":"Generate an ember-data model.",
      "arguments":[
        "name",
        {
          "argument":"attr:type",
          "description":"Add attributes to the model, e.g. 'name:String age:Number'",
          "multiple":true
        }]
    }, {…}]
}

Note that this output contains a bit more info than the current --help, specifically in the attr:type argument for the model command. This is something I feel is currently missing (I did not understand the model generator command without consulting a colleague, for example), and would be nice to add while we're at it.

It should be pretty straight forward to generate a human readable output from this JSON. There are a few things missing: However: The generate help command specifically groups commands by addon. I'm not sure how this should be accomplished, and if this matches the other help outputs. Ideally, any tools reading the JSON should be able to rely on the format being the same for all commands. This would keep the internals cleaner as well, including the human readable parser.

Drawbacks

  • Requires rewrite of help methods, possibly also for some addons (unless we can provide backwards compatability)
  • Increases codebase size

Alternatives

  • We could standardize help output enough that it can be safely regexed by other tools
  • We could not do this, and require any tools to update whenever Ember-CLI changes any commands

Unresolved questions

  • Internal architecture specifics (rewrite printBasicHelp or create a new setup, etc)
  • Specifying JSON format details
  • List any dependencies, like docs, that will need to be updated with this change
  • Start Date: 2014-12-03
  • RFC PR: https://github.com/emberjs/rfcs/pull/15
  • Ember Issue: This RFC is implemented over many Ember PRs

The Road to Ember 2.0

Intro

Today, we're announcing our plan for Ember 2.0. While the major version bump gives us the opportunity to simplify the framework in ways that require breaking changes, we are designing Ember 2.0 with migration in mind.

This is not a big-bang rewrite; we will continue development on the master branch, and roll out changes incrementally on the 1.x release train. The 2.0.0 release will simply remove features that have been deprecated between now and then. Our goal is that you can move your Ember app to 2.0 incrementally, one sprint at a time.

This RFC captures the results of the last two core team face-to-face meetings, where we discussed community feedback about the future of the project. While it explains the high-level goals and tries to paint a picture of how all the pieces fit together, this document will be updated over time with links to individual RFCs that contain additional implementation detail.

We plan to flesh out these more-detailed RFCs in the next few weeks, as the discussion here progresses, before finalizing this plan.

We are announcing Ember 2.0 through our community RFC process in advance of a release, both so our proposals can be vetted by the community and so the community can understand the goals and contribute their own ideas back.

Motivation

Stability without Stagnation

Ember is all about identifying common patterns that emerge from the web development community and rolling them into a complete front-end stack. This makes it easy to get started on new projects and jump into existing ones, knowing that you will get a best-of-breed set of tools that the community will continue to support and improve for years to come.

In the greater JavaScript community, getting the latest and greatest often means rewriting parts of your apps once a year, as the community abandons existing solutions in search of improvements. Progress is important, but so is ending the constant cycle of writing and rewriting that plagues so many applications.

The Ember community works hard to introduce new ideas with an eye towards migration. We call this "stability without stagnation", and it's one of the cornerstones of the Ember philosophy.

Below, we introduce some of the major new features coming in Ember 2.0. Each section includes a transition plan, with details on how we expect existing apps to migrate to the new API.

When breaking changes are absolutely necessary, we try to make those changes ones you can apply without too much thought. We call these "mechanical" refactors. Typically, they'll involve a change to syntax without changing semantics. These are significantly easier to adopt than those that require fundamental changes to your application architecture.

To further aid in these transitions, we are planning to add a new tab to the Ember Inspector that will list all deprecations in your application, as well as a list of the locations in the source code where the deprecated code was triggered. This should serve as a convenient "punch list" for your transitional work.

Every member of the core team works on up-to-date Ember applications, and we feel the tension between stability and progress acutely. We want to deliver cutting-edge products, but need to keep shipping, and many companies that have adopted Ember for their products tell us the same thing.

Big Bets

In 2014, we made big bets in two areas, and they've paid off.

The first bet was on open standards: JavaScript modules, promises and Web Components. We started the year off with globals-based apps, callbacks and "views", and incrementally (and compatibly) built towards standards-based solutions as those standards solidified.

The second bet was that the community was as tired as we were of hand-rolling their own build scripts for each project. We've invested heavily in Ember CLI, giving us a single tool that unifies the community and provides a venue for disseminating great ideas.

In Ember 2.0, Ember CLI and ES6 modules will become first-class parts of the Ember experience. We will update the website, guides, documentation, etc. to teach new users how to build Ember apps with the CLI tools and using JavaScript's new module syntax.

While globals-based apps will continue to work in 2.0, we may introduce new features that rely on either Ember CLI or ES6 modules. You should begin moving your app to Ember CLI as soon as possible.

All of the apps maintained by the Ember core team have been migrated to Ember CLI, and we believe that most teams should be able to make the transition incrementally.

Learning from the Community

We're well aware that we don't have a monopoly on good ideas, and we're always analyzing competing frameworks and libraries to discover great ideas that we can incorporate.

For example, AngularJS taught us the importance of making early on-ramp easy, how cool directives/components could be, and how dependency injection improves testing.

We've been analyzing and discussing React's approach to data flow and rendering for some time now, and in particular how they make use of a "virtual DOM" to improve performance.

Ember's view layer is one of the oldest parts of Ember, and was designed for a world where IE7 and IE8 were dominant. We've spent the better part of 2014 rethinking the view layer to be more DOM-aware, and the new codebase (codenamed "HTMLBars") borrows what we think are the best ideas from React. We cover the specifics below.

React's "virtual DOM" abstraction also allowed them to simplify the programming model of component-based applications. We really like these ideas, and the new HTMLBars engine, landing in the next Ember release, lays the groundwork for adopting the simplified data-flow model.

In Ember 2.0, we will be adopting a "virtual DOM" and data flow model that embraces the best ideas from React and simplifies communication between components.

Interestingly, we found that well-written Ember applications are already written with this clear and direct data flow. This change will mostly make the best patterns more explicit and easier for developers to find when starting out.

A Steady Flow of Improvement

Ember 1.0 shipped over a year ago and we have continued to improve the framework while maintaining backwards-compatibility. We are proud of the fact that Ember apps tend to track released versions.

You might expect us to do Ember 2.0 work on a separate "2.0" branch, accumulating features until we ship. We aren't going to do that.

Instead, we plan to do the vast majority of new work on master (behind feature flags), and land new features in 1.x as they become stable.

The 2.0.0 release will simply remove the cruft that naturally builds up when maintaining compatibility with old releases.

If we add features that change Ember idioms, we will add clear deprecation warnings with steps to refactor to new patterns.

Our goal is that, as much as possible, people will be able to boot up their app on the last 1.x version, update to the latest set of idioms by following the deprecation prompts, and have things working on 2.0.

Because going from the last version of Ember 1.x to Ember 2.0 will be just another six-week release, there simply won't be much time for us to make it an incredibly painful upgrade. ;)

Simplifying Ember Concepts

Ember evolved organically from a view-layer-only framework in 2011 into the route-driven, complete front-end stack it is today. Along the way, we've accumulated several concepts that are no longer widely used in idiomatic Ember apps.

These vestigial concepts make file sizes larger, code more complex, and make Ember harder to learn.

Ember 2.0 is about simplification. This lets us reduce file sizes, reduce code complexity, and generally make Ember apps easier to pick up and maintain.

The high-level set of improvements that we have planned are:

  • More intuitive attribute bindings
  • New HTML syntax for components
  • Block parameters for components
  • More consistent template scope
  • One-way data binding by default, with opt-in to mutable, two-way bindings
  • More explicit communication between components, which means less implicit communication via two-way bindings
  • Routes drive components, instead of controller + template
  • Improved actions that are invoked inside components as simple callbacks

In some sections, we provide estimates for when a feature will land. These are our best-guesses, but because of the rapid-release train model of Ember, we may be off by a version or two.

However, all features that are slated for "before 2.0" will land before we cut over to a major new version.

More Intuitive Attribute Bindings

Today's templating engine is the oldest part of Ember.js. Under the hood, it generates a string of HTML and then inserts it into the page.

One unfortunate consequence of this architecture is that it is not possible to intuitively bind values to HTML attributes.

You would expect to be able type something like:

<a href="{{url}}">Click here</a>

But instead, in today's Ember, you have to learn about and use the bind-attr helper:

<a {{bind-attr href=url}}>Click here</a>

The new HTMLBars template engine makes bind-attr a thing of the past, allowing you to type what you mean. It also makes it possible to express many attribute-related concepts simply:

<a class="{{active}} app-link" href="{{url}}.html">Click here</a>

Transition Plan

The HTMLBars templating engine is being developed on master, and parts of it have already landed in Ember 1.8. Doing the work this way means that the new engine continues to support the old syntax: your existing templates will continue to work.

The improved attribute syntax has not yet landed, but we expect it to land before Ember 1.10.

We do not plan to remove support for existing templating syntax (or no-longer-necessary helpers like bind-attr) in Ember 2.0.

More Intuitive Components

In today's Ember, components are represented in your templates as Handlebars "block helpers".

The most important problem with this approach is that Handlebars-style components do not work well with attribute bindings or the action helper. In short, a helper that is meant to be used inside an HTML tag cannot be used inside a call to a component.

Beginning in Ember 1.11, we will support an HTML-based syntax for components. The new syntax can be used to invoke existing components, and new components can be called using the old syntax.

<my-video src={{movie.url}}></my-video>

<!-- equivalent to -->

{{my-video src=movie.url}}

Transition Plan

The improved component syntax will (we hope) land in Ember 1.11. You can transition existing uses of {{component-name}} to the new syntax at that time. You will likely benefit by eliminating uses of computed properties that can now be more tersely expressed using the interpolation syntax.

We have no plans to remove support for the old component syntax in Ember 2.0.

Block Parameters

In today's templates, there are two special forms of built-in Handlebars helpers: #each post in posts and #with post as p. These allow the template inside the helper to retain the parent context, but get a piece of helper-provided information as a named value (such as post in the previous examples).

{{#with contact.person as p}}
  {{!-- this block of code is still in the parent's scope, but
        the #with helper provided a `p` name with a
        helper-provided value --}}
  <p>{{p.firstName}} {{p.lastName}}</p>

  {{!-- `title` here refers to the outer scope's title --}}
  <p>{{title}}</p>
{{/with}}

Today, this capability is hardcoded into the two special forms, but it can be useful for other kinds of components. For example, you may have a calendar component (ui-calendar) that displays a specified month.

The ui-calendar component may want to allow users to supply a custom template for each day in the month, but each repetition of the template will need information about the day it represents (its day of the week, date number, etc.) in order to render it.

With the new "block parameters" feature, any component will have access to the same capability as #each or #with:

<ui-calendar month={{currentMonth}} as |day|>
  <p class="title">{{day.title}}</p>
  <p class="date">{{day.date}}</p>
</ui-calendar>

In this case, the ui-calendar component iterates over all of days in currentMonth, rendering each instance of the template with information about which date it should represent.

We also think that this feature will be useful to allow container components (like tabs or forms) to supply special-case component definitions as block params. We are still working on the details, but believe that an approach along these lines could make these kinds of components simpler and more flexible.

Transition Plan

Block parameters will hopefully land in 1.12, and at that point the two special forms for {{each}} and {{with}} will be deprecated. You should refactor your templates to use the new block parameters syntax once it lands, as it is a purely mechanical refactor.

We have no plans to remove support for the {{each}} and {{with}} special forms in Ember 2.0.

More Consistent Handlebars Scope

In today's Ember, the each and with helpers come in two flavors: a "context-switching" flavor and a "named-parameter" flavor.

{{#each post in posts}}
  {{!-- the context in here is the same as the outside context,
        and `post` references the current iteration --}}
{{/each}}

{{#each posts}}
  {{!-- the context in here has shifted to the individual post.
        the outer context is no longer accessible --}}
{{/each}}

This has proven to be one of the more confusing parts of the Ember templating system. It is also not clear to beginners which to use, and when they choose the context-shifting form, they lose access to values in the outer context that may be important.

Because the helper itself offers no clue about the context-shifting behavior, it is easy (even for more experienced Ember developers) to get confused when skimming a template about which object a value refers to.

In Ember 1.10, we will deprecate the context-shifting forms of #each and #with in favor of the named-parameter forms.

Transition Plan

To transition your code to the new syntax, you can change templates that look like this:

{{#each people}}
  <p>{{firstName}} {{lastName}}</p>
  <p>{{address}}</p>
{{/each}}

with:

{{#each people as |person|}}
  <p>{{person.firstName}} {{person.lastName}}</p>
  <p>{{person.address}}</p>
{{/each}}

We plan to deprecate support for the context-shifting helpers in Ember 1.10 and remove support in Ember 2.0. This change should be entirely mechanical.

One-Way Bindings by Default

After a few years of having written Ember applications, we have observed that most of the data bindings in the templating engine do not actually require two-way bindings.

When we designed the original templating layer, we figured that making all data bindings two-way wasn't very harmful: if you don't set a two-way binding, it's a de facto one-way binding!

We have since realized (with some help from our friends at React), that components want to be able to hand out data to their children without having to be on guard for wayward mutations.

Additionally, communication between components is often most naturally expressed as events or callbacks. This is possible in Ember, but the dominance of two-way data bindings often leads people down a path of using two-way bindings as a communication channel. Experienced Ember developers don't (usually) make this mistake, but it's an easy one to make.

When you use the new component syntax, the {{}} interpolation syntax defaults to creating one-way bindings in the components.

<my-video src={{url}}></my-video>

In this example, the component's src property will be updated whenever url changes, but it will not be allowed to mutate it.

If a template wishes to allow the component to mutate a property, it can explicitly create a two-way binding using the mut helper:

<my-video paused={{mut isPaused}}></my-video>

This can help ease the transition to a more event-based style of programming.

It also eliminates the boilerplate associated with an event-based style when working with form controls. Instead of copying state out of a model, listening for callbacks, and updating the model, the input helper can be given an explicit mutable binding.

<input value={{mut firstName}}>
<input value={{mut lastName}}>

This is similar to the approach taken by React.Link, but we think that the use-case of form helpers is sufficiently common to make it ergonomic.

Transition Plan

The new one-way default is triggered by the use of new component syntax. This means that component invocations in existing templates will continue to work without changes.

When transitioning to the new HTML-based syntax, you will likely want to evaluate whether bindings are actually being mutated, and avoid using mut for values that the component never changes. This will make it easier for future readers of your template to get an understanding of what properties might be changed downstream.

To preserve the same semantics during a refactor to the new HTML-based syntax, you can simply mark all bindings as mut.

{{!-- these are semantically equivalent --}}

{{my-video src=movie.url paused=controller.isPaused}}

<my-video src={{mut movie.url}} paused={{mut controller.isPaused}}>
</my-video>

While the above example preserves the same mutability semantics, it should be clear that the video player component should never change the url of the movie model.

To make sure you get an exception should this ever happen, simply remove the mut:

<my-video src={{movie.url}} paused={{mut controller.isPaused}}>
</my-video>

We have no plans to remove the old-style component syntax in Ember 2.0, so the semantics of existing component invocations will not change.

Separated Component Parameters

In today's Ember, parameters passed to components as attributes become properties of the component itself, putting them in the same place as other internal state.

This can be somewhat confusing, because it may not be obvious to the reader of a component's JavaScript or template which values are internal, and which are passed in as part of the public API.

To remind themselves, many Ember users write their components like this:

export default Component.extend({
  /* Public API */

  src: null,
  paused: null,
  title: null,

  /* Internal */
  scrubber: null
})

It can also be unclear how to react to a change in the external properties. It is possible to use observers for this purpose in Ember, but observers feel low-level and do not coordinate very well with the rendering process.

To reduce confusion, we plan to move external attributes into a new attrs hash.

If you invoke a component like this:

<my-video src={{movie.url}}></my-video>

then the my-video component accesses the passed-in src attribute as this.attrs.src.

We also plan to provide lifecycle callbacks (modelled after React's lifecycle callbacks) for changes to attrs that will integrate with the rendering lifecycle. We plan to supplement the API with callbacks for changes in individual properties as well.

Transition Plan

In Ember 1.10, we will begin installing provided attributes in the component's attrs hash. If a provided attribute is accessed directly on the component, a deprecation warning will be issued.

In applications, you should update your component JavaScript and templates to access provided attributes via the component's attrs property.

In Ember 2.0, we will stop setting attributes as properties on the component itself.

We will also provide a transitional mixin that Ember addons can use that will make provided attributes available as attrs.*. This will allow add-ons to move to the new location, while maintaining support for older versions of Ember. We expect people to upgrade to Ember 1.10 relatively quickly, and do not expect addons to need to maintain support for Ember 1.9 indefinitely.

Routeable Components

Many people have noticed that controllers in Ember look a lot like components, but with an arbitrary division of responsibilities. We agree!

In current versions of Ember, when a route is entered, it builds a controller, associates a model with it, and hands it off to an (old-style) view for rendering. The view itself is invisible; you just write a template with the correct name.

We plan to transition to: when a route is entered, it renders a component, passing along the model as an attr. This eliminates a vestigial use of old-style views, and associates the top-level template with a regular component.

Transition Plan

Initially, we will continue to support routing to a controller+template, so nothing will break. Going forward, routes will route to a component instead.

In order to do that refactoring, several things will change:

  • Instead of referring to model properties directly (or on this), you will refer to them as model.propName.
  • Similarly, computed properties that move to your component will need to depend on model.propName if they are migrated from an ObjectController.
  • In both cases, the short version is that you can no longer rely on the proxying behavior of ObjectController or ArrayController, but you can remedy the situation by prefixing model. to the property name.
  • Unlike controllers, top-level components do not persist across navigation. Persistent state should be stored in route objects and passed as initial properties to routable components.
  • In addition to the asynchronous model hook in routes, routes will also be able to define a attrs hook, which can return additional asynchronous data that should be provided to the component.
  • Routeable Components should be placed in a "pod" naming convention. For example, the component for the blog-post route would be app/blog-post/component.js.

We plan to land support for routeable components in Ember 1.12, and deprecate routeable controllers at the same time. We plan to remove support for routeable controllers in Ember 2.0. This will allow you to move your codebases over to routeable components piecemeal before making the jump to 2.0.

We will also provide an optional plugin for Ember 2.0 apps that restores existing behavior. This plugin will be included in the Ember automated test suite to ensure that we do not introduce accidental regressions in future releases on the 2.x series.

We realize that this is the change has the largest transitional cost of all the planned features, and we plan to dedicate time to the precise details in the full RFC on this topic.

Improving Actions

Today's components can communicate with their parent component through actions. In particular, the sendAction method allows a child component to invoke a named action on the parent (inside of the actions hash).

Part of the reason for this API was a limitation in the original Handlebars syntax:

{{!-- we can't get too fancy with the value of key-press --}}
{{input key-press="valueChanged"}}

In this example, when the input component calls this.sendAction('key-press'), it invokes the valueChanged action on its parent component.

With the new HTML syntax for components, we have more flexibility:

<input key-press={{action "valueChanged"}}>

This will package up the parent's valueChanged action (in the actions hash) as a callback function that is available to the child component as this.attrs['key-press'].

export default Ember.Component.extend({
  keypress: function(event) {
    this.attrs['key-press'](event.target.value);
  }
});

The benefit of this approach is twofold:

  • Actions are no longer treated specially in the component API. They are simply properties packaged up to be called by the child component.
  • It is possible to pass an alternative function as the key-press, reducing the child component's knowledge of what the callback is doing. This has testing and abstraction benefits.

Transition Plan

We will continue to support the sendAction API for the forseeable future in today's Handlebars syntax.

When calling an existing component with new HTMLBars syntax, you do not need to change your existing actions hash. You should change syntax that looks like this:

{{video-player playing="playingBegins"}}

To this:

<video-player playing={{action "playingBegins"}}>

The video-player component's internal use of sendAction will work with both calling styles.

New components should use this.attrs.playing(), but existing components that want to continue supporting legacy callers should continue to use sendAction for now. The sendAction API will seamlessly support both calling styles, and will be supported for the forseeable future.

// instead of
this.sendAction('progress', value);

// new code can use
this.attrs.progress(value);

Onward

Version 2.0 marks the transformation of Ember from simply an MVC framework to a complete front-end stack. Between Ember's best-in-class router, revamped components with virtual DOM, easy-to-use build tools, and a growing ecosystem that makes taking advantage of additional libraries a breeze, there's no better way to get started and stay productive developing web apps today.

Hopefully, this plan demonstrates that staying on the cutting-edge can be done without rewriting your app. There are a huge number of Ember apps in production today, and we're looking forward to a time in the very near future where they can start to take advantage these new features.

Expect to see many more RFCs covering these features in depth soon (including a roadmap for Ember Data 1.0). We look forward to hearing your feedback!

  • Start Date: 2015-07-10
  • Relevant Team(s): Ember CLI
  • RFC PR: #20

Summary

Enable Subresource Integrity [SRI] checks by default.

Motivation

To promote the use of SRI in Ember apps as a safe default. Applications should be built with integrity attributes when it is safe to do so. (Unfortunately the main advantage won't be met by default, however confirming one attribute will)

This solves having poisoned CDN content: An introduction to JavaScript-based DDoS

Detailed design

Install ember-cli-sri by default.

  • Applications with relative paths will get SRI.
  • Applications with SRI.crossorigin will get SRI on fingerprint.prepend assets
  • Applications with fingerprint.prepend and origin specified and matching get a SRI.crossorigin of anonymous on fingerprint.prepend assets

By default development environments wont run SRI for performance reasons.

Further explanation available in: ember-cli-sri

Drawbacks

  • SRI won't always be on for sites with prepend due to SRI requiring CORS.
  • CORS requirement adds a barrier to entry to some users.
  • Broken SRI attrs would break the application.

Alternatives

No other alternatives appear suitable.

Unresolved questions

  • Adding origin attribute to add a safe same-origin check that doesn't need CORS.
  • Could users be warned until they explicitly set SRI.enabled = false or SRI.crossorigin = ?
  • Start Date: 2015-08-18
  • Relevant Team(s): Ember CLI
  • RFC PR: #23

Summary

Adds command line completion (tab completion) to ember-cli that fills in partially typed commands by the user and suggests available sub commands or options. (from now on i will refer to "strings in the command line" as "cli-commands" and "generate" and "new" as unprefixed "commands")

Motivation

With all the already existing commands and especially all blueprints, plus the fact that any addon can add even more blueprints to your tool kit, users can get overwhelmed. Currently, when you want to execute a specific task and you don't quite know the correct cli-command you have to invoke ember help which is noisy and slow ( especially when you just want to know the spelling of a specific thing). This feature will enable the user to choose from all existing cli-commands by pressing [tab] or just to let ember-cli fill partially typed cli-commands for speed.

Detailed design

The two main components of that feature are a completion function that is responsible for the actual tab-completion, and a generation function that will write the cli-command hierarchy together with metadata into a JSON file for fast processing.

completion function

To enable this feature, a completion function will run at the main entry point of the process (making use of omelette for shell completion). On every [tab] it will parse the command line and either completes a partially typed, unambiguous cli-command, or suggests possible cli-commands for the current context.

The user interface will work as you would expect it from a shell completion:

  • it suggests all commands if none are typed yet

      $ ember <tab>
      > addon     destroy   help      install   serve     version
        build     generate  init      new       test
    
  • it completes partially typed commands

      $ ember gen<tab>
      $ ember generate
    
  • it completes to suggests commands based on user input (note how it should understand aliases)

      $ ember g ad<tab>
      > adapter       adapter-test  addon
    
  • it, by default, will not suggest options

      $ ember g resource <tab>
    
  • it will suggest options on demand (note how it should know when an option needs a value)

      $ ember g resource post --<tab>
      --dry-run         --in-repo-addon=  --verbose
      --dummy           --pod
    

generation function

For a good user experience we don't want to figure out those suggestions on runtime or the completion feature would not be substantially faster then the ember help command. So there will be a generation function that generates a JSON file once after ember-cli is installed and then during every ember install some-addon command to ensure that blueprints added by new addons are recognized aswell.

Here an example snippet of a cli-command with one cli-subcommand:

  ...
  {
    "name": "command-name",
    "aliases": [
      "cn",
      "c"
    ],
    "options": [
    ],
    "commands": [
      {
        "name": "some-subcommand",
        "aliases": [
        ],
        "options": [
          {
            "name": "pods",
            "type": "boolean"
          }
        ],
        "commands": [
        ]
      }
    ]
  }
  ...

The generation function will, as a first step, iterate over all commands and reads the following properties:

  • name: a string, the autocompletion function will suggest
  • aliases: this is what the autocompletion function will accept in the cli-command chain
  • availableOptions: an array of options that need to have a name and a type property those will be accumulated for every cli-command in the cli-command chain and suggested on some-command --<tab>
  • cliCommands: can either be an array or a function that returns an array of objects that themselfs will be parsed for the properties in this list those cli-commands will be accepted as subcommands of the current cli-command.
  • skipHelp: whenever this property is set to true, the cli-command will also not be suggested by the autocompletion

Whenever an object does not have one of the above properties, a reasonable default is chosen (except for name. If it has no name, it will not be shown at all). This way it is easy to extend that feature in the future to handle arbitrary nested cli-commands.

Alternatives

  • Currently the completion function will just expect certain properties to be on a cli-command, this way most commands and blueprints work out of the box but maybe some architectual pattern, like a cli-command mixin or the like would be more robust and obvious.
  • Someone with more experience with ember-cli could have an idea of how to generate all cli-commands fast enough at runtime. So that we would not need to store the data in a JSON file.

Unresolved questions

Currently the autocompletion will figure out your default shell and configures it to allow tab-completion for ember. However on first-time usage you would need to resource your config file (or close and open your terminal) and I haven't figured out how to do this programmatically.

  • 2014-11-26
  • RFC PR: https://github.com/emberjs/rfcs/pull/24
  • Ember Issue:

Summary

Unlike Handlebars, HTMLBars parses HTML as it parses a template. Bound attributes are one syntax now possible.

For example, this variable color is bound to set a class:

<div class="{{color}}"></div>

Though traditional HTML attribute syntax should be preserved (using class and not className for example), the default path will be to set attributes as properties on the DOM node.

However this happy path has several important exceptions, and results in a few strange edge cases. This rfc will go into detail about the expected behavior without talking about the implementation of attribute on the Ember rendering pipeline.

Motivation

{{bind-attr is a verbose syntax and difficult for new developers to understand.

Detailed design

Given a use of bound attributes:

<input type="checkbox" checked={{isChecked}}>

There are three important inputs:

  • The element (tagName, namespaceURI)
  • The attribute name
  • The value (literal or stream)

The following described the algorithm for updating the attribute/property value on an element.

  1. If the element has an SVG namespace, use setAttribute. Setting SVG attributes as properties is not supported.
  2. If the attribute name is style, use setAttribute.
  3. Normalize the property name as described in propertyNameFor below. If a normalized name is returned, set that property on the element (element[normalizedPropName]). If it is not returned, set with setAttribute.

propertyNameFor is a normalization setup for attribute names that takes the element and attribute name as input.

  1. Build a list of normalized properties for the passed element normalizedAttrs[element.tagName][elementAttrName.toLowerCase()] = elementAttrName
  2. Fetch the normalized property name from this list normalizedAttr = normalizedAttrs[element.tagName][passedAttrName.toLowerCase()]
  3. Return this normalized attr. If an attrName is did not normalize to a property (for example class), null is returned

Acknowledged edge cases

  • Boolean attrs with blank string won't work like they would in HTML: <input disabled="{{blankString}}"> would be false
  • Some selectors may not work as expected. <input value="{{color}}"> will not result in a working [value=red] selector

Drawbacks

None.

Alternatives

Two obvious alternatives considered in detail are Angular and React.

In Angular 2.0, a new prop/attr/event syntax is being introduced.

Setting an attribute just like setting an HTML attribute:

<pui-tab title="What a nice tab!">

Properties are flagged with the [] syntax:

<input [disabled]="controller.isInputDisabled">

Angular is limited by it's HTML templating here. The value must be quoted to have complex content, where as in HTMLBars it is easier to bend the rules to introduce literal values: disabled={{controller.isInputDisabled}}.

Events are out of our immediate purview in this RFC, but for completeness note Angular's syntax:

<button (click)="hide()">hide image</button>

React's JSX has its own property syntax, one that diverges from traditional HTML by focusing entirely on properties instead of attributes. This means the templates are well prepared for use with components, but also that JSX must maintain a large whitelist of special cases such as supported tags and some HTML attributes.

In general we would prefer to have Ember templates be as close to HTML as possible, without requiring developers to learn a new set of property names replacing the attribute names they already know.

Unresolved questions

  • How do we deal with on* attributes?
  • Should we do anything special about generic element properties like <div outerhtml={{lol}}></div>?
  • Should HTMLBars unbound attributes use the same alorithm?

There is a spike of significant depth in PR #9721 and a followup in PR #9977.

  • Start Date: 2015-11-02
  • Relevant Team(s): Ember CLI
  • RFC PR: #28

Summary

Allow app.import to specify outputFIle of a given import. The default app.import would be considered to have an outputFile of assets/vendor.js

Motivation

It is common for individuals to want control over the outputFile for a given dependency. For example, one may want to load some asm.js code independently rather then via the single vendor.js blob.

It is also common for developers to want to group various dependencies together, and then lazy-load them in the routes they are required.

Although not as automatic as we would like, it does provide a rather elegant escape valve. Further work will likely continue to explore automation.

Detailed design

outputFile: option, specifies the target file for the given import. If multiple imports share an outputFile, they will be concatenated (regardless of type, css/images/videos/js/txt) in the order they where imported.

outputFile: will default to assets/vendor.js

Examples

variation 0: the default

app.import('vendor/vim.js', { outputFile: 'assets/vendor.js'});

variation 1: 1 file -> 1 outputFile

app.import('vendor/vim.js', { outputFile: 'assets/vim.js'});
  • vendor/vim.js becomes assets/vim.js
  • in prod it is:
    • uglified (unless using the uglify options it is excluded)
    • fingerprinted (unless it is excluded via the asset-rev options)

variation 2, multiple files to same outputFile

app.import('vendor/dependency-1.js', { outputFile: 'assets/alternate-vendor.js'});
app.import('vendor/dependency-2.js', { outputFile: 'assets/alternate-vendor.js'});
  • in-order of the corresponding app.import invocation, using sourceMap concat, the files are combined into assets/alternate-vendor.js
    • vendor/dependency-1.js + vendor/dependency-2.js >> assets/alternative-vendor.js

variation n, multiple files to same outputFile

app.import('vendor/dependency-1.js', { outputFile: 'assets/alternate-vendor.js'});
app.import('vendor/dependency-2.js', { outputFile: 'assets/alternate-vendor.js'});
app.import('vendor/dependency-n.js', { outputFile: 'assets/alternate-vendor.js'});
  • resulting concat is:
    • vendor/dependency-1.js + vendor/dependency-2.js ... vendor/dependency-n.js>> assets/alternative-vendor.js

Drawbacks

  • potential overlap with @chadhietala's packager/linker work
  • does not offer additional build-pipeline hooks for these files

Alternatives

Alternatives exist, such as adding support to the linker/packager effort, or instructing developers to drop down and use broccoli-funnel/source-map-concat.

The linker/packager effort is still a ways off, and could be thought of as complementary.

Dropping down to broccoli is a solution available today, but for this problem, it feels like a slightly too low level of abstraction.

Unresolved questions

  • how does this relate to type in app.import(..., { type: ... }) ?
  • should additional build-steps be allowed for specific output files? (I suspect maybe, but a future RFC can likely explore)
  • Start Date: 2015-11-11
  • Relevant Team(s): Ember CLI
  • RFC PR: #29

Summary

It should be possible to white- and/or blacklist addons in EmberApp.

Motivation

If there are two (or more) EmberApps, it's very likely that not all applications need all addons. E.g. if there is a main page application and one application for embeddable widgets, the main page might need all sorts of addons like ember-modal-dialog which adds completely useless bytes to widgets javascript and css files for the widgets. Other addons may add useless initializers or other things that have runtime performance penalties for no benefit.

Detailed design

When EmberApp ctor gets passed a blacklist like this

  EmberApp({
    addonBlacklist: ['ember-modal-dialog']
  });

it won't add addons whose name matches ember-modal-dialog to the list of addons for this app, just as if the addon's isEnabled() hook returned false.

C.f. https://github.com/ember-cli/ember-cli/blob/master/lib/broccoli/ember-app.js#L344, this is also were I would add this check.

Whitelist could work analogously.

Drawbacks

  • It adds a bit of API surface while you (possibly) don't care for the multiple app use case of ember-cli.

  • It kinda makes the name of an addon public api, so people might change it, not noticing they are breaking people's build (and people might not notice it either). Some addons have names like Ember CLI ic-ajax which seems awkward to use as an identifier.

Alternatives

  • Add a unified way to enable/disable an addon via normal config
    • if that is added one day, the white/blacklist could still be an abstraction for that

Unresolved questions

None.

  • Start Date: 2015-06-07
  • RFC PR: https://github.com/emberjs/rfcs/pull/45
  • Ember Issue: (leave this empty)

Summary

Solicit feedback about the support timeframe for Internet Explorer 8 and Internet Explorer 9.

Motivation

As Ember heads towards version 2.0, it is a good time to evaluate our browser support matrix. Ember follows Semantic Versioning, and we consider browser compatibility to be under the umbrella of those guarantees. In other words, we will continue to support whatever browsers we officially support in Ember 2.0 until Ember 3.0.

Ember 1.x did not have an official browser support matrix, but we would like to correct this for Ember 2.0.

We want to make this decision on the basis of the browsers that our community still needs to support, while weighing that against the costs we bear as a community to support older browsers. This RFC will lay out some of those costs, so we can decide what tradeoff is most appropriate.

Members of the core team maintain many different kinds of apps across many different kinds of companies. Some of us work on applications with small, agile teams, while others work inside of large corporations with many engineers. When this topic came up amongst the team, we discovered that, across all these different companies and Ember apps, no one was still supporting IE8.

Because of this, the core team's impression is that the costs of IE8 support now far exceed the benefits, and we are considering dropping support for IE8 in Ember 2.0. Before we make the decision, we want to hear from the rest of the community. Supporting IE8 incurs significant cost, both in terms of features and maintenance, and we want the community to help us think through the cost-benefit analysis.

Ember is more than just the framework's code. When people use Ember, they expect to be able to use Ember's tooling, read Ember's documentation, find solutions to problems on Stack Overflow, and read tutorials produced by community members. All of these are shackled to the limitations of IE8, and by dropping support for IE8, people can begin to rely on the improved baseline of ES5.

Below, we outline the costs of continuing to support IE8, so that you can help us make a considered decision.

Detailed design

IE8

Eliminate get()

Currently, accessing properties on an Ember object requires using the .get() method. By using this abstraction, we have been able to implement several powerful features, such as proxies and computed properties, even on older browsers like IE8 that lack getters and setters.

However, ECMAScript 5, which shipped in 2009, added support for getters to JavaScript itself, and we would like to use this feature in Ember to eliminate the explicit calls to get. Developers new to the framework tell us that having to remember to use .get() is a big source of confusion. More seasoned developers get used to it, but moving the Ember object model closer to the pure JavaScript object model is a major goal for Ember 2.x. While many of the features of ES6 classes can be transpiled, getters and setters require engine support, and could not be used if we needed to support IE8.

More ES6 Features, Today

While much of ES6 can be transpiled correctly to ES3 (the version of JavaScript included with IE8), transpiling ES6 modules and classes requires defineProperty.

Continued support for IE8 limits our ability to adopt new ES6 features in the internals of Ember, and to talk about them in our documentation.

One example: In ES6, classes define their methods as non-enumerable properties. Transpiling this to existing browsers is only possible with defineProperty, which is not included in IE8. Trying to transpile ES6 classes to work on IE8 would lead to apps exhibiting subtly different behavior that would be painful to debug. IE8 users would discover that the larger Ember ecosystem was incompatible with their apps in hard-to-predict ways, and we think the ecosystem is one of the biggest advantages Ember offers.

In other words, we don't think we can make the full transition to JavaScript classes a first-class part of the Ember experience if we still support IE8. As we did with modules, we would like to move more of our core to JavaScript features in the future, which would be significantly stymied by the lack of defineProperty in IE8.

Remove the jQuery Dependency

For its entire lifetime, Ember has relied on jQuery to smooth the rough edges of browser compatibility when interacting with the DOM. When people think about that dependency, they often assume that we could just replace calls to things like .attr with their more verbose DOM counterpart and call it a day.

jQuery does more than just patch over IE8 rough spots; it also serves as the central place for normalizing behavior that can differ significantly across browsers. If we tried to pick-and-choose pieces of jQuery to pull into Ember, we would also be responsible for backporting any changes made to jQuery. We'd rather just rely on jQuery directly; that's what dependencies are for.

The jQuery dependency has helped us with a few cross-browser areas:

  • Portable DOMContentLoaded (via jQuery.ready)
  • Support for event delegation across a wide variety of events.
  • Attribute and property normalization, which has already been implemented by HTMLBars
  • HTML parsing, which has also been implemented by HTMLBars

Of these, proper support for event delegation is the largest remaining reason to rely on jQuery. IE9's support for the capture phase of events makes it simpler to support event delegation properly across all event types without a normalization layer.

Support More Event Types

Many newly specified events in the web platform (such as the media events) do not bubble, which is a problem for frameworks like Ember that rely on event delegation. However, the capture API, which was added in IE9, is invoked properly for all events, and does not require a normalization layer. Not only would supporting the capture API allow us to drop the jQuery dependency, but it would allow us to properly handle these non-bubbling events. This would allow you to use events like playing in your components without having to manually set up event listeners.

CSS Improvements in Ember 2.x

Today, the main Ember framework does very little to directly help with CSS. We expect that to change in the 2.x series, as we explore ways to help tame the CSS beast.

However, a number of important CSS features landed in IE9: CSS3 selectors, full support for querySelectorAll, getComputedStyle, calc() to name a few. Productively tackling the CSS problem without these features would be like fighting with both hands tied behind our backs, and it may be impossible for us to robustly tackle the problem until Ember 3.0 if we needed to continue to support IE8.

While it may be theoretically possible to implement some form of this feature in IE8, it is likely that the cost of doing so in a backwards-compatible way would significantly add to development time; perhaps so significantly it would be better to wait until we drop support for IE8 than attempt to bolt it on to a browser released half a decade ago.

Maintenance Costs

While it's very easy to weigh the costs of features that we could not implement at all due to IE8, there is a much more pernicious cost that is harder to see.

Support for IE8 adds costs, sometimes significant, to every new feature we work on. For example, broken support for text nodes in IE8 significantly impeded early work on Glimmer. Every new area of work requires budgeting a significant amount of time for IE8 support.

This is not surprising. When asked many years ago what jQuery could do when IE6 was gone, John Resig replied that we would gain little from dropping IE6, and that the benefits would not come until jQuery could drop IE8, the last version of IE featuring the bugs that made IE6 so difficult to develop for.

Quite often, we will assume that a feature is ready to ship, and only discover subtle issues in IE8 very close to the release once it has been tested. We estimate that support for legacy Internet Explorer slowed down the development of HTMLBars by 2x.

In short, we would be able to implement more features more quickly without the burden of bugs that were first introduced 15 years ago.

What About IE9?

In the first decade of 2000, browsers were updated very slowly, and every new release took a long time to be supplanted by the next release. As the last version of Internet Explorer supported by Windows XP, IE8 is a relic of this bygone era. In contrast, IE9 usage was quickly supplanted by IE10, and that pattern continues with IE11.

The public trackers have IE9 at a lower share of total usage than IE8, so it might be worth considering dropping them together. Our decision for Ember 2.0 will likely hold until late 2016, so it's worth considering more than just the current moment when making the decision.

While IE9 added support for the ES5 features we need to move into the future for JavaScript, IE10 added support for the last great wave of web features. Here is a sampling:

  • Flexbox and Grid Layout
  • Offline storage (IndexedDB, File, Blob)
  • Web Workers
  • Typed Arrays
  • Web Sockets
  • App Cache
  • History API

Several of these features are required for asm.js, and in total, they make the web platform a capable application runtime. While we don't have any immediate plans to take advantage of these web features right now, the best experiments that people are doing today rely on them. By assuming IE10 as the baseline across the entire ecosystem, we would be able to do much more aggressive experimentation on the web platform.

Drawbacks

Many users have told us that they chose Ember because of the community's commitment to backwards compatibility. When we announced in early 2014 that we would continue to support IE8 for at least another year, other libraries and frameworks had already dropped support. That being said, there will always be organizations using Ember that exist on the tail-end of browser adoption patterns. We risk alienating or upsetting those users by dropping support for a browser that, while on the way out, is not yet completely gone.

However, in many cases, the requirement of IE8 support is driven by non-technical management who do not have a strong sense of the experience of using apps in IE8. In practice, many applications are not rigorously tested in older browsers, and the performance of IE8 is so bad that applications written using any framework perform poorly. Techniques that framework and application developers use to make Chrome fast quite often have pathological characteristics on browsers with a DOM and JavaScript engine written in the 90s.

Still, some people make it work, and dropping IE8 support may prevent those teams from staying with the community as it migrates to Ember 2.0.

Alternatives

Drop IE8 Support During 2.x

One alternative we have considered is deprecating IE8 support prior to releasing 2.0, but still maintaining it for a few point releases to give IE8 more time to lose market share.

After discussing with the core team, we believe that this would be a violation of our Semantic Versioning commitment to users. Specifically, we want to avoid a large group of apps getting stuck midway through the 2.x cycle. Version numbers are an important tool for developers, maintainers and ecosystems to communicate compatibility. Tools such as package managers rely on version numbers correctly indicating breaking changes.

We consider browser compatibility to be a feature of Ember, and dropping IE8 support in a minor release would be akin to stripping out any other major feature. While the ecosystem would muddle along in either case, such a move would cause exactly the kind of ecosystem fragmentation that Semantic Versioning is designed to prevent.

If we want to communicate the idea that changing versions comes with a reduction in functionality, we should do that the same way we always do, by incrementing the major version.

Early 3.0

Another option is to release 3.0 in six months, rather than the nearly two years between Ember 1.0 and Ember 2.0.

Correctly tuning the cadence of major releases is a delicate tradeoff. Semantic Versioning allows us to easily communicate about breaking changes, and some take this as a license to make them frequently. However, a robust ecosystem relies on a certain measure of stability.

We believe that the frustration of breaking changes every six months (or even a year) would outweigh whatever benefits it would provide. Ember's biggest goal is building a shared foundation for our ecosystem to build on, and this requires a careful commitment to stability.

While we could make a "small" breaking release soon after 2.0, breaking changes inherently fragment the ecosystem, and we hope that the years to come bring more stability for add-on authors and tool-makers, not less.

Bring Your Own Compatibility

Some libraries attempt to thread the needle of IE8 compatibility by asking users to bring their own compatibility libraries. They write the internals of their framework as if IE8 did not exist, and require end users to use polyfills to make the environment look equivalent to newer browsers. For example, React asks users to bring libraries such as es5-shim, es5-sham, console-polyfill and html5shiv if they want IE8 support.

Facebook.com supports IE8, and uses React, so there is a path to using React with IE8. This path is partially documented on the React website. This gives us a perfect opportunity to evaluate the impact of this strategy in the real world. We admire the React team's work in this area: support for IE8 is difficult and triaging and fixing IE8 bugs requires diligent effort.

After reviewing the IE8-compatibility issues filed on React.js tracker, we believe there are significant user experience costs to this strategy.

We have spent considerable effort on first-class IE8 support in Ember 1.x, and we feel that users who require IE8 support will have a better experience using Ember 1.14 (with the subset of the ecosystem that supports 1.x) than trying to cobble together a solution that works reliably in a version of Ember with second-class, bring-your-own-compatibility support.

Unresolved questions

We are relying on the community to help us weigh the above tradeoffs. The more data you can provide about the browser makeup of your customers (especially as it affects revenue), the better we can reason whether now is the time to remove IE8 (and possibly IE9) support.

If you cannot share the information publicly, please email whatever information you consider useful to browserusage@emberjs.com. We will keep it in the strictest of confidence.

  • Start Date: 2016-03-26
  • Relevant Team(s): Ember CLI
  • RFC PR: #46

Improved Release Process

Summary & Motivation

ember-cli has followed an ad hoc release process throughout its existence which has made it difficult to know exactly when releases would come out, what features would and would not be supported, and the degree to which it would support existing Ember applications. With the proposal for lockstep SemVer there were ideals of guaranteeing compatibility, which we have mostly met, but that resulted in making decisions of delaying an official 2.X release of ember-cli to avoid additional major version bumps.

We propose that we adopt a pattern similar to Ember itself in order to align with the expectations of the Ember community, more-clearly communicate around our release lifecycle, and provide rigor around our support structure. Anything which is not specifically called out as a difference in this document is inferred to be following the patterns specified by Ember itself.

Channel Design

To begin there will be three separate channels: canary, beta, and release. We intend to investigate an LTS channel after this process has matured.

  • Canary: represents the latest work in ember-cli, and is synonymous with the HEAD of the master branch and is the least stable of all channels.
  • Beta: branched off of master every six weeks, exact commit decided upon manually. Updated and released weekly with commits that are prefixed [BUGFIX beta]. Less stable than release as it is a proving ground. No new features will be added once the branch has been created to allow for existing features to mature. Tags will match Ember's patterns, for example v2.6.0-beta.1. Branch name: beta.
  • Release: branched off of Beta every six weeks. Only rarely will this be updated, but possible for security issues and uncaught regressions. Branch name: release.

ember-cli will not support daily releases as time-based packaging doesn't make a lot of sense.

New Features

New features to ember-cli must be protected by feature flags. Incomplete and WIP features will be available in the Canary channel, but will not be available in the Beta or release channels.

Tooling Design

We must create additional tooling and patterns in order to make this efficient. Since ember-cli successfully installs and works from npm without modification we don't need to bundle and publish an asset for each Canary build. We'll publish tags to npm for beta and release channel releases so that they're not tied to a git remote URL. The latest tag for npm (the default when installing via npm install -g ember-cli) will track our release channel at all times. We will publish tagged releases (i.e. v2.6.0-beta.1) to the npm beta tag which is used via npm install --save-dev ember-cli@beta.

Timeline

Since the ember-cli project is presently designed to track Ember development, we'll run our release schedule on a one week delay from Ember itself. This ensures that we're able to incorporate the latest changes from Ember into ember-cli and gives us a week to check for Ember-introduced regressions. As Ember itself becomes an npm module this will become less of a concern and we can diverge on our release schedule as best suits the ember-cli project. We will ship the last beta coincidentally with the newest Ember release.

Drawbacks

The largest drawback is also a feature: we require more rigor in our release processes. This process presently requires a weekly manual review of new commits to master and their prefixes which then get cherry-picked to the appropriate release and beta branches.

We've also encountered issues with npm in the past which may require investigation into other tools.

Effort

In order to undertake this task, there are multiple workflows which must occur:

  • Updates to the website and documentation communicating this plan.
  • Teaching new patterns to ember-cli contributors, most specifically commit tagging and feature flagging.
  • Increased automation of the release process.
  • Tooling to support feature flags.

References

  • Start Date: 2015-04-09
  • RFC PR: https://github.com/emberjs/rfcs/pull/46
  • Ember Issue: https://github.com/emberjs/ember.js/pull/11440

Summary

Fully encapsulate and privatize the Container and Registry classes by exposing a select subset of public methods on Application and ApplicationInstance.

Motivation

The Container and Registry classes currently lead a confusing life of semi-private exclusion within Ember applications. They are undocumented publicly but not fully private either, as knowledge of their particulars is required for developing both initializers and unit tests. This situation has become untenable as the new Registry class has been extracted from Container, and the complexity of their usage has grown across Application and ApplicationInstance classes.

We can bring sanity to this situation by continuing the work started at the Application level to expose methods such as register and inject from the internally maintained Registry.

Furthermore, once Container and Registry are fully private, their architecture and documentation can be cleaned up. For instance, a Container can freely reference its associated Registry as registry rather than _registry, as it can be assumed that only framework developers will reference this property.

Detailed design

Application will expose the following methods from its internally maintained registry:

  • register
  • inject
  • registerOptions - mapped to Registry#options
  • registerOptionsForType - mapped to Registry#optionsForType

ApplicationInstance will also expose the the same methods. However, these methods will be exposed from its own internally maintained registry, which has the associated Application's registry configured as a "fall back". No direct path will be provided from the ApplicationInstance to the Application's registry.

ApplicationInstance will also expose the following methods from its internally maintained container:

  • lookup
  • lookupFactory

ApplicationInstance will cease exposing container, registry, and applicationRegistry publicly.

Application initializers will receive a single argument to initialize: application.

Likewise, ApplicationInstance initializers will receive a single argument to initialize: applicationInstance.

Container and Registry will be made fully private and documented as such. Each Container will freely reference its associated Registry as registry rather than _registry.

ember-test-helpers will provide an isolatedApplicationInstance method instead of an isolatedContainer for unit testing. A mechanism will be developed to specify which initializers should be engaged in the initialization of this instance. In this way, we can avoid duplication of registration logic, as is currently done in a most un-DRY manner in the isolatedContainer.

Drawbacks

This refactor will require maintaining backwards compatibility and deprecation warnings until Ember 2.0. This will temporarily increase internal code complexity and file sizes.

Alternatives

The obvious alternative is to make Container and Registry fully public and documented. An application's registry would be available as a registry property. An application instance's container would remain available as container.

We could still pass an Application into application initializers and an ApplicationInstance into application instance initializers.

If this alternative is taken, I would suggest that Application should deprecate register and inject in favor of calling the equivalents on its public registry.

Regardless of which alternative is chosen, we should ensure that the public aspects of container and registry usage are well documented.

Unresolved questions

  • Are the public methods listed above sufficient or should any others be exposed?

  • What mechanism should be used to engage initializers in unit and integration tests? Should test modules simply have an initializers array, similar to the current needs array?

  • Given the semi-private nature of containers and registries, we may not need to worry about semver for deprecations. However, we should be good citizens and properly deprecate as much as possible. Some real world use cases in initializers will no doubt be a surprise, so we need to tread carefully.

  • Start Date: 2016-04-06
  • Relevant Team(s): Ember CLI
  • RFC PR: #50

Summary

A number of Ember framework function calls are no-ops in production. Ember CLI should strip these no-op function invocations from production builds by default.

Motivation

Removing code that isn't required in production results in smaller and faster applications.

Detailed design

The following framework function calls will be removed from ember-cli production builds by default:

The API documentation will be updated where necessary to indicate that these function calls will be stripped from production builds.

A babel plugin will execute the removal of these function calls based on provided configuration. The plugin will affect the code of the current app or addon only and won't affect code in child or grandchild addons. As this change becomes part of the default ember-cli configuration, addons will adopt the code stripping as they upgrade to newer ember-cli versions.

The plugin configuration will define an array of modules or global functions to remove. Here's an example of what this configuration might look like:

{
  removals: [
    {
      module: 'ember', //eg. import Em from 'ember';
      paths: [
        'assert', //Em.assert will be removed
        'debug',  //Em.debug will be removed
        'a.b.c'   //Em.a.b.c will be removed
      ]
    }, {
      global: 'Ember',
      paths: [
        'deprecate' //Ember.deprecate will be removed
      ]
    }, {
      paths: [
        'console.log' //console.log will be removed
      ]
    }
  ]
}

The plugin will support removal of destructured and reassigned invocations of these functions and will support both Babel 5 and 6.

An app or addon can disable the code removal by removing the babel plugin.

How We Teach This

This change doesn't bring any new functionality. Other than updating the Ember API docs, we don't need to make guide or other documentation changes. At the time of releasing, we may want to point out the possible side effects in a release blog post (see the Drawbacks section below).

If we want to expose the configuration options so that application authors can customize the settings, we can include a new section in the Ember CLI docs.

Drawbacks

This may introduce an unexpected change in production builds as arguments that have side effects will no longer be executed. For example:

Ember.assert('Some assertion', someSideEffect());

Currently, the someSideEffect function will be executed in production. When this RFC lands, it won't.

Alternatives

An Ember addon could provide opt-in function stripping for applications that want it. If this RFC isn't deemed a good default for Ember CLI, that option should be explored.

  • Start Date: 2014-05-06
  • RFC PR: https://github.com/emberjs/rfcs/pull/50
  • Ember Issue: (leave this empty)

Summary

The {{action helper should be improved to allow for the creation of closed over functions that can be passed between components and passed the action handlers.

See this example JSBin from @rwjblue for a demonstration of some of these ideas.

Motivation

Block params allow data to be passed from one component to a downstream component, however there is currently no way to pass a callback to a downstream component.

Detailed design

First, the existing uses of {{action will be maintained. An action can be attached to an element by using the helper in element space:

{{! app/index/template.hbs }}
{{! submit action will hit immediate parent }}
<button {{action "submit"}}>Save</button>

An action can be passed to a component as a string:

{{! app/index/template.hbs }}
{{my-button on-click="submit"}}
// app/components/my-button/component.js
export default Ember.Component.extend({
  click: function(){
    this.sendAction('on-click');
  }
});

Or a default action can be passed:

{{! app/index/template.hbs }}
{{my-button action="submit"}}
// app/components/my-button/component.js
export default Ember.Component.extend({
  click: function(){
    this.sendAction();
  }
});

In all these cases, submit is called on the parent context relative to the scope action is attached in. The value "submit" is attached to the component in the last two as this.attrs.on-click or this.attrs.action, although it is not directly used.

Creating closure actions

Closure actions are created in a template and may be used in all places a string action name can be used. For example, this current functionality:

<button {{action "submit" on="click"}}>Save</button>

Would be written using a closure action as:

<button {{action (action "submit") on="click"}}>Save</button>

The functionality is exactly the same as the string-based action example. How does that happen?

  • (action "submit") reads the submit function off the current scope's actions.submit property.
  • It then creates a closure to call that function.
  • {{action receives that function as a param. It registers a listener (in this case on click) and when fired calls the closure function.

Consider usage on the calling side. With the current string-based actions:

{{my-component action="submit"}}
export default Ember.Component.extend({
  click: function(){
    this.sendAction(); // submit action, legacy
    // this.attrs.action is a string
    this.attrs.action; // => "submit"
  }
});

With closure actions, the action is available to call directly. The (action helper wraps the action in the current context and returns a function:

{{my-component action=(action "submit")}}
export default Ember.Component.extend({
  click: function(){
    this.sendAction(); // submit action, legacy
    // this.attrs.action is a function
    this.attrs.action(); // submit action, new style
  }
});

A more complete example follows, with a controller for context:

// app/index/controller.js
export default Ember.Controller.extend({
  actions: {
    submit: function(){
      // some submission task
    }
  }
});
{{! app/index/template.hbs }}
{{my-button save=(action 'submit')}}
// app/components/my-button/component.js
export default Ember.Component.extend({
  click: function(){
    this.attrs.save();
    // for backwards compat, you may also this.sendAction('save');
  }
});

Hole punching with a closure-based action

The current system of action bubbling falls down quickly when you want to pass a message through multiple levels of components. A closure based action system helps address this.

Instead of relying on bubbling, a closure action wraps an action from the current context's actions hash in a function that will call it on that context. For example:

{{! app/index/template.hbs }}
{{my-form submit=(action 'submit')}}
{{! app/components/my-form/template.hbs }}
{{my-button on-click=attrs.submit}}
{{! app/components/my-button/template.hbs }}
<button></button>
// app/components/my-button/component.js
export default Ember.Component.extend({
  click: function(){
    this.attrs['on-click']();
    // for backwards compat, you may also this.sendAction();
  }
});

A closure action can also be called by an action handler:

{{! app/index/template.hbs }}
{{my-form submit=(action 'submit')}}
{{! app/components/my-form/template.hbs }}
{{my-button on-click=submit}}
{{! app/components/my-button/template.hbs }}
<button {{action on-click}}></button>

Lastly, closure actions allow for yielding an action to a block. For example:

{{! app/index/template.hbs }}
{{my-form save=(action 'submit') as |submit reset|}}
  <button {{action submit}}>Save</button>
  {{! ^ goes to my-form's save attr property, which
        is the submit action on the outer scope }}
  <button {{action reset}}>Reset</button>
  {{! ^ goes to my-form }}
  <button {{action "cancel"}}>Cancel</button>
  {{! ^ goes to outer scope }}
{{/my-form}}
{{! app/components/my-form/template.hbs }}
{{yield attrs.save (action 'reset')}}
// app/components/my-form/component.js
export default Ember.Component.extend({
  actions: {
    reset: function(){
      // rollback
    }
  }
});

Currying arguments with a closure-based action

With string-based actions, an argument can be passed to the called function. For example:

<button {{action "save" model}}></button>
export default Ember.Component.extend({
  actions: {
    save: function(model) {
      model.save();
    }
  }
});

Closure actions allow for another opportunity to curry arguments. Arguments set by an element action helper simply add to the end of the arguments list:

{{! app/index/template.hbs }}
{{my-component save=(action "save" model)}}
{{! app/components/my-component/template.hbs }}
<button {{action attrs.save prefs}}></button>
// app/index/controller.js
export default Ember.Controller.extend({
  actions: {
    save: function(model, prefs) {
      model.set('prefs', prefs);
      model.save();
    }
  }
});

Multiple arguments can be curried or set at any level. If an action is called ala this.attrs.save(additionalPrefs), that final argument is added to the end of the arguments list.

Re-targeting the scope of a closure action

The target option may be provided to specify what scope the closure is called with. For example:

{{! app/index/template.hbs }}
<my-component on-click={{action "save" model target=someComponentInstance}}></my-component>

Much like with the {{action helper, passing both a target and a bound argument will throw.

The default target for a closure is always the current scope.

  • When routable components land, the current component will be the default target.
  • If a controller is the current scope, that controller will also be a default target.
  • A route will never be a closure action target. String actions will continue to have their current behavior of bubbling to the route.

A later proposal will determine how actions on a route are passed to a routable component.

Return values of a closure action

Closure actions return the returned value of their called function. For example:

// app/index/controller.js
export default Ember.Controller.extend({
  actions: {
    submit: function(){
      return 'great success';
    }
  }
});
{{! app/index/template.hbs }}
{{my-button save=(action 'submit')}}
// app/components/my-button/component.js
export default Ember.Component.extend({
  click: function(){
    var result = this.attrs.save();
    // for backwards compat, you may also this.sendAction('save') but
    // in that case you do not have access to the return value.
    result; // => 'great success'
  }
});

Actionable object with INVOKE

{{mut is a new helper in Ember.js. It is not yet widely used in Ember apps, but its interaction with the action helper is important to align early on.

Mut objects represent a modifiable value. For example with tag-based components:

{{! app/index/template.hbs }}
<my-form name={{mut model.name}}></my-component>

This will cause a mutable property to be added to attrs. To update the name, this.attrs.name.update(newName) can be called. The value can be read (in JavaScript) as this.attrs.name.value.

Often, a mutable value will be set as the result of an action. Mutable values can be called actionable. For example:

{{! app/index/template.hbs }}
<my-form submit={{action (mut model.name)}}></my-component>
// app/components/my-form/component.js
export default Ember.Component.extend({
  click() {
    const value = this.get('newValue');
    this.attrs.submit(value);
  }
});

What is happening here?

  • (mut model.name) creates a mutable object for the model.name value.
  • {{action (mut model.name)}} tests the passed object for a property with the key INVOKE (an internal symbol). This value is a function that updates the mutable value.
  • Action wraps the calling of the INVOKE property in a function like any other action, and passes it to the attrs.

Thus, when the action is called the argument is passed to INVOKE which uses it to update the mutable value. This is a simple way to enable the "actions up" part of component-driven app architecture without ceremony around changing state.

Plucking a property from the first argument with value

A component (or when Ember supports this better, an element) may emit an event object and pass it to an action. In this case the value will need to be read off the event before it can be passed to the action function. For example:

{{input input=(action 'setName')}}
export default Ember.Component.extend({
  actions: {
    setName(event) {
      this.get('model').set('name', event.target.value);
    }
  }
});

The action serves only to read the value off of the event. Here the value option can be used as sugar to accomplish the same task:

{{input input=(action (mut model.name) value="target.value")}}

The value path is read off of whatever the first argument to the actions is.

  • (mut model.name) becomes a function, our action
  • When the input event fires, the function is called with the event as the first argument.
  • The first argument is re-written to the value of event.target.value
  • The function wrapping the mut is set
  • The mut is updated.

This option is designed to align with future plans for on-some-event handlers for html elements.

Drawbacks

Currently {{action is only used in an element space:

<button {{action "booyah"}}>Fire</button>

The closure usage is a new, perhaps action is not the right word. However the two behaviors are pretty similar in their conceptual behavior.

  • {{action in element space attaches an event listener that fires a bubbling action.
  • (action closes over an action from the current scope so it can be attached via {{action or passed around and called later.

This confusion should go away as we move to an on-click event listener pattern, ala <button on-click={{someClosureAction}}>.

Additionally, there may be developers who still have {{action someActionName}} instead of the quoted version. This is long deprecated, but these apps may see some unexpected behavior.

Also additionally, some emergent behaviors exist that may not be desired as real APIs. For example, an action being a function means it can be passed directly to event handlers:

{{my-component mouseEnter=(action 'didEnter')}}

The actual API we plan for 2.0 (ideally) is:

{{my-component on-mouse-enter=(action 'didEnter')}}

These behaviors should not be documented, and we should make clear that they rely on behavior that will be deprecated. A mitigating move is to not proxy actions through to get on a component, and only allow them to be accessed on attrs.

Lastly, default actions may look a bit confusing:

{{my-button action=(action 'action')}}
{{! ^ this is valid }}

But the quoted string syntax is not being removed.

Alternatives

There is maybe a thing called ref that solves this same problem. There has also been discussion of accessing properties on outlet across all child components and their layouts, which would allow easy targetting of the top level component.

Unresolved questions

Interaction with ref or outlet. if any..

  • Start Date: 2015-05-17
  • RFC PR: https://github.com/emberjs/rfcs/pull/53
  • Ember Issue: https://github.com/emberjs/ember.js/pull/11278

Summary

Ember.js 1.13 will introduce a new API for helpers. Helpers will come in two flavors:

Helpers are a class-based way to define HTMLBars subexpressions. Helpers:

  • Have a single return value.
  • Must have a dash in their name.
  • Cannot be used as a block ({{#some-helper}}{{/some-helper}}).
  • Can store and read state.
  • Have lifecycle hooks analogous to components where appropriate. For example, a helper may call recompute at any time to generate a new value (this is akin to rerender).
  • Are a superset of shorthand helpers, the function-based syntax described below. They can do more, but in many cases a shorthand helper is appropriate.

Shorthand helpers are a function-based way to define HTMLBars subexpressions. Helpers written this way:

  • Have all the limitations of regular helpers.
  • Have no instance associated with them, cannot store or read state.
  • Have no lifecycle hooks. The function is simply re-computed when any input changes.

These improved helpers fill a gap in Ember's current template APIs:

has positional paramshas layout (shadow DOM)can yield templatehas lifecycle, instancecan control rerender
componentsYesYesYesYesYes
helpersYesNoNoYesYes
shorthand helpersYesNoNoNoNo

An example helper:

// app/helpers/full-name.js
import Ember from "ember";

export default Ember.Helper.extend({
  nameBuilder: Ember.inject.service(),
  compute(params) {
    const builder = this.get('nameBuilder');
    return builder.fullName(params[0], params[1]);
  }
});

An example shorthand helper:

// app/helpers/full-name.js
import Ember from "ember";

export default Ember.Helper.helper(function(params, hash) {
  let fullName = params.join(' ');
  if (hash.honorific) {
    fullName = `${hash.honorific} ${fullName}`
  }
  return fullName;
});

Helpers can be used anywhere an HTMLBars subexpression is valid:

{{full-name 'Bigtime' 'Beagle'}}
{{input value=(full-name 'Gyro' 'Gearloose') readonly=true}}
{{#if (eq (full-name 'Webbigail' 'Vanderquack') selectedFullName))}}
  You have chosen wisely.
{{/if}}

Motivation

Ember.js 1.13 make a private API change that removed the ability to access application containers. Ember.HTMLBars._registerHelper was previously passed the env object, and this was removed as it is an internal implementation detail.

Ember's helper API has not kept pace with improvements possible after the introduction of HTMLBars. This has resulted in the community using a variety of private APIs, many of which leak information about the outer context of a helpers invocation as well as the render layer implementation.

The current public API is:

This API is sorely lacking in functionality required by addon authors.

  • Has no access to other parts of the app, like services
  • Leaks a private API for dealing with blocks
  • Results in less efficient helpers due to the Handlebars compatibility layer
  • Has poor support for hash arguments

Additionally it remains difficult to write a helper that recomputes due to something besides the change of its input.

Specifically, this RFC addresses many of the concerns in emberjs/ember.js#11080. Libraries such as yahoo/ember-intl, dockyard/ember-cli-i18n, and minutebase/ember-can will be provided a viable public API to couple to.

Detailed design

Helpers must have a dash in their name. In an Ember-CLI app, they can be named according to the app/helpers/full-name.js convention (app/full-name/helper.js in pods mode). For a globals app, naming a helper App.FullNameHelper is sufficient.

Definition and lifecycle

A helper is defined as a class inheriting from Ember.Helper. For example:

// app/helpers/hello-world.js
import Ember from "ember";

// Usage: {{hello-world}}
export default Ember.Helper.extend({
  compute() {
    return "Hello Helper World";
  }
});

Upon initial render:

  • The helper instance is created.
  • The compute method is called. The return value is outputted where the helper is used. For example in <div class={{some-helper}}></div> the return value is set to the class.

The compute function is always called with params (the bare, ordered arguments) and hash (the named arguments). For example:

// app/helpers/greet-someone.js
import Ember from "ember";

// Usage: {{greet-someone 'bob' greeting='say hello'}}
export default Ember.Helper.extend({
  compute(params, hash) {
    return `Hello ${params[0]}, nice to ${hash.greeting}`;
  }
});

Which functions the same as this shorthand:

// app/helpers/greet-someone.js
import Ember from "ember";

// Usage: {{greet-someone 'bob' greeting='say hello'}}
export default Ember.Helper.helper(function(params, hash) {
  return `Hello ${params[0]}, nice to ${hash.greeting}`;
});

When the params or hash contents change, the compute method is called again. The instance of the helper is preserved across rerenders of the parent. A shorthand helper, having no instance, is called every time a bound argument changes.

The init and destroy methods can be subclassed for setup and teardown.

Consuming a helper

Helpers can be used anywhere an HTMLBars subexpression can be used. For example:

{{#if (can-access 'admin')}}
  {{link-to 'login'}}
{{/if}}
{{#if (eq (can-access 'admin') false)}}
  No login for you
{{/if}}
<my-login-button isAdmin={{can-access 'admin'}} />
Can access? {{can-access 'admin'}}

Passing a helper to a {{- invoked component skips the auto-mut behavior:

{{my-login-button isAdmin=(can-access 'admin')}}

Let's step through exactly what happens when using an helper like this:

<my-login-button isAdmin={{can-access 'admin'}} />

Upon initial render:

  • The helper can-access is looked up on the container
  • The helper is identified as a full helper, not a shorthand helper function
  • The helper is initialized (init is called)
  • The compute function is called on the helper.
  • The return value from compute is passed as an attr to my-login-button.
  • The helper instance remains in memory.

If the parent scope is rerendered:

  • The compute function is called again.
  • The return value from compute is passed as an attr to my-login-button.

Upon teardown:

  • The helper is destroyed, calling the destroy method.

Returning a value

The return value of helper is passed through to where their subexpression is called. For example, given a helper (this one a shorthand helper):

// app/helpers/full-name.js
import Ember from "ember";

export default Ember.Helper.helper(function fullName(params, hash) {
  return params.join(' ');
}

The following are effectively the same:

<div data-name={{full-name "Fenton" "Crackshell"}}></div>
<div data-name={{"Fenton Crackshell"}}></div>
{{my-component name=(full-name "Magica" "De Spell")}}
{{my-component name="Magica De Spell"}}
<p>{{full-name "Bentina" "Beakley"}}</p>
<p>{{"Bentina Beakley"}}</p>

An exclusion to this pattern is the following form:

<div {{full-name "Webbigail" "Vanderquack"}}></div>

This is a legacy form of mustache usage. Helpers will throw an exception when used in this manner.

Consuming services and recompute

Helpers are a valid target for service injection. For example:

// app/helpers/current-user-name.js
import Ember from "ember";

export default Ember.Helper.extend({
  // Same API as components:
  session: Ember.inject.service(),
  compute() {
    return this.get('session.currentUser.name');
  }
});

However consuming a property from a service does not bind the data being displayed to that property. After {{current-user-name}} has been computed and rendered, it will never be invalidated.

For this reason, helpers are granted some control over their computation lifecycle. A helper will recompute when:

  • A value passed via the template changes (params or hash)
  • The recompute method is called

For example, this helper checks if the current use has access to a resource type:

// app/helpers/can-access.js
import Ember from "ember";

// Usage {{if (can-access 'admin') 'Welcome, boss' 'Heck no!'}}
export default Ember.Helper.extend({
  session: Ember.inject.service(),
  onCurrentUserChange: Ember.observes('session.currentUser', function() {
    this.recompute();
  }),
  compute(params) {
    const currentUser = this.get('session.currentUser');
    return currentUser.can(params[0]);
  }
});

Drawbacks

Helpers may superficially appear similar to components, but in practice they have none of the special behavior of components such as managing DOM. The intent of this RFC is that full class-based helpers remain very close to the spirit of a pure function (as in the shorthand). However, despite this intent they are a new concept for the framework.

Alternatives

A previous RFC explored creating a new class called Expressions, which would have more closely modeled the API of components (using positional params, attrs). After discussion and consideration it was clear that a third kind of template API would be very challenging to document and teach well.

Unresolved questions

Perhaps there should be hooks in place for the lifecycle, instead of relying on init and destroy.

  • Start Date: 2016-06-14
  • Relevant Team(s): Ember CLI
  • RFC PR: #55

Summary

This RFC proposes extending the app.import API to consume anonymous AMD modules. It accompanies ember-cli PR 5976.

Motivation

AMD modules come in two flavors: named and anonymous. Anonymous AMD is the more portable distribution format, but it typically requires preprocessing into named AMD before it can be included into an application.

/* Anonymous AMD Examples */

// direct value
define({ color: 'black' });

// function returning value
define(function() { return { color: 'black' }; });

// function returning value, with declared dependencies
define(["jquery", "moment"], function(jQuery, moment) {
  return {
    injectTime: function() {
      jQuery('#time-box').html(moment().format('HH:MM'));
    }
  }
});

/* Named AMD Examples */

// direct value
define('my-config', { color: 'black' });

// function returning value
define('my-config', function() { return { color: 'black' }; });

// function returning value, with declared dependencies
define('time-utils', ["jquery", "moment"], function(jQuery, moment) {
  return {
    injectTime: function() {
      jQuery('#time-box').html(moment().format('HH:MM'));
    }
  }
});

Today, ember-cli users can add arbitrary third-party named AMD modules into their application via:

app.import('/path/to/module.js');

But this does not support anonymous AMD modules, which is annoying because anonymous AMD is the better format for library distribution and is widely used.

Detailed design

In order to de-anonymize AMD, it's necessary to choose a name for the module for use within a given application. So I propose extending the:

app.import('/path/to/module.js');

API with an additional argument:

app.import('/path/to/module.js', {
  using: [
    { transformation: 'amd', as: 'some-dep' }
  ]
});

using provides a list of transformations. Each transformation is identified by its transformation property. Any other properties are treated as arguments to the transformation implementation -- they are opaque to ember-cli. Transformations will run in the given order.

In this particular case, the amd transformation will run and receive the argument {as: 'some-dep'}.

The exactly meaning of the amd transformation is: within this Javascript file, any call(s) to the global define() function will be intercepted and the given module name (some-dep in the above example) will be prepended to the argument list.

A complete implementation is available here. (As of this edit it lags behind updates to this RFC.)

Learning

An appropriate place to document this feature is here. That existing documentation is silent on the distinction between named and anonymous AMD, which probably trips people up.

Drawbacks

I am not attempting to specify static error detection, mostly because doing that well would require fully parsing and understanding the imported module, which is likely to be more expensive and fragile.

Examples of static errors that would theoretically be nice to detect would be the presence of a named AMD module in the file, the lack of any AMD module in the file, or the present of multiple anonymous AMD modules in the file.

The current implementation causes any sourcemap information inside the imported file to be discarded (you don't get an invalid sourcemap, but you lose detail).

I have not specified a pluggable way to add additional transformations. My intent is to reserve space in our public API so that future extraction and pluggability is fully backward compatible.

Alternatives

Many libraries fall back to global variables if they cannot detect a valid AMD loader. I suspect this is the most common alternate pattern that's in use in the community.

Some applications include their own manually written shims in vendor or elsewhere.

Unresolved questions

We should confirm that my implementation performs well in apps with very large dependency directories.

  • Start Date: 2015-10-02
  • RFC PR: https://github.com/emberjs/rfcs/pull/56
  • Ember Issue: (leave this empty)

Refining the Release Process

Ember balances a desire for overall stability with a desire for continued improvements using a two-pronged approach:

  • General adherence to Semantic Versioning, which means that we don't make breaking changes to public, documented APIs except when the major version changes.
  • A rapid release cycle that allows us to ship additive changes to the framework on a regular, digestible basis.

Since Ember 1.0, we have refined this approach:

  • All new public APIs are added using feature flags onto the master branch. Feature flagged features are not included in beta or release builds until they are "Go"ed by the core team.
  • We avoid breaking heavily used but private APIs in minor versions.
  • When we feel we must break a private API that is heavily used, we use a two-step deprecation approach: deprecate the private API in one release and remove it in a subsequent release, once apps and add-ons have had an opportunity to upgrade.
  • When we plan to make breaking changes in a future major release, we first deprecate the changes in a previous minor release.
  • We never deprecate features until there is an already-landed transition path to a new approach whose feature flag has already been "Go"ed.

And finally:

  • A major release does not introduce any new breaking changes that were not previously deprecated. Major versions simply remove deprecated features that already landed.

Ember 2.0 is the first major release cycle where we have followed these refinements; this document is an attempt to outline some additional refinements that we might adopt going forward.

Benefits of the 1.x Model

  • New features are added predictably, and it's relatively easy to follow the list of new APIs that are under development, and where they are in the process.
  • There is little pressure for contributors to land a feature prematurely, because missing a release deadline isn't catastrophic–there will be another train six weeks hence.
  • We have a lot of very good automation tools that keep the trains running–commits can be (mostly) automatically backported to the current beta or release version.
  • Upgrading Ember itself from version to version is typically a quick process, except when private APIs are in use. We aim for upgrades to be possible to slot into existing product sprints, and the nature of the process means that we tend to hit this goal for most users.
  • Upgrading Ember across a number of versions is typically pretty straightforward, at least in theory.

In total, this process provides a way for us to clearly message medium-term changes in a way that helps you make the changes predictably and as mechanically as possible.

The process of getting from here to there is a series of incremental releases with deprecations, which gives you a trail of breadcrumbs to follow as things change.

Problems with the 1.x Model

While the approach we're using has provided a lot of benefits, there are a number of areas that could still use improvement:

  • While it is in theory possible to upgrade only once every few releases, there is no guidance about exactly how to do that, and little clarity about how many releases we plan to support with security fixes. (Because of the two-step deprecations of heavily used private APIs, it is in practice important to go through each intermediate release to clear deprecation warnings before proceeding.)
  • While SemVer guarantees apply to public APIs, many addons are forced to use private APIs as part of experiments. These experiments are a crucial part of the evolution of the Ember ecosystem, and the Ember 1.x series has had a fair bit of churn in these APIs.
  • While the SemVer guarantees apply to Ember proper, they do not apply to parts of the blessed experience that have not yet reached 1.0.
  • While the SemVer guarantees promise that your code will continue working, they do not address changes to idiomatic Ember usage, which can change over time. In practice, this means that there can be churn in the experience of using Ember without actual breakages.
  • While deprecations technically don't force you to change anything, in practice clearing deprecations is a part of the upgrade process. A constant stream of deprecations, like in the lead-up to Ember 2.0, can feel almost as bad as breaking changes.
  • In the lead-up to Ember 2.0, a desire to remove as much cruft as quickly as possible led to a need to land new features with much more urgency than usual.

In total, these problems introduce churn in the experience of using Ember. In practice, things like moving to ES6 modules, moving to Ember CLI, and the changes in Ember Data have made the experience of "keeping up" more frenetic than we would have liked.

Because Ember releases a new version every six-weeks, it's easy to associate the overall churn with the rapid pace of releases.

Non-Goals of the Improvements

The release process does not attempt to change the overall pace of change, but rather to make changes more predictable, easy to track, and easy to upgrade to.

The six-week cycle can incidentally affect the pace of change, because it means that large changes usually need to be broken up into pieces that can land a bit at a time. However, in practice this speeds up ecosystem-wide adoption of the entire feature, because people do not find themselves stuck behind a big-bang change that they can't schedule the time to upgrade to.

A recent survey of the Ember ecosystem, which had close to 1,000 respondents, showed that the vast majority of Ember users are using one of the past three versions of Ember.

Proposal: LTS Releases

In theory, it's possible to upgrade every few releases, instead of every release. This has a few drawbacks:

  • Because of the two-step deprecation process for heavily-used private APIs that we want to remove, it is in practice necessary to go through all intermediate releases in order to catch possible deprecations.
  • We currently don't have any official policy about which exact releases we backport security patches to, other than a promise that we will always backport to the previous released version.
  • Since different people upgrade at different rates, it's hard for add-ons and other parts of the Ember ecosystem that are not bound by the same SemVer guarantees to know which versions to continue to support.

I propose that every 4 releases is considered a "Long-Term-Support (LTS) release" . With the six-week cycle, that means every 24 weeks, or roughly twice per year.

This means:

  • We will only remove heavily used private APIs if they were deprecated in a previous LTS release. This means that if a feature is deprecated in 2.3, the first LTS release that the deprecation will appear in is 2.4, and it can therefore be removed in 2.5.
  • We will provide release notes for each LTS release that roll up the changes for the releases it includes, including new deprecations and new features.
  • We will use the LTS releases to provide better big-picture messaging on the goals of any deprecations and changes to idiomatic Ember.
  • Security fixes will always be backported to the most recent LTS release.
  • We will encourage the Ember ecosystem to maintain support for the LTS releases, and lead by example with our own projects that have not yet reached SemVer stability. Ideally, this will give more of a voice to people who are upgrading less frequently.

This means that people who want to stay on the latest and greatest can continue to upgrade every six weeks (with the same SemVer guarantees we've come to expect), and people who want to upgrade less frequently can do so.

In practice, since these releases still abide by SemVer, upgrading from LTS release to LTS release should not be significantly more work than upgrading along the six-week release cycle.

Upgrading less frequently will mean, of course, that you would need to wait to take advantage of new features, and experience less gradual changes to idioms. It will also mean that every upgrade will come with a bigger bundle of deprecations to clear.

It is important for us to keep an eye on the situation to see whether less frequent updates result in people getting left behind.

Proposal: Svelte Releases and Major Releases

Another problem worth addressing is that, as Ember gradually deprecates old idioms to make way for new ones, SemVer guarantees require that we continue shipping deprecated features until the next major release.

This has two related problems:

  1. Ember users who are not using deprecated features need to continue shipping deprecated code, which increases both code bloat and an opportunity to accidentally slip back into older idioms.
  2. Ember itself needs to continue maintaining support for deprecated features in its internals, which, over time, results in cruft that impacts our ability to improve Ember.

However, we also need to be cognizant of the fact that changes to Ember idioms take time to be reflected in online materials, so it's important for snippets copy-and-pasted from tutorials to continue to produce deprecation notices for some time.

In general, this is the question of how to "garbage collect" cruft in the framework gradually and with minimal impact.

Leading up to the 2.0 release, we thought we would address this issue with periodic "cruft removal" major releases. Every so often, we would issue a major release with the primary purpose of clearing out accumulated cruft. Minor releases could create deprecations, but not purge their associated code.

Unfortunately, because of the fact that Ember does not generally deprecate features without a clear transition to something else, this meant that the 2.0 release became a critical release for adding new features as well. In the run-up to 2.0, we felt a higher degree of urgency to add new features in the programming model to replace ones we expected to want to remove early in the 2.x series.

The goal of the train release model is to eliminate big-bang releases and the attendant stress on releasing particular features by a given date, and the 2.0 release has been far too disruptive to that goal.

In the 2.x cycle, I propose a few enhancements:

  1. Ember itself will more clearly mark deprecated features in a similar way that it marks new features, including with the release it was deprecated in.
  2. Ember CLI will support "svelte builds", which strip out deprecated features.
  3. In development mode, Ember CLI will convert deprecated features into errors, to ensure that people running svelte builds can still get clear messages when using code that was designed for earlier builds, including addons.
  4. We will still use major releases to remove built up cruft, especially deeply intertwined cruft, but the svelte releases should take the pressure off of the major release timeline.

The 1.x release cycle helped us establish an orderly process for adding features; this proposal establishes a more orderly process for removing them.

Proposal: Plugin APIs

Since the release of Ember 1.0, we have worked on refining the public APIs while maintaining stability. However, those public APIs do not cover all possible use-cases, and add-ons have cropped up to fill the gaps.

Unfortunately, this has placed a heavy compatibility burden on add-on authors who want to maintain stability in their public APIs even as versions of Ember have changed the private APIs they rely on.

In practice, the costs of the six-week release cycle weigh most heavily on add-on authors, who are often forced into using private APIs, but still want to keep their add-ons working with every release.

The canary and beta cycles help to ensure that popular add-ons work by the time the release version comes out, but only because add-on authors keep a close eye on the beta releases and absorb the churn on behalf of their users.

I propose that as of Ember 2.0, any use of a private API in a plugin is considered a bug in Ember to be fixed.

That doesn't mean that add-on authors should never use private APIs: to the contrary, use of private APIs when no other choice is available helps us discover what APIs are missing.

But a major goal of the 2.x series of Ember should be to identify ways to extend the stability promises that Ember offers to application authors to add-on authors.

  • Start Date: 2015-05-20
  • RFC PR: https://github.com/emberjs/rfcs/pull/57
  • Ember Issue: https://github.com/emberjs/data/pull/3303

Summary

This RFC adds a unified way to perform meta-operations on records, has-many relationships and belongs-to relationships:

  • get the current local data synchronously without triggering a fetch or producing a promise
  • notify the store that a fetch for a given record has begun, and provide a promise for its result
  • similarly, notify a record that a fetch for a given relationship has begun, and provide a promise for its result
  • retrieve server-provided metadata about a record or relationship

Motivation

When we initially designed the Ember Data API for relationships, we focused on consumption and mutation of the relationship data. For example, you can retrieve the value of a belongsTo relationship via get('post'), or adding new records to a has-many relationship via get('comments').pushObject(newComment).

The top-level reading operations are designed to be zalgo-proof: regardless of whether or not the record or relationship has been loaded already, you get back a promise for the result. Behind the scenes, this will trigger a fetch if needed, or simply return the current value if it has already been fetched. From a programming model perspective, this simplifies your code because you can handle locally-available data and remotely-available data in a single code path.

However, in sophisticated applications, there is often a need to refer to a record without triggering side effects.

For example, you may want to initiate the fetch for a record or relationship yourself, and provide Ember Data with a promise representing the result of that fetch. That use-case is supported by the store.push API, but it has a few problems:

  • The store.push API supports pushing data once the fetch has completed, but no way of telling Ember Data that a fetch has begun. As a result, any calls to store.find in the interim will trigger unnecessary fetches.
  • The store.push API works only for top-level records with already-known types and IDs. It does not support any way of "feeding" the data for a relationship to Ember Data.

In sum, this makes it difficult to front-load work (especially asynchronous work). Instead, Ember Data is currently optimized for reacting to requests from the application layer, which is sometimes a very awkward way of structuring your code.

Second, Ember Data was originally designed with APIs that refer to data and operations on data. Over time, we have come to realize that people quite often need to look at metadata about records or relationships, as well as perform meta-operations on them.

Some examples:

  • getting the expected count of a has-many relationship before it has been fetched
  • learning whether a relationship is already loaded or not
  • examining server-sent metadata
  • working with pages of records in a has-many relationship, especially when pages are loaded asynchronously ("pagination")

Third, because has-many relationships are represented as a RecordArray, we have been able to kludge around some of these issues by adding meta-operations to the has-many relationship itself. In contrast, belongs-to relationships have remained anemic. For example, there is no way to trigger a reload of a belongs-to relationship, whereas has-many relationships can be reloaded by calling .reload() on the RecordArray.

Detailed design

This RFC proposes the addition of three new public APIs:

  • RecordReference
  • HasManyReference
  • BelongsToReference

Getting References

  • store.getReference(type, id)
  • record.getReference(name)

References

push

/**
  This API allows you to provide a reference with new data. The simplest usage
  of this API is similar to `store.push`: you provide a normalized hash of data
  and the object represented by the reference will update.

  If you pass a promise to `push`, Ember Data will not ask the adapter for the
  data if another attempt to fetch it is made in the interim. When the promise
  resolves, the underlying object is updated with the new data, and the promise
  returned by *this function* is resolved with that object.

  For example, `recordReference.push(promise)` will be resolved with a record.

  @method
  @param {Promise|Object}
  @returns Promise<T> a promise for the value (record or relationship)
*/

pushPayload

/**
  This API is similar to `push`, but it invokes the serializer with the
  resolved data. This makes it possible to share normalization logic
  across multiple calls to `pushPayload` or between proactive pushes
  and reactive responses from the adapter.

  @method
  @param {Promise|Object}
  @returns Promise<T> a promise for the value (record or relationship)
*/

state

/**
  The current state of the entity, based on the semantics of the
  entity in question. For records, this should expose a subset of
  the named states in the internal state machine.

  @property
  @type String
*/

value

/**
  If the entity referred to by the reference is already loaded, it
  is present as `reference.value`. Otherwise, the value of this
  property is `null`.

  @property
*/

data

/**
  The value of the (normalized) representation of this entity. For
  example, `recordReference.data` will return a normalized dictionary
  of attributes and links.

  @property
*/

metadata

/**
  The most recent value of the metadata returned by the server for
  the value represented by this reference.

  @property
*/

load

/**
  Triggers a fetch for the backing entity based on its `remoteType`
  (see `remoteType` definitions per reference type).

  @method
  @param {Object} an options hash, similar to the one currently
    passed to `store.find`.
*/

unload

/**
  Unload the entity referred to by this relationship. After this
  operation, its `value`, `data` and `metadata` members will return
  to `null`, and the record itself will be purged from the identity
  map.

  @method
*/

RecordReference

remoteType

/**
  How the reference will be looked up with it is loaded:

  * `link`, a URL
  * `identity`, by the `type` and `id`
*/

type

/**
  The type of the record that this reference refers to.

  @property
*/

id

/**
  The `id` of the record that this reference refers to.

  Together, the `type` and `id` properties form a composite key
  for the identity map.

  @property
*/

HasManyReference

remoteType

/**
  How the reference will be looked up when it is fetched:

  * `link`, a URL provided by the server
  * `ids`, a list of IDs provided by the server
  * `dynamic`, a dynamic URL will be created based on the identity
    of the parent.

  @property
*/

link

/**
  If the `remoteType` is `link`, the URL to use to load the relationship.

  @property
*/

ids

/**
  If the `remoteType` is `ids`, a list of IDs that is used to formulate
  the query to the server (together with `type`).

  @property
*/

type

/**
  The model type represented by this relationship.

  @property
*/

parent

/**
  A reference to the record that has this `hasMany` on it.

  @property
*/

inverse

/**
  If there is an inverse relationship, this property is a reference
  to it.

  @property
*/

BelongsToReference

remoteType

/**
  How the reference will be looked up when it is fetched:

  * `link`, a URL provided by the server
  * `id`, an ID to use to form the URL
  * `dynamic`, a dynamic URL will be created based on the identity
    of the parent.

  @property
*/

link

/**
  If the `remoteType` is `link`, the URL to use to load the relationship.

  @property
*/

ids

/**
  If the `remoteType` is `id`, an ID that is used to formulate
  the query to the server (together with `type`).

  @property
*/

type

/**
  The model type represented by this relationship.

  @property
*/

parent

/**
  A reference to the record that has this `belongsTo` on it.

  @property
*/

inverse

/**
  If there is an inverse relationship, this property is a reference
  to it.

  @property
*/

Drawbacks

The main drawback to this proposal is that it adds significant surface area to Ember Data, which could easily be perceived as significant additional complexity. However, we believe that the unification of the various entities in Ember Data, as well as exposing internal tools that were previously only available to the store, will actually reduce the complexity of many common patterns.

Alternatives

The main alternative is to address each use case with a new API:

  • store.peek(record, id), record.peek(relationship) to retrieve the current value of the relationship only if it was loaded
  • extend store.push and store.pushPayload to take promises
  • APIs like record.inverseFor(relationship), record.typeFor(relationship), etc.
  • APIs like record.idsFor(relationship), record.metadataFor(relationship), and store.metadataFor(type, id)

We believe that the cumulative overhead of all of these APIs is far more than the overhead of the reference APIs.

Unresolved questions

Is there a need to represent "prefetch metadata" separately? This is metadata that the app knows about before fetch, and which it would want to persist through an unload() operation (along with identity information like type, id and link).

  • Start Date: 2015-05-24
  • RFC PR: https://github.com/emberjs/rfcs/pull/58
  • Ember Issue: https://github.com/emberjs/ember.js/pull/11393

Summary

This RFC outlines a new strategy for the registration of helpers in Ember 1.13. In previous versions of Ember, helper lookup was a two-phase process of:

  • Look in a whitelist of registered helpers. If in the whitelist, resolve that path in the container.
  • If the path has a dash, try to resolve it in the container
  • If the container does not have a factory for this path, treat the path as a bound value.

This logic runs for every {{somePath}} in an Ember application.

This proposal attempts to simplify and unify that logic in a a single pass against a whitelist, thus removing the special behavior of dashed paths. Additionally, it attempts to design a solution that removes the current registerHelper ceremony for undashed helpers.

Motivation

In RFC #53 a new API for helpers is outlined. This RFC presumes helpers will continue to have the naming requirement of including a dash character.

The dash requirement for helpers exists for two reasons:

  • For every {{path}} in an Ember application, it must be decided if that path is a bound value, component, or helper. Component and helper lookup (the discovery of a class or template) is lazy in Ember, thus for every {{path}} a lookup for that string in the container is required. Container lookups (the first time) are fairly slow, and performing this lookup for every path may significantly impact initial render time. Thus, helpers are either added to a whitelist (with registerHelper) or require a way to differentiate themselves from the majority of data-binding cases (the dash).
  • In Ember 1.x, components were treated as helpers for certain code paths. This made the dash requirement for components a natural extension to helpers.

The Glimmer engine has removed some of these concerns and limitations.

Addon authors and app authors have both felt a need for non-dashed helper names, for example {{t 'some-string-to-translate'}}. New developers to Ember often find the dash requirement arbitrary and the registerHelper work around difficult to understand and use.

For the new helper API to provide feature parity with APIs available to addon authors in 1.12, a path to dashless helpers must be present in 1.13.

Given that a solution exists that addresses the performance concern, dropping the dash requirement would resolve a significant amount of developer pain and confusion.

Detailed design

At application boot, all known helper items (according to the resolver) are iterated and added to a helper-listing service. This service is merely a Set object with the names of all helpers.

When handling a {{path}}, the helper-listing service is consulted for the presence of that path. If it is present, the path is looked up on the container as a helper and the helper is used. Dashed paths are treated no differently than any other path (for helpers).

Boot time discovery

To discover what paths may be helpers in Ember-CLI, the module names are iterated. For example:

not helper: app/components/foo-bar/component
not helper: app/controllers/foo-bar
not helper: app/foo-bar/route
helper "t": app/t/helper
helper "t": app/helpers/t
helper "foo-bar": app/helpers/foo-bar
helper "foo/bar": app/helpers/foo/bar

In a globals-mode application, The app namespace is iterated:

not helper: App.FooBarComponent
not helper: App.FooBarController
not helper: App.FooBarRoute
helper "t": App.THelper
helper "foo-bar": App.FooBarHelper <- should dasherize

In both cases the resolver is responsible for providing a list of modules by type. The proposed API is eachOfType, here with Ember-CLI as an example:

// Given helperListing as a Set:
resolver.eachOfType(‘helper’, function(parsedName, item) {
  helperListing.add(parsedName.fullName);
})

In Ember-CLI, the app/ tree of an addon is merged with the app tree of an application. This means for a helper like t to be discovered, nothing besides adding it to app/helpers/t.js must be done.

In 1.13, this will impact existing apps by discovering all helpers regardless of if registerHelper has been called. This is a small behavior change that should match intent, and will not impact sanely written apps.

Note that only the path of the helper is added to the listing. During discovery, the helper is not looked up from the container, instead lookup still occurs at render time.

The helper listing is intended to be a private service in Ember, and will be registered at services:-helper-listing. If the discovery semantics described here are not sufficient for some edge-cases, wrapping this service in a public API on application instances may be required.

Render-time lookup and use

Let us consider how a path is rendered. For example:

{{date}}
  • The service:-helper-listing service is fetched
  • The path date is checked for on the listing: helperListing.has(path)
  • If the path is not in the listing, date is treated like a bound value
  • If the path is in the listing, the helper is looked up from Ember's container as helper:date
  • depending on the instance returned from the factory (a helper, shorthand helper, or legacy Ember.Handlebars or Ember.HTMLBars._registerHelper helper) the proper invocation for that helper is executed

Every rendered path will hit the helper-listing service, but the check against a well-implemented Set should be inexpensive.

Drawbacks

Removing the dash requirement will likely result in a larger number of naming conflicts between addons and apps than has existed before now. In general, encouraging verbose helper names may mitigate this concern. Long term, there have been several discussions to date about how to implement namespaces in Ember templates and for Ember engines.

That the helper listing is eagerly discovered at application boot time may impact the design of Ember engines and lazy-loading parts of an app. The discovery cache may need to be flushed and re-generated, however this limitation already exists for the container lookup itself (which caches failures).

That the helper listing is not based on the container means helpers registered, but not added to the listing because of non-standard naming, may need to manually register against the private helper listing API.

Alternatives

Instead of a new across-the-board solution, Ember could continue to use a registerHelper pattern very similar to what exists today. This would perpetuate the existing pain, but would perhaps be more similar to what devs already know.

Unresolved questions

The exact timing of helper discovery in Ember-CLI and globals mode has not been decided.

  • Start Date: 2015-06-03
  • RFC PR: https://github.com/emberjs/rfcs/pull/61
  • Ember Issue: This RFC is implemented over many Ember Data PRs

Summary

This RFC proposes new methods on the adapter to signal to the Ember Data store when it should re-request a record that is already cached in the store. It also proposes a new adapter options object that can be used by to provide instructions to the adapter from the place where the store's find method is called.

Motivation

Use Cases

When it comes to fetching records, there are several different behaviors that users may expect. The behavior that users expect is influenced by unique quirks in their data model, pre-existing expectations based on traditional development models, and implementation details of their adapter.

Fundamentally, users may expect or want one of the following sets of behavior when fetching a model for the model() hook:

  • Fetching data from the server the first time a record is requested, but using only cached data subsequent times the route is entered. (This is the current behavior of find().)
  • Fetching new data every time the route is entered. The route will "block" (show a loading spinner) until fresh data is received.
  • Using local data if available, but otherwise not triggering any fetches if the data is not available. This is useful if records will be pushed into the store ahead of time, e.g. by a socket, and non-existence in the store means non-existence on the server.
  • Immediately returning a model with local data if available, rendering the route's template immediately, and updating the record in the background. If the record changes after conferring with the server, the template is re-rendered.

Discussion

Fetch Only On Initial Render

In this model, the record data is fetched from the server the first time a record with a given ID is requested. Subsequent requests (e.g. leaving and re-entering the same route) use locally cached data. This is the strategy used by the current find() method.

The advantage of this model is that it keeps conferring with the server to a minimum. Once data is loaded, the client can render new routes with the model data that it has cached, without a network roundtrip.

Additionally, in some data models, records are immutable. For example, on Twitter, tweets never change. In an email app, emails cannot change once they are sent. Asking the server for the most up-to-date version of an immutable record is a waste of resources.

The downsides of this model are two-fold.

First, this model is surprising to new developers. When navigating between pages, they expect the most up-to-date representation to be fetched and displayed every time.

Second, even for developers who understand what is happening, it is very easy for long-lived applications to accumulate stale information, particularly if the model they are displaying updates frequently. Developers must somehow disambiguate between the first time a model is looked up, and allowing it to proceed, and detecting when a cached model is being used and updating it manually.

New Fetch Every Render

The advantages of this model are that it most closely matches the mental model for developers coming from server-rendered and jQuery backgrounds. In that model, every time a new page is loaded, the most up-to-date information is guaranteed to be displayed. Because each page navigation triggers a fetch from the database, the only way for information to become stale is for the user to stop navigating.

The downside of this model is that it eliminates many of the advantages of client-side routing. In traditional client apps, data is stored locally, and navigations use that local data. In this model, every page transition is blocked awaiting a network response from the server. It's a slight improvement in that the data should be much smaller than a full HTML page, but it is often latency and not bandwidth that causes slowdowns.

Never Fetch

While an edge case, many Ember Data users have requested the ability to fetch a record out of the store only if it exists locally.

One use case is for stores that are optimistically populated via pushed data from a socket. In that case, if the record doesn't exist in the store, it means that it doesn't exist on the server.

For obvious reasons, this is an uncommon case for the majority of apps. While we should support it, it should not be part of the happy path for new developers.

Immediate Render, Background Refresh

In this model, the first time a record is requested, it blocks the render and shows a loading spinner. On subsequent requests, the locally cached data is displayed and the render happens immediately without making the user wait.

However, in the background, the store also kicks off a request to the adapter to update the record. When the new data comes in, the record is updated, and if there have been changes to the record since the initial render, the template is re-rendered with the new information.

This is the model that I believe strikes the best tradeoff among the options available.

First, it preserves the speed of client-side navigation. Once data for a record is cached, transitioning to any route that relies on it is nearly instantaneous and has no network bottleneck.

Second, because it triggers a background update, even users who expect a new fetch every time will not be surprised as, ideally after a few milliseconds, the new data will arrive and be persisted into the DOM.

Third, in most applications, models are not changing frequently. Most of the time, the cached version in the Ember Data store will be identical to the latest server revision. In those cases, there is no point in making users stare at a loading spinner

Of course, there are several downsides to this model that we should keep in mind. For immutable records, fetching a new version in the background is wasteful of bandwidth and server capacity and we should allow developers to opt out of this behavior.

A second related case is apps using a socket to subscribe to record changes once a record is fetched. In those cases, fetching up-to-date information on subsequent requests for the model is wasteful because they have guaranteed that they will keep the model up-to-date via change events from the server. In this case, we need a way for adapter authors to signal that subsequent update requests for a record are a no-op.

Third, it may be an unpleasant user experience for new information to pop in suddenly after the initial render, particularly for records that frequently change in dramatic ways. In those instances, we should make sure we give developers the tools to build UIs that can indicate to the user that the information is being updated, perhaps by greying it out or displaying a loading spinner.

Detailed design

Proposal

New Adapter Methods.

shouldRefetchRecord is a new method on the adapter that will be called by the store to make initial decision whether to refetch the record form the adapter or return the cached record from the store. This could method could be used to implement caching logic (e.g. only refetch this record after the time specified in its cache expires token) or for improved offline support (e.g. always refetch unless there is no internet connection then use cached record).

This record would only be called if the record was already found in the store and is in the loaded state.

This method is only checked by store.findById and store.findAll. Methods with fetch in their name always refetch the record(s) from the adapter.

{
  /**
   `shouldRefetchRecord` returns true if the adapter determines the record is
   stale and should be refetch. It should return false if the record
   is not stale or some other condition indicates that a fetch should
   not happen at this time (e.g. loss of internet connection). 

   This method is synchronous.

   @method shouldRefetchRecord
   @param {DS.Store} store
   @param {DS.Model} record
   @param {Object} adapterOptions
   @return {Boolean}
   */
  shouldRefetchRecord: function(store, record, adapterOptions),
}

The method shouldBackgroundUpdate would be used by the store to make the decision to re-fetch the record after it has already been returned to the user. This would allow realtime adapter to opt out of the background fetch if the adapter is already subscribing to changes on the record.

{
  /**
   `shouldBackgroundUpdate` returns true if the store should re fetch a
   record in the store after returning it to the user to ensure the
   record has the most up to date data.
   
   This method is synchronous.

   @method shouldBackgroundUpdate
   @param {DS.Store} store
   @param {DS.Model} record
   @param {Object} adapterOptions
   @return {Boolean}
  */
  shouldBackgroundUpdate: function(store, record, adapterOptions),
}

In the next major version of Ember Data the recommend way of finding a record will be:

this.store.findById('person', 1);

This will return a promise that:

  1. Waits to resolve until the data is fetched from the server, on the initial request.
  2. Resolves immediately with the locally cached request for subsequent requests, but triggers a request to the server for the updated version and updates the record in-place if there are changes.

In terms of the above methods shouldRefetchRecord will always return false and shouldBackgroundUpdate will always return true in the default RESTAdapter.

The fundamental guarantee of findById()/findAll() when using the default RESTAdapter is:

Give me the information you have available locally, then give me the most up-to-date information as soon as possible.

Currently, the find() method takes an optional third parameters that is passed to the adapter. In this API, that data structure is moved to a field in the new options hash:

this.store.findById('person', 1, {
  preload: { comment_id: 1 }
});

isUpdating Flag

To assist developers in building UIs that communicate the state of models to their users, we should provide a helper that allows developers to show UI elements when a model is in the process of being updated via fetch().

I propose adding an isUpdating flag to models, which can be used to conditionally show a spinner:

<h1>{{post.title}}</h1>
{{#if isUpdating post}}
  <small>Updating...</small>
{{/if}}

<p>{{post.body}}</p>

(Currently, only RecordArrays have an isUpdating flag.)

Models have an isReloading flag. This will be deprecated in favor of the new isUpdating flag.

Drawbacks

Why should we not do this?

After the record has been updated in the background Ember's Data binding will cause any views to automatically update with the latest changes. This can result an a surprising "popping" effect which is especially pronounced when the background fetch resolves quickly (The user sees an initial render with the stale data then a quick re-render with the fresh data).

Alternatives

What other designs have been considered? What is the impact of not doing this?

One alternate option could be for Ember Data to track an expires token on a model. This would allow Ember Data to behave like a caching proxy when fetching. If the record is expired, fetch should block. If the record is not expired it would return a resolve the record right away however still issue a second request.

When used with backends that do not return an expires token. Ember Data would assume that the record is stale (this could be configured on the adapter).

Unresolved questions

  • Start Date: 2015-06-12
  • RFC PR: https://github.com/emberjs/rfcs/pull/64
  • Ember Issue: (leave this empty)

Summary

The goal of this RFC is to allow for better component composition and the usage of components for domain specific languages.

Ember components can be invoked three ways:

  • {{a-component
  • {{component someBoundComponentName
  • <a-component (coming soon!)

In all these cases, attrs passed to the component must be set at the place of invocation. Only the {{component someBoundComponentName syntax allows for the name of the component invoked to be decided elsewhere.

All component names are resolved to components through one global resolution path.

To improve composition, four changes are proposed:

  • The (component helper will be introduced to close over component attrs in a yielding context.
  • The {{component helper will accept an argument of the object created by (component for invocation (as it invokes strings today).
  • Property lookups with a value containing a dot will be considered for rendering as components. {{form.input}} would be considered, for instance. Helper invocations with a dot will also be treated like a component if the key has a value of a component, for instance {{form.input value=baz}}.
  • A (hash helper will be introduced.

Motivation

When building a complex UI from several components, it can be difficult to share data without breaking encapsulation. For example this template:

{{#great-toolbar role=user.role}}
  {{great-button role=user.role}}
{{/great-toolbar}}

Causes the user to pass the role data twice for what are obviously related components. A component can yield itself down:

{{! app/components/great-toolbar/template.hbs }}
{{yield this}}
{{#great-toolbar role=user.role as |toolbar|}}
  {{great-button toolbar=toolbar}}
{{/great-toolbar}}

And great-button can have knowledge about properties on great-toolbar, but this break the isolation of components. Additionally the calling syntax is not much better, toolbar must still be passed to each downstream component.

Often nearestOfType is used as a workaround for these limitations. This API is poorly performing, and still results in the downstream child accessing the parent component properties directly.

Consequently there is a demand by several addons for improvement. Our goal is a syntax similar to DSLs in Ruby:

{{#great-toolbar role=user.role as |toolbar|}}
  {{toolbar.button}}
  {{toolbar.button orWith=additionalProperties}}
{{/great-toolbar}}

As laid out in this proposal, the great-toolbar implementation would look like:

{{! app/components/great-toolbar/template.hbs }}
{{yield (hash
  button=(component 'great-button' role=user.role)
)}}

Detailed design

The (component helper and {{component helper

Much like (action creates a closure, it is proposed that the (component helper create something similar. For example with actions:

{{#with (action "save" model) as |save|}}
  <button {{action save}}>Save</button>
{{/with}}

The returned value of the (action nested helper (a function) closes over the action being called (actions.save on the context and the model property). The {{action helper can accept this resulting value and invoke the action when the user clicks.

The (component helper will close over a component name. The {{component helper will be modified to accept this resulting value and invoke the component:

{{#with (component "user-profile") as |uiPane|}}
  {{component uiPane}}
{{/with}}

Additionally, a bound value may be passed to the (component helper. For example (component someComponentName).

Attrs for the final component can also be closed over. Used with yield, this allows for the creation of components that have attrs from other scopes. For example:

{{! app/components/user-profile.hbs }}
{{yield (component "user-profile" user=user.name age=user.age)}}
{{#user-profile user=model as |profile|}}
  {{component profile}}
{{/user-profile}}

Of course attrs can also be passed at invocation. They smash any conflicting attrs that were closed over. For example {{component profile age=lyingUser.age}}

Passing the resulting value from (component into JavaScript is permitted, however that object has no public properties or methods. Its only use would be to set it on state and reference it in template somewhere.

Hash helper

Unlike values, components are likely to have specific names that are semantically relevent. When yielded to a new scope, allowing the user to change the name of the component's variable would quickly lead to confusing addon documentation. For example:

{{#with (component "user-profile") as |dropDatabaseUI|}}
  {{component dropDatabaseUI}}
{{/with}}

The simplest way to enforce specific names is to make building hashes of components (or anything) easy. For example:

{{#with (hash profile=(component "user-profile")) as |userComponents|}}
  {{component userComponents.profile}}
{{/with}}

The (hash helper is a generic builder of objects, given hash arguments. It would also be useful in the same manner for actions:

{{#with (hash save=(action "save" model)) as |userActions|}}
  <button {{action userActions.save}}>Save</button>
{{/with}}

Component helper shorthand

To complete building a viable DSL, . invocation for {{ components will be introduced. For example this {{component invocation:

{{#with (hash profile=(component "user-profile")) as |userComponents|}}
  {{component userComponents.profile}}
{{/with}}

Could be converted to drop the explicit component helper call.

{{#with (hash profile=(component "user-profile")) as |userComponents|}}
  {{userComponents.profile}}
{{/with}}

A component can be invoked like this only when it was created by the (component nested helper form. For example unlike with the {{component helper, a string is not acceptable.

To be a valid invocation, one of two criteria must be met:

  • The component can be called as a path. For example {{form.input}} or {{this.input}}
  • The component can be called as a helper. For example {{form.input value=baz}} or {{this.input value=baz}}

And of course a . must be present in the path.

Drawbacks

This proposal encourages aggressive use of the ( nested helper syntax. Encouraging this has been slightly controversial.

No solution for angle components is presented here. The syntax for . notation in angle components is coupled to a decision on the syntax for bound, dynamic angle component invocation (a {{component helper for angle components basically).

(component 'some-component' may be too verbose. It may make sense to simply allow (some-component.

Other proposals have leaned more heavy on extending factories in JavaScript then passing an object created in that space. Some arguments against this:

  • Getting the container correct is tricky. Who sets it when?
  • Properties on the classes would not be naturally bound, as they are in this proposal.
  • As soon as you start setting properties, you likely want a mut helper, action helper, etc, in JavaScript space.
  • Keeping the component lookup in the template layer allows us to take advantage of changes to lookup semantics later, such as local lookup in the pods proposal.

Alternatives

All pain, no gain. Addons really want this.

Unresolved questions

There has been discussion of if a similar mechanism should be available for helpers.

  • Start Date: 2015-06-30
  • RFC PR: https://github.com/emberjs/rfcs/pull/65
  • Ember Issue: (leave this empty)

Summary

Deprecations and warnings in Ember.js should have configurable runtime handlers. This allows default behavior (logging, raise when RAISE_ON_DEPRECATION is true) to be overridden by an enviornment (Ember's tests), addon, or other tool (like the Ember Inspector).

Ember-Data and the Ember Inspector have both requested a public API for changing how deprecation and warning messages are handled. The requirements for these and other requests are complex enough that deferring the message behavior into a runtime hook is the suggested path.

Motivation

Ember.deprecate and Ember.warn usually log messages. With ENV.RAISE_ON_DEPRECATION all deprecations will throw an exception. In some scenarios, this is less than ideal:

  • Ember itself needs a way to silence some deprecations before their usage is completely removed from tests. For example, many view APIs in Ember 1.13.
  • The Ember inspector desires to raise on specific deprecations, or silence specific deprecations.
  • Ember-Data also desires to silence some deprecations in tests

In PR #1141 a private log level API has been introduced, which allows finer grained control if specific deprecations should be logged, throwing an error or be silenced completely. For example:

Ember.Debug._addDeprecationLevel('my-feature', Ember.Debug._deprecationLevels.LOG);
// ...
Ember.deprecate("x is deprecated, use Y instead", false, { id: 'my-feature' });

Initially a public version of this API was discussed, but it quickly became clear that a runtime hook provided more flexibility without incurring the cost of a complex log-level API.

Note that "runtime" refers to Ember itself. A custom handler could be injected into Ember-CLI's template compilation code. "runtime" in this context still refers to handling deprecations raised during compilation.

Detailed design

A handler for deprecations can be registered. This handler will be called with relevent information about a deprecation, including guarantees about the presence of these items:

  • The deprecation message
  • The version number where this deprecation (and feature) will be removed
  • The "id" of this deprecation, a stable identifier independent of the message

Additionally, an application instance may be passed with the options. An example handler would look like:

import { registerHandler } from "ember-debug/deprecations";

registerHandler(function deprecationHandler(message, options) {
  // * message is the deprecation message
  // * options.until is the version this deprecation will be removed at
  // * options.id is the canonical id for this deprecation
  if (options.until === "2.4.0") {
    throw new Error(message);
  } else {
    console.log(message);
  }
});

Warnings are similar, but will not recieve an until value:

import { registerHandler } from "ember-debug/warnings";

registerHandler(function warningHandler(message, options) {
  // * message is the warning message
  // * options.id is the canonical id for this warning
  if (options.id !== 'view.rerender-on-set') {
    console.log(message);
  }
});
chained handlers

Since several handlers may be registered, a method of deferring to a previously registered handler must be allowed. A third option is passed to handlers, the function next which represents the previously registered handler.

For example:

import { registerHandler } from "ember-debug/deprecations";

registerHandler(function firstDeprecationHandler(message, options, next) {
  console.warn(message);
});

registerHandler(function secondDeprecationHandler(message, options, next) {
  if (options.until === "2.4.0") {
    throw new Error(message);
  }
  next(...arguments);
});

The first registed handler will receive Ember's default behavior as next.

new assertions for deprecate and warn

Ember's APIs for deprecation and warning do not currently require any information beyond a message. It is proposed that deprecations be required to pass the following information:

  • Message
  • Test
  • Canonical id (with a format of package-name.some-id)
  • Release when this deprecation will be stripped

For example:

import Ember from "ember";

Ember.deprecate("Some message", false, {
  id: 'ember-routing.query-params',
  until: '3.0.0'
});

If this information is not present and assertion will be made.

Warnings likewise will be required to pass a canonical id:

import Ember from "ember";

Ember.warn("Some warning", {id: 'ember-debug.something'});
default handlers

The default handler for deprecation should be quite simple, and mirrors current behavior:

function defaultDeprecationHandler(message, options) {
  if (Ember.ENV.RAISE_ON_DEPRECATION) {
   throw new Error(format(message, options));
  } else {
   console.log(format(message, options));
  }
}

The default handler for warnings would be simple console.log.

Drawbacks

By not providing a robust log-level API, we are punting complexity to the consumer of this API. For a low-level tooling API such as this one, it seems and appropriate tradeoff.

Alternatives

Each app can stub out deprecate and warn.

Unresolved questions

RAISE_ON_DEPRECATION could be considered deprecated with this new API.

  • Start Date: 2016-11-21
  • Relevant Team(s): Ember CLI
  • RFC PR: #80

Summary

This RFC attempts to expose a public API in ember-cli to allow other platforms/infrastructure to serve the base page (index.html) and other assets from the tmp directory in their own custom way. This is only for development as this will only be used with ember serve. Currently ember serve serves files from the tmp directory (which is built as part of the build process) using the broccoli-middleware. This middleware in addition to serving the files also sets the correct headers for the assets it is serving. This RFC aims to split the work of setting the header and serving the files into two different addons such that any other infrastructure can easily create a middleware to serve assets using its own logic.

Motivation

FastBoot and other infrastructure (for example the infrastructure at LinkedIn to serve the base page) does not require the index.html to be served from the disk directly. FastBoot requires to serve the index.html after it has appended the serialized template for the current request. It therefore requires to do some runtime replacements in the index.html before it can be served to the client. At LinkedIn, we stream the index.html in chunks for performance reasons and require to do some string replacements in index.html on per request basis.

During development, this requires us to create our own express middleware via serverMiddleware which should run before the serve-files middleware. It also requires us to almost copy paste the headers that are set by broccoli-middleware. In addition to the above, the ability to be able to serve from tmp directory allows FastBoot and other infrastructure to correctly serve the assets from the directory pointing to the current build. Currently (with using their own middleware) FastBoot serves assets from the dist directory which is not the correct behavior.

In order to mitigate the need to diverge into another middleware which behaves almost same as broccoli-middleware, this RFC proposes to split the work of setting the headers and serving the files via broccoli-middleware and expose a public API that will allow an addon to define how it wants to serve the assets. It will be a low level public API that will be invoked by certain addons.

Detailed design

Currently the serve-files addon defines how it will serve the incoming asset requests. It invokes the broccoli-middleware which is responsible for three sets of things:

  1. Setting the response headers

  2. serving the files and ending the response

  3. Shows an error template if it is a build error

Public API

This RFC proposes expose two new in-repo addons in ember-cli which will now split the above work and remove the serve-files addon:

  1. ember-cli:broccoli:watcher: This addon will contain the middleware which will be responsible for making sure the build is done and will set the response headers that broccoli-middleware is doing today. After setting the response headers, it will call the next middleware in the chain. In addition, if the build results in an error, it will show the error template and not terminate the response.

  2. ember-cli:broccoli:serve-files: This addon will always run after ember-cli:broccoli:watcher addon. It will contain a middleware that will be responsible for serving the files from the filesystem and ending the response.

For any infrastructure that needs to serve the assets in its own way will be create an addon that will be injected between the above two addons. It will use the serverMiddleware public hook to provide its own middleware. Specifically the custom addon should run before ember-cli:broccoli:serve-files so that it can either override any response headers or can serve the files using its own logic and end the response. This will ensure that when the build is successful ember-cli:broccoli:watcher can call the correct next middleware in the chain.

Implementation Details

In order for the above API to be exposed, we need to drop the serve-files addon in ember-cli, refactor broccoli-middleware and create the two new addons.

Refactor broccoli-middleware to expose additional middlewares

Note: This refactor section is only for making the reader understand how the integration is meant to work in ember-cli. This is not going to be ember-cli public API.

broccoli-middleware is currently responsible for setting the response headers and serving the files. It is a middleware that does these two tasks. It doesn't expose a proper middleware API to do the two tasks differently. We would like to refactor broccoli-middleware such that it exposes two additional middlewares:

  • forWatcher(watcher):
  /**
   * Function responsible for setting the response headers or creating the build error template
   *
   * @param {Object} watcher ember-cli watcher
   * @return {Function} middleware function
   */
   forWatcher: function(watcher) {
     var outputPath = watcher.builder.outputPath;
     ...
     return function middleware(request, response, next) {
       watcher.then(function() {
         // mostly all of this https://github.com/ember-cli/broccoli-middleware/blob/master/lib/middleware.js#L96
         request.headers['x-broccoli'] = {
           outputPath: outputPath
         };
         next();
       }, function(buildError) {
         // mostly this: https://github.com/ember-cli/broccoli-middleware/blob/master/lib/middleware.js#L121
       })
     }

   }
  • serveFiles():
 /**
  * This function will be responsible for serving the files from the filesystem
  *
  * @param {HTTP.Request} request
  * @param {HTTP.Response} response
  * @param {Function} next
  */
  serveFiles: function() {
    return function(req, resp, next) {
      // get the output path from from the request headers
      // most of `broccoli-middleware` https://github.com/ember-cli/broccoli-middleware/blob/master/lib/middleware.js#L115
    }
  }

Create ember-cli:broccoli:watcher addon

The current serve-files addon invokes the broccoli-middleware and delegates the task to this middleware to serve the files and set the headers. We would like to change that and instead this new in-repo addon ember-cli:broccoli:watcher should only call setResponseHeaders function from broccoli-middleware. The serverMiddleware function of this addon will now look as follows:

  ServeFilesAddon.prototype.serverMiddleware = function(options) {
    var app = options.app;
    var watcher = options.options.watcher;
    var broccoliMiddleware = require('broccoli-middleware');

    app.use(function(req, resp, next) {
      // copy over this: https://github.com/ember-cli/ember-cli/blob/375f3a32f4564465d2eccc3815cb61b570ce29f0/lib/tasks/server/middleware/serve-files/index.js#L33
      if (options.options.middleware) {
        // call the middleware that is provided for testemMiddleware
      } else {
        var watcherMiddleware = broccoliMiddleware.forWatcher(watcher);

        watcherMiddleware(req, resp, function(err) {
          if (err) {
            // log error
          }
          next(err);
        });
      }
    });
  }

As seen above ember-cli:broccoli:watcher will only be responsible for setting the headers and calling the the next middleware which will serve the files.

Create ember-cli:broccoli:serve-files addon

We will create a new in-repo addon called as ember-cli:broccoli:serve-files which will be responsible for serving the the files. This addon will run after ember-cli:broccoli:watcher addon.

This function will be responsible for serving the incoming asset request from the filesystem. It will use the serverMiddleware API to serve the files using broccoli-middleware.

  BroccoliServeFilesAddon.prototype.serverMiddleware = function(options) {
    var broccoliMiddleware = require('broccoli-middleware');
    var outputPath = options.watcher.builder.outputPath;
    var autoIndex = false;

    var options = { outputPath, autoIndex };
    app.use(function(req, resp, next) {
      var serveFileMiddlware = broccoliMiddleware.serveFiles();
      serveFileMiddlware(req, resp, function(err) {
        next(err);
      })
    });
  }

In order for FastBoot to be able to serve the assets using its own logic, it will specific that it run before ember-cli:broccoli:serve-files addon so that it can serve the assets. In this way, FastBoot will be able to inject itself into the correct order and be able to serve assets from the tmp directory.

Drop serve-files addon in ember-cli

Since the work that serve-files does today is now split into two new in-repo addons, serve-files addon doesn't need to be present any longer. It is not exposing any public API or functionality that users may be using today and therefore can be dropped.

How We Teach This

We will need to update the ember-cli website with this new in-repo addon and specify the above usecase with an example.

Drawbacks

The only drawback is addon authors wanting to serve assets using their own logic, will need to know the correct order of middleware execution. Moreover, if someone has forked ember-cli to hack serve-files addon logic, it will be a breaking change for them.

Alternatives

N/A

Unresolved questions

  • Should this addons be better named?
  • Start Date: 2016-12-04
  • Relevant Team(s): Ember CLI
  • RFC PR: #86

Summary

Replace PhantomJS with Firefox as the default browser for continuous integration testing.

Motivation

We want to provide the best possible out-of-the-box continuous integration testing experience for Ember apps. Today that means shipping with configurations for testem and TravisCI. Those configurations use PhantomJS.

But PhantomJS is a weird environment. Users must often fix Phantom-specific browser bugs, which is wasted effort since real users never run your app in Phantom. And "how to debug in Phantom" is an entire extra skill people are forced to learn.

A user-targeted, standards-compliant, modern browser makes a better default choice. Firefox is a good candidate because it's 100% open source, well-supported by Testem on all major operating system, and built-in to TravisCI. Debugging in Firefox has a dramatically nicer learner curve than PhantomJS.

Detailed design

This is a proposed change to the blueprints for new apps and addons. Existing apps and addons would only be affected when they re-run ember init as part of an upgrade and choose to take the updated configuration.

Changes in testem.js

Replace PhantomJS with Firefox.

Changes in travis.yml

Add the following new section to start up a virtual display:

before_script:
  - export DISPLAY=:99; sh -e /etc/init.d/xvfb start; sleep 3

How We Teach This

In the guides, replace instructions for installing PhantomJS with instructions for installing Firefox. Since Firefox is a consumer-facing browser with widely-understood installers and behavior, this is one less intimidating thing for newbies to learn.

Drawbacks

PhantomJS has two primary benefits over other browsers: being headless and being scriptable.

Headlessness

Firefox is not headless, so it needs to render to a display. That is why the Travis configuration needs xvfb.

Scriptability

PhantomJS is scriptable, but we don't rely on that functionality anyway. We want cross-browser test suites, so Phantom's scriptability is not particularly useful.

Alternatives

The default alternative is to do nothing and keep PhantomJS.

Another alternative would be to pick Chrome, since it is a very popular browser. However, Chrome is not 100% open source, which complicates distribution. It's not built into Travis, and the popular methods of installing it there require users to opt into non-container-based images, which are heavier and slower to boot.

Chromium is the fully-open-source parts of Chrome, but like PhantomJS it is an odd duck that's not really well-packaged for end users. It's also not installed by default in Travis.

  • Start Date: 2016-12-11
  • Relevant Team(s): Ember CLI
  • RFC PR: #90

Summary

In Ember CLI today, all addons at each level are built through the standard treeFor / treeFor* hooks. These hooks are responsible for preprocessing the JavaScript included by the tree returned from that specific hook (e.g., treeForAddon preprocesses the JS for the addon tree). This RFC proposes a mechanism that would allow these returned trees to be cached by default (when no build time customization is done) and expose proper hooks for addon authors to control the degree to which we dedupe these trees.

Motivation

Today, given the dependency graph:

ember-basic-dropdown:
  ember-wormhole@0.4.1

ember-modal-dialog:
  ember-wormhole@0.4.1

ember-paper:
  ember-wormhole@0.4.1

We would actually build ember-wormhole's addon tree 3 different times, even though as you can see there is absolutely no build time customization being done. After all of these ember-wormhole tree instances are built, we merge them such that the last tree wins (thus making all of the work to preprocess these trees completely moot). If you extrapolate this out to larger applications or ones using multiple engines (lazy or not) it is fairly common to see these sorts of dependencies shared upwards of 4 to 5 times. This can lead to significant build performance degradation.

Detailed design

  • Add a Addon.prototype.cacheKeyForTree method to lib/models/addon.js that is invoked prior to calling treeFor for the same tree name. The Addon.prototype.cacheKeyForTree method is expected to return a cache key allowing multiple builds of the same tree to simply return the original tree (preventing duplicate work). If Addon.prototype.cacheKeyForTree returns null / undefined the tree in question will opt out of this caching system.
  • ember-cli's custom mergeTrees implementation (which is already aware of other tree reduction techniques) will be updated so that calling mergeTrees([treeA, treeA]); simply returns treeA, and mergeTrees([treeA, treeB, treeA]) removes the duplicated treeA in the input nodes.

The proposed declaration for Addon.prototype.cacheKeyForTree in Typescript syntax is:

function cacheKeyForTree(treeType: string): string;

The default implementation for Addon.prototype.cacheKeyForTree will:

  • Utilize a shared NPM package (e.g. calculate-cache-key-for-tree) that will generate a cache key that incorporates at least the following pieces of information:

    • this.name - The addon's name (generally from package.json).
    • this.pkg - This builds a checksum accounting for the addon's package.json.
    • treeType - The specific tree in question (e.g. addon, vendor, addonTestSupport, templates, etc).
  • Resort to disabling all addon tree caching in the following scenarios

    • The addon implements a custom treeFor
    • The addon implements a custom treeFor* method (where * represents the tree type)

Addons that implement custom treeFor or treeFor* methods can still opt-in to caching in scenarios that they can confirm are safe. To do this, they would implement a custom cacheKeyForTree method and return a cache key as appropriate for their caching needs.

How We Teach This

This is something that we do not expect 99% of ember-cli users to have to learn and understand, however it is still important for it to be possible to determine what is going on and how to work within the system when building addons.

The following should help us teach this to the correct audience (roughly "addon power users"):

  • Document the shared NPM package (referred to above as calculate-cache-key-for-tree). This will help authors of addons that need to implement treeFor* hooks understand how they can properly implement Addon.prototype.cacheKeyForTree.
  • Write API docs for the newly added Addon.prototype.cacheKeyForTree method.

Drawbacks

  • Cache invalidation is difficult to get right, and it is possible to accidentally troll our users. This can be mitigated by thorough review of the implementation and this RFC.

Alternatives

Unresolved questions

  • Confirm if including the same tree multiple times will only trigger a single build of that tree (this should be a Broccoli feature). We have confirmed that code exists in broccoli-builder (see here), but still need to actually confirm .build / .read / .rebuild are not called twice within the same build.
  • Start Date: 2016-12-14
  • Relevant Team(s): Ember CLI
  • RFC PR: #91

Summary

Add an instrumentation hook that is available to addons. This enables users to write addons that do things like summarize and report build performance information.

  • see https://github.com/ember-cli/ember-cli/issues/6349 for additional context.
  • see https://github.com/ember-cli/ember-cli/pull/6606 for an experimental implementation.

Motivation

Build performance is important to users. We want to enable users to:

  1. Easily discover which portions of their build are costly;
  2. Be able to summarize and report build information in an addon;
  3. Be able to write addons that analyze build performance instrumentation so that they can more easily help diagnose build performance issues in projects to which they do not have direct access. This is of particular interest to @ember-cli/core &c.

In order to provide these hooks to enable iteration and experimentation prior to making firm commitments to format, this rfc propose to initially expose them as experiments (see the experiments section below).

Detailed design

Experiments

Experiments live in lib/experiments/index.js. Unlike feature flags, there is no need to strip them from production. Experiments allow us to provide power user features that are not fully stable without their resorting to private API usage.

Experiments are available only in canary builds. This is achieved by only including lib/experiements/index.js in canary, and making it the entry point for all experiments.

Instrumentation Hook

We have already a build instrumentation hook as an experiment in https://github.com/ember-cli/ember-cli/pull/6546

A more encompassing instrumentation hook is implemented in https://github.com/ember-cli/ember-cli/pull/6606

The goal of this RFC is:

  1. To make the concept of experiments supported and explicit
  2. To promote this particular experiment to public API

Enabling Instrumentation

Instrumentation is enabled if either the environment variable BROCCOLI_VIZ is set to 1 or if EMBER_CLI_INSTRUMENTATION is set to 1.

If BROCCOLI_VIZ=1 then in addition to instrumentation hooks being invoked, a serialized form of the instrumentation information is written to disk, that is appropriate for consumption by broccoli-viz which is the current behaviour.

Instrumentation

Hook

An addon that implements instrumentation will have this hook invoked when instrumentation is enabled.

module.exports = {
  name: 'my-great-addon',

  instrumentation(name, payload) {
    // format of instrumentation payload outlined below
  }
};
name

The name argument indicates what phase the instrumentation payload describes. In beta and released versions this will always be a string.

On canary it could be a symbol from lib/experiments if we add more phases (eg more fine-grained phases) for instrumentation information.

The initial set of phases this RFC advocates are:

  • init
  • command
  • build
  • shutdown
payload

payload is an object with two properties, summary and graph.

payload.summary

The exact format of payload.summary depends on the specific phase for which the instrumentation hook was called. In each case, the keys listed are the minimum keys that are guaranteed to be present, but there is no guarantee that additional information might not also be present.

init

init covers the period up to, but not including, command execution. This means it's mostly dealing with require time.

For init, the summary object has the following shape.

{
  totalTime,
  platform: {
    name,
  },
}
  • summary.totalTime The total time spent during init
  • summary.platform.name The value of process.platform
build

build covers the time spent in an individual build or rebuild.

For build, the summary object has the following shape.

{
  build: {
    type,
    count,
    outputChangedFiles

    // additional fields for rebuilds
    primaryFile,
    primaryFileCount,
    changedFiles
  },
  platform: {
    name,
  },
  output,
  totalTime,
  buildSteps,
}
  • summary.build.type one of 'initial' or 'rebuild'
  • summary.build.count the number of the build (0 for initial build, > 0 for rebuilds).
  • summary.build.outputChangedFiles an array of paths to output files that changed during this build. These paths are relative to the dist directory.
  • summary.build.primaryFile only present for rebuilds. Indicates the first file the watcher noticed had changed.
  • summary.build.changedFileCount only present for rebuilds. The number of files the watcher had noticed changed before the build started.
  • summary.build.changedFiles only present for rebuilds. The first 10 files the watcher had noticed changed before the build started.
  • summary.platform.name The value of process.platform
  • summary.output The temp directory containing the results of the build.
  • summary.totalTime The total time (in nanoseconds) of the build.
  • summary.buildSteps The number of broccoli nodes built in this tree
command

command covers the time spent during a command. When the command includes a build, there will be overlap between command and build. When the command is serve, this overlap will include only the last build, to avoid memory leaks.

For command, the summary object has the following shape.

{
  totalTime,
  platform: {
    name,
  },
  name,
  args
}
  • summary.totalTime The total time spent during init
  • summary.platform.name The value of process.platform
  • summary.name The name of the command that was run
  • summary.args The args of the command that was run
shutdown

shutdown covers the period from the command completing to process exit, ie cleanup time.

For shutdown, the summary object has the following shape.

{
  totalTime,
  platform: {
    name,
  },
}
  • summary.totalTime The total time spent during init
  • summary.platform.name The value of process.platform
payload.graph

graph is an object that represents the instrumentation information we have gathered for the build. It is a DAG, whose flow is inverted from the broccoli graph. It has a single source node (currently TreeMerger (all trees)). payload.graph is this single source node.

Each node in the graph provides an API for iterating its subgraph as well as iterating its own stats. The specific nodes in the graph will change over time as the instrumentation within ember-cli changes. There is no particular guarantee about what the nodes will be, although we will continue to ensure that its toJSON format is consumable by broccoli-viz

The API that each node supports is:

  • label
  • toJSON
  • adjacentIterator
  • dfsIterator
  • bfsIterator
label

A POJO property that describes the node. It will always include a name property and for broccoli nodes will include a broccoliNode property.

Example:

node.label === {
  name: 'TreeMerger (allTrees)',
  broccoliNode: true,
}
toJSON()

Returns a POJO that represents the serialized subgraph rooted at this node (the entire tree if called on the root node).

There is no particular guarantee about the format, except that whatever it is will be supported by broccoli-viz.

Example:

// for a graph
//  TreeMerger
//    |- Babel_1
//    |- Babel_2
//    |--|- Funnel
console.log(JSON.stringify(node.toJSON(), null, 2));
// might print
//
{
  nodes: [{
    id: 1,
    children: [2,3],
    stats: {
      time: {
        self: 5000000,
      },
      fs: {
        lstat: {
          count: 2,
          time: 2000000
        }
      },
      own: {
      }
    }
  }, {
    // ...
  }]
}
adjacentIterator

Returns an iterator that yields each adjacent outbound node. There is no guarantee about the order in which they are yielded.

// for a tree
//  TreeMerger
//    |- Babel_1
//    |--|- Funnel
//    |- Babel_2
node.label.name === "TreeMerger";
for (n of node.adjacentIterator()) {
  console.log(n.label.name);
}
// prints
//
// Babel_1
// Babel_2


for (n of node.preOrderIterator(x => x.label.name === 'Babel_2')) {
  console.log(n.label.name);
}
// prints
//
//  TreeMerger
//  |- Babel_1
dfsIterator(until)

Returns an iterator that yields every node in the subgraph sourced at this node. Nodes are yielded in depth-first order. If the optional parameter until is passed, nodes for which until returns true will not be yielded, nor will nodes in their subgraph, unless those nodes are reachable by some other path.

Example:

// for a graph
//  TreeMerger
//    |- Babel_1
//    |--|- Funnel
//    |- Babel_2
for (n of node.dfsIterator()) {
  console.log(n.label.name);
}
// prints
//
// TreeMerger
// Babel_1
// Funnel
// Babel_2
bfsIterator()

Returns an iterator that yields every node in the subgraph sourced at this node. Nodes are yielded in breadth-first order. If the optional parameter until is passed, nodes for which until returns true will not be yielded, nor will nodes in their subgraph, unless those nodes are reachable by some other path.

Example:

// for a tree
//  TreeMerger
//    |- Babel_1
//    |--|- Funnel
//    |- Babel_2
for (n of node.bfsIterator()) {
  console.log(n.label.name);
}
// prints
//
// TreeMerger
// Babel_1
// Babel_2
// Funnel
statsIterator()

Returns an iterator that yields [name, value] pairs of stat names and values.

Example:

  //  for a typical broccoli node
  for ([statName, statValue] of node.statsIterator()) {
    console.log(statName, statValue);
  }
  // prints
  //
  // "time.self" 64232794
  // "fs.statSync.count" 40
  // "fs.statSync.time" 401232123
  // ...

How We Teach This

This has no effect on day-to-day usage of ember-CLI. It is a tool to help users monitor and analyze their build performance, so documentation and teaching belong primarily in PERF_GUIDE.md. Having said that, we should also add a section to https://ember-cli.com/extending/ and the API docs to make using this feature easier for addon authors and CLI power users.

Drawbacks

  • No drawbacks come to mind, besides the ever present issue of maintenance

Alternatives

One alternative is the status quo: with BROCCOLI_VIZ=1 users can output a file with a similar format that they can post-process offline. Although this works for manual analysis, it is considerably more cumbersome for any automated system (such as ongoing monitoring of build performance). It also does not include instrumentation outside of the build, most notably startup.

Unresolved questions

  • heimdalljs-tree supports Symbol.Iterator; should we commit to this as part of our API?
  • Start Date: 2015-09-11
  • RFC PR: https://github.com/emberjs/rfcs/pull/91
  • Ember Issue: #12224 / #12990 / #13688

Summary

Introduce Ember.WeakMap (@ember/weakmap), an ES6 enspired WeakMap. A WeakMap provides a mechanism for storing and retriving private state. The WeakMap itself does not retain a reference to the state, allowing the state to be reclaimed when the key is reclaimed.

A traditional WeakMap (and the one that will be part of the language) allows for weakness from key -> map, and also from map -> key. This allows either the Map, or the key being reclaimed to also release the state.

Unforunately, this bi-directional weakness is problemative to polyfil. Luckily, uni-directional weakness, in either direction, "just works". A polyfil must just choose a direction.

Note: Just like ES2015 WeakMap, only non null Objects can be used as keys Note: Ember.WeakMap can be used interchangibly with the ES2015 WeakMap. This will allow us to eventually cut over entirely to the Native WeakMap.

Motivation

It is a common pattern to want to store private state about a specific object. When one stores this private state off-object, it can be tricky to understand when to release the state. When one stores this state on-object, it will be released when the object is released. Unfortunately, storing the state on-object without poluting the object itself is non-obvious.

As it turns out, Ember's Meta already solves this problem for listeners/caches/chains/descriptors etc. Unfortunately today, there is no public API for apps or addons to utilize this. Ember.WeakMap aims to be exactly that API.

Some examples:

  • https://github.com/offirgolan/ember-cp-validations/blob/master/addon/utils/cycle-breaker.js
  • https://github.com/stefanpenner/ember-state-services/ (will soon utilize the user-land polyfil of this) to prevent common leaks.

Detailed design

Public API

import WeakMap from '@ember/weak-map'

var private = new WeakMap();
var object = {};
var otherObject = {};

private.set(object, {
  id: 1,
  name: 'My File',
  progress: 0
}) === private;

private.get(object) === {
  id: 1,
  name: 'My File',
  progress: 0
});


private.has(object) === true;
private.has(otherObject) === false;

private.delete(object) === private;
private.has(object) === false;

Implementation Details

The backing store for Ember.WeakMap will reside in a lazy ownMap named weak on the key objects __meta__ object.

Each WeakMap has its own internal GUID, which will be the name of its slot, in the key objects meta weak bucket. This will allow one object to belong in multiple weakMaps without chance of collision.

Concrete Implementation: https://github.com/emberjs/ember.js/pull/12224 Polyfill: https://www.npmjs.com/package/ember-weakmap

Drawbacks

  • implementing bi-direction Weakness in userland is problematic.
  • Using WeakMap will insert a non-enumerable meta onto the key Object.

Alternatives

  • Weakness could be implemented in the other direction, but this has questionable utility.

Unresolved questions

N/A

  • Start Date: 2016-12-17
  • Relevant Team(s): Ember CLI
  • RFC PR: #92

Summary

Give blueprint generators the ability to clean up old files.

Motivation

We want to eliminate the noise of having old files laying after updating ember-cli using ember init.

Detailed design

We'd like an API for blueprints to delete files instead of only create. It would be essentially syntactic sugar for removing the file yourself in an afterInstall hook. It would be a returned array on the blueprint's index.js.

// ember-cli/bluprints/blah/index.js
module.exports = {
  // ...

  get oldFilesToRemove() {
    return [
      'brocfile.js',
      'LICENSE.MD',
      'testem.json'
    ];
  }
};

How We Teach This

The guides could use this addition in the blueprints section, but I envision it being used by mostly power users.

A changelog entry should be sufficient to teach this.

Drawbacks

The only reason to not do this is to hold out for a large blueprint reworking. We would be locked into this API.

Alternatives

The key name can be bikeshed. I chose oldFilesToRemove to be verbose and explicit, but it can be changed.

  • Start Date: 2017-01-03
  • Relevant Team(s): Ember CLI
  • RFC PR: #95

Summary

This RFC proposes the introduction of a official convention to specify the target browsers and node versions of an application.

Motivation

Javascript and the platforms it runs on are moving targets. NodeJS and browsers release new versions every few weeks. Browsers auto update and each update brings new language features and APIs.

Developers need an easy way to express intention that abstracts them from the ever-changing landscape of versions and feature matrices, so this RFC proposes the introduction of a unique place and syntax to let developers express their intended targets that all addons can use, instead of having each addon define it in a different way.

This configuration should be easily available to addons, but this RFC doesn't impose any mandatory behavior on those addons. All addons that want to customize their behavior depending on the target browsers will have a single source of truth to get that information but it's up to them how to use it.

The advantage of having a single source of truth for the targets compared to configure this on a per-addon basis like we do today is mostly for better ergonomics and across-the-board consistency.

Examples of addons that would benefit from this conventions are babel-preset-env, autoprefixer, stylelint and eslint (vía eslint-plugin-compat) and more. Even Ember itself could, once converted into an addon, take advantage of that to avoid polyfilling or even taking advantage of some DOM API (node.classList?) deep in Glimmer's internals, helping the goal of Svelte Builds.

Detailed design

What seems to be the most popular tool and the state of the art on building suport matrices for browser targets is the browserlist npm package.

That package is the one behind babel-preset-env, autoprefixer and others, and uses the data from Can I Use for knowing the JS, CSS and other APIs available on every browser.

The syntax of this package is natural but also pretty flexible, allowing complex queries like Firefox >= 20, >2.5% in CA (browsers with a market share over 2.5% in Canada) and logical combinations of the previous.

The way this library work is by calculating the minimum common denominator support on a per-feature basis.

Per example, if the support matrix for an app is ['IE11', 'Firefox latest'] and we have a linter that warns us when we use an unsupported browser API, it would warn us if we try to use pointer events (supported in IE11 but not in Firefox), would warn us also when using fetch (supported in firefox but not in IE) and would not warn us when using MutationObserver because it is supported by both.

This library is very powerful and popular, making relatively easy to integrate with a good amount of tools that already use it with low effort.

This configuration must be made available to addons but it's up to the addon authors to take advantage of it.

Browser support

The configution of target browsers must be placed in a file that allows javascript execution and exports an object with the configuration. The reason to prefer a javascript file over a JSON one is to allow users to dinamically generate different config depending on things like the environment they are building the app in or any other environment variable.

One possible location for this configuration is the .ember-cli file. A new dedicated named /config/targets.js also seems a good option, similar way how addons use config/ember-try.js to configure the test version matrix.

Ember CLI will require this file when building the app and make the configuration available to addons in a this.project.targets property.

This targets object contains a getter named browsers that returns the provided configuration or the default one if the user didn't provide any.

Example usage:

module.exports = {
  name: 'ember-data',

  included(app) {
    this._super.included.apply(this, arguments);

    console.log(this.project.targets.browsers); // ['>2%', 'last 3 iOS versions', 'not ie <= 8']
  }
};

This targets object can, and probably will, be expanded in the future with new properties for different kind of targets, like cordoba apps or fastboot, but that will be done in a different RFC.

How We Teach This

This is a new concept in Ember CLI, so guides will have to be updated to explain this concept. The good part is that this new concept can help enforcing with tools a task were traditionally enforced only with peer reviews.

To ease the transition Ember CLI can also, in the absence of a specific value provided by the user, default to a predefined matrix of browsers that matches the browsers officially supported by the framework.

As of today, the supported browser list for Ember.js, according to the platforms we test in saucelabs, is:

['IE9', 'Chrome current', 'Safari current', 'Firefox current']

There is no mention to IOS/Android, so this must be validated still.

Drawbacks

While this RFC standardizes a concept that will open the door to better and more comprehensive tooling, it makes us choose one syntax (the one used by browserlist) over any other perhaps superior choice that may exist or appear in the future.

Alternatives

Let every addon that wants to deal with targets to have a targets-like option in its configuration instead of standardizing a single configuration option, which effectively leaves things as they are right now.

Example:

var app = new EmberApp(defaults, {
  'ember-cli-autoprefixer': {
    browsers: ...
  },
  'ember-cli-babel': {
    targets: ...
  },
  'ember-cli-eslint': {
    engines: ...
  },
  ...
});

Unresolved questions

The proposed syntax for node only supports a single version of node. Is it reasonable to make this property an array of versions? P.e. ["4.4", "6", "7"]

  • Start Date: 2015-09-24
  • RFC PR: https://github.com/emberjs/rfcs/pull/95
  • Ember Issue: https://github.com/emberjs/ember.js/pull/14805

Summary

This RFC proposes:

  • creating a public router service that is a superset of today's Ember.Router.

  • codifying and expanding the supported public API for the transition object that is currently passed to Route hooks.

  • introducing the get-route-info template helper

  • introducing the #with-route-info template keyword

  • introducing the readsRouteInfo static property on Component and Helper.

These topics are closely related because they share a unified RouteInfo type, which will be described in detail.

Motivation

Given the modern Ember concepts of Components and Services, it is clear that routing capability should be exposed as a Service. I hope this is uncontroversial, given that we already implement it as a service internally, and given that usage of these nominally-private APIs is already becoming widespread.

The immediate benefit of having a RouterService is that you can inject it into components, giving them a friendly way to initiate transitions and ask questions about the current global router state.

A second benefit is that we have the opportunity to add new capabilities to the RouterService to replace several common patterns in the wild that dive into private internals in order to get things done. There are several places where we leak internals from router.js, and we can plug those leaks.

A RouterService is great for asking global questions, but some questions are not global and today we incur complexity by treating them as if they are. For example:

  • {{link-to}} can use implicit models from its context, but that breaks when you're trying to animate to or from a state where those models are not present.

  • {{link-to}} has a lot of complexity and performance cost that deals with changing its active state, and the precise timing of when that should happen.

  • there is no way to ask the router what it would do to handle a given URL without actually visiting that URL.

All of the above can be addressed by embracing what is already internally true: "the current route" is not a single global, it's a dynamically-scoped variable that can have different values in different parts of the application simultaneously.

Detailed design

RouterService

By way of a simple example, the router service behaves like this:

import Component from 'ember-component';
import service from 'ember-service/inject';

export default Component.extend({
  router: service(),
  actions: {
    goToMars() {
      this.get('router').transitionTo('planet.mars');
    }
  }
});

Like any Service, it can also be injected into Helpers, Routes, etc.

Relationship between EmberRouter and RouterService

Q: "Why are you calling this thing 'router' when we already have a router? Shouldn't the new thing be called 'routing' or something else?".

A: We shouldn't have two things. From the user's perspective, there is just "the router", and it happens to be available as a service. While we're free to continue implementing it as multiple classes under the hood, the public API should present as a single, coherent concept.

Terminology:

  • EmberRouter is the class that we already have today, defined in ember-routing/system/router and available publicly as Ember.Router
  • RouterService is the new class I am proposing.

EmberRouter has the following public API today:

  • map
  • location
  • rootURL
  • willTransition
  • didTransition

That API will be carried over verbatim to RouterService, and the publicly accessible Ember.Router class will become RouterService. In terms of implementation, I expect the existing EmberRouter class will continue to exist mostly unchanged. But public access to it will be moderated through RouterService.

New Methods: Initiating Transitions

transitionTo(routeName, ...models, queryParams)
replaceWith(routeName, ...models, queryParams)

These two have the same semantics as the existing methods on Ember.Route:

New Method: Checking For Active Route

  • isActive(routeName, ...models, queryParams)

The arguments have the same semantics as transitionTo, the return value is a boolean. This should provide the same logic that determines whether to put an active class on a link-to. Here's an example of how we can implement is-active as a helper, using this method:

import Helper from 'ember-helper';
import service from 'ember-service/inject';
import observer from 'ember-metal/observer';

export default Helper.extend({
  router: service(),
  compute([routeName, ...models], hash) {
    let allModels;
    if (hash.models) {
      allModels = models.concat(hash.models);
    } else {
      allModels = models;
    }
    return this.get('router').isActive(routeName, ...allModels, hash.queryParams);
  },
  didTransition: observer('router.currentRoute', function() {
    this.recompute();
  })
});
{{!- Example usage -}}
<li class={{if (is-active "person.detail" model) 'chosen'}} >

{{!- Example usage with generic routeName and list of models (avoids splat) -}}
<a class={{if (is-active routeName models=models) 'chosen'}} >

{{!- Note that the complexities of currentWhen can be avoided by composing instead. }}
<a class={{if (or (is-active 'one') (is-active 'two')) 'active'}} href={{url-for 'two'}} >

New Method: URL generation

urlFor(routeName, ...models, queryParams)

This takes the same arguments as transitionTo, but instead of initiating the transition it returns the resulting root-relative URL as a string (which will include the application's rootUrl).

A url-for helper can be implemented almost identically to the is-active example above.

New Method: URL recognition

recognize(url)

Takes a string URL and returns a RouteInfo for the leafmost route represented by the URL. Returns null if the URL is not recognized. This method expects to receive the actual URL as seen by the browser including the app's rootURL.

Example: this feature can replace this use of private API in ember-href-to.

New Method: Recognize and load models

recognizeAndLoad(url)

Takes a string URL and returns a promise that resolves to a RouteInfoWithAttributes for the leafmost route represented by the URL. The promise rejects if the URL is not recognized or an unhandled exception is encountered. This method expects to receive the actual URL as seen by the browser including the app's rootURL.

Deprecating willTransition and didTransition

Application-wide transition monitoring events belong on the Router service, not spread throughout the Route classes. That is the reason for the existing willTransition and didTransition hooks/events on the Router. But they are not sufficient to capture all the detail people need. See for example, https://github.com/nickiaconis/rfcs/blob/1bd98ec534441a38f62a48599ffa8a63551b785f/text/0000-transition-hooks-events.md

In addition, they receive handlerInfos in their arguments, which are an undocumented internal implementation detail of router.js that doesn't belong in Ember's public API. Everything you can do with handlerInfos can be done with the RouteInfo type that is proposed in this RFC, with the benefit of sticking to supported public API.

So we should deprecate willTransition and didTransition in favor of the following new events.

New Events: routeWillChange & routeDidChange

The routeWillChange event fires whenever a new route is chosen as the desired target of a transition. This includes transitionTo, replaceWith, all redirection for any reason including error handling, and abort. Aborting implies changing the desired target back to where you already were. Once a transition has completed, routeDidChange fires.

Both events receive a single transition argument as described in the "Transition Object" section below, which explains the meaning of from and to in more detail.

Redirection example:

  1. current route is A
  2. user initiates a transition to B
  3. routeWillChange fires from A to B.
  4. B redirects to C
  5. routeWillChange fires from A to C.
  6. routeDidChange fires from A to C.

Abort example:

  1. current route is A
  2. user initiates a transition to B
  3. routeWillChange fires from A to B.
  4. in response to the previous routeWillChange event, the transition is aborted.
  5. routeWillChange fires from A to A.
  6. routeDidChange fires from A to A.

Error example:

  1. current route is A
  2. user initiates a transition to B.index
  3. routeWillChange fires from A to B.
  4. B throws an exception, and the router discovers a "B-error" template.
  5. routeWillChange fires from A to B-error
  6. routeDidChange fires from A to B-error

These are events, not extension hooks -- now that we are exposing a Service, it makes more sense to subscribe to its events than extend it.

New Properties

currentRoute: an observable property. It is guaranteed to change whenever a route transition happens (even when that transition only changes parameters and doesn't change the active route). You should consider its value deeply immutable -- we will replace the whole structure whenever it changes. The value of currentRoute is a RouteInfo representing the current leaf route. RouteInfo is described below.

currentRouteName: a convenient alias for currentRoute.name.

currentURL: provides the serialized string representing currentRoute.

Query Parameter Semantics

Today, queryParams impose unnecessarily high cost because we cannot generate URLs or determine if a link is active without taking into account the default values of query parameters. Determining their default values is expensive, because it involves instantiating the corresponding controller, even in cases where we will never visit its route.

Therefore, the queryParams argument to the new urlFor, transitionTo, replaceWith, and isActive methods defined in this document will behave differently.

  • default values will not be stripped from generated URLs. For example, urlFor('my-route', { sortBy: 'title' }) will always include ?sortBy=title, whether or not title is the default value of sortBy.

  • to explicitly unset a query parameter, you can pass the symbol Ember.DEFAULT_VALUE as its value. For example, transitionTo('my-route', { sortBy: Ember.DEFAULT_VALUE }) will result in a URL that does not contain any ?sortBy=.

(Sticky parameters are still allowed, because they only apply when the destination controller has already been instantiated anyway.)

RouteInfo Type

A RouteInfo object has the following properties. They are all read-only.

  • name: the dot-separated, fully-qualified name of this route, like "people.index".
  • localName: the final part of the name, like "index".
  • params: the values of this route's parameters. Same as the argument to Route's model hook. Contains only the parameters valid for this route, if any (params for parent or child routes are not merged).
  • paramNames: ordered list of the names of the params required for this route. It will contain the same strings as Object.keys(params), but here the order is significant. This allows users to correctly pass params into routes programmatically.
  • queryParams: the values of any queryParams on this route.
  • parent: another RouteInfo instance, describing this route's parent route, if any.
  • child: another RouteInfo instance, describing this route's active child route, if any.

Notice that the parent and child properties cause RouteInfos to form a linked list. So even though the currentRoute property on RouterService points at the leafmost route, it can be traversed to discover everything about all active routes. As a convenience, RouteInfo also implements Enumerable over all the reachable RouteInfos from topmost to leafmost. This makes it possible to say things like:

router.currentRoute.find(info => info.name === 'people').params

RouteInfoWithAttributes

This type is almost identical to RouteInfo, except it has one additional property named attributes. The attributes contain the data that was loaded for this route, which is typically just { model }.

Transition Object

A transition argument is passed to Route#beforeModel, Route#model, Route#afterModel, Route#willTransition, and Router#willTransition. Today transition's public API is only really abort() and retry().

New Properties: from and to

I'm proposing we add from and to properties on transition whose values are RouteInfo instances representing the initial and final leafmost routes for this transition. Like all RouteInfos, these are read-only and internally immutable. They are not observable, because a transition instance is never changed after creation.

On an initial full-page load, the from property will be null. This creates a public API for distinguishing in-app transitions from full-page reloads.

Example: testing whether route will remain active

Here's an example showing how willTransition can figure out if the current route will remain active after the transition:

willTransition(transition) {
  if (!this.transition.to.find(route => route.name === this.routeName)) {
    alert("Please save or cancel your changes.");
    transition.abort();
  }
}

Example: parent redirecting to a fallback model

Here's an example of a parent route that can redirect to a fallback model, without losing its child route:

this.route('person', { path: '/person/:person_id' }, function() {
  this.route('index');
  this.route('detail');
});

//PersonRoute
const fallbackPersonId = 0;
model({ personId }, transition) {
  return this.get('store').find('person', personId).catch(err => {
    this.replaceWith(transition.to.name, fallbackPersonId);
  });
}

// If personId 5 is invalid, and the user visits /person/5/detail, they will get
// redirected to /person/0/detail. And /person/5 will get redirected to /person/0.

Actively discourage use of private API

This RFC provides public API for doing the things people have become accustomed to doing via private API. To eliminate confusion over the correct way, we should hide all the private API away behind symbols, and provide deprecation warnings per our usual release policy around breaking "widely-used private APIs".

Some of the private APIs we should mark and warn include:

  • transition.state
  • transition.params
  • lookup('router:main') (should use service:router instead)

Dynamically-Scoped Route Info

"The current route" is not a global value -- it varies from place to place within an application. Internally, Ember already models route info as a dynamically-scoped variable (currently named outletState). This RFC proposes publicly exposing that value in order to make things like link-to easier to implement directly on public primitives, and in order to enable stable public API for addons usage like {{liquid-outlet}}.

We propose get-route-info for reading the current route info in handlebars:

{{!- retrieve the value of a dynamically scoped variable }}
{{some-component currentRoute=(get-route-info)}}

We propose readsRouteInfo for defining a component that reads route info:

let MyComponent = Ember.Component.extend({
  didInsertElement() {
    // Accessing routInfo here is intended to be indistinguishable
    // from a normal, explicitly-passed input argument. 
    doSomethingWith(this.get('routeInfo'));
  }
});
MyComponent.reopenClass({
  // This is where we declare that we need access to routeInfo
  readsRouteInfo: true
});

And readsRouteInfo also works on Helper:

let MyHelper = Ember.Helper.extend({
  compute(params, hash) {
    // routeInfo is indistinguishable from a normally-passed hash argument
    return doSomethingWith(hash.routeInfo);
  }
});
MyHelper.reopenClass({
  readsRouteInfo: true
});

We propose the #with-route-info keyword for setting a new route info:

{{#with-route-info someValue}}
  {{!-
    within this block AND ALL ITS DESCENDANTS until
    otherwise overridden by another set-route-info statement, 
    `get-route-info` returns someValue.
  -}}
{{/with-route-info}}

Note that there is no set-route-info. You can only introduce new scopes, not mutate your containing scope. There is also no way to set routeInfo directly from Javascript -- your component must use a with-route-info block within its handlebars template.

routeInfo's type, and examples

The value returned from get-route-info and acceptd by with-route-info is always a RouteInfoWithAttributes object. This enables several nice things, which I will illustrate with examples:

  1. Here is a simplified is-active helper that will always update at the appropriate time to match exactly what is rendered in the current outlet. It will maintain the correct state even during animations. Instead of injecting the router service, it consumes the routeInfo from its containing environment:
Ember.Helper.extend({
  compute([routeName], { routeInfo }) {
    return !!routeInfo.find(info => info.name === routeName);
  }
}).reopenClass({
  readsRouteInfo: true
});

A more complete version that also matches models and queryParams can be written in the same way.

  1. We can improve link-to so that it always finds implicit model arguments from the local context, rather than trying to locate them on the global router service. This will fix longstanding bugs like https://github.com/ember-animation/liquid-fire/issues/347 and it will make it easier to test components that contain {{link-to}}. This would also open the door to relative link-tos.

  2. liquid-outlet can be implemented entirely via public API. It would become:

{{#liquid-bind (get-route-info) as |currentRouteInfo|}}
  {{#with-route-info currentRouteInfo}}
    {{outlet}}
  {{/with-route-info}}
{{/liquid-bind}}
  1. Prerendering of non-current routes becomes possible. You can use recognizeAndLoad to obtain a RouteInfoWithAttributes and then use {{#with-route-info myRouteInfo}} {{outlet}} {{/with-route-info}} to render it.

Drawbacks

This RFC deprecates only two public extension hooks API, so the API-churn burden may appear low. However, we know that use of the private APIs we're deliberately disabling is widespread, so users will experience churn. We can provide our usual deprecation cycle to give them early warning, but it still imposes some cost.

This RFC doesn't attempt to change the existing and fairly rich semantics for initiating transitions. For example, you can pass either models or IDs, and those have subtle semantic differences. I think an ideal rewrite would also change the semantics of the route hooks and transitionTo to simplify that area.

Alternatives

Less Churn

We could adopt some of the existing broadly used APIs as de-facto public. This avoids churn, but imposes a complexity burden on every new learner, who needs to be told "this is a weird API, but it's what we're stuck with".

Semver Lawyering

I'm interpreting router.js's public/private documentation as out-of-scope for Ember's semver. The fact that we pass an instance of router.js's Transition as our transition argument is not documented. An alternative interpretation is that we need to continue supporting those methods marked as public in router.js's docs.

Optional Helpers

I didn't propose shipping is-active and url-for template helpers -- I merely showed that they're easy to build using the router service. But we should arguably just ship them as part of the framework too.

Branching Route Hierarchies

I am implicitly assuming we will only ever have linear route hierarchies, where a given route has at most one child. I can imagine eventually wanting a way to support branching route hierarchies, where each branch can transition independently. I'm not trying to account for that future.

Route.parentRoute

This RFC makes it possible for a route to determine its parent's name dynamically via public API, and thus access its parent's model/params/controller:

beforeModel(transition) {
  const parentInfo = transition.to.find(info => info.name === this.routeName).parent;
  const parentModel = this.modelFor(parentInfo.name);
}

However, this pattern feels awkward, and I think it justifies just adding a public parentRouteName() method to Route that would simplify to:

beforeModel(transition) {
  const parentModel = this.modelFor(this.parentRouteName());
}

Possibly we want this to feel awkward because it's a weird thing to do.

Naming of Ember.DEFAULT_VALUE Symbol

Should we introduce new API via the Ember global and switch to a module export once all the rest of Ember does, or should we just start with a module export right now? If so, what module?

import { DEFAULT_VALUE } from 'ember-routing';
  • Start Date: 2017-02-02
  • Relevant Team(s): Ember CLI
  • RFC PR: #96

Summary

Enable Ember CLI users to opt into using yarn for packagement management.

Motivation

Ember CLI currently uses the npm command line tool to install dependencies when you run ember install or ember new/ember addon. However, several problems usually arise from npm's semantics. Dependency resolution and install times can be significant enough to disrupt workflows, as well as offline support, non-deterministic, non-flat dependency graphs.

Yarn was introduced to the JavaScript community with the intent to provide a better developer experience in these areas:

  • Faster installs
  • Offline support
  • Deterministic dependency graphs
  • Lockfile semantics

While Ember CLI users can currently use Yarn to manage their dependencies, Ember CLI will use the npm client internally when running the above mentioned commands. By allowing users to specify that Ember CLI should use Yarn for everything, we're hoping to provide a more consistent experience.

Detailed design

Enabling Yarn is designed as opt-in to prevent disruptions to the developer's current workflow. We will address the two moments where this can happen.

ember install

There are two mechanisms through which to opt-in. The first one is the presence of a yarn.lock file in the project root.

The yarn.lock file is generated by Yarn when you run yarn install (or the shorter yarn), so we assume that its presence means the developer intends to use Yarn to manage their dependencies.

Alternatively you, you can force Ember CLI to use Yarn with the --yarn flag, and symmetrically, you can force Ember CLI to not use Yarn with the --no-yarn flag.

To recap:

  • ember install ember-cli-mirage with yarn.lock present will use Yarn
  • ember install ember-cli-mirage without yarn.lock present will use npm
  • ember install ember-cli-mirage --yarn will use Yarn
  • ember install ember-cli-mirage --no-yarn will use npm

ember init, ember new, ember addon

Since this triad of commands is generally ran before a project is set up, there is no yarn.lock file presence to check. This means we are left with the --yarn/--no-yarn pair of flags, that will also be added to these commands:

  • ember new my-app will use npm
  • ember new my-app --yarn will use Yarn

The above also applies to ember addon and ember init, noting that ember init doesn't receive any arguments.

How We Teach This

Both the Ember.js Guides as well as the Ember CLI Guides will be updated to reflect the new flags, as well as the new semantics of ember install in the presence of yarn.lock.

In addition, the built-in instructions for ember help will be updated to reflect this.

Drawbacks

To be determined.

Alternatives

Do nothing.

Unresolved questions

To be determined.

  • Start Date: 2015-10-23
  • RFC PR: https://github.com/emberjs/rfcs/pull/101
  • Ember Issue: https://github.com/emberjs/data/pull/3930

Summary

Add more illustrative detail to the default Ember Data Adapter Errors.

Motivation

With a production Ember project, it's common to have many errors of the form "Adapter Error", originating from deep in the Ember Data stack and carrying little context about what the original error cause was.

The intent is to add the original request URL, the response code, and some payload information to the default Error message for DS.AdapterErrors. From there Errors can be handled or tracked as normal.

Detailed design

I've been using something similar to the following Adapter (friendly-error-adapter.js):

import ActiveModelAdapter from 'active-model-adapter';

import DS from 'ember-data';

export default ActiveModelAdapter.extend({

  ajax(url, method)  {
    this.lastRequest = {
      url:    url,
      method: method
    };
    return this._super(...arguments);
  },

  handleResponse: function (status, headers, payload) {
    let payloadContentType = headers["Content-Type"].split(";").get("firstObject");
    let shortenedPayload;

    if (payloadContentType === "text/html" && payload.length > 250) {
      shortenedPayload = "[omitted long blob of HTML]";
    } else {
      shortenedPayload = payload;
    }

    let errorMessage = `Ember Data Error (${this.lastRequest.method} ${this.lastRequest.url} returned a ${status}). \n Payload (${payloadContentType}): \n\n ${shortenedPayload}`;

    if (this.isSuccess(status, headers, payload)) {
      return payload;
    } else if (this.isInvalid(status, headers, payload)) {
      return new DS.InvalidError(payload.errors, errorMessage);
    }

    let errors = this.normalizeErrorResponse(status, headers, payload);

    return new DS.AdapterError(errors, errorMessage);
  }
});

(Note that the code inside the adapter could be MUCH simpler and cleaner, the above is a very quick hacked up example! :bomb:)

The intent is to get an error message out of the form:

  1. "Ember Data Error"
  2. Request Method & URI
  3. Response Status
  4. Response Content Type
  5. A sane representation of the Response payload

Drawbacks

Adding complexity to an Error handler always runs the risk of generating errors inside the handler itself, which would not be overly friendly.

Alternatives

There's probably quite a few different pieces of information that could be included in the message.

We could also potentially look at attaching the extra information to other fields on the AdapterError (and its subclasses). The only drawback there would be that most error reporters would then not include that information by default.

Unresolved questions

  • Exact Error Message Format
  • Start Date: 2017-4-23
  • Relevant Team(s): Ember CLI
  • RFC PR: #105

Summary

It should be possible to specify packages/addons in optionalDependencies of the package.json of an ember-cli project, and ember-cli should scan for packages/addons mentioned in optionalDependencies while processing the build so that such packages/addons could also be included into the consuming application.

The build need not fail asserting "missing dependency" if any of the dependencies specified in optionalDependencies is missing/absent.

Motivation

In general, the current ember-cli build process will scan for the packages specified in the dependencies hash and devDependencies hash from the downloaded packages in the node_modules folder, discovers and then includes them into the consuming application. The build is designed to fail if any of the packages specified in these two dependencies hash is missing in the node_modules folder. But this procedure may not be sufficient for a variety of cases.

So there could be an option for the developer to specify packages in optionalDependencies and ember-cli can lookup optionalDependencies while processing the build. The Build need not fail if there is any package specified in optionalDependencies is missing, since it is only optional and moreover may only be required for developmental purposes. This way the developer can have more control over the choice of packages he wishes to use for development and skip for production by giving appropriate commands like npm install --no-optional, thereby preventing the installation of packages itself rather than blacklisting in ember-cli-build.js which suggests preventing the installed addons sepcifed in the blacklist array from being included into the consuming application.

Detailed design

We can tweak ember-cli addon/package discovery process to lookup for optionalDependencies as well and if the package is missing, we can make ember-cli proceed the build without terminating.

How We Teach This

This functionality can simply be documented in ember-cli guides to teach.

Alternatives

None.

  • Start Date: 2017-06-18
  • Relevant Team(s): Ember CLI
  • RFC PR: #108

Summary

This RFC proposes to add a new API to allow addons to register a custom transformation. This transformation can then be used by other addons when calling app.import with using API.

Motivation

Addons or apps may want import browser only compatible libraries using app.import via bower or npm. These libraries should not be running in Node.

When FastBoot was doing two builds (to generate different assets for browser and Node environment), addon or apps often conditionally imported these libraries relying on the value of process.env.EMBER_CLI_FASTBOOT. With the new scheme of the build where only additional Node assets are built, this enviornment is no longer exposed.

In order to expose better semantics to allow apps and addon authors to easily import these libraries without much overhead (see issue here), we need to have these libraries wrapped with an FastBoot check. This can be achieved by extending the using API of app.import. FastBoot addon would like to register a custom transformation that other FastBoot compatible addons may chose to use in a declarative API.

Detailed design

Today, Ember CLI supports transforming anonymous AMD modules imported via app.import into named AMD modules:

app.import('/path/to/module.js', {
  using: [
    { transformation: 'amd', as: 'some-dep' }
  ]
});

The amd transform is hardcoded in Ember CLI. However, it is not possible for addon authors to provide any additional transformation that other addons can use when importing third-party modules. Addons like, FastBoot would like to provide custom transformation for other addons to use so that they can wrap their third party libraries in Node environments.

In order to do this, we would like to expose an API that allows addons to register a custom transformation. This API will be an advanced API and will only be used by addons that want to provide custom transformation. Other addons can chose to use that custom transformation using its name.

The API to register a custom transformation in Ember CLI will be defined in index.js of the addon and will be an advanced API:

importTransforms() {
  return {
    'fastboot-shim': function(tree, options) {

      return stew.map(tree, function(content, relativePath) {
        return `if (typeof FastBoot === 'undefined) { ${content} }`;
      });
    }
  }
}

importTransforms returns a map of the name of the transform and a callback function that will be run on every module that uses the transform. The callback function takes the tree as broccoli tree contain all the files that want to run this transform and options map (optional) that contains the additional key value pairs that a consumer transformer provides. The later argument would be used by transformations like amd (explained below).

With this, we also should move the hard coded amd transform into an in-repo addon in Ember CLI. This would allow other addons that define their own transformation to also control the order of their transformation (using before or after hooks of addon initialization). The registeration of amd transform would be:

importTransforms() {
  return {
    'amd': function(tree, options) {

      return stew.map(tree, function(content, relativePath) {
        const name = options[relativePath].using;
        if (name) {
          return [
            '(function(define){\n',
            content,
            '\n})((function(){ function newDefine(){ var args = Array.prototype.slice.call(arguments); args.unshift("',
            name,
            '"); return define.apply(null, args); }; newDefine.amd = true; return newDefine; })());',
          ].join('');
        } else {
          return content;
        }
      });
    }
  }
}

As seen above, options contains the optional AMD module ID that the consumer of amd transform can provide. If registered transforms want to depend on any other user provided values, those can easily be available during the transforms.

When the addons are initialized, we will check if importTransforms is defined and store these callbacks and transform names in an array.

Now, if addon authors would like to use these transforms when importing libraries, they would simply do the following:

app.import('/path/to/module.js', {
  using: [
    {  transformation: 'fastboot-shim' },
    { transformation: 'amd', as: 'some-dep' }
  ]
});

As seen above, an addon author could provide the list of transformations to run and Ember CLI would run them in the order of when the transformations were registered. Internally, for every transform we will maintain an array of file paths that need to run this transform. When the transformations need to run, we will read the registration order, run the transformation on those files. The output of the transformation will then be merged back and then the next transformation would run. This will ensure that more than one transformation can be correctly applied to a module.

Same name conflict

Allowing addons to define custom transform could lead to naming conflicts where more than two addons may provide transform functions with the same name but slightly or totally different functionality. Therefore, if more than one addon provides a same name for a transform by default the last addon in the order that registered its transform will win. In addition, we will also warn the users of the name conflicts and which addon's registered transformation is going to run.

How We Teach This

The registeration of transform is an advanced API of Ember CLI that very few addons would use. We will be updating the guides here.

Drawbacks

The drawback of this approach is that the order of running the transformation is controlled by the addon that provides the transform rather than the addon that uses the transform. The reasoning for this is mainly for performance reasons (in order to not create a funnel per asset path) and to make sure the more than one transform can be applied correctly on an asset path.

Alternatives

Currently the alternative is for addons to import their bower or npm dependency in vendor via treeForVendor and manually use broccoli plugins to do transformations. The alternative for apps is to create an in-repo addon to do this.

Unresolved questions

N/A

  • Start Date: 2017-09-07
  • Relevant Team(s): Ember CLI
  • RFC PR: #110

Summary

The goal of this RFC is to solidify the next version of the mechanism that ember-cli uses to build final assets. It will allow for a more flexible build pipeline for Ember applications. It also unlocks building experimental features on top. It is a backward compatible change.

Motivation

The Packager RFC submitted by Chad Hietala is a little over 2 years old. A lot of things have changed since then and it requires a revision.

The current application build process merges and concatenates input broccoli trees. This behaviour is not well documented and is a tribal knowledge. While the simplicity of this approach is nice, it doesn't allow for extension. We can refactor our build process and provide more flexibility when desired.

Most importantly, the approach described below helps us achieve:

  • defining and developing a common language around the subject
  • removing highly coupled code and streamline technical implementation (Ember Engines and Fastboot)
  • unlock a whole different set of plugins we couldn't have before:
    • ability to create custom bundles (i.e per-engine and per-route bundles)
    • take advantage of HTTP2 multiplexing and cache pushing
    • optimising plugins (JavaScript and CSS tree-shaking)

Scope

  • New public API for customising build process and giving more granular control over the final build output

Terminology

  • Packaging - The process of designing, evaluating, and producing final build assets.

Detailed design

The detailed design is separated in various sections so that it is easier for a reader to understand.

Packaging

It gives you granular control over the final build output. It could be used in many different ways (we are going to go over use cases below). Note, it isn't meant to be used for "postprocess" transformations; "postprocess" is called after packaging is finished.

Currently, Ember.js application and all of its depedencies get assembled under one directory with the following structure:

bundler:js:input/
├── addon-tree-output/
├── the-app-name-folder/
├── node_modules/
└── vendor/

where:

  • addon-tree-output is a folder that contains dependencies from Ember add-ons.
  • the-app-name-folder is a folder that contains Ember application code.
  • node_modules is a folder that contains node dependencies.
  • tests is a folder that contains test code.
  • vendor is a folder that contains other dependencies.

Note, for clarity purposes we should rename addon-tree-output to addon-modules as both tree and output don't communicate well about the contents of the folder.

During packaging process the final output will be generated (everything that currently resides under dist/ folder when a developer runs ember build).

package API

A new public package method will be introduced to open up a way to customise packaging process:

// ember-cli-build.js
const EmberApp = require('ember-cli/lib/broccoli/ember-app');

module.exports = function(defaults) {
  const app = new EmberApp(defaults, {
    package(inputTree) {
      // customise `inputTree`
      // and return customised `inputTree`
    }
  });

  return app.toTree();
}

package function has the following signature:

interface EmberApp {
  package(inputTree: BroccoliTree): BroccoliTree;
}

where inputTree will have the following structure:

bundler:js:input/
├── addon-modules/
├── the-app-name-folder/
├── node_modules/
├── tests/
└── vendor/

Note, that package method must return a broccoli tree.

This change should be behind an experiment flag, PACKAGING. This will allow us to start experimenting right away and not being tied to a particular release cycle.

Note, that package is optional. If you don't define it, you're effectively "opting out" of the feature and using the default behaviour.

defaultPackager API

It's important to make it easy for users to still use default Ember CLI packaging.

defaultPackager is a way for the users to access out-of-the-box packaging while still be able to customise the final build output.

// ember-cli-build.js
const EmberApp = require('ember-cli/lib/broccoli/ember-app');
const defaultPackager = require('ember-cli-default-packager');

module.exports = function(defaults) {
  const app = new EmberApp(defaults, {
    package(inputTree) {
      // customise `inputTree`

      return defaultPackager(app, inputTree);
    }
  });

  return app.toTree();
}

defaultPackager has the following signature:

function defaultPackager(app: EmberApp, inputTree: BroccoliTreel): BroccoliTree;

defaultPackager must return a BroccoliTree.

Possible usages

Debug/Analyse

One of the applications of package API would be to run different analysis on the Ember applications. Take broccoli-concat-analyser, for example. This could be easily incorporated into the build.

// ember-cli-build.js
const EmberApp = require('ember-cli/lib/broccoli/ember-app');
const defaultPackager = require('ember-cli-default-packager');

module.exports = function(defaults) {
  const app = new EmberApp(defaults, { });

  app.package = function(inputTree) {
    const analysedTree = new BroccoliConcatAnalyser(inputTree);

    return defaultPackager(app, analysedTree);
  }

  return app.toTree();
}

Static Assets Split

One of the techniques for improving site speed is isolating changes throughout application deployments. Assuming the application assets are uploaded to CDN, the reasoning is very simple: if ember.js or jQuery (possibly along with other third party libraries) don't change with every deployment, why bust CDN cache for them?

ES6 Modules

ES6 modules are starting to land in browsers. This means that you can use <script type="module" src="/my/app.js"></script>.

This article explains the benefits of using ES6 modules over ES2015 (smaller total file sizes, faster to parse and evaluate).

package API will make it possible to package your application for both ES2015 only browsers as well the ones with ES6 modules support.

Topics for Future RFCs

While working on this RFC, some ideas were brought into focus regarding existing and new features in Ember CLI. They all likely require separate discussions in future RFCs, but the discussion points have been included below.

Tree-shaking

Firstly, what's tree-shaking? AFAIK, the term originated in Lisp. The gist of the idea is "how about we start using only the code that we actually need?"

Secondly, how is it different from Dead Code Elimination? Rich Harris offers a pretty good explanation in the context of Rollup. The gist is dead code elimination happens on a final product by removing bits that are unused. Tree-shaking is quite different - given an object we want to construct, what is the exact set of dependencies we need?.

With this RFC, we lay out the foundation and create a framework by which both dead code elimination and tree-shaking code be implemented.

However, there are still several things that are missing:

  • Linker - Responsible for resolving and reducing the graph to a tree containing only reachable modules.
  • File System Resolver - Responsible for connecting a module name with a file path.

Linker would be responsible for:

  • building a minimal dependency graph as well as check for redundant edges in the graph (more on the topic, Transitive reduction of a directed graph);
  • producing an application tree with only used modules

Dependency graph represents dependencies using module names, there is a need to be able to convert module name to file path. This is where File System Resolver comes in. Here's couple of examples:

fileSystemResolver.resolve('lodash') => `some-path/node_modules/lodash/lodash.js`
fileSystemResolver.resolve('ember-ajax') => `some-path/addon-modules/ember-ajax/index.js`
fileSystemResolver.resolve('ember-data') => `some-path/addon-modules/modules/ember-data/index.js`
fileSystemResolver.resolve('ember-data/-private') => `some-path/addon-modules/modules/-private.js`

This effort could be broken down into several phases:

  • dead modules elimination inside of the addons/ (application would be the main entry point and unused modules are removed only from addons/)
  • dead modules elimination inside of the app/
    • removing unused components and helpers (requires analysing templates)
    • removing unused initializers/services (this likely entails work on dependency injection layer as we would need access to a resolver resolution map)
  • tree-shaking (Rollup-like tree-shaking where we include only the code that is used)

Linker would be able to take an exclude list of modules as a parameter. Although, valuable in some situations, it should be clearly marked as advanced API. It should be used as a last resort and serve as an "escape hatch".

It would make sense to implement Linker as a strategy. Developers would be able to "opt in"/"opt out" of optimising behaviour.

Deprecating app.import API

Ember applications which choose to use Linker strategy should be able to remove usages of app.import.

Tools

With growing complexity of Ember applications, it is crucial to provide more insights into final assets.

Main goals are:

  • report raw/uglified/compressed asset sizes; broccoli-concat-analyser
  • find source code duplication across your javascript assets (enables you to fine tune code splitting parameters to reduce bundle invalidation rates as well as improve repeat page load performance)

How We Teach This

This is a backward compatible change to the existing Ember CLI ecosystem. In order to teach users how to use package API, we need to update the API docs with a section for this and the best practices of when to use this. A more general purpose blog post could be beneficial as well.

Drawbacks

There are several potential drawbacks that are worth noting.

Build performance. There is minimal overhead in instantiating strategies and calling methods on them and I believe this approach shouldn't degrade build performance.

A note on add-ons. Add-ons don't rely on the way Ember CLI does bundling. That means existing build system continues to work as expected and add-ons won't have to change their implementation.

Alternatives

This RFC allows us to customise packaging when needed. Webpack has become very popular in solving this similar problem. One could implement a package function that would use Webpack for packaging. Ultimately, we need something that is aware of how Ember apps are assembled and how Ember apps utilise dependency injection that takes advantage of existing tools. The long term plan is to have a dependency graph that is aware of application structure so can avoid the "wall of configuration" that other asset packaging systems are susceptible to.

Unresolved questions

  • Will it increase build time?
  • Should we introduce the same API on add-on level?

Thanks

Many thanks for @stefanpenner, @rwjblue and @chadhietala for helping me to drive this forward.

  • Start Date: 2018-01-04
  • Relevant Team(s): Ember CLI
  • RFC PR: #114

Summary

Add https://github.com/rwjblue/ember-cli-template-lint as a default addon for the app and addon blueprints using the recommended rules.

Motivation

Linting and security in templates would help not only individual developers write better apps with better accessibility and security, but would also help teams to be on the same page and stick to a handful of standards.

Detailed design

  1. Move ember-cli-template-lint to the ember-cli org (better for contributing and getting work off one person, @rwjblue)
  2. Add the dependency to the app blueprint here: https://github.com/ember-cli/ember-cli/blob/master/blueprints/app/files/package.json#L19
  3. Also add it to the addon blueprint, like the eslint addon here: https://github.com/ember-cli/ember-cli/blob/master/blueprints/addon/index.js#L66

How We Teach This

The same way that we teach ESLint being on by default.

Drawbacks

  • More chatter in the terminal.
  • An additional dependency.
  • Recommended rules might not be good for everyone.. but that same issue probably exists with ESLint.

Alternatives

Do nothing and have people write sub par template code.

Unresolved questions

None

  • Start Date: 2018-02-12
  • Relevant Team(s): Ember CLI
  • RFC PR: #116

Summary

Introduce qunit-dom as a dependency by default in the app and addon blueprints.

Motivation

Why are we doing this?

In a modern Ember application making assertions around the state of the DOM is fundamental to confirming your applications functionality. These assertions are often quite verbose:

assert.equal(this.element.querySelector('.title').textContent.trim(), 'Hello World!');

Using the find() helper of @ember/test-helpers we can simplify the DOM element lookup, but the signal-to-noise ratio of the code is still not great:

assert.equal(find('.title').textContent.trim(), 'Hello World!');

With qunit-dom we can write much more readable assertions for DOM elements:

assert.dom('.title').hasText('Hello World!');

What use cases does it support?

It supports the most common assertions on DOM elements, like:

  • what text does the element have?
  • what value does the <input> element have?
  • is a certain CSS class applied to the element

The full API is documented at https://github.com/simplabs/qunit-dom/blob/master/API.md.

What is the expected outcome?

Using qunit-dom will lead to more simple and readable test code.

Detailed design

The necessary changes to ember-cli are relatively small since we only need to add the dependency to the app blueprint, and the addon blueprint will inherit it automatically.

This has the advantage (over including it as an implicit dependency), that apps and addons that don't want to use it for some reason can opt-out by removing the dependency from their package.json file.

A WIP pull request has been created already at https://github.com/ember-cli/ember-cli/pull/7605.

How We Teach This

Would the acceptance of this proposal mean the Ember guides must be re-organized or altered? Does it change how Ember is taught to new users at any level?

Once we decide that this is the right way to go, we should update the official Ember.js testing guides to use qunit-dom assertions by default. This has the nice side effect of making the testing code in the guides easier to read too.

At the same time (same minor release) we should update the relevant blueprints in the ember-source package to use qunit-dom by default. This should be a relatively small change as only the component and helper tests use DOM assertions.

How should this feature be introduced and taught to existing Ember users?

We should also explicitly mention this change in the release blog post and recommend that people use this from now on. For those users that want to migrate their existing tests to qunit-dom a basic codemod exists at https://github.com/simplabs/qunit-dom-codemod.

Drawbacks

Why should we not do this? Please consider the impact on teaching Ember, on the integration of this feature with other existing and planned features, on the impact of the API churn on existing apps, etc.

There are tradeoffs to choosing any path, please attempt to identify them here.

  • qunit-dom is "owned" by a third-party consulting company (simplabs) and the Ember CLI team is not directly in control.

  • qunit-dom has not reached v1.0.0 yet so there might be small breaking changes in the future.

  • qunit-dom is another abstraction layer on top of the raw QUnit assertions which adds to the existing learning curve.

  • Adding qunit-dom to the default blueprint could make it look even more like ember-mocha is only a second-class citizen. Since we add it to the default package.json file it is easy to opt-out though and can be replaced with chai-jquery or chai-dom for a roughly similar API.

Alternatives

What other designs have been considered?

  • Using the find() helper functions can be considered an alternative, but as mentioned above they still result in more verbose code than using qunit-dom. Another advantage is that qunit-dom generates a useful assertion description by default, while assert.equal() will just show something like "A does not match B".

What is the impact of not doing this?

We will keep using hard-to-read assertions by default and leave it up to our users to discover qunit-dom by themselves.

Unresolved questions

  • Should the ember-source blueprints detect qunit-dom usage and fallback to raw QUnit assertions if the dependency can't be found?
  • Start Date: 2018-07-30
  • Relevant Team(s): Ember CLI
  • RFC PR: #120

Ember CLI Docs

Summary

This RFC proposes converting the existing Ember CLI website into an Ember app, restructuring the table of contents, replacing a significant portion of the learning material, and inviting community members to participate in writing new content.

Motivation

The purpose of these changes are to empower new contributors, create a consistent narrative structure, correct outdated information, and lead new readers through an easier learning progression.

Ember's public sites are being migrated from Ruby apps to Ember apps in order to improve maintainability and empower new contributors. The CLI docs are currently a Jekyll app. Similar migrations have been very successful.

The rewrite and/or reorganization of content is driven by an audit of the existing content's relevance and balance. While trying to plan a refactor in place, it became clear that a greenfield approach is more time efficient and will lead to a better learning experience. A significant portion of the content from the current guides site can be ported over once a new structure is in place.

Detailed design

This app will have a new table of contents. The architecture will follow the same patterns successfully used in other apps that have been converted from Middleman apps to Ember.

Writing process

Writing new content and porting over existing information is a job that will require the help of many contributors! After this RFC is accepted, a call for contributors will be made.

Here are some strategies to help contributor work to be successful:

  • A quest issue will outline sections that need work so that people can volunteer
  • Collaboration will be encouraged so that no one person blocks writing on a particular topic
  • Contributing can take multiple forms. For example, developers with some CLI expertise who don't have time/interest for formal writing can share some brief notes or suggestions to help out the writers. Writers don't need to be experts. In some cases, it's better when someone isn't very familiar with the content because they can help identify gaps.
  • Each unwritten section will have comments in the markdown indicating which topics to cover. In cases where content has been ported over, comments will indicate which sections to fact-check, clarify, or revise.
  • A strike team channel will be created on a chat
  • A writing styleguide will be provided for contributors
  • Following a verson one release, writing work will be organized via normal GitHub issues.

Since maintaining consistent voice and structure across a blank slate is a challenge, beta content for the core learning experience has already been drafted, including Basic Use guides and a tutorial for creating an addon from start to finish.

The beta version of the CLI Guides content can be found at ember-learn/cli-guides-source. The Markdown files there are rendered by ember-learn/cli-guides-app. The app is currently deployed to a temporary endpoint for testing and UX validation. The link is available on the repositories.

User Personas

The content layout should follow the progression of an Ember developer's learning experience. There are four main user personas for the CLI documentation:

  1. A new or "typical" Ember CLI user - someone whose primary work is running common commands like ember serve and who has a "zero config" type of experience with Ember
  2. Power users - developers who make their own configurations to the build pipeline
  3. Beginner addon authors - those who are looking to build simple shared UI components, methods, or wrappers for existing npm libraries
  4. Advanced addon authors - those who dig into internals to make their addon work, or who are planning for broad extensibility

Table of Contents

Applying these User Personas to the CLI content, the following topics layout emerges. "Beginner" topics will include links to later "Advanced" topics, similar to how the Guides link to the API docs.

  • Introduction
    • how to install ember cli
    • a very simple, short definition of what it is (the official way to create, build, and test an Ember app)
    • Why is the CLI needed
    • Guidance on learning path
    • How to contribute
  • Basic use (explain options of each)
    • CLI Commands: Explain how to use the help command and common commands like ember new, ember server, ember generate, etc. Each is explained briefly, together with an example usage and a link to the Main Ember Guides with more information about how to use those files.
    • How to find and use addons
    • How to use npm packages
    • Installation and Upgrading the CLI (including a note about upgrading your app, with a link to more resources)
    • feature flags & configurations
  • Advanced use
    • shims
    • broccoli
    • custom blueprints
    • CSS compilation
    • Using another testing library
    • more on dependencies
    • more configurations
  • Writing Addons
    • Overview
    • Tutorial: Creating a standalone addon and an in-repo addon,
    • Using the dummy app
    • Including assets
    • Configuration
    • Nested addons
    • Testing your addon
    • Sharing your addon (deploying)
  • API Documentation
    • brief description of the target audience and a link

Versioning

Only one version of the documentation will be deployed and maintained. The documentation app itself will have clear releases as major changes are made, so that users working on older apps can still go back in time if they need to.

The url will contain /release/ so that if versioning is needed in the future, the option is available.

Transition and legacy links

While the project is in development, it will be worked on as a separate site, and the main site, https://ember-cli.com will remain in place.

Legacy links should be maintained because deprecating the links would cause SEO problems. Consensus seems to be that the best option is to create individualized redirects from pages within https://ember-cli.com to the new site.

Upon reaching feature parity, https://ember-cli.com will redirect to the new site. Ultimately, content will be hosted at https://cli.emberjs.com/. This improves the SEO of our emberjs domain.

Application architecture

The application architecture will follow similar patterns as other Middleman apps that have been successfully turned into Ember apps. Some examples of past conversions are:

Chris Manson has a project in development that automates the creation of documentation apps, integrating the lessons learned from these past conversions. Early results are looking great!

The resulting app will make use of typography and UI assets from ember-styleguide

Although only one version will be deployed/maintained for the forseeable future, the URL structure will allow for future growth, i.e. https://cli.emberjs.com/release/some-topic

Maintaining content

With module unification and tree shaking refactors underway, there may be some big changes to Ember's file structure. There are a few ways to mitigate this, while still maintaining only one version of these guides:

  1. Whenever possible, the CLI guides should link to the Ember Guides. The details of file layout and syntax are best handled in a resource that is versioned.
  2. The CLI guides can also frequently give a nod to past configurations/features. A url checker will make sure that these "legacy" resource links still exist. The pace of major version releases is slow enough that this should be sustainable.
  3. As mentioned earlier, the urls for the cli guides will include /release/ in case future versioning is needed

Members of both the Learning Core Team and Ember CLI Core team will have merge access.

How we teach this

Overall, bringing the CLI docs content up to speed and making it more maintainable should result in better integration of the CLI documentation into the Guides. The current content is out of date, and so it is not frequently linked.

The impact to new users will be a better experience. Existing Ember users may have an adjustment period to learn the new layout, but the current layout is confusing, so we believe there will be net improvement from day one. The addition of search tools will help with the transition.

Links in the Guides will need to be updated to point to the new documentation app. There are 41 links to the current ember-cli website, but only a handful are unique.

The Ember CLI website is not referenced in the API docs.

Drawbacks

Some potential drawbacks include:

  • Old bookmarks will still point to old content, and it is significant engineering effort to maintain those legacy links
  • Users may be used to finding content in a particular place
  • Some existing content will be deemphasized or removed
  • It's another app to keep in step with the main website

Alternatives

An alternative is to refactor the content in place. This will be more time consuming, and will not achieve a consistent narrative voice or cumulative learning experience.

  • Start Date: 2016-02-11
  • RFC PR: emberjs/rfcs#120
  • Ember Issue: https://github.com/emberjs/ember.js/pull/13016

Summary

This RFC proposes replacing the existing Route#serialize method with an equivalent method on the options hash passed into this.route within Router#map. The primary goal here is to enable asynchronous Engines by decoupling information about how to link to a route from the actual route object.

Motivation

As we move towards an increasingly asynchronous world with Engines, we need to separate knowledge about how to link to a route and how to enter a route. Linking to a route should be able to happen before a route object is instantiated, which is the behavior needed to asynchronously load Engines. However, in our current reality, these concerns are coupled and a route object must be instantiated before being able to link to or enter a route.

By separating these concerns, we can preemptively load the information on how to link to a route without also requiring all the knowledge of how to enter that route. This would be beneficial in both the asynchronous and synchronous worlds by allowing us to defer work.

Since the serialize method is the only method currently used by the Route class to define how to link to a route, the proposal is to extract this method into the space which currently contains the other linking information (e.g., the Router's map).

Note: this separation of concerns will also need to be implemented in router.js for the preemptive loading proposed here to actually work, but we can prepare for that future world by creating a separation of concerns within application code now.

Detailed Design

Since the current API is a simple function, the new hash option will also be a simple function that mirrors the signature of the original. Here's an example:

// app/router.js
function serializePostRoute(model, params) {
  // serialize the model into the dynamic paths
}

export default Router.map(function() {
  this.route('post', { path: '/post/:id', serialize: serializePostRoute });
});

Preserving the current function signature means that refactoring existing code should be simple in most cases. Here's the example currently given in the Ember docs (updated to reflect Ember-CLI):

// app/routes/post.js
import Ember from 'ember';
export default Ember.Route.extend({
  model(params) {
    // the server returns `{ id: 12 }`
    return Ember.$.getJSON('/posts/' + params.post_id);
  },

  serialize(model) {
    // this will make the URL `/posts/12`
    return { post_id: model.id };
  }
});

// app/router.js
export default Router.map(function() {
  this.route('post', {
    path: '/post/:id'
  });
});

Here is that same code refactored for the proposal:

// app/routes/post.js
import Ember from 'ember';
export default Ember.Route.extend({
  model(params) {
    // the server returns `{ id: 12 }`
    return Ember.$.getJSON('/posts/' + params.post_id);
  }
});

// app/router.js
function serializePostRoute(model) {
  // this will make the URL `/posts/12`
  return { post_id: model.id };
}

export default Router.map(function() {
  this.route('post', { path: '/post/:id', serialize: serializePostRoute });
});

Migration Plan

Even though the refactoring needed here is easy, we still need a clear (though simple) migration plan.

The first step will be to introduce the new option into the Router's callback route function. Once that is done, we can deprecate Route#serialize over the remainder of the 2.x series to give developers the time to update their code base. We can then remove support in 3.x.

As noted in the "Motivation" section, there is still work to be done in router.js in order to support this separation of concerns. Due to this, the initial implementation of this new option will essentially be a polyfill that proxies to the corresponding Route#serialize property internally. This will set us up for an internal migration at a later point to actually separate the two; this, however, should not affect developers as it will be internal.

Pedagogy (How We Teach This)

Once the new option is introduced, the Ember guides will need to be updated to reflect this. Those changes should be relatively straightforward as shown in the example above. This will help introduce the feature to new users and those users that haven't used Route#serialize before. Since inline serializers in the router map can be distracting to understanding the general layout of a codebase, we should teach them as defined outside the map itself (as in the code example in this RFC).

For existing users, we can introduce this feature through deprecation warnings (as mentioned above). The deprecations should briefly introduce the new option and point to an appropriate deprecation guide that explains how to migrate.

Drawbacks

  • Adds another option to the Router map. Though this is largely mitigated due to the fact that this feature is not in wide use currently.
  • Can be sort of ugly to format.

Alternatives

  • Introduce a standalone module to represent the Route#serialize. This was the first proposal of this RFC and there is much opposition to introducing yet another construct for Ember-CLI and developers to manage. The approach proposed above avoids this major drawback.
  • Introduce a holistic construct to represent route linking information. Instead of introducing a new option as a function, we could introduce a class that would represent all the information needed to link to a route. Since there is not currently much other information needed, this seems overkill and would suffer similar opposition as the first alternative.
  • Don't do this and continue loading and instantiating all route information upfront. This prevents us from improving performance by keeping concerns coupled with prevents introducing async engines. It also requires all Route classes be instantiaed upfront.

Unresolved Questions

  • Do we wish to apply a similar approach for default query params? And if so, do we wish to incorporate that approach into this new construct?
  • Start Date: 2018-08-13
  • Relevant Team(s): Ember CLI
  • RFC PR: #121

Summary

Remove https://github.com/ember-cli/ember-cli-eslint from projects generated by ember-cli.

ember-cli-eslint is an addon designed to show lint errors during test runs. Tooling around eslint has improved enough where this feature may no longer be necessary.

To be clear, the proposal is not to remove linting in tests. It is to follow the rest of JavaScript community and follow the standard tooling process.

There are multiple ways to run eslint:

  1. Integration with editors
  2. Utilize precommit hooks with eslint
  3. Support a standard way to run eslint (such as yarn lint:js)

We can also discuss configuring testem to automatically run eslint as part of yarn test

Motivation

  1. Improve our build speed
  2. Simplicity. eslint is common among JS stack, and integrations with editors / precommit-hooks are ubiquitous. Removing this layer of abstraction will simplify how eslint is used throughout ember-cli. Most editors have plugins available for eslint, and as long as the .eslint.rc is not removed, we should still see the benefits of eslint in our Ember projects.
  3. Hacks required to support features such as PR #122 broccoli-lint-eslint

Detailed design

  1. Change blueprint to pull in eslint as opposed to ember-cli-eslint under devDependencies.
  2. Provide documentation on eslint and editor integration as well as precommit hooks

Redefine npm test or yarn test (depending on whether the --yarn option was used to create project) to

ember test && npm run lint:js && npm run lint:hbs

and

ember test && yarn lint:js && yarn lint:hbs

How We Teach This

Providing documentation regarding how to run linting should suffice as well as documentation to editor integration.

Deleting abstractions and going towards a explicit path, eslint within the ember-cli ecosystem becomes easier to teach.

Drawbacks

  1. No console warnings during builds
  2. lint failures are no longer included in browser tests

Alternatives

  1. Leave ember-cli-eslint alone

Unresolved questions

N/A

  • Start Date: 2016-04-16
  • RFC PR: https://github.com/emberjs/rfcs/pull/136
  • Ember Issue: https://github.com/emberjs/ember.js/pull/13553

Summary

contains is implemented on Ember.Array, but [contains was renamed to includes in 2014] (https://github.com/tc39/Array.prototype.includes/commit/4b6b9534582cb7991daea3980c26a34af0e76c6c)

  • this proposal is for contains to be deprecated in favour of an includes method on Ember.Array

Motivation

Motivation is to stay in line with web standards

Detailed design

First, implement includes polyfill in compliance with includes spec. Polyfill sample from MDN is:

if (!Array.prototype.includes) {
  Array.prototype.includes = function(searchElement /*, fromIndex*/ ) {
    'use strict';
    var O = Object(this);
    var len = parseInt(O.length) || 0;
    if (len === 0) {
      return false;
    }
    var n = parseInt(arguments[1]) || 0;
    var k;
    if (n >= 0) {
      k = n;
    } else {
      k = len + n;
      if (k < 0) {k = 0;}
    }
    var currentElement;
    while (k < len) {
      currentElement = O[k];
      if (searchElement === currentElement ||
         (searchElement !== searchElement && currentElement !== currentElement)) { // NaN !== NaN
        return true;
      }
      k++;
    }
    return false;
  };
}

Then, alias contains to includes with deprecation warning, deprecate in line with standard deprecation process. I don't believe that adding the additional parameter will have any affect on existing usage of contains.

How We Teach This

  • Update any references in docs and guides to includes
  • Write a deprecation guide, mentioning any edge cases where the new includes behaves differently to contains, and giving migration examples
  • Indicate in api docs that this is a polyfill

Drawbacks

Alternatives

Keep current methods

Unresolved questions

None

Summary

Introduce Ember.String.isHtmlSafe() to provide a reliable way to determine if an object is an "html safe string", i.e. was it created with Ember.String.htmlSafe().

Motivation

Using new Ember.Handlebars.SafeString() is slated for deprecation. Many people are currently using the following snippet as a mechanism of type checking: value instanceof Ember.Handlebars.SafeString. Providing isHtmlSafe offers a cleaner method of detection. Beyond that, the aforementioned test is a bit leaky. It requires the developer to understand htmlSafe returns a Ember.Handlerbars.SafeString instance and thus limits Ember's ability to change htmlSafe without further breaking it's API.

Based on our app at Batterii and some research on Github, I see two valid use cases for introducing this API.

First, and most commonly, is to make it possible to test addon helpers that are expected to return a safe string. I believe this test on ember-i18n says it all: "returns HTML-safe string".

The second use case is to do type checking. In our app, we have an isString utility that is effectively:

import Ember from 'ember';

export default function(value) {
  return typeof value === 'string' || value instanceof Ember.Handlebars.SafeString;
}

Newer versions of ember-i18n, doing this.get('i18n').t('someTranslatedValue') will return a safe string. Thus our isString utility has to consider that.

Detailed design

isHtmlSafe will be added to the Ember.String module. The implementation will basically be:

function isHtmlSafe(str) {
  return str && typeof str.toHTML === 'function';
}

It will be used as follows:

if (Ember.String.isHtmlSafe(str)) {
  str = str.toString();
}

Transition Path

As part of landing isHtmlSafe we will simultaneously re-deprecate Ember.Handlebars.SafeString. This deprecation will take care to ensure that str instanceof Ember.Handlebars.SafeString still passes so that we can continue to maintain backwards compatibility.

Additionally, a polyfill will be implemented to help provide forward compatibility for addon maintainers and others looking to get a head while still on older versions of Ember. Similar to ember-getowner-polyfill.

How We Teach This

I think we'll continue to refer to these strings as "html safe strings". This RFC does not introduce any new concepts, rather it builds on an existing concept.

I don't believe this feature will require guide discussion. I think API Docs will suffice.

The concept of type checking is a pretty common programming idiom. It should be relatively self explanatory.

Drawbacks

The only drawback I see is that it expands the surface of the API and it takes a step towards prompting "html safe string" as a thing.

Alternatives

An alternative would be to expose Ember.Handlerbars.SafeString publicly once again. Users could revert back to using instanceof as their type checking mechanism.

Unresolved questions

There are no unresolved questions at this time.

  • Start Date: 2016-05-09
  • Relevant Team(s): Ember.js
  • RFC PR: https://github.com/emberjs/rfcs/pull/143
  • Tracking: https://github.com/emberjs/rfc-tracking/issues/27 / https://github.com/emberjs/ember.js/issues/14882

Note: This RFC replaces the closely related RFC for Module Normalization. As discussed in the Alternatives section below, many concepts are shared between the two proposals, but there is also a fundamental difference.

Summary

Create a unified pattern for organizing and naming modules in Ember projects that is deterministic, extensible, and ergonomic.

Motivation

Ember CLI's conventions for project layouts and file naming are central to every Ember developer's experience. It's crucial to get both the technical and ergonomic details right.

The existing conventions used by Ember CLI have evolved gradually and organically over the years. Ember CLI and its predecessor Ember App Kit were early adopters of ES modules and have always leveraged strong conventions to deduce an understanding of modules based on their locations. Ember CLI's resolver encodes those conventions to enable run-time module resolutions.

The current system works fairly well, but has some complexities and inconsistencies that both steepen its learning curve and limit its technical potential.

Drawbacks include:

  • Confusion over which of two orthogonal approaches to use for organizing modules:

    • classic - modules are organized at the top-level by "type" (components, templates, etc.) and then by namespace and name.

    • pods - modules are organized by namespace, then name, then type.

  • Addons define modules to be merged into an application through a special app directory. These public modules are typically private modules that are imported and re-exported, which introduces an extra module per export and an extra level of abstraction to learn.

  • Because addons' modules are mixed into an application, there's the possibility of naming collisions between two addons or an addon and its consuming application.

  • Modules don't have a clear sense of "locality", which prevents the ability to declare modules that are available only in a "local" namespace (this as-yet unsupported feature has been called "local lookup").

  • Resolution rules that are declared only in JavaScript are difficult to analyze and optimize.

  • Module resolution is inefficient due to the number of potential places to lookup a particular module by name.

Recognizing these drawbacks, the Core Team compiled a set of design constraints for a rethink of Ember's approach to modules:

  1. Reasonable branching factor. Users should see a reasonable number of items at any given level in their hierarchy. Flattening out too much results in an unreasonably large number of items.
  2. No slashes in component names. The existing system allows this, but we don't want to support invocation of nested components in Glimmer Components.
  3. Addons need to participate in the naming scheme, most likely with namespace prefix and double-colon separator.
  4. Subtrees should be relocatable. If you move a directory to a new place in the tree, its internal structure should all still work.
  5. There should be no cognitive overhead when adding a new thing. The right way should be obvious and not impose unnecessary decisions.
  6. We need clean separation between the namespace of the user's own components, helpers, routes, etc and the framework's own type names ("component", "helper", etc) so that we can disambiguate them and add future ones.
  7. Ideally we will have a place to put tests and styles alongside corresponding components.
  8. Local relative lookup for components and helpers needs to work.
  9. Avoid the "titlebar problem", in which many files are all named "component.js" and you can't tell them apart in your editor.
  10. The resolver should be configured via declarative rules, not imperative JavaScript. In addition to enforcing consistency, this allows addons to augment the system with their own types in a predictable way.
  11. Module structures must be statically analyzable at build time to enable efficiency optimizations.
  12. Module classifications must be extensible and allow for customizations by apps, engines, and addons.

Note: Constraints > 9 were added based on discussions subsequent to the initial meeting.

This proposal attempts to address these constraints with a single consistent approach to modules that will make Ember easier to use and learn and improve the efficiency of Ember's resolver at run-time.

Detailed Design

This proposal introduces a new top-level directory, src, and establishes conventions for organizing modules within it. Also proposed is a refactor of the Ember resolver to enable efficient and flexible resolutions based upon the new module conventions.

The src directory will be used to contain the core ES modules within an Ember CLI project, whether that project contains an application, addon, or engine. To maintain backward compatibility, the src directory will be allowed to co-exist alongside existing app and/or addon directories, although these directories should eventually be deprecated.

Examples

Let's start by taking a look at some examples of Ember projects organized according to the proposed conventions.

Example Application

A simple blogging application could be structured as follows:

src
├── data
│   ├── models
│   │   ├── comment
│   │   │   ├── adapter.js
│   │   │   ├── model.js
│   │   │   └── serializer.js
│   │   ├── post
│   │   │   ├── adapter.js
│   │   │   ├── model.js
│   │   │   └── serializer.js
│   │   └── author.js
│   └── transforms
│       └── date.js
├── init
│   ├── initializers
│   │   └── i18n.js
│   └── instance-initializers
│       └── auth.js
├── services
│   └── auth.js
├── ui
│   ├── components
│   │   ├── date-picker
│   │   │   ├── component.js
│   │   │   └── template.hbs
│   │   └── list-paginator
│   │       ├── paginator-control
│   │       │   ├── component.js
│   │       │   └── template.hbs
│   │       ├── component.js
│   │       └── template.js
│   ├── partials
│   │   └── footer.hbs
│   ├── routes
│   │   ├── application
│   │   │   └── template.hbs
│   │   ├── index
│   │   │   ├── controller.js
│   │   │   ├── route.js
│   │   │   └── template.hbs
│   │   └── posts
│   │       ├── -components
│   │       │   ├── -utils
│   │       │   │   └── strings.js
│   │       │   ├── capitalize.js
│   │       │   └── titleize.js
│   │       ├── post
│   │       │   ├── -components
│   │       │   │   └── post-viewer
│   │       │   │       ├── component.js
│   │       │   │       └── template.hbs
│   │       │   ├── edit
│   │       │   │   ├── -components
│   │       │   │   │   ├── post-editor
│   │       │   │   │   │   ├── post-editor-button
│   │       │   │   │   │   │   ├── component.js
│   │       │   │   │   │   │   └── template.hbs
│   │       │   │   │   │   ├── calculate-post-title.js
│   │       │   │   │   │   ├── component.js
│   │       │   │   │   │   └── template.hbs
│   │       │   │   │   ├── route.js
│   │       │   │   │   └── template.hbs
│   │       │   │   ├── route.js
│   │       │   │   └── template.hbs
│   │       │   ├── route.js
│   │       │   └── template.hbs
│   │       ├── route.js
│   │       └── template.hbs
│   ├── styles
│   │   └── app.scss
│   └── index.html
├── utils
│   └── md5.js
├── main.js
└── router.js

Example Engine

An engine could provide the same blogging functionality with almost entirely the same module structure as the example blog application. Only the following notable changes would be needed:

  • An engine should declare its routes in src/routes.js instead of src/router.js
  • An engine would require a dummy app within tests
  • An engine should export an Engine instead of an Application from src/main.js

Example Addon

Here's how the ember-power-select addon could be restructured:

src
├── styles
│   └── ember-power-select.scss
├── ui
│   └── components
│       ├── main
│       │   ├── before-options
│       │   │   ├── component.js
│       │   │   └── template.hbs
│       │   ├── options
│       │   │   ├── component.js
│       │   │   └── template.hbs
│       │   ├── trigger
│       │   │   ├── component.js
│       │   │   └── template.hbs
│       │   ├── component.js
│       │   └── template.hbs
│       ├── multiple
│       │   ├── trigger
│       │   │   ├── component.js
│       │   │   └── template.hbs
│       │   ├── component.js
│       │   └── template.hbs
│       └── is-selected.js
└── main.js

Migration Tool

As a proof of concept for the module layout described in this RFC, Robert Jackson has created a migration tool and used it to migrate the following repos:

You can also use Robert's migration tool on your own projects to gain a feel for how this RFC will affect your work.

ES Modules

It's important to understand how ES module paths are mapped from the file system so that you can import modules from elsewhere in your project and its associated dependencies.

ES module paths will be formed from a project's package name followed by a direct mapping of file paths from the project root. The file's final extension (e.g. js or hbs) will be excluded because all ES modules will of course be compiled into JavaScript from their original format.

For example, the file src/ui/components/date-picker.js in the my-calendar app will be exported with the module path my-calendar/src/ui/components/date-picker.

An application and its associated addons and engines will all be merged into the same ES module space, as is done today. Any module can import from any other module within this space, although cross-package imports should be done with care.

Module Naming and Organization

This section describes the conventions proposed for naming and organizing a project's modules within src. These conventions will allow Ember CLI's resolver to determine the purpose of each module at run-time. They will also enable static analysis of modules to lint against errors and to prepare a normalized map for efficient resolutions.

Every resolvable module must have both a name and a type. The type typically corresponds to the base class of the module's default export (e.g. route, template, etc.).

Modules can be grouped together with other modules of related types in "collections". Collections are directories with type-aware resolution rules which allow related modules to share a namespace. For example, the models collection contains models, adapters, and serializers.

Collections that are related to each other can be further organized in "group" directories. For example, the ui group contains the components, partials, and routes collections.

Ember CLI will have a build step that normalizes modules to a common form and builds a mapping between that form and the ES module path described above. While building this normalized map, the build must error and provide useful messages if any module naming errors are detected. Unregistered collections and types should not be allowed. Also, the same normalized module path must not be repeated through alternative naming forms.

Module Type

The type of a module can be determined through the following file naming and module export rules:

  1. src/${type} - typed modules named main (explained further below), in which default exports match the type specified by the file name.
  2. src/${collection}/${namespace}/${name}/${type} - expanded collection modules, in which default exports match the type specified by the file name.
  3. src/${collection}/${namespace}/${name} - in which type can be inferred based on the module's exports. Default exports must match the default type for the collection. If there is no default export, named exports will be scanned for a matching type allowed in the collection.

Note that template precompilers will need to use default vs. named exports appropriately in order to satisfy the expectations of Rules 2 and 3.

Here are a few example applications of the module type determination rules:

// Rule 1

src/router (with `export default Ember.Router.extend()`)
=> name: 'main',
   type: 'router'

// Rule 2

src/ui/routes/posts/post/route.js (with `export default Ember.Route.extend()`)
=> collection: 'ui/routes',
   namespace: 'posts',
   name: 'post',
   type: 'route'

src/ui/routes/posts/post/template (with `export default Ember.HTMLBars.template(COMPILED)`)
=> collection: 'ui/routes',
   namespace: 'posts',
   name: 'post',
   type: 'template'

// Rule 3

src/data/models/author (with `export default DS.Model.extend()`)
=> collection: 'data/models',
   name: 'author',
   type: 'model' (the default type for the models collection)

src/ui/components/titleize (with `export let helper = Ember.Helper.helper(function() { })`)
=> collection: 'ui/components',
   name: 'titleize',
   type: 'helper'

src/ui/components/show-title (with `export let template = Ember.HTMLBars.template(COMPILED)`)
=> collection: 'ui/components',
   name: 'show-title',
   type: 'template'

Main Modules

Every project must have a "main" module, named src/main.js, that serves as an entry point into the project.

The main module must export an Application, Engine, or (new) Addon class. This class must define a modulePrefix, which must match the node package name for the project.

The main module also declares other properties that help the Ember resolver understand relationships between projects. For instance, the main module can declare which modules in an addon are available to a consuming app's resolver.

The main module of an addon can also declare a rootName, which is used by the resolver to lookup main modules. Initially, the rootName will be a read-only property that equals the modulePrefix with any ember- and ember-cli- prefixes stripped (e.g. ember-power-select becomes power-select). It's possible that we may allow overrides / aliases in the future.

Modules that appear alongside main.js in src are also considered main modules for their respective type. For instance, src/router.js is registered with a name of main and a type of router.

Module Collections

Top-level namespaces within src serve to group modules into type-aware "collections".

The following rules apply to module collections and types:

  1. Each collection can contain one or more types. The types allowed in a particular collection MUST be explicitly declared.
  2. Each type MAY exist in any number of collections.
  3. Each type MUST have only one "definitive collection", which is the collection the resolver will use for resolutions if a module can't be found in the local (i.e. originating) collection.
  4. Each collection MAY have a single "default type". If a module does not indicate its type through its file name, then its default export should align with the default type for its collection.
  5. Each collection can allow "private collections" to be defined at a namespace. Private collections are localized additions to a top-level collection, available only from the namespace at which they're defined.
  6. Top-level collections may be grouped for organization purposes. No resolvable modules must be placed in a group directory.
  7. A collection can appear only once in a project (i.e. it can not be contained in multiple group directories, or in a group as well as at the top-level).

The following collections and allowed types (rules 1 & 2) are proposed:

  • components - COMPONENT, HELPER, template
  • initializers - INITIALIZER
  • instance-initializers - INSTANCE-INITIALIZER
  • models - MODEL, ADAPTER, SERIALIZER
  • partials - PARTIAL
  • routes - ROUTE, CONTROLLER, template
  • services - SERVICE
  • transforms - TRANSFORM
  • utils - UTIL

Note: ALL CAPS indicates which collections are definitive (rule 3) for a type.

The following default types are proposed for collections (rule 4):

  • components - component
  • initializer - initializer
  • instance-initializers - instance-initializer
  • models - model
  • partials - partial
  • routes - route
  • services - service
  • transforms - transform
  • utils - util

The following private collections are allowed within collections (rule 5):

  • components - utils
  • models - utils
  • initializers - utils
  • instance-initializers - utils
  • routes - components, utils
  • services - utils
  • transforms - utils

The following groups are proposed for collections (rule 6):

  • data - models, transforms
  • init - initializers, instance-initializers
  • ui - components, partials, routes

The collection and type system is designed to be extensible, so that addons can contribute their own collections and types. The data collection and its corresponding types should be defined in ember-data. Liquid-fire might want to define an animations collection and a transition type, and expand routes to allow animations as a private collection.

The specific format of collection and type declarations for addons is TBD.

"Components" Collection

This proposal broadens the scope of the term "component" to include all template-invocable parts of Ember. This includes today's components and helpers, and the future implementation of "glimmer components" (with angle brackets) and element modifiers.

Grouping template-invocable elements together in a single collection recognizes that they already coexist in the same namespace. After all, only one helper OR component can be invoked as {{foo-bar}}. Using a common collection will not only simplify file management and searching, it will also provide implicit linting against creating a helper and class-based component of the same name.

Private Collections

You may wish to make a component available in a particular template without polluting the top-level components collection with a more local concern. Private collections allow you to augment a top-level collection's contents for use at a particular namespace.

Private collections are declared as a directory sharing the name of the top-level collection, prefixed with a -. So the top-level routes collection could be augmented via a private -components collection.

Say that you want to define a post-viewer component to be available only from within src/ui/routes/posts/post/template.hbs. You could achieve this by creating your component module in src/ui/routes/posts/post/-components/post-viewer.js.

Non-resolved Files

The rules above apply to modules that are resolved, namely *.js and *.hbs files. Other files that are used for documenting code, such as *.md and *.html files, can be freely co-located in any directories.

Conventions will still be used for non-resolved files that have significance within an Ember project, including:

  • src/ui/styles - A project's stylesheets.
  • src/ui/index.html - A project's html container.

Packages

In-repo addons (including engines) will be placed in a new top-level packages directory (a sibling of src). We can begin to use the term "packages" instead of the rather clumsy "in-repo addons". This differentiation will emphasize that packages are internal and addons are external to a project. Packages should be seen as a lightweight way to add new namespacing within a project without the overhead of a full addon.

The packages directory will provide a separate space away from other library modules that might be kept in lib, the current directory used for in-repo addons. Introducing a new top-level directory will allow a clear migration path for in-repo addons, in the same way that there's a clear migration path from app to src.

Inside packages, packages should be grouped by name. Each package can have its own index.js, package.json, and src directory.

Ember Resolver Refactor

The Ember resolver must be refactored significantly to be made aware of the new src and packages directories and associated conventions.

Module Normalization

As discussed above, Ember CLI will perform a normalization process for all the modules in a project and its associated projects. The normalization step will involve the construction of a map from each module's normalized form to its corresponding ES module path. If any conflicts are detected, the process should error and notify the developer.

The Ember resolver will only look up modules in their normalized form, utilizing the pre-built normalization map to resolve the actual module path.

Addon modules

A resolver will only implicitly consider an addon's top-level modules named main (e.g. a main component) to be public and available for resolution. More explicit control over an addon's public modules can be declared in the addon's main module (details TBD). An addon's public modules will all be resolvable at the rootName of the addon (see above).

Public components and helpers can be invoked in templates using the rootName as a namespace. For modules named main, the bare root name will suffice.

Let's say that the ember-power-select addon has a rootName of power-select and a top-level main component declared in src/ui/components/main.js. An app could invoke this component in a template as {{power-select::main}} or more simply as {{power-select}}.

Addons should use the same namespacing that will be used by consuming apps when invoking their own components and helpers from templates. For instance, if the ember-power-select addon has a date-picker component that invokes multiple main components, it should also invoke them in a template as {{power-select::main}} or more simply as {{power-select}}.

Module Resolutions

Module resolution rules must account for the following:

  • The requested module's type, name, and (potentially) namespace.
  • (Optional) A "source" rootName, collection, and namespace from which the lookup originates.
  • (Optional) An "associated type" for lookups that should start in a collection that is not definitive for the requested type.

Module resolutions occur in the following order:

  1. Local - If a source module is specified and the requested type is allowed in the source module's collection, look in a namespace based on the source module's namespace + name.
  2. Private - If a source module is specified, look in a private collection at the source module's namespace, if one exists that is definitive for the requested type.
  3. Associated - If an associated type is specified, look in the definitive collection for that associated type. Only resolve if the collection can contain the requested type.
  4. Top-level - In the definitive collection for the requested type, defined at its top-level.

The resolver must maintain mappings of modules at multiple levels to make these resolutions efficient. A lookup tree can be pre-built for production builds.

Example Resolutions

Let's walk through some example resolutions from the above blogging app paired with the ember-power-select addon. We'll assume that the package name for the app is blogmeister, and the package name for the addon is ember-power-select. The addon has a rootName of power-select for cleaner references.


From blogmeister/src/ui/components/list-paginator/template:

{{paginator-control}} resolves to blogmeister/src/ui/components/list-paginator/paginator-control/component

{{date-picker}} resolves to blogmeister/src/ui/components/date-picker/component

{{power-select}} resolves to ember-power-select/src/ui/components/main/component

{{power-select::multiple}} resolves to ember-power-select/src/ui/components/multiple/component


From blogmeister/src/routes/posts/post/template:

{{post-viewer}} resolves to blogmeister/src/ui/routes/posts/post/-components/post-viewer/component

{{date-picker}} resolves to blogmeister/src/ui/components/date-picker/component

{{power-select}} resolves to ember-power-select/src/ui/components/main/component

Other Refactorings

Generators and Blueprints

Generators and blueprints will need to be made aware of the new module conventions.

Let's take a look at the files that some generators will create (note: tests have been left out of these examples for now):

ember g component date-picker:

  • src/ui/components/date-picker/component.js
  • src/ui/components/date-picker/template.hbs

ember g component ui/routes/posts/post-editor:

  • src/ui/routes/posts/-components/post-editor/component.js
  • src/ui/routes/posts/-components/post-editor/template.hbs

ember g helper titleize:

  • src/ui/components/titleize.js

How We Teach This

The Ember guides will need to be updated significantly to reflect the new conventions.

Teaching Conventions through Tooling

As discussed above, generators and blueprints will be made aware of the new module conventions. This will help new projects start on track and stay on track as modules are added.

Developers with existing projects will be able to use Robert Jackson's migration tool to move their projects over to use the new conventions. This tool is a WIP and will continue to be refined to work well with both the classic and pods structures. It's possible these migration capabilities will eventually be rolled into Ember Watson.

Furthermore, the Ember Inspector should be enhanced to understand the new conventions and become more type and collection aware.

New Concepts

It will be important for both new and experienced Ember developers to understand some core concepts that are proposed in this RFC.

Collections and Types

This proposal's concept of collections and types should feel familiar enough to users of both the classic and pods layouts to enable a smooth transition. In many ways, this proposal merges the classic and pods layouts into a single uniform layout.

The core driver to collections is to store "like with like". However, instead of the classic layout's narrow definition of "like" to be of a single type, this proposal takes the pods approach that multiple types can be related. A good test of whether multiple module types should be stored together is whether they should be considered to share a common namespace. Routes, controllers, and templates are a good example, as are models, adapters, and serializers.

A related concept to understand about collections is the notion of a default type. Every top-level module within a collection can be considered to match its default type (unless named exports are used in those modules to represent types other than the default). Within a collection's namespaces, every module must be either that default type or related to it. It's helpful to consider that every namespace within a collection represents a set of named module exports, and that the default type represents the default export for that collection.

Here's an illustration of exports from a collection:

src
  data
    models
      author.js <- exports an Author `model`, the default type in the `models` collection
      comment
        adapter.js     <- exports a Comment `adapter`
        model.js       <- exports a Comment `model`
        serializer.js  <- exports a Comment `serializer`

Components

The term "component" has been widely adopted across most front-end frameworks to describe a broad swath of UI concerns. Using the same term for the collection of template-invocable UI elements will lower the learning curve for developers who are new to Ember, while allowing for a useful set of specialized terms to flourish to describe particular types of components.

We've already started down the road of component specialization by introducing the concept of "routable components". Once we start actually using "routable components" in practice, it will become necessary to refer to plain old components as something more specific, like "template components". And this distinction will probably lead to plain old helpers being referred to as "template helpers". Other concepts, such as "Glimmer components" and "template component modifiers" will soon be mixed in. We will end up with a multi-faceted toolbox available at the template layer which deserves a simple name that matches developer expectations. The general term "components" seems a good fit.

Scope

Developers should understand the available levels of module scope, as well as when each is appropriate to use. Scope should be considered when modules are generated, and developers should feel free to move modules if they expand or contract in scope.

The following levels of scope should be understood:

  • Private - private collections should be used when a component or utility function is needed from a single namespace.

  • Project - top-level, project-wide collections should be used for modules that are needed throughout a project.

  • Local package - namespaced collections can be useful to group a common set of cross-cutting concerns within a project.

  • Local engine - a type of local package that encapsulates a set of functionality that benefits from run-time isolation and strict dependency sharing.

Testing

Unit, integration, and some acceptance tests can now be co-located with their associated modules. Co-location should be encouraged because it makes test modules easier to locate in the file system, and easier to move if a module's scope changes.

Robert Jackson plans to adapt the Grand Testing Unification RFC to illustrate test co-location and to introduce module types for tests.

Drawbacks

Any change to a pattern as fundamental as file naming will incur some mental friction for developers who are accustomed to the current conventions. It is hoped that tooling like Robert's migrator and Ember Watson can lessen this friction by automating transitions, and that updated guides, generators, and blueprints can make these conventions easy to follow.

Of course, we won't prevent usage of the currently used patterns for some time, but they will eventually be deprecated. Some efficiencies, especially in the resolver, may not be fully realized until the new patterns are used throughout a project.

Alternatives

The Module Normalization RFC

Perhaps the most prominent alternative that has been explored is the Module Normalization RFC. Module Unification shares many aspects with Module Normalization, but with one fundamental difference: buckets in Module Normalization are normalized away for the resolver, while collections in Module Unification play an important role in module resolution.

The Ember Core Team decided that the sleight of hand required to allow buckets to be used for organization only, and not for resolution, could create confusion. Essentially, modules could conflict across buckets, because they could have matching namespaces, names, and types. This kind of conflict could not be allowed, so developers would need to understand too much about the resolution strategy to make it ergonomic.

Other Alternatives

A large number of other alternatives have been explored before settling on this recommendation. Feel free to explore the history of any of the linked gists to understand some of the subtle alternatives.

Of course, one alternative is to simply not change anything and accept the drawbacks discussed in the Motivation section above. However, even if we accept inefficiencies in our resolver and confusion over divergent file structuring strategies, we still need to solve the "local lookup" problem, which does not have a clean solution in today's module system.

Unresolved questions

How should tests be co-located in src?

Should tests be allowed within src via *-test types (e.g. component-integration-test, component-unit-test, etc.) within respective collections?

If this RFC is approved, then Robert Jackson plans to adapt the Grand Testing Unification RFC to propose answers to these questions.

What about routable components?

Should routable components have a type that's unique from other components?

Should they exist alongside route and template types in the routes collection?

It seems plausible that routable components could simply use the component type, and that we could lint against allowing template-invocable components alongside routes.

How should configuration declarations be made in the main module?

For example:

  • How should resolvable exports be declared from addons?
  • Can apps override the root names of addons? For example, if ember-power-select has a root name of power-select, could a consuming app override this?
  • How do addons and apps declare their collection and type exports? For example, how could liquid-fire allow for a transition type and an animations collection?

Should we allow collection groups?

Do the organizational benefits of collection groups outweigh the potential confusion over where lines are drawn between a group/collection/namespace when viewing a project structure.

  • Start Date: 2016-06-11
  • RFC PR: #150
  • Ember Issue: #14360

Summary

With the goal of making significant performance improvements and of adding public API to support use cases long-served by a private API, a new API of factoryFor will be added to ApplicationInstance instances.

Motivation

Ember's dependency injection container has long supported fetching a factory that will be created with any injections present. Using the private API that provided this support allows an instance of the factory to be created with initial values passed via create. For example:

// app/logger/main.js
import Ember from 'ember';

export default Ember.Logger.extend({
  someService: Ember.inject.service()
});
import Ember from 'ember';
const { Component, getOwner } = Ember;

export default Component.extend(
  init() {
    this._super(...arguments);
    let Factory = getOwner(this)._lookupFactory('logger:main');
    this.logger = Factory.create({ level: 'low' });
  }
});

In this API, the Factory is actually a subclass the original main logger class. When _lookupFactory is called, an additional extend takes place to add any injections (such as someService above). The class/object setup looks like this:

  • In the module: MyClass = Ember.Object.extend(
  • In _lookupFactory: MyFactoryWithInjections = MyClass.extend(
  • And when used: MyFactoryWithInjections.create(

The second call to extend implements Ember's owner/DI framework and permits someService to be resolved later. The "owner" object is merged into the new MyFactoryWithInjections class along with any registered injections.

This "double extend" (once at define time, once at _lookupFactory time) takes a toll on performance booting an app. This design flaw has motivated a desire to keep _lookupFactory private.

The MyFactoryWithInjections class also features as a cache. Because it is attached to the owner/container, it is cleared between test runs or application instances. To illustrate, this flow-chart shows how MyFactoryWithInjections diverges between tests:

               +-------------------------------+
               |                               |
               |      /app/models/foo.js       |
               |                               |
               +-------------------------------+
                               |
              first test run   |    nth test run
              +----------------+---------------+
              |                                |
              v                                v
   +---------------------+          +--------------------+
   |resolve('model:foo') |   ===    |resolve('model:foo')|
   +---------------------+          +--------------------+
              |                                |
              |                                |
              v                                v

     extend(injections)               extend(injections)

              |                                |
              |                                |
              |                                |
              v                                v
+--------------------------+     +---------------------------+
|lookupFactory('model:foo')| !== |lookupFactory('model:foo') |
+--------------------------+     +---------------------------+

Despite the design flaws in this API, it does fill a meaningful role in Ember's DI solution. Use of the private API is common. Some examples:

The goal of this RFC is to create a public API for fetching factories with better performance characteristics than _lookupFactory.

Detailed design

Throughout this document I reference Ember 2.12 as it is the next LTS at writing. This proposal may ship for 2.12-LTS or be bumped to the next LTS.

This feature will be added in these steps.

  1. In Ember introduce a ApplicationInstance#factoryFor based on _lookupFactory. It should be documented that certain behaviors inherent to "double extend" are not supported. In development builds and supporting browsers, wrap return values in a Proxy. The proxy should throw an error when any property besides create or class is accessed. class must return the registered factory, not the double extended factory.
  2. In the same release add a deprecation message to usage of _lookupFactory. As this API is intimate it must be maintained through at least one LTS release (2.12 at this writing).
  3. In 2.13 drop _lookupFactory and migrate the factoryFor implementation to avoid "double-extend" entirely.

Additionally, a polyfill will be released for this feature supporting prior versions of Ember.

Design of ApplicationInstance#factoryFor

A new API will be introduced. This API will return both the original base class registered into or resolved by the container, and will also return a function to generate a dependency-injected instance. For example:

import Ember from 'ember';
const { Component, getOwner } = Ember;

export default Component.extend(
  init() {
    this._super(...arguments);
    let factory = getOwner(this).factoryFor('logger:main');
    this.logger = factory.create({ level: 'low' });
  }
});

Unlike _lookupFactory, factoryFor will not return an extended class with DI applied. Instead it will return a factory object with two properties:

// factoryFor returns:
let {

  // a function taking an argument of initial properties passed to the object
  // and returning an instance
  create,

  // The class registered into (or resolved by) the container
  class

} = owner.factoryFor('type:name');

This API should meet two requirements of the use-cases described in "Motivation":

  • Because factoryFor only returns a create method and reference to the original class, its internal implementation can diverge away from the "double extend". A side-effect of this is that the class of an object instantiated via _lookupFactory(name).create() and factoryFor(name).create() may not be the same, given the same original factory.
  • The presence of class will make it easy to identify the base class of the factory at runtime.

For example today's _lookupFactory creates an inheritance structure like the following:

                    Current:
       +-------------------------------+
       |                               |
       |      /app/models/foo.js       |
       |                               |
       +-------------------------------+
                       |
                       |
                       |
                       v
            +--------------------+
            |  Class[model/Foo]  |
            +--------------------+
                       |
                       |
                       |
       first test run  |   nth test run
           +-----------+----------+
           |                      |
           |                      |
           |                      |
           v                      v
+--------------------+ +--------------------+
|     subclass of    | |     subclass of    |
|  Class[model/Foo]  | |  Class[model/Foo]  |
+--------------------+ +--------------------+

Between test runs 2 instances of model:foo will have a common shared ancestor the grandparent Class[model/Foo].

This implementation of factoryFor proposes to remove the intermediate subclass and instead have a generic factory object which holds the injections and allows for injected instances to be created. The resulting object graph would look something like this:

                  Proposed:
      +-------------------------------+
      |                               |
      |      /app/models/foo.js       |
      |                               |
      +-------------------------------+
                      |
                      |
                      |
                      v
           +--------------------+
           |  Class[model/Foo]  |
           +--------------------+
                      |
                      |
                      |
      first test run  |   nth test run
           +----------+-----------+
           |                      |
           |                      |
           |                      |
           v                      v
+--------------------+ +--------------------+
|     Factory of     | |     Factory of     |
|  Class[model/Foo]  | |  Class[model/Foo]  |
+--------------------+ +--------------------+

With factoryFor instances of model:foo will share a common constructor. Any state stored on the constructor would of course leak between the tests.

An example implementation of factoryFor can be reviewed on this GitHub comment.

Implications for owner.register

Currently, factories registered into Ember's DI system are required to provide an extend method. Removing support for extend-based DI in _lookupFactory will permit factories without extend to be registered. Instead factories must only provide a create method. For example:

let factory = {
  create(options={}) {
    /* Some implementation of `create` */
    return Object.create({});
  }
};
owner.register('my-type:a-factory', factory);
let factoryWithDI = owner.factoryFor('my-type:a-factory');

factoryWithDI.class === factory;
Development-mode Proxy

Because many developers will simply re-write _lookupFactory to factoryFor, it is important to provide some aid and ensure they actually complete the migration completely (they they avoid setting state on the factory). A proxy wrapping the return value of factoryFor and raising assertions when any property besides create or class is accessed will be added in development.

Additionally, using instanceof on the result of factoryFor should be disallowed, causing an exception to be raised.

A good rule of thumb is that, in development, using anything besides class or create on the return value of factoryFor should fail with a helpful message.

Releasing a polyfill

A polyfill addon, similar to ember-getowner-polyfill will be released for this feature. This polyfill will provide the factoryFor API going back to at least 2.8, provide the API and silence the deprecation in versions before factoryFor is available, and be a no-op in versions where factoryFor is available.

How We Teach This

This feature should be introduced along side lookup in the relevant guide. The return value of factoryFor should be taught as a POJO and not as an extended class.

Example deprecation guide: Migrating from _lookupFactory to factoryFor

Ember owner objects have long provided an intimate API used to fetch a factory with dependency injections. This API, _lookupFactory, is deprecated in Ember 2.12 and will be removed in Ember 2.13. To ease the transition to this new public API, a polyfill is provided with support back to at least Ember 2.8.

_lookupFactory returned the class of resolved factory extended with a mixin containing its injections. For example:

let factory = Ember.Object.extend();
owner.register('my-type:a-name', factory);
let klass = owner._lookupFactory('my-type:a-name');
klass.constructor.superclass === factory; // true
let instance = klass.create();

factoryFor instead returns an object with two properties: create and class. For example:

let factory = Ember.Object.extend();
owner.register('my-type:a-name', factory);
let klass = owner.factoryFor('my-type:a-name');
klass.class === factory; // true
let instance = klass.create();

A common use-case for _lookupFactory was to fetch an factory with specific needs in mind:

  • The factory needs to be created with initial values (which cannot be provided at create-time via lookup.
  • The instances of that factory need access to Ember's DI framework (injections, registered dependencies).

For example:

// app/widgets/slow.js
import Ember from 'ember';

export default Ember.Object.extend({
  // this instance requires access to Ember's DI framework
  store: Ember.inject.service(),

  convertToModel() {
    this.get('store').createRecord('widget', {
      widgetType: 'slow',
      name, canWobble
    });
  }

});
// app/services/widget-manager.js
import Ember from 'ember';

export default Ember.Service.extend({

  init() {
    this.set('widgets', []);
  },

  /*
   * Create a widget of a type, and add it to the widgets array.
   */
  addWidget(type, name, canWobble) {
    let owner = Ember.getOwner(this);
    // Use `_lookupFactory` so the `store` is accessible on instances.
    let WidgetFactory = owner._lookupFactory(`widget:${type}`);
    let widget = WidgetFactory.create({name, canWobble});
    this.get('widgets').pushObject(widget);
    return widget;
  }

});

For these common cases where only create is called on the factory, migration to factoryFor is mechanical. Change _lookupFactory to factoryFor in the above examples, and the migration would be complete.

Migration of static method calls

Factories may have had static methods or properties that were being accessed after resolving a factory with _lookupFactory. For example:

// app/widgets/slow.js
import Ember from 'ember';

const SlowWidget = Ember.Object.extend();
SlowWidget.reopenClass({
  SPEEDS: [
    'slow',
    'verySlow'
  ],
  hasSpeed(speed) {
    return this.SPEEDS.contains(speed);
  }
});

export default SlowWidget;
let factory = owner._lookupFactory('widget:slow');
factory.SPEEDS.length; // 2
factory.hasSpeed('slow'); // true

With factoryFor, access to these methods or properties should be done via the class property:

let factory = owner.factoryFor('widget:slow');
let klass = factory.class;
klass.SPEEDS.length; // 2
klass.hasSpeed('slow'); // true

Drawbacks

The main drawback to this solution is the removal of double extend. Double extend is a performance troll, however it also means if a single class is registered multiple times each _lookupFactory returns a unique factory. It is plausible that some use-case relying on this behavior would get trolled in the migration to factoryFor, however it is unlikely.

For example these cases where state is stored on the factory would no longer be scope to one instance of the owner (like one test). Instead, setting a value on the class would set it on the registered class.

Some real-world examples of setting state on the factory class:

  • ember-model
    • https://github.com/ebryn/ember-model/blob/master/packages/ember-model/lib/model.js#L404 and https://github.com/ebryn/ember-model/blob/master/packages/ember-model/lib/model.js#L457 with factoryFor will increment a shared counter across application and container instances.
    • https://github.com/ebryn/ember-model/blob/master/packages/ember-model/lib/model.js#L723-L725 would also set properties on the base Ember.Model factory instead of an extension of that class.
  • ember-data
    • If attrs change between test runs (seems very unlikely) then https://github.com/emberjs/data/blob/387630db5e7daec6aac7ef8c6172358a3bd6394c/addon/-private/system/model/attr.js#L57 would be affected. The CP of attributes will have a value cached on the factory, and where with _lookupFactory's double-extend the cache would be on the extended class, in factoryFor that CP cache will be on the class registered as a factory.
  • Any other of the following:
    • lookupFactory(x).reopen / reopenClass at runtime (or test time to monkey patch code)
    • lookupFactory(x).something = value

Alternatives

More aggressive timelines have been considered for this change.

However we have considered the possibility that removing _lookupFactory in 2.13 (something LTS technically permits) would be too aggressive for the community of addons. Providing a polyfill is part of the strategy to handle this change.

Unresolved questions

Are there any use-cases for the double extend not considered?

  • Start Date: 2016-11-05
  • RFC PR: https://github.com/emberjs/rfcs/pull/176
  • Ember Issue: (leave this empty)

Summary

Make Ember feel less overwhelming to new users, and make Ember applications start faster, by replacing the Ember global with a first-class system for importing just the parts of the framework you need.

Motivation

ECMAScript 2015 (also known as ES2015 or ES6) introduced a syntax for importing and exporting values from modules. Ember aggressively adopted modules, and if you've used Ember before, you're probably familiar with this syntax:

import Ember from "ember";
import Analytics from "../mixins/analytics";

export default Ember.Component.extend(Analytics, {
  // ...
});

One thing to notice is that the entire Ember framework is imported as a single package. Rather than importing Component directly, for example, you import Ember and subclass Ember.Component. (And this example still works even if you forget the import, because we also create a global variable called Ember.)

Using Ember via a monolithic package or global object is not ideal for several reasons:

  • It's overwhelming for learners. There's a giant list of classes and functions with no hints about how they're related. The API documentation reflects this.
  • Experienced developers who don't want all of Ember's features feel like they're adding unnecessary and inescapable bloat to their application.
  • The Ember object must be built at boot time, requiring that we ship the entire framework to the browser. This has two major costs:
    1. Increased download time, particularly noticeable on slower connections.
    2. Increased parsing/evaluation cost, which still must be paid even when assets are cached. On some browsers/devices, this can far exceed the cost of the download itself.

Defining a public API for importing parts of Ember via JavaScript modules helps us lay the groundwork for solving all of these problems.

Reducing Load Time

Modules help us eliminate unneeded code. The module syntax is statically analyzable, meaning that a tool like Ember CLI can analyze an application's source code and reliably determine which files, in both the framework and the application, are actually needed. Anything that's not needed is omitted from the final build.

This allows us to provide the file size benefits of a "small modules" approach to building web applications while retaining the productivity benefits of a complete, opinionated framework.

For example, if your application never used the Ember.computed.union computed property helper, Ember could detect this and remove its code when you build your application. This technique for slimming down the payload automatically is often referred to as tree shaking or dead code elimination.

Building the module graph doesn't just mean we get a list of files used by the application— we also know which files are used route-by-route.

We can use this knowledge to optimize boot time even more, by prioritizing sending only the JavaScript needed for the requested route, rather than the entire application.

For example, if the user requests the URL https://app.example.com/articles/123, the server could first send the code for ArticlesRoute, the Article model, the articles template, and any components and framework code used in the route. Only after the route is rendered would we start to send the remainder of the application and framework code in the background.

Guiding Learners

We can group framework classes and utilities by functionality, making it clear what things are related and how they should work together. People can feel confident they are getting only what they need at that moment, not an entire framework that they're not sure they're benefiting from.

Modernizing Ember

Lastly, developers are growing increasingly accustomed to using JavaScript modules to import libaries. If we don't adapt to modules, Ember will feel clunky and antiquated compared to modern alternatives.

Prior Art

Initial efforts to define a module API for Ember began with the ember-cli-shims addon. This addon provides a set of "shim" modules that re-export a value off the global Ember. While this setup doesn't offer the benefits of true modules, it did allow us to rapidly experiment with a module API without making changes to Ember core.

Common feedback from shim users was that, while they were a net improvement, they introduced too much verbosity and were hard for beginners to remember.

An oft-cited example of this verbosity is that implementing an object and using Ember.get and Ember.set requires three different imports:

import EmberObject from "ember-object";
import get from "ember-metal/get";
import set from "ember-metal/set";

In fact, one of the principles outlined in this RFC is designed to correct this verbosity; namely, that utility functions and the class they are related to should share a module.

For those who have already adopted modules via the ember-cli-shims package, we will provide a migration tool to rewrite shim modules into the final module API. The static nature of the import syntax makes this even easier and more reliable than migrating globals-based apps. The upgrade process should take no more than a few minutes (see Migration).

This RFC also builds significantly on @zeppelin's previous ES6 modules RFC, which drove initial discussion, including the idea to use scoped packages.

Detailed Design

Terminology

  • Package - a bundle of JavaScript addressable by npm and other package managers, it may contain many modules (but has a default module, usually called index.js).
  • Scoped Package - a namespaced package whose name starts with an @, like import Thing from "@scope/thing".
  • Module - a JavaScript file with at least one default export or named export.
  • Top-Level Module - the module provided by importing a package directly, like import Component from "@ember/component".
  • Nested Module - a module provided at a path inside a package, like import { addObserver } from "@ember/object/observers".

Module Naming & Organization

Because our goal is to eliminate the Ember global object, any public classes, functions or properties that currently exist on the global need an equivalent module that can be imported.

Given how fundamental modules are to the development process, how we organize and name them impacts new learners and seasoned veterans alike. Thus we must try to find a balance between predictability for new and intermediate users, and terseness for experienced developers with large apps.

There is another goal at play: we would like to help dispel the misconception that Ember is a monolithic framework. Ideally, module names help us tell a story about Ember's layered features. Rather than inheriting the entire framework at once, you can pull in just the pieces you need.

For that reason, package names should assist the developer in understanding what capabilities are added by bringing in that new package. We should pick meaningful names, not let our public API be a by-product of how Ember's internals are organized.

A full table of proposed mappings from global to module is available in Addendum 1 - Table of Module Names and Exports by Global and Addendum 2 - Table of Module Names and Exports by Package. Because there is some implicit functionality that you get when loading Ember that is not encapsulated in a global property (for example, automatically adding prototype extensions), there is also Addendum 3 - Table of Modules with Side Effects.

Before diving in to these tables, however, it may be helpful to understand some of the thinking that guided this proposal. And keep in mind, this RFC specifies a baseline module API. Nothing here precludes adding additional models in the future, as we discover missing pieces.

Use Scoped Packages

Last year, npm introduced support for scoped packages. Scopes are similar to organizations on GitHub. They allow us to use any package name, even if it's already in use on npm, by namespacing it inside a scope.

For example, the component package is already reserved by an unmaintained tool; we couldn't use component as a package name even if we wanted to.

However, scopes allow us to create a package named component that lives under the @ember scope: import Component from "@ember/component".

The advantages of using scoped packages, as this proposal does, are two-fold:

  1. "Official" packages are clearly differentiated from community packages.
  2. There is no risk of naming conflicts with existing community packages.

Note that actually publishing packages to npm may not be immediately necessary to implement this RFC. We should still design around this constraint so that we have the option available to us in the future. For more discussion, see the Distribution unresolved question.

Prefer Common Terminology

Module names should use terms people are more likely to be familiar with. For example, instead of the ambiguous platform, polyfills should be in a module called polyfill.

Similarly, the vast majority of advanced Ember developers couldn't crisply articulate the difference between ember-metal and ember-runtime. Instead, we should prefer ember-object, to match how people actually talk about these features: the Ember object model.

Organize by Mental Model

One of the biggest barriers to learning is the fact that short-term memory is limited. To understand a complex system like a modern web application, the learner must hold in their head many different concepts—more concepts than most people can reason about at once.

Chunking is a strategy for dealing with this. It means that you present concepts that are conceptually related together. When the learner needs to reason about the overall system, in their mind they can replace a group of related concepts with a single, overarching concept.

For example, if you tell someone that in order to build an Ember app, they will need to understand computed properties, actions (bubbling and closure), components, containers, registries, routes, helpers (stateful and stateless), dependent keys, controllers, route maps, observers, transitions, mixins, computed property macros, injected properties, the run loop, and array proxies—they will rightfully feel like Ember is an overwhelming, overcomplicated framework. Most people (your RFC author included) simply cannot keep this many discrete concepts in their head at once.

The day-to-day reality of building an Ember app, of course, is not nearly so complex. For those developers who stick through the learning curve, they end up with a greatly simplified mental model.

This proposal attempts to re-align module naming with that simplified mental model, placing everything into packages based on the chunk of functionality they provide:

  • @ember/application - Application-level concerns, like bootstrapping, initializers, and dependency injection.
  • @ember/component - Classes and utilities related to UI components.
  • @ember/routing - Classes used for multi-page routing.
  • @ember/service - Classes and utilities for cross-cutting services.
  • @ember/controller - Classes and utilities related to controllers.
  • @ember/object - Classes and utilities related to Ember's object model, including Ember.Object, computed properties and observers.
  • @ember/runloop - Methods for scheduling behavior on to the run loop.

It includes a few other packages that, over time, your author hopes become either unneeded or can be moved outside of core into standalone packages:

  • @ember/array - Array utilities and observation. Ideally these can be replaced with a combination of ES2015+ features and array diffing in Glimmer.
  • @ember/enumerable - Replaced by iterables in ES2015.
  • @ember/string - String formatting utilities (dasherize, camelize, etc.).
  • @ember/map - Replaced by Map and WeakMap in ES2015.
  • @ember/polyfills - Polyfills for Object.keys, Object.assign and Object.create.
  • @ember/utils - Grab bag of utilities that could likely be replaced with something like lodash.

And finally, some packages that may be used by internals, extensions, or addons but are not used day-to-day by app developers:

  • @ember/instrumentation - Instrumentation hooks for measuring performance.
  • @ember/debug - Utility functions for debugging, and hooks used by debugger tools like Ember Inspector.

Classes are Default Exports

Classes that the user is supposed to import and subclass are always the default export, never a named export. In the case where a package has more than one primary class, those classes live in a nested module.

This rule ensures there is no ambiguity about whether something is a named export or a default export: classes are always default exports. In tandem with the following rule (Utility Functions are Named Exports), this also means that classes and the functions that act on them are grouped into the same import line.

Examples

Primary class only:

import EmberObject from "@ember/object";

Primary class plus secondary classes:

import Component from "@ember/component";
import Checkbox from "@ember/component/checkbox";

import Map from "@ember/map";
import MapWithDefault from "@ember/map/with-default";

Multiple primary classes:

import Router from "@ember/routing/router";
import Route from "@ember/routing/route";

Utility Functions are Named Exports

Functions that are only useful with a particular class, or are used most frequently with that class, are named exports from the package that exports the class.

Examples

import Service, { inject } from "@ember/service";
import EmberObject, { get, set } from "@ember/object";

In cases where there are many utility functions associated with a class, they can be further subdivided into nested packages but remain named exports:

import EmberObject, { get, set } from "@ember/object";
import { addObserver } from "@ember/object/observers";
import { addListener } from "@ember/object/events";

In the future, decorators would be included under this rule as well. In fact, designing with an eye towards decorators was a large driver behind this principle. For more discussion, see the Everything is a Named Export alternative.

One Level Deep

To avoid deep directory hierarchies with mostly-empty directories, this proposal limits nesting inside a top-level package to a single level. Deep nesting like this can add additional time to navigating the hierarchy without adding much benefit.

Java packages often have this problem due to their URL-based namespacing; see e.g. this Java library where you end up with deeply nested directories, like xLog/library/src/test/java/com/elvishew/xlog/printer/AndroidPrinterTest.java.

This rule leads to including the type in the name of the module in some cases where it might otherwise be grouped instead. For example, instead of @ember/routing/locations/none, we prefer @ember/routing/none-location to avoid the second level of nesting.

No Non-Module Namespaces

The global version of Ember includes several functions that also act as a namespace to group related functionality.

For example, Ember.run can be used to run some code inside a run loop, while Ember.run.scheduleOnce is used to schedule a function onto the run loop once.

Similarly, Ember.computed can be used to indicate a method should be treated as a computed property, but computed property macros also live on Ember.computed, like Ember.computed.alias.

When consumed via modules, these functions no longer act as a namespace. That's because tacking these secondary functions on to the main function requires us to eagerly evaluate them (not to mention the potential deoptimizations in JavaScript VMs by adding properties to a function object).

In practice, that means that this won't work:

// Won't work!
import { run } from "@ember/runloop";
run.scheduleOnce(function() {
  // ...
});

Instead, you'd have to do this:

import { scheduleOnce } from "@ember/runloop";
scheduleOnce(function() {
  // ...
});

The migration tool, described below, is designed to detect these cases and migrate them correctly.

Prototype Extensions and Other Code with Side Effects

Some parts of Ember change global objects rather than exporting classes or functions. For example, Ember (by default) installs additional methods on String.prototype, like the camelize() method.

Any code that has side effects lives in a module without any exports; importing the module is enough to produce the desired side effects. For example, if I wanted to make the string extensions available to the application, I could write:

import "@ember/extensions/string"

Generally speaking, modules that have side effects are harder to debug and can cause compatibility issues, and should be avoided if possible.

Migration

To assist in assessing this RFC in real-world applications, and to help upgrade apps should this RFC be accepted and implemented, your author has provided an automatic migration utility, or "codemod":

ember-modules-codemod

To run the codemod, cd into an existing Ember app and run the following commands.

npm install ember-modules-codemod -g
ember-modules-codemod

Note: The codemod currently requires Node 6 or later to run.

This codemod uses jscodeshift to update an Ember application in-place to the module syntax proposed in this RFC. It can update apps that use the global Ember, and will eventually also support apps using ember-cli-shims.

Make sure you save any changes in your app before running the codemod, because it modifies files in place. Obviously, because this RFC is speculative, your app will not function after applying this codemod. For now, the codemod is only useful for assessing how this proposal looks in real-world applications.

For example, it will rewrite code that looks like this:

import Ember from 'ember';

export default Ember.Component.extend({
  isAnimal: Ember.computed.or('isDog', 'isCat')
});

Into this:

import Component from '@ember/component';
import { or } from '@ember/object/computed';

export default Component.extend({
  isAnimal: or('isDog', 'isCat')
});

For more information, see the README.

How We Teach This

This RFC makes changes to one of the most foundational (and historically stable) concepts in Ember: how you access framework code. Because of that, it is hard to overstate the impact these changes will have. We need to proceed carefully to avoid confusion and churn.

It is possible that the work required to update the documentation and other learning materials will be significantly more than the work required to do the actual implementation. That means we need to start getting ready now, so that when the code changes are ready, it is not blocked by a big documentation effort.

That said, we do have the advantage of the new modules being "just JavaScript." We can lean heavily on the greater JavaScript community's learning materials, and any teaching we do has the benefit of being transferable and not an "Ember-only" skill.

Documentation Examples

Examples in the Getting Started tutorial, guides and API docs will need to be updated to the new module syntax.

Probably the most efficient and least painful way to do this would be to write a tool that can extract code snippets from Markdown files and run the migrator on them, then replace the extracted code with the updated version. For the API docs, this tool would need to be able to handle Markdown embedded in JSDoc-style documentation.

The benefit of this approach is that, once we have verified the script works reliably, we can wait until the last possible moment to make the switch. If we attempt to update everything by hand, the duration and tediousness of that process will likely take out an effective "lock" on the documentation code base, where people will put off making big changes because of the potential for merge conflicts.

Generators

Generators are used by new users to help them get a handle on the framework, and by experienced users to avoid typing repetitive boilerplate. We need to ensure that the generators that ship with Ember are updated to use modules as soon as they are ready. The recent work by the Ember CLI team to ship generators with the Ember package itself, rather than Ember CLI, should make this relatively painless.

API Documentation

Our API documentation has long been a source of frustration, because the laundry list of (often rarely used or internal) classes makes Ember feel far more overwhelming than it really is.

The shift to modules gives us a good opportunity to rethink the presentation of our API documentation. Instead of the imposing mono-list, we should group the API documentation by package–which, conveniently in this proposal, means they will also be grouped by area of functionality.

We should investigate the broader ecosystem to see if there is a good tool that generates package-oriented documentation for JavaScript projects. If not, we may wish to adapt an existing tool to do so.

Explaining the Migration

Once the guides and API documentation are updated, modules should be straightforward for new learners—indeed, more and more new learners are starting with JavaScript modules as the baseline.

The most challenging aspect of teaching the new modules API, counterintuitively, will likely be existing users. In particular, for changes that touch nearly every file, most teams working on large apps cannot pause work for a week to implement the change.

Our focus needs to be:

  • Communicating clearly that the existing global build will work for the foreseeable future.
  • Making clear the file size benefits of moving to modules.
  • Building robust tooling that allows even large apps to migrate in a day or two, not a week.

It is important to frame the module transition as a carrot, not a stick. We should avoid dire warnings or deprecation notices. Instead, we should provide good reporting when doing Ember CLI builds. If an app is compiled in globals mode, we can offer suggestions for how to reduce the file size, providing a helpful pointer to the modules migration guide. This will make the transition feel less like churn and more like an optimization opportunity that developers can take advantage of when they have the time or resources.

Addons

One pitfall is that a single use of the Ember global means we have to include the entire framework. That means that a developer could migrate their entire app to modules, but a single old addon that uses the Ember globals will negate the benefits.

This requires a two-pronged strategy:

  • Tight integration into Ember CLI
    • Good reporting to make it obvious when a fallback to globals mode occurs, and which addons/files are causing it.
    • An opt-in mode to prohibit globals mode. Installing an incompatible addon would produce an error.
  • Community outreach and pull requests to help authors update addons.

Drawbacks

Complexity

There is something elegantly simple about a single Ember global that contains everything. Introducing multiple packages means you don't just have to know what you need—you also need to know where to import it from.

JavaScript module syntax is also something not everyone will be familiar with, given its newness. However, this is something we must deal with anyway because module syntax is already in use within apps.

Module Churn

The ember-cli-shims package is already included by default in new Ember apps, and is in fairly common usage. Many developers are already familiar with its API. This drawback can be at least partially mitigated by the automated migration process, which will be easily applied to existing shimmed apps.

Scoped Packages Are an Unknown Quantity

This proposal relies on scoped packages. Despite being released over a year ago, scoped packages are not always well supported.

For example, scoped packages currently wreak havoc on Yarn. Until very recently, the npmjs.com search did not include scoped packages. Generally speaking, there will be a long-tail of tools in the ecosystem that will choke on scoped packages.

That said, Angular 2 is distributed under the @angular scope, and TypeScript recently adopted the @types scope for publishing TypeScript typings to npm. The popularity of both of these should drive compatibility. Despite this, we can expect similar compatibility issues for some time.

Nested Modules

To satisfy the Classes are Default Exports rule, this RFC proposes the use of nested modules. That is, a module name may contain an additional path segment beyond the package name. For example, @ember/object/observers is a nested module, while @ember/object is not.

In the Node/CommonJS world, nested modules are unusual but not unheard of. For example, Lodash offers a functional programming style accessed by calling require('lodash/fp').

There are two drawbacks associated with nested modules:

  1. Because they are uncommon, developers may be confused by the syntax.
  2. Because they allow you to "reach in" to the package for an arbitrary file, encouraging the end user to use nested modules may inadvertently also encourage them to access private modules, thinking they are public.

The first issue is surmountable with education, good reference documentation, and good tools to help guide developers in the right direction. That this style is uncommon in the Node ecosystem seems to be more a function of dogma than any technical shortcoming of nested modules.

To ensure that developers don't inadvertently access private modules, we have two good options:

  1. Package modules in such a way that private modules cannot be accessed.
  2. Take a page from Ember Data and put all private modules in a -private directory, hopefully making it clear accessing this module is not playing by the rules.

We could avoid using this uncommon style by hoisting nested modules up to their own package. For example, @ember/object/observers could become @ember/observers or @ember/object-observers. However, because I could not find a strong technical reason against it, and because having more packages is in tension with the explicit goal to make Ember feel less overwhelming, I decided it was worth the small cost.

Alternatives

ember- prefix

One alternative to the @ember scope is to use the ember- prefix. This avoids the drawbacks around scoped packages described above. However, they would be indistinguishable from the large number of community packages that begin with ember-.

Everything is a Named Export

This proposal argues that classes should be a module's default export, and any utility functions should be a named export. That means you can never have more than one class per module, and that means, inherently, more import statements than a system where multiple classes can live in one module.

Additionally, in cases where there is not a clear "primary" class, this can feel a little awkward:

import Route from "@ember/routing/route";
import Router from "@ember/routing/router";

One commonly proposed alternative is to say that classes become named exports, and default exports are not used at all. The above example would become:

import { Route, Router } from "@ember/routing";

In this case, classes are distinguished by being capitalized, rather than by being a default export.

There is one major change coming to JavaScript and Ember that, your author believes, deals a fatal blow to this approach: decorators.

If you're unfamiliar with decorators, see Addy Osmani's great overview. Decorators provide a mechanism for adding declarative annotations to classes, methods, properties and functions.

For example, Robert Jackson has an experimental library for using decorators to annotate computed properties in a class. Something like this will probably make its way into Ember in the future:

import EmberObject, { computed } from "@ember/object";

export default class Cat extends EmberObject {
  @computed("hairLength")
  isDomesticShortHair(hairLength) {
    return hairLength < 3;
  }
}

Most decorators are tightly coupled to a particular class because they configure some aspect of behavior that is only relevant to that class. If every decorator and every class share a namespace, it is hard to identify which go with each other.

import { Router, Route, resource, model, location, inject, queryParam } from "@ember/routing";

Can you tell me which of these decorators goes with which class?

And this import is getting so long, you'd probably be tempted to break it up into multiple lines anyway, so it's not clear that it's actually a win over separate imports.

Contrast this with the same thing expressed using the rules in this RFC:

import Router, { resource, location } from "@ember/routing/router";
import Route, { model, inject, queryParam } from "@ember/routing/route";

Here, the decorators are clearly tied to their class. And it's far nicer from a refactoring perspective: if you delete a class from a file, you then delete a single line from your imports.

Contrast that with making fiddly edits to a long list of named exports unrelated to each other.

Unresolved Questions

Intimate APIs

How much do we want to provide module API for so-called "intimate APIs"—technically private, but in widespread use?

Backwards Compatibility for Addons

How do we provide an API for addons to use modules but fall back to globals mode in older versions of Ember? We should ensure that, at minimum, addons can continue to support LTS releases. At the same time, it's critical that adding an addon doesn't opt your entire application back in to the entire framework.

Because there is a lot of implementation-specific detail to get right here, and because it doesn't otherwise block landing this module naming RFC, the final design of API for addon authors should be broken out into a separate RFC.

Distribution

In practice, how do we ship this code to end users of Ember CLI?

When building client-side apps, it's very important to avoid duplicate dependencies, which can quickly cause file size to balloon out of control.

Unfortunately, npm@3's de-duping is so naïve that it's likely that users would end up in dependency hell if we shipped the framework as separate npm packages. There's no good way to ship dependencies in version lockstep and feel confident that they will reliably be de-duped.

Until Yarn usage is more widespread, and to eliminate significant complexity in the first iteration, it probably makes sense for the first phase of implementation to continue shipping a single npm package that Ember CLI apps can depend on. This gives us atomic updates and makes sure you never have one piece of the framework interacting with a different piece that is inadvertently three versions old.

What this means is that, rather than shipping @ember/object on npm, we'd ship a single ember-source (or something) package that includes the entire framework. At build time, the Ember build process would virtually map the @ember/object package to the right file inside ember-source. In essence, all of the benefits of smaller bundles without the boiling hellbroth of managing dependencies.

That said, because this RFC is designed with an eye towards eventually publishing each package to npm individually, we will have that option available to us in the future once we determine that we can do so without causing lots of pain.

Addenda

(Ed. note: These tables are automatically generated from the scripts in the codemod repository.)

Addendum 1 - Table of Module Names and Exports by Global

BeforeAfter
Ember.$import $ from "jquery"
Ember.Aimport { A } from "@ember/array"
Ember.Applicationimport Application from "@ember/application"
Ember.Arrayimport EmberArray from "@ember/array"
Ember.ArrayProxyimport ArrayProxy from "@ember/array/proxy"
Ember.AutoLocationimport AutoLocation from "@ember/routing/auto-location"
Ember.Checkboximport Checkbox from "@ember/component/checkbox"
Ember.Componentimport Component from "@ember/component"
Ember.ContainerDebugAdapterimport ContainerDebugAdapter from "@ember/debug/container-debug-adapter"
Ember.Controllerimport Controller from "@ember/controller"
Ember.DataAdapterimport DataAdapter from "@ember/debug/data-adapter"
Ember.DefaultResolverimport GlobalsResolver from "@ember/application/globals-resolver"
Ember.Enumerableimport Enumerable from "@ember/enumerable"
Ember.Eventedimport Evented from "@ember/object/evented"
Ember.HashLocationimport HashLocation from "@ember/routing/hash-location"
Ember.Helperimport Helper from "@ember/component/helper"
Ember.Helper.helperimport { helper } from "@ember/component/helper"
Ember.HistoryLocationimport HistoryLocation from "@ember/routing/history-location"
Ember.LinkComponentimport LinkComponent from "@ember/routing/link-component"
Ember.Locationimport Location from "@ember/routing/location"
Ember.Mapimport EmberMap from "@ember/map"
Ember.MapWithDefaultimport MapWithDefault from "@ember/map/with-default"
Ember.Mixinimport Mixin from "@ember/object/mixin"
Ember.MutableArrayimport MutableArray from "@ember/array/mutable"
Ember.NoneLocationimport NoneLocation from "@ember/routing/none-location"
Ember.Objectimport EmberObject from "@ember/object"
Ember.RSVPimport RSVP from "rsvp"
Ember.Resolverimport Resolver from "@ember/application/resolver"
Ember.Routeimport Route from "@ember/routing/route"
Ember.Routerimport Router from "@ember/routing/router"
Ember.Serviceimport Service from "@ember/service"
Ember.String.camelizeimport { camelize } from "@ember/string"
Ember.String.capitalizeimport { capitalize } from "@ember/string"
Ember.String.classifyimport { classify } from "@ember/string"
Ember.String.dasherizeimport { dasherize } from "@ember/string"
Ember.String.decamelizeimport { decamelize } from "@ember/string"
Ember.String.fmtimport { fmt } from "@ember/string"
Ember.String.htmlSafeimport { htmlSafe } from "@ember/string"
Ember.String.locimport { loc } from "@ember/string"
Ember.String.underscoreimport { underscore } from "@ember/string"
Ember.String.wimport { w } from "@ember/string"
Ember.TextAreaimport TextArea from "@ember/component/text-area"
Ember.TextFieldimport TextField from "@ember/component/text-field"
Ember.addListenerimport { addListener } from "@ember/object/events"
Ember.addObserverimport { addObserver } from "@ember/object/observers"
Ember.aliasMethodimport { aliasMethod } from "@ember/object"
Ember.assertimport { assert } from "@ember/debug"
Ember.assignimport { assign } from "@ember/polyfills"
Ember.cacheForimport { cacheFor } from "@ember/object/internals"
Ember.compareimport { compare } from "@ember/utils"
Ember.computedimport { computed } from "@ember/object"
Ember.computed.aliasimport { alias } from "@ember/object/computed"
Ember.computed.andimport { and } from "@ember/object/computed"
Ember.computed.boolimport { bool } from "@ember/object/computed"
Ember.computed.collectimport { collect } from "@ember/object/computed"
Ember.computed.deprecatingAliasimport { deprecatingAlias } from "@ember/object/computed"
Ember.computed.emptyimport { empty } from "@ember/object/computed"
Ember.computed.equalimport { equal } from "@ember/object/computed"
Ember.computed.filterimport { filter } from "@ember/object/computed"
Ember.computed.filterByimport { filterBy } from "@ember/object/computed"
Ember.computed.filterPropertyimport { filterProperty } from "@ember/object/computed"
Ember.computed.gtimport { gt } from "@ember/object/computed"
Ember.computed.gteimport { gte } from "@ember/object/computed"
Ember.computed.intersectimport { intersect } from "@ember/object/computed"
Ember.computed.ltimport { lt } from "@ember/object/computed"
Ember.computed.lteimport { lte } from "@ember/object/computed"
Ember.computed.mapimport { map } from "@ember/object/computed"
Ember.computed.mapByimport { mapBy } from "@ember/object/computed"
Ember.computed.mapPropertyimport { mapProperty } from "@ember/object/computed"
Ember.computed.matchimport { match } from "@ember/object/computed"
Ember.computed.maximport { max } from "@ember/object/computed"
Ember.computed.minimport { min } from "@ember/object/computed"
Ember.computed.noneimport { none } from "@ember/object/computed"
Ember.computed.notimport { not } from "@ember/object/computed"
Ember.computed.notEmptyimport { notEmpty } from "@ember/object/computed"
Ember.computed.oneWayimport { oneWay } from "@ember/object/computed"
Ember.computed.orimport { or } from "@ember/object/computed"
Ember.computed.readOnlyimport { readOnly } from "@ember/object/computed"
Ember.computed.readsimport { reads } from "@ember/object/computed"
Ember.computed.setDiffimport { setDiff } from "@ember/object/computed"
Ember.computed.sortimport { sort } from "@ember/object/computed"
Ember.computed.sumimport { sum } from "@ember/object/computed"
Ember.computed.unionimport { union } from "@ember/object/computed"
Ember.computed.uniqimport { uniq } from "@ember/object/computed"
Ember.copyimport { copy } from "@ember/object/internals"
Ember.createimport { create } from "@ember/polyfills"
Ember.debugimport { debug } from "@ember/debug"
Ember.deprecateimport { deprecate } from "@ember/application/deprecations"
Ember.deprecateFuncimport { deprecateFunc } from "@ember/application/deprecations"
Ember.getimport { get } from "@ember/object"
Ember.getOwnerimport { getOwner } from "@ember/application"
Ember.getPropertiesimport { getProperties } from "@ember/object"
Ember.guidForimport { guidFor } from "@ember/object/internals"
Ember.inject.controllerimport { inject } from "@ember/controller"
Ember.inject.serviceimport { inject } from "@ember/service"
Ember.inspectimport { inspect } from "@ember/debug"
Ember.instrumentimport { instrument } from "@ember/instrumentation"
Ember.isArrayimport { isArray } from "@ember/array"
Ember.isBlankimport { isBlank } from "@ember/utils"
Ember.isEmptyimport { isEmpty } from "@ember/utils"
Ember.isEqualimport { isEqual } from "@ember/utils"
Ember.isNoneimport { isNone } from "@ember/utils"
Ember.isPresentimport { isPresent } from "@ember/utils"
Ember.keysimport { keys } from "@ember/polyfills"
Ember.makeArrayimport { makeArray } from "@ember/array"
Ember.observerimport { observer } from "@ember/object"
Ember.onimport { on } from "@ember/object/evented"
Ember.onLoadimport { onLoad } from "@ember/application"
Ember.platform.definePropertyimport { defineProperty } from "@ember/polyfills"
Ember.platform.hasPropertyAccessorsimport { hasPropertyAccessors } from "@ember/polyfills"
Ember.removeListenerimport { removeListener } from "@ember/object/events"
Ember.removeObserverimport { removeObserver } from "@ember/object/observers"
Ember.resetimport { reset } from "@ember/instrumentation"
Ember.runimport { run } from "@ember/runloop"
Ember.run.beginimport { begin } from "@ember/runloop"
Ember.run.bindimport { bind } from "@ember/runloop"
Ember.run.cancelimport { cancel } from "@ember/runloop"
Ember.run.debounceimport { debounce } from "@ember/runloop"
Ember.run.endimport { end } from "@ember/runloop"
Ember.run.joinimport { join } from "@ember/runloop"
Ember.run.laterimport { later } from "@ember/runloop"
Ember.run.nextimport { next } from "@ember/runloop"
Ember.run.onceimport { once } from "@ember/runloop"
Ember.run.scheduleimport { schedule } from "@ember/runloop"
Ember.run.scheduleOnceimport { scheduleOnce } from "@ember/runloop"
Ember.run.throttleimport { throttle } from "@ember/runloop"
Ember.runInDebugimport { runInDebug } from "@ember/debug"
Ember.runLoadHooksimport { runLoadHooks } from "@ember/application"
Ember.sendEventimport { sendEvent } from "@ember/object/events"
Ember.setimport { set } from "@ember/object"
Ember.setOwnerimport { setOwner } from "@ember/application"
Ember.setPropertiesimport { setProperties } from "@ember/object"
Ember.subscribeimport { subscribe } from "@ember/instrumentation"
Ember.tryInvokeimport { tryInvoke } from "@ember/utils"
Ember.trySetimport { trySet } from "@ember/object"
Ember.typeOfimport { typeOf } from "@ember/utils"
Ember.unsubscribeimport { unsubscribe } from "@ember/instrumentation"
Ember.warnimport { warn } from "@ember/debug"

Addendum 2 - Table of Module Names and Exports by Package

Each package is sorted by module name, then export name.

@ember/application

ModuleGlobal
import Application from "@ember/application"Ember.Application
import { getOwner } from "@ember/application"Ember.getOwner
import { onLoad } from "@ember/application"Ember.onLoad
import { runLoadHooks } from "@ember/application"Ember.runLoadHooks
import { setOwner } from "@ember/application"Ember.setOwner
import { deprecate } from "@ember/application/deprecations"Ember.deprecate
import { deprecateFunc } from "@ember/application/deprecations"Ember.deprecateFunc
import GlobalsResolver from "@ember/application/globals-resolver"Ember.DefaultResolver
import Resolver from "@ember/application/resolver"Ember.Resolver

@ember/array

ModuleGlobal
import EmberArray from "@ember/array"Ember.Array
import { A } from "@ember/array"Ember.A
import { isArray } from "@ember/array"Ember.isArray
import { makeArray } from "@ember/array"Ember.makeArray
import MutableArray from "@ember/array/mutable"Ember.MutableArray
import ArrayProxy from "@ember/array/proxy"Ember.ArrayProxy

@ember/component

ModuleGlobal
import Component from "@ember/component"Ember.Component
import Checkbox from "@ember/component/checkbox"Ember.Checkbox
import Helper from "@ember/component/helper"Ember.Helper
import { helper } from "@ember/component/helper"Ember.Helper.helper
import TextArea from "@ember/component/text-area"Ember.TextArea
import TextField from "@ember/component/text-field"Ember.TextField

@ember/controller

ModuleGlobal
import Controller from "@ember/controller"Ember.Controller
import { inject } from "@ember/controller"Ember.inject.controller

@ember/debug

ModuleGlobal
import { assert } from "@ember/debug"Ember.assert
import { debug } from "@ember/debug"Ember.debug
import { inspect } from "@ember/debug"Ember.inspect
import { runInDebug } from "@ember/debug"Ember.runInDebug
import { warn } from "@ember/debug"Ember.warn
import ContainerDebugAdapter from "@ember/debug/container-debug-adapter"Ember.ContainerDebugAdapter
import DataAdapter from "@ember/debug/data-adapter"Ember.DataAdapter

@ember/enumerable

ModuleGlobal
import Enumerable from "@ember/enumerable"Ember.Enumerable

@ember/instrumentation

ModuleGlobal
import { instrument } from "@ember/instrumentation"Ember.instrument
import { reset } from "@ember/instrumentation"Ember.reset
import { subscribe } from "@ember/instrumentation"Ember.subscribe
import { unsubscribe } from "@ember/instrumentation"Ember.unsubscribe

@ember/map

ModuleGlobal
import EmberMap from "@ember/map"Ember.Map
import MapWithDefault from "@ember/map/with-default"Ember.MapWithDefault

@ember/object

ModuleGlobal
import EmberObject from "@ember/object"Ember.Object
import { aliasMethod } from "@ember/object"Ember.aliasMethod
import { computed } from "@ember/object"Ember.computed
import { get } from "@ember/object"Ember.get
import { getProperties } from "@ember/object"Ember.getProperties
import { observer } from "@ember/object"Ember.observer
import { set } from "@ember/object"Ember.set
import { setProperties } from "@ember/object"Ember.setProperties
import { trySet } from "@ember/object"Ember.trySet
import { alias } from "@ember/object/computed"Ember.computed.alias
import { and } from "@ember/object/computed"Ember.computed.and
import { bool } from "@ember/object/computed"Ember.computed.bool
import { collect } from "@ember/object/computed"Ember.computed.collect
import { deprecatingAlias } from "@ember/object/computed"Ember.computed.deprecatingAlias
import { empty } from "@ember/object/computed"Ember.computed.empty
import { equal } from "@ember/object/computed"Ember.computed.equal
import { filter } from "@ember/object/computed"Ember.computed.filter
import { filterBy } from "@ember/object/computed"Ember.computed.filterBy
import { filterProperty } from "@ember/object/computed"Ember.computed.filterProperty
import { gt } from "@ember/object/computed"Ember.computed.gt
import { gte } from "@ember/object/computed"Ember.computed.gte
import { intersect } from "@ember/object/computed"Ember.computed.intersect
import { lt } from "@ember/object/computed"Ember.computed.lt
import { lte } from "@ember/object/computed"Ember.computed.lte
import { map } from "@ember/object/computed"Ember.computed.map
import { mapBy } from "@ember/object/computed"Ember.computed.mapBy
import { mapProperty } from "@ember/object/computed"Ember.computed.mapProperty
import { match } from "@ember/object/computed"Ember.computed.match
import { max } from "@ember/object/computed"Ember.computed.max
import { min } from "@ember/object/computed"Ember.computed.min
import { none } from "@ember/object/computed"Ember.computed.none
import { not } from "@ember/object/computed"Ember.computed.not
import { notEmpty } from "@ember/object/computed"Ember.computed.notEmpty
import { oneWay } from "@ember/object/computed"Ember.computed.oneWay
import { or } from "@ember/object/computed"Ember.computed.or
import { readOnly } from "@ember/object/computed"Ember.computed.readOnly
import { reads } from "@ember/object/computed"Ember.computed.reads
import { setDiff } from "@ember/object/computed"Ember.computed.setDiff
import { sort } from "@ember/object/computed"Ember.computed.sort
import { sum } from "@ember/object/computed"Ember.computed.sum
import { union } from "@ember/object/computed"Ember.computed.union
import { uniq } from "@ember/object/computed"Ember.computed.uniq
import Evented from "@ember/object/evented"Ember.Evented
import { on } from "@ember/object/evented"Ember.on
import { addListener } from "@ember/object/events"Ember.addListener
import { removeListener } from "@ember/object/events"Ember.removeListener
import { sendEvent } from "@ember/object/events"Ember.sendEvent
import { cacheFor } from "@ember/object/internals"Ember.cacheFor
import { copy } from "@ember/object/internals"Ember.copy
import { guidFor } from "@ember/object/internals"Ember.guidFor
import Mixin from "@ember/object/mixin"Ember.Mixin
import { addObserver } from "@ember/object/observers"Ember.addObserver
import { removeObserver } from "@ember/object/observers"Ember.removeObserver

@ember/polyfills

ModuleGlobal
import { assign } from "@ember/polyfills"Ember.assign
import { create } from "@ember/polyfills"Ember.create
import { defineProperty } from "@ember/polyfills"Ember.platform.defineProperty
import { hasPropertyAccessors } from "@ember/polyfills"Ember.platform.hasPropertyAccessors
import { keys } from "@ember/polyfills"Ember.keys

@ember/routing

ModuleGlobal
import AutoLocation from "@ember/routing/auto-location"Ember.AutoLocation
import HashLocation from "@ember/routing/hash-location"Ember.HashLocation
import HistoryLocation from "@ember/routing/history-location"Ember.HistoryLocation
import LinkComponent from "@ember/routing/link-component"Ember.LinkComponent
import Location from "@ember/routing/location"Ember.Location
import NoneLocation from "@ember/routing/none-location"Ember.NoneLocation
import Route from "@ember/routing/route"Ember.Route
import Router from "@ember/routing/router"Ember.Router

@ember/runloop

ModuleGlobal
import { begin } from "@ember/runloop"Ember.run.begin
import { bind } from "@ember/runloop"Ember.run.bind
import { cancel } from "@ember/runloop"Ember.run.cancel
import { debounce } from "@ember/runloop"Ember.run.debounce
import { end } from "@ember/runloop"Ember.run.end
import { join } from "@ember/runloop"Ember.run.join
import { later } from "@ember/runloop"Ember.run.later
import { next } from "@ember/runloop"Ember.run.next
import { once } from "@ember/runloop"Ember.run.once
import { run } from "@ember/runloop"Ember.run
import { schedule } from "@ember/runloop"Ember.run.schedule
import { scheduleOnce } from "@ember/runloop"Ember.run.scheduleOnce
import { throttle } from "@ember/runloop"Ember.run.throttle

@ember/service

ModuleGlobal
import Service from "@ember/service"Ember.Service
import { inject } from "@ember/service"Ember.inject.service

@ember/string

ModuleGlobal
import { camelize } from "@ember/string"Ember.String.camelize
import { capitalize } from "@ember/string"Ember.String.capitalize
import { classify } from "@ember/string"Ember.String.classify
import { dasherize } from "@ember/string"Ember.String.dasherize
import { decamelize } from "@ember/string"Ember.String.decamelize
import { fmt } from "@ember/string"Ember.String.fmt
import { htmlSafe } from "@ember/string"Ember.String.htmlSafe
import { loc } from "@ember/string"Ember.String.loc
import { underscore } from "@ember/string"Ember.String.underscore
import { w } from "@ember/string"Ember.String.w

@ember/utils

ModuleGlobal
import { compare } from "@ember/utils"Ember.compare
import { isBlank } from "@ember/utils"Ember.isBlank
import { isEmpty } from "@ember/utils"Ember.isEmpty
import { isNone } from "@ember/utils"Ember.isNone
import { isPresent } from "@ember/utils"Ember.isPresent
import { tryInvoke } from "@ember/utils"Ember.tryInvoke
import { typeOf } from "@ember/utils"Ember.typeOf

jquery

ModuleGlobal
import $ from "jquery"Ember.$

rsvp

ModuleGlobal
import RSVP from "rsvp"Ember.RSVP

Addendum 3 - Table of Modules with Side Effects

ModuleDescription
import "@ember/extensions"Adds all of Ember's prototype extensions.
import "@ember/extensions/string"Adds just String prototype extensions.
import "@ember/extensions/array"Adds just Array prototype extensions.
import "@ember/extensions/function"Adds just Function prototype extensions.
  • Start Date: 2016-11-18
  • RFC PR: #178
  • Ember Issue: #14746

Summary

The Ember.K utility function is a low level utility that has lost most of its value today.

Motivation

Let's start explaining what Ember.K is.

It is an utility function to avoid boilerplace code and limit the creation of function instances in Ember's internals. The source code for this API is the following:

Ember.K = function() {
  return this;
}

In a world of globals, writing somefn: Ember.K was effectively shorter than writing

someFn: function() {
  return this;
}

and generated fewer function allocations.

However with the introduction of ES6 modules and the modularization of Ember in process (#176), keeping this feature would require to design an import path for it.

While doable, the transpiled output is actually bigger then defining the functions inline, specially with the ES6 shorthand method syntax, and the perf difference of saving a few function allocations is negligible.

The second downside of reusing the same instance in many places is that if for some reason the VM deoptimizes that function, that deoptimization is spreaded across all the usages of Ember.K.

Third, the chainable nature of Ember.K tends to surprise the users:

let derp = {
  foo: Ember.K,
  bar: Ember.K,
  baz: Ember.K
}

derp.foo().bar().baz(); // O_o

And lastly but more importantly for simplicity. Consider the following code:

export default Component.extend({
  onSubmit() {}
});

Any JS developer will understand that this is an empty function and will probably understand that is a placeholder to provide your own function instead. However, JS developers that come across Ember.K for the first time will se this:

export default Component.extend({
  onSubmit: Ember.K
});

and will think that it is some cryptic Ember magic that they have to learn.

Transition Path

The necessary first step is to make sure Ember, Ember Data and other pieces of the ecosystem don't use Ember.K internally.

Phased approach:

  • Deprecate Ember.K: Use the deprecation API to signal the deprecation, and deprecation guide entry. Target version will be 3.0, as usual.
  • Add rule to ember-watson
  • Do not include export path in https://github.com/emberjs/rfcs/pull/176, but include it until 3.0 in the "globals" build.

How We Teach This

Since it is a very low-level utility, the amount of people that will have to update their code should be a limited set of developers, working mostly on addons. This allows us to cover most use cases with the following strategy:

  • Improve the current documentation to help developers finding the API for the first time in the future;
  • Provide an automated path forward through tooling such as ember-watson. (see Addendum 1)
  • Introduce the mandatory entry in the deprecations guide referencing the automated tooling.

If this RFC is done as part of https://github.com/emberjs/rfcs/pull/176 as suggested, it will be in the document or blog post announcing the final transition to modules.

Drawbacks

Although this utility is not very used, there is a chance that is used by some addons and as a placeholder of a hook that is called a lot and would trigger hundreds of deprecation warnings.

Alternatives

The feature could continue to exist.

Addenda

Addendum 1 - Codemod to automatically remove all usages of Ember.K on any project.

https://github.com/cibernox/ember-k-codemod

To use it you can install it globally and invoke the command on any app or addon.

The commands requires the user to decide the approach to replace occurenced of Ember.K. The possible flags are --empty and --return-this.

  • --empty replaces Ember.K with an empty function. This leads to the most idiomatic and intention-revealing code, but does not allow chaining, like the original Ember.K did. Despite of that, chaining Ember.K was such an uncommon patterns that we thing virtually everybody can use this option.
  • --return-this replaces Ember.K with a function that just returns this. This allows chaining like the original one.

Example usage:

npm install -g ember-k-codemod && ember-k-codemod --empty

Versions of ember-watson starting in 0.8.5 wrap this codemod so you can achieve the same transformation with it:

ember-watson remove-ember-k --empty

// or if installed as an addon

ember watson:remove-ember-k --empty

Addendum 2 - Ember.K usage across published addons

ae-select/addon/components/ae-select.js:  action: Ember.K, // action to fire on change
antd-ember/addon/components/io-searchable-select/searchable-select.js:    'on-change': Ember.K,
antd-ember/addon/components/io-searchable-select/searchable-select.js:    'on-add': Ember.K,
antd-ember/addon/components/io-searchable-select/searchable-select.js:    'on-search': Ember.K,
antd-ember/addon/components/io-searchable-select/searchable-select.js:        return this.get('on-add') !== Ember.K;
antd-ember/addon/components/io-searchable-select/searchable-select.js:        return this.get('on-search') === Ember.K;
ella-list-view/addon/views/list-item.coffee:  prepareContent: Ember.K
ella-list-view/addon/views/list-item.coffee:  teardownContent: Ember.K
ella-list-view/addon/views/list.coffee:  arrayWillChange: Ember.K
ella-list-view/addon/views/list.coffee:  didScrollToTop: Ember.K
ella-list-view/addon/views/list.coffee:  didScrollToBottom: Ember.K
ella-list-view/addon/views/list.coffee:  visibleItemsDidChange: Ember.K
ella-sparse-array-controller/addon/controllers/sparse-array.coffee:    if @didRequestRange isnt Ember.K
ella-sparse-array-controller/addon/controllers/sparse-array.coffee:    unless (@didRequestLength is Ember.K) or get(@, 'isRequestingLength')
ella-sparse-array-controller/addon/controllers/sparse-array.coffee:  sparseContentWillChange: Ember.K
ella-sparse-array-controller/addon/controllers/sparse-array.coffee:  sparseContentDidChange: Ember.K
ella-sparse-array-controller/addon/controllers/sparse-array.coffee:  didReplaceSparseArrayItem: Ember.K
ella-sparse-array-controller/addon/controllers/sparse-array.coffee:  didRequestIndex: Ember.K
ella-sparse-array-controller/addon/controllers/sparse-array.coffee:  didRequestRange: Ember.K
ella-sparse-array-controller/addon/controllers/sparse-array.coffee:  didRequestLength: Ember.K
elvis-network/addon/components/visjs-child.js:  didCreateLayer: Ember.K,
elvis-network/addon/components/visjs-child.js:  willDestroyLayer: Ember.K,
ember-animate/ember-animate.js:        willAnimateIn : Ember.K,
ember-animate/ember-animate.js:        willAnimateOut : Ember.K,
ember-animate/ember-animate.js:        didAnimateIn : Ember.K,
ember-animate/ember-animate.js:        didAnimateOut : Ember.K,
ember-animate/ember-animate.js:        _currentViewWillChange : Ember.K,
ember-aupac-typeahead/addon/components/aupac-typeahead.js:  action: Ember.K, //@public
ember-aupac-typeahead/addon/components/aupac-typeahead.js:  source : Ember.K, //@public
ember-autoresize/addon/mixins/autoresize.js:let trim = Ember.K;
ember-bootstrap/addon/components/bs-form-element.js:  setupValidations: Ember.K,
ember-bootstrap/addon/components/bs-select.js:  action: Ember.K, // action to fire on change
ember-bootstrap-components/addon/components/bs-select.js:  action: Ember.K, // action to fire on change
ember-bugsnag/app/instance-initializers/bugsnag.js:  let originalOnError = Ember.onerror || Ember.K;
ember-charts/addon/components/bubble-chart.js:      return Ember.K;
ember-charts/addon/components/bubble-chart.js:      return Ember.K;
ember-charts/addon/components/horizontal-bar-chart.js:      return Ember.K;
ember-charts/addon/components/horizontal-bar-chart.js:      return Ember.K;
ember-charts/addon/components/pie-chart.js:      return Ember.K;
ember-charts/addon/components/pie-chart.js:      return Ember.K;
ember-charts/addon/components/scatter-chart.js:      return Ember.K;
ember-charts/addon/components/scatter-chart.js:      return Ember.K;
ember-charts/addon/components/stacked-vertical-bar-chart.js:      return Ember.K;
ember-charts/addon/components/stacked-vertical-bar-chart.js:      return Ember.K;
ember-charts/addon/components/time-series-chart.js:      return Ember.K;
ember-charts/addon/components/time-series-chart.js:      return Ember.K;
ember-charts/addon/components/vertical-bar-chart.js:      return Ember.K;
ember-charts/addon/components/vertical-bar-chart.js:      return Ember.K;
ember-charts/addon/mixins/legend.js:      return Ember.K;
ember-charts/addon/mixins/legend.js:      return Ember.K;
ember-charts/addon/mixins/resize-handler.js:  onResizeStart: Ember.K,
ember-charts/addon/mixins/resize-handler.js:  onResizeEnd: Ember.K,
ember-charts/addon/mixins/resize-handler.js:  onResize: Ember.K,
ember-charts/dependencies/ember-addepar-mixins/resize_handler.js:  onResizeStart: Ember.K,
ember-charts/dependencies/ember-addepar-mixins/resize_handler.js:  onResizeEnd: Ember.K,
ember-charts/dependencies/ember-addepar-mixins/resize_handler.js:  onResize: Ember.K,
ember-cli-adapter-pattern/tests/dummy/app/starships/starship.js:  _makeItSo: Ember.K
ember-cli-airbrake/README.md:In all cases, an `airbrake` service will be exposed. If airbrake isn't configured the airbrake service uses the Ember.K "no-op" function for its methods. This facilitates the usage of the airbrake service without having to add environment-checking code in your app.
ember-cli-airbrake/README.md:exist, but all its methods will be no-ops (`Ember.K`). This way your tests will still run happily even
ember-cli-airbrake/app/instance-initializers/ember-cli-airbrake.js:  let originalOnError = Ember.onerror || Ember.K;
ember-cli-analytics/addon/integrations/base.js:  trackPage: Ember.K,
ember-cli-analytics/addon/integrations/base.js:  trackEvent: Ember.K,
ember-cli-analytics/addon/integrations/base.js:  trackConversion: Ember.K,
ember-cli-analytics/addon/integrations/base.js:  identify: Ember.K,
ember-cli-analytics/addon/integrations/base.js:  alias: Ember.K
ember-cli-analytics/tests/unit/mixins/trackable-test.js:  const analytics = { trackPage: Ember.K };
ember-cli-aptible-shared/tests/unit/utils/changeset-test.js:      initialValue: Ember.K
ember-cli-aptible-shared/tests/unit/utils/changeset-test.js:      key: Ember.K
ember-cli-bugsnag/app/instance-initializers/bugsnag.js:      const originalDidTransition = router.didTransition || Ember.K;
ember-cli-coreweb/app/initializers/ember-coreweb.js:  initialize: Ember.K
ember-cli-dialog/packages/ember-dialog/lib/ember-initializer.js:// var K = Ember.K;
ember-cli-dimple/addon/components/dimple-chart/component.coffee:  customizeChart: Ember.K
ember-cli-dimple/addon/components/dimple-chart/component.js:  customizeChart: Ember.K,
ember-cli-dimple/addon/mixins/resize.js:  onResizeStart: Ember.K,
ember-cli-dimple/addon/mixins/resize.js:  onResizeEnd: Ember.K,
ember-cli-dimple/addon/mixins/resize.js:  onResize: Ember.K,
ember-cli-dynamic-forms/addon/components/dynamic-form.js:  renderSchema: Ember.K,
ember-cli-erraroo/addon/erraroo.js:    const oldEmberOnerror = Ember.onerror || Ember.K;
ember-cli-fullpagejs-view/addon/initializers/remove-fullpage.js:  initialize: Ember.K
ember-cli-infinite-scroll/addon/mixins/infinite-scroll.js:  beforeInfiniteQuery: Ember.K,
ember-cli-ion-rangeslider/addon/ember-ion-rangeslider.js:          onChange: Ember.K,
ember-cli-ion-rangeslider/addon/ember-ion-rangeslider.js:      options.onFinish = Ember.K;
ember-cli-jsoneditor/addon/components/json-editor.js:  onChange: Ember.K,
ember-cli-jsoneditor/addon/components/json-editor.js:  onError: Ember.K,
ember-cli-jsoneditor/addon/components/json-editor.js:  onModeChange: Ember.K,
ember-cli-jsoneditor/addon/components/json-editor.js:  onEditable: Ember.K,
ember-cli-maskedinput/addon/components/masked-input.js:  'on-change': Ember.K,
ember-cli-nvd3/addon/components/nvd3-chart.js:  beforeSetup: Ember.K,
ember-cli-nvd3/addon/components/nvd3-chart.js:  afterSetup: Ember.K,
ember-cli-nvd3-multichart/addon/components/nvd3-multichart.js:  chartContextFn: Ember.K,
ember-cli-remote-inspector/tests/acceptance.js:    this.ember.kill('SIGINT');
ember-cli-remote-inspector/tests/acceptance.js:    this.ember.kill('SIGINT');
ember-cli-selectize/addon/components/ember-selectize.js:  _groupedContentArrayWillChange: Ember.K,
ember-cli-visjs/addon/components/visjs-child.js:  didCreateLayer: Ember.K,
ember-cli-visjs/addon/components/visjs-child.js:  willDestroyLayer: Ember.K,
ember-collection/addon/components/ember-collection.js:          willChange: Ember.K,
ember-collection/addon/components/ember-collection.js:          willChange: Ember.K,
ember-concurrency/addon/utils.js:      let disposer = typeof maybeDisposer === 'function' ? maybeDisposer : Ember.K;
ember-confirm-dialog/addon/components/confirm-dialog.js:  confirmAction: Ember.K,//optional action executed when user confirms the dialog
ember-confirm-dialog/addon/components/confirm-dialog.js:  cancelAction: Ember.K,//optional action executed when user cancels confirmation dialog
ember-cookie-consent-cnil/app/mixins/click-else-where.js:  onOutsideClick: Ember.K,
ember-data-model-fragments/addon/states.js:  propertyWasReset: Ember.K,
ember-data-model-fragments/addon/states.js:  becomeDirty: Ember.K,
ember-data-model-fragments/addon/states.js:  rolledBack: Ember.K,
ember-data-model-fragments/addon/states.js:      pushedData: Ember.K,
ember-data-model-fragments/addon/states.js:      didCommit: Ember.K,
ember-data-sails/addon/initializers/ember-data-sails.js:      methods[level] = Ember.K;
ember-dev-fixtures/private/utils/dev-fixtures/module.js:      define(this.get('fullPath'), ['exports'], Ember.K);
ember-dp-map/addon/components/_dp-base-map-element.js:  didLoadMap: Ember.K
ember-drag-drop/addon/mixins/droppable.js:  acceptDrop: Ember.K,
ember-drag-drop/addon/mixins/droppable.js:  handleDragOver: Ember.K,
ember-drag-drop/addon/mixins/droppable.js:  handleDragOut: Ember.K,
ember-form-object/tests/unit/forms/model-form-test.js:  }).catch(Ember.K);
ember-froala/addon/components/froala-editor.js:        var buttons = this.get('customButtons') || Ember.K;
ember-google-charts/tests/integration/components/options-change-test.js:    this.on('chartDidRender', Ember.K);
ember-img-cache/app/initializers/ember-img-cache.js:  initialize: Ember.K
ember-img-manager/app/utils/img-manager/img-clone-holder.js:  this.handler = Ember.K;
ember-img-manager/app/utils/img-manager/img-clone-holder.js:      this.handler = Ember.K;
ember-img-manager/app/utils/img-manager/img-clone-holder.js:   * @param {Function} [handler=Ember.K]
ember-img-manager/app/utils/img-manager/img-clone-holder.js:    this.handler = handler || Ember.K;
ember-infinity/tests/unit/mixins/route-test.js:      pushObjects: Ember.K,
ember-infinity/tests/unit/mixins/route-test.js:        pushObjects: Ember.K,
ember-infinity/tests/unit/mixins/route-test.js:        pushObjects: Ember.K,
ember-jsonapi-resources/addon/adapters/application.js:    let cleanup = Ember.K;
ember-jsonapi-resources/tests/unit/adapters/application-test.js:  adapter.serializer = {deserialize: sandbox.spy(), deserializeIncluded: Ember.K};
ember-jsonapi-resources/tests/unit/adapters/application-test.js:  adapter.serializer = { deserialize: sandbox.spy(), deserializeIncluded: Ember.K };
ember-jsonapi-resources/tests/unit/mixins/fetch-test.js:  this.subject.serializer = { deserialize: function(res) { return res.data; }, deserializeIncluded: Ember.K };
ember-jsonapi-resources/tests/unit/mixins/resource-operations-test.js:        trigger: Ember.K
ember-jsonapi-resources/tests/unit/mixins/resource-operations-test.js:      guns: {kind: 'hasMany', mapBy: Ember.K }, // hasMany('guns')
ember-jsonapi-resources/tests/unit/mixins/resource-operations-test.js:      horse: {kind: 'hasOne', get: Ember.K } // hasOne('horse')
ember-jsonapi-resources-form/addon/components/resource-form.js:    if (!action) { return Ember.K; /* fail silently if no action */ }
ember-jsonapi-resources-list/addon/mixins/controllers/jsonapi-list.js:    filtering: Ember.K,
ember-key-responder/app/key-responder.js:    comments for Ember.KeyResponderStack above for more insight.
ember-list-card/addon/components/list-card/header-dropdown-item.js:   onSelect: Ember.K,
ember-list-card/addon/components/list-card/header-dropdown-item.js:   onDeselect: Ember.K,
ember-list-card/addon/components/list-card/header-dropdown.js:  onItemSelect: Ember.K,
ember-list-card/addon/components/list-card/header.js:  onQueryOptionSelect: Ember.K,
ember-material-design/addon/mixins/events.js:    start: Ember.K,
ember-material-design/addon/mixins/events.js:    move: Ember.K,
ember-material-design/addon/mixins/events.js:    end: Ember.K
ember-material-design/addon/mixins/gesture-events.js:    onStart: Ember.K,
ember-material-design/addon/mixins/gesture-events.js:    onMove: Ember.K,
ember-material-design/addon/mixins/gesture-events.js:    onEnd: Ember.K,
ember-material-design/addon/mixins/gesture-events.js:    onCancel: Ember.K,
ember-material-design/app/services/ripple.js:            return Ember.K;
ember-metrics/tests/dummy/app/metrics-adapters/local-dummy-adapter.js:  init: Ember.K,
ember-metrics/tests/dummy/app/metrics-adapters/local-dummy-adapter.js:  willDestroy: Ember.K
ember-mixpanel/addon/mixpanel.js:          return Ember.K;
ember-mixpanel/addon/mixpanel.js:      return Ember.K;
ember-notifyme/addon/objects/notification-message.js:  onClick: Ember.K,
ember-notifyme/addon/objects/notification-message.js:  onClose: Ember.K,
ember-notifyme/addon/services/notification-service.js:           onClick: options.onClick || Ember.K,
ember-notifyme/addon/services/notification-service.js:           onClose: options.onClose || Ember.K,
ember-notifyme/addon/services/notification-service.js:           onCloseTimeout: options.onCloseTimeout || Ember.K,
ember-off-canvas-components/addon/initializers/custom-events.js:  initialize: Ember.K
ember-pardon/addon/mixins/ember-pardon.js:	beforeDestroy: Ember.K,
ember-phoenix-channel/tests/integration/components/socket-message-log-test.js:  on: Ember.K
ember-pikaday/addon/components/pikaday-inputless.js:  onPikadayOpen: Ember.K,
ember-pikaday/addon/components/pikaday-inputless.js:  onPikadayClose: Ember.K,
ember-pikaday/addon/mixins/pikaday.js:  onOpen: Ember.K,
ember-pikaday/addon/mixins/pikaday.js:  onClose: Ember.K,
ember-pikaday/addon/mixins/pikaday.js:  onSelection: Ember.K,
ember-pikaday/addon/mixins/pikaday.js:  onDraw: Ember.K,
ember-pikaday-with-time/addon/components/pikaday-input.js:  onPikadayOpen: Ember.K,
ember-pikaday-with-time/addon/components/pikaday-input.js:  onPikadayRedraw: Ember.K,
ember-processes/addon/utils.js:      let disposer = typeof maybeDisposer === 'function' ? maybeDisposer : Ember.K;
ember-render-stack/addon/route-mixin.js:  renderStack: Ember.K,
ember-restless/dist/ember-restless.js:  var noop = Ember.K;
ember-restless/dist/ember-restless.js:    _onPropertyChange: Ember.K
ember-restless/src/model/read-only-model.js:  _onPropertyChange: Ember.K
ember-restless/src/model/state.js:var noop = Ember.K;
ember-reveal-js/addon/components/reveal-presentation/component.js:      before: Ember.K,
ember-rl-dropdown/addon/mixins/rl-dropdown-component.js:  onOpen: Ember.K,
ember-rl-dropdown/addon/mixins/rl-dropdown-component.js:  onClose: Ember.K,
ember-searchable-select/addon/components/searchable-select.js:  'on-change': Ember.K,
ember-searchable-select/addon/components/searchable-select.js:  'on-add': Ember.K,
ember-searchable-select/addon/components/searchable-select.js:  'on-search': Ember.K,
ember-searchable-select/addon/components/searchable-select.js:  'on-close': Ember.K,
ember-searchable-select/addon/components/searchable-select.js:    return this.get('on-add') !== Ember.K;
ember-searchable-select/addon/components/searchable-select.js:    return this.get('on-search') === Ember.K;
ember-searchable-select/tests/dummy/app/pods/components/options-table/template.hbs:    on-change                 | Specify your own named action to trigger when the selection changes. eg. <code>(action "update")</code> <br> For single selection (default behaviour), the selected object is sent as an argument. For multiple selections, an array of options is sent. | Ember action  | Ember.K
ember-searchable-select/tests/dummy/app/pods/components/options-table/template.hbs:    on-add                    | Allow unfound items to be added to the content array by specifying your own named action. eg. `(action "addNew")` The new item name is sent as an argument. You must handle adding the item to the content array and selecting the new item outside the component. | Ember action  | Ember.K
ember-searchable-select/tests/dummy/app/pods/components/options-table/template.hbs:    provided.     | Ember action    | Ember.K
ember-searchable-select/tests/dummy/app/pods/components/options-table/template.hbs:    on-close                  | Specify your own named action to trigger when the menu closes. Useful hook for clearing out content that was previously passed in with AJAX. | Ember action | Ember.K
ember-select-list/addon/components/select-list.js:  action: Ember.K, // action to fire on change
ember-smart-banner/addon/components/smart-banner.js:      const visitFn = Ember.getWithDefault(this, 'attrs.onvisit', Ember.K);
ember-smart-banner/addon/components/smart-banner.js:      const closeFn = Ember.getWithDefault(this, 'attrs.onclose', Ember.K);
ember-sqlite-adapter/addon/migration.js:  run: Ember.K,
ember-stripe-service/addon/services/stripe.js:      this.card[name] = Ember.K;
ember-table/addon/components/ember-table.js:    addColumn: Ember.K,
ember-table/addon/components/ember-table.js:    sortByColumn: Ember.K
ember-table/addon/mixins/mouse-wheel-handler.js:  onMouseWheel: Ember.K,
ember-table/addon/mixins/resize-handler.js:  onResizeStart: Ember.K,
ember-table/addon/mixins/resize-handler.js:  onResizeEnd: Ember.K,
ember-table/addon/mixins/resize-handler.js:  onResize: Ember.K,
ember-table/addon/mixins/scroll-handler.js:  onScroll: Ember.K,
ember-table/addon/mixins/touch-move-handler.js:  onTouchMove: Ember.K,
ember-table/addon/models/column-definition.js:  setCellContent: Ember.K,
ember-table/addon/views/lazy-item.js:  prepareContent: Ember.K,
ember-table/addon/views/lazy-item.js:  teardownContent: Ember.K,
ember-ted-select/README.md:      <td><code>Ember.K</code> (noop)</td>
ember-ted-select/tests/dummy/app/pods/application/template.hbs:        <td><code>Ember.K</code> (noop)</td>
ember-theater/addon/ember-theater/director/directions/sound.js:  fadeTo(volume, duration, callback = Ember.K) {
ember-to-string/tests/unit/helpers/to-string-test.js:    lookup: Ember.K
ember-ui-components/addon/mixins/click-outside.js:  handleClickOutside: Ember.K,
ember-unauthorized/tests/unit/mixins/access-test.js:  subject.set('authorize', Ember.K);
ember-unauthorized/tests/unit/mixins/access-test.js:  subject.set('authorize', Ember.K);
ember-unauthorized/tests/unit/mixins/route-access-test.js:      transitionTo: Ember.K
ember-watson/tests/fixtures/resource-router-mapping/new-complex-ember-cli-sample.js:    }, Ember.K);
ember-watson/tests/fixtures/resource-router-mapping/old-complex-ember-cli-sample.js:    this.resource('dashboard', Ember.K);
emberx-select/addon/components/x-select.js:  "on-blur": Ember.K,
emberx-select/addon/components/x-select.js:  "on-click": Ember.K,
emberx-select/addon/components/x-select.js:  "on-change": Ember.K,
emberx-select/addon/components/x-select.js:  "on-focus-out": Ember.K,
fireplace/addon/collections/indexed.js:      then(Ember.K.bind(this));
fireplace/addon/collections/object.js:    const promise = this.listenToFirebase().then(Ember.K.bind(this));
justa-table/addon/components/table-vertical-collection.js:  'on-row-click': Ember.K
list-view/addon/list-view-mixin.js:  _scrollTo: Ember.K,
list-view/addon/list-view-mixin.js:  arrayWillChange: Ember.K,
list-view/addon/reusable-list-item-view.js:  prepareForReuse: Ember.K,
mantel/addon/fireplace/collections/indexed.js:      then(Ember.K.bind(this));
mantel/addon/fireplace/collections/object.js:    var promise = this.listenToFirebase().then(Ember.K.bind(this));
plaid/addon/mixins/dimensions.js:  didMeasureDimensions: Ember.K,
plaid/addon/mixins/global-resize.js:  didResize: Ember.K
spree-ember-paypal-express/addon/services/paypal-express.js:  spree: Ember.K,
torii/addon/services/torii-session.js:  setUnknownProperty: Ember.K,
torii/tests/unit/redirect-handler-test.js:    close: Ember.K
torii/tests/unit/services/popup-test.js:    focus: Ember.K,
torii/tests/unit/services/popup-test.js:    close: Ember.K

Addendum 3 - Ember.K usage via destructuring across published addons

CogAuth/tests/helpers/flash-message.js:const { K } = Ember;
ember-annotative-models/addon/utils/action.coffee:{K, isBlank, A} = Ember
ember-annotative-models/tests/unit/utils/action-test.coffee:{K} = Ember
ember-cli-airbrake/addon/utils/get-client.js:const { K } = Ember;
ember-cli-flash/blueprints/ember-cli-flash/files/tests/helpers/flash-message.js:const { K } = Ember;
ember-cli-mapkit/addon/components/ui-abstract-map.js:const {isEmpty, computed, on, K, run} = Ember;
ember-cli-pixijs/addon/components/pixi-canvas.js:const { Component, computed, K } = Ember;
ember-click-outside/addon/mixins/click-outside.js:const { computed, K } = Ember;
ember-composable-helpers/addon/-private/create-needle-haystack-helper.js:const { K, isEmpty } = Ember;
ember-composable-helpers/tests/unit/helpers/pipe-test.js:const { RSVP: { resolve, reject }, K } = Ember;
ember-composable-helpers/tests/unit/helpers/queue-test.js:const { RSVP: { resolve, reject }, K } = Ember;
ember-d3-helpers/tests/unit/helpers/d3-line-test.js:const { K } = Ember;
ember-form-object/addon/forms/model-form.js:const { ObjectProxy, computed, computed: { readOnly }, assert, Logger, run, A: createArray, K: noop, String: { camelize } } = Ember;
ember-form-tool/addon/mixins/drag-drop.coffee:{K, Mixin, computed: {equal}} = Ember
ember-functional-helpers/addon/-private/create-needle-haystack-helper.js:const { K, isEmpty } = Ember;
ember-functional-helpers/tests/unit/helpers/pipe-test.js:const { RSVP: { resolve, reject }, K } = Ember;
ember-functional-helpers/tests/unit/helpers/queue-test.js:const { RSVP: { resolve, reject }, K } = Ember;
ember-imdt-magic-crud/tests/helpers/flash-message.js:const { K } = Ember;
ember-keyword-complete/addon/components/keyword-complete.js:const {observer, computed, run, assert, K, $} = Ember;
ember-leaflet/addon/components/base-layer.js:const { assert, computed, Component, run, K, A, String: { classify } } = Ember;
ember-light-table/tests/helpers/responsive.js:const { K, getOwner } = Ember;
ember-metrics/tests/unit/services/metrics-test.js:const { get, set, K } = Ember;
ember-paper/addon/components/paper-autocomplete.js:const { assert, computed, inject, isNone, defineProperty, K: emberNop } = Ember;
ember-paper/addon/mixins/events-mixin.js:const { Mixin, K } = Ember;
ember-redux/app/services/redux.js:const { assert, isArray, K } = Ember;
ember-responsive/blueprints/ember-responsive/files/tests/helpers/responsive.js:const { K, getOwner } = Ember;
ember-select-box/addon/mixins/select-box/select-box/inputtable.js:const { K } = Ember;
ember-shepherd/addon/services/tour.js:const { Evented, K, Service, isPresent, run, $, isEmpty, observer } = Ember;
ember-simple-auth/addon/session-stores/cookie.js:const { RSVP, computed, run: { next, cancel, later, scheduleOnce }, isEmpty, typeOf, testing, isBlank, isPresent, K, A } = Ember;
ember-simple-auth/tests/unit/internal-session-test.js:const { RSVP, K, run: { next } } = Ember;
ember-simple-auth/tests/unit/session-stores/shared/store-behavior.js:const { run: { next }, K } = Ember;
ember-simple-auth-chrome-app/tests/unit/session-stores/shared/store-behavior.js:const { K, run: { next } } = Ember;
ember-sinon-qunit/tests/helpers/assert-sinon-in-test-context.js:const { K: EmptyFunc, typeOf } = Ember;
ember-user-activity/tests/unit/services/user-activity-test.js:const { A: emberArray, K: noOp, typeOf } = Ember;
ui-bootstrap/tests/helpers/flash-message.js:const { K } = Ember;
yes-or-no/tests/helpers/responsive.js:const { K, getOwner } = Ember;
  • Start Date: 2016-11-22
  • RFC PR: (leave this empty)
  • Ember Issue: (leave this empty)

Summary

The goal of this RFC is to remove the data-adapter, injectStore, transforms, and store Ember application initializers that Ember Data injects into apps. The ember-data initializer will not be changed and any code that previously depended on the ordering of these initializers (via the before or after properties on an initalizer) can be changed to use the ember-data initializers for ordering.

Motivation

The initializers data-adapter, injectStore, transforms, and store have not been used by Ember Data since Apr 8, 2014. However, they are still injected into every Ember app that depends on Ember Data because existing apps may depend on these initializers for ordering their own initializers to run before or after Ember Data's setup code.

Removing these initializers will help reduce the amount of code Ember Data needs to support.

Since these initializers are noop functions that run after the ember-data initializer, any initializers that depends on one of the deprecated initializers listed in this rfc can easly be replaced by depending on the ember-data initializer instead.

Detailed design

Ember Data's instance initializer will start checking for any initializers whose before or after properties depend on one of these deprecated initalizer. If it finds an initalizer that references one of the deprecated initalizers, Ember Data will then log a deprecation message that states the name of the offending initalizers and suggest changing the before or after property (the deprecation message will refer to the correct property dynamically) to depend on Ember Data instead.

This deprecation message will continue to appear until Ember Data 3.0.0 when these initalizers and the deprecation code will be finally removed.

How We Teach This

This change should have no impact on how we teach Ember or Ember Data. The initalizers that will be removed have been unused for a long time and are not mentioned anywhere in today's guides or API docs.

Users who need to run initalizer code before or after Ember Data injects the store into routes should be taught to use before: 'ember-data', or after: 'ember-data' on their initializers.

Drawbacks

  • This change will require users who depend on these deprecated initalizers to update their code.

Alternatives

  • We could leave the noop initalizers in Ember Data and continue to support them in Ember Data 3.0.0 and beyond.

Unresolved questions

None

  • Start Date: 2016/12/05
  • RFC PR: https://github.com/emberjs/rfcs/pull/186
  • Ember Issue: (leave this empty)

Summary

Track unique history location states

Motivation

The path alone does not provide enough information. For example, if you visit page A, scroll down, then click on a link to page B, then click on a link back to page A. Your actual browser history stack is [A, B, A]. Each of those nodes in the history should have their own unique scroll position. In order to record this position we need a UUID for each node in the history.

This API will allow other libraries to reflect upon each location to determine unique state. For example, ember-router-scroll is making use of a modified Ember.HistoryLocation object to get this behavior.

Tracking unique state is required when setting the scroll position properly based upon where you are in the history stack, as described in Motivation

Detailed design

Code: PR#14011

We simply unique identifier (UUID) so we can track uniqueness on two dimensions. Both path and the generated uuid. A simple UUID generator such as https://gist.github.com/lukemelia/9daf074b1b2dfebc0bd87552d0f6a537 should suffice.

How We Teach This

We could describe what meta data is generated for each location in the history stack. For example, it could look like:

// visit page A

[
  { path: '/', uuid: 1 }
]

// visit page B

[
  { path: '/about', uuid: 2 },
  { path: '/', uuid: 1 }
]

// visit page A

[
  { path: '/', uuid: 3 },
  { path: '/about', uuid: 2 },
  { path: '/', uuid: 1 }
]

// click back button

[
  { path: '/about', uuid: 2 },
  { path: '/', uuid: 1 }
]

Drawbacks

  • The property access is writable

Alternatives

The purpose for this behavior is to enable scroll position libraries. There are two other solutions in the wild. One is in the guides that suggests resetting the scroll position to (0, 0) on each new route entry. The other is ember-memory-scroll which I believe is better suited for tracking scroll positions for components rather than the current page.

However, in both cases neither solution provides the experience that users have come to expect from server-rendered pages. The browser tracks scroll position and restores it when you revisit the page in the history stack. The scroll position is unique even if you have multiple instances of the same page in the stack.

Unresolved questions

None at this time.

  • Start Date: 2016-12-14
  • RFC PR: #191
  • Ember Issue: #14711

Summary

We would like to deprecate and remove the arguments passed to the didInitAttrs, didReceiveAttrs and didUpdateAttrs component lifecycle hooks. These arguments are currently undocumented on purpose and considered a private API, imposes an unnecessary performance hit on all components whether they are used or not, and can be easily replicated by the users in cases where they are needed.

Motivation

In the road leading up to Ember.js 2.0, new lifecycle hooks were introduced to components in order to help users shift to a new mental model, dubbed Data Down Actions Up. The hooks were introduced by name, and their semantics explained, but there were no mentions of possible arguments received by them.

This lack of documentation for lifecycle hook arguments was deliberate. The hooks were introduced as an experiment with an eye to the then-upcoming angle bracket components, so the arguments to the hooks were considered private by the framework maintainers, as their design was still ongoing.

However, references to the lifecycle hook arguments started appearing in community resources. Users started betting on these arguments as the way forward, which in conjunction with the lack of an RFC process and clear messaging from the Ember.js maintainers lead to confusion.

This left the core team in a difficult position. Despite no longer endorsing lifecycle hook arguments, trying to communicate such could have the reverse effect by pointing a spotlight at them. The purpose of this RFC is then to clarify that lifecycle hook arguments have no future in the framework, and you should update your code to not make use of them.

The reason to officially deprecate lifecycle hook arguments is not only about messaging, but also because providing these arguments imposes an unnecessary performance penalty to every component in your application even if the arguments are not used.

To provide the arguments to the lifecycle hooks, Ember.js has to eagerly "reify" and save-off any passed-in attributes to allow diffing and construct several wrapper objects. In the few occasions where this logic is actually necessary, developers should be able to use programmatic patterns familiar to them and manually track changes as needed, as exemplified in the Transition Path section below.

Transition Path

The transition path followed will be the standard one, which encompasses using the deprecation API to deprecate the feature and the related deprecation guide. While the lifecycle hooks share a deprecation identifier, they will be addressed in turn.

didInitAttrs

Since this lifecycle hook is already deprecated, we suggest taking this chance to address two deprecations at the same time. Imagine you have a component that stores a timestamp when it's initialized for later comparison.

Before:

Ember.Component.extend({
  didInitAttrs({ attrs }) {
    this.set('initialTimestamp', attrs.timestamp);
  }
});

After:

Ember.Component.extend({
  init() {
    this._super(...arguments);

    this.set('initialTimestamp', this.get('timestamp'));
  }
});

didReceiveAttrs

Let's say you want to animate a map widget from the old coordinates to the new coordinates.

Before:

Ember.Component.extend({
  didReceiveAttrs({ oldAttrs, newAttrs }) {
    if (oldAttrs && oldAttrs.coordinates !== newAttrs.coordinates) {
      this.map.move({ from: oldAttrs.coordinates, to: newAttrs.coordinates });
    }
  }
});

After:

Ember.Component.extend({
  didReceiveAttrs() {
    let oldCoordinates = this.get('_previousCoordinates');
    let newCoordinates = this.get('coordinates');

    if (oldCoordinates && oldCoordinates !== newCoordinates) {
      this.map.move({ from: oldCoordinates, to: newCoordinates });
    }

    this.set('_previousCoordinates', newCoordinates);
  }
});

didUpdateAttrs

This hook is very similar to didReceiveAttrs, except it only runs on re-renders and not the initial render.

Before:

Ember.Component.extend({
  didUpdateAttrs({ oldAttrs, newAttrs }) {
    if (oldAttrs && oldAttrs.coordinates !== newAttrs.coordinates) {
      this.map.move({ from: oldAttrs.coordinates, to: newAttrs.coordinates });
    }
  }
});

After:

Ember.Component.extend({
  didUpdateAttrs() {
    let oldCoordinates = this.get('_previousCoordinates');
    let newCoordinates = this.get('coordinates');

    if (oldCoordinates && oldCoordinates !== newCoordinates) {
      this.map.move({ from: oldCoordinates, to: newCoordinates });
    }

    this.set('_previousCoordinates', newCoordinates);
  }
});

How We Teach This

Due to the previous undocumented nature of the arguments, there is no official documentation that will require updating deprecated usage.

As required for framework deprecations, there will be a deprecation guide written up and linked from within the deprecation message. This deprecation guide will address the more common usage patterns associated with lifecycle hook arguments, such as the Transition Path example.

Additionally, the usage patterns present in the deprecation guide could also be documented in the component section of the official Guides, as a proactive approach for teaching newcomers.

Drawbacks

One immediate drawback of this proposal is that due to references to the arguments in community resources, there are uses of them in the wild. Updating deprecated code will have to be done mostly manually, as automation might prove difficult.

Another drawback is that by the very nature of publishing this RFC, attention will be drawn to the arguments. It is our hope that the increased awareness will be a net positive due to the clear guidance gained by users of the framework.

It is then our assessment that these drawbacks are outweighed by the benefits of the change.

Alternatives

There are two standout alternatives to the proposal presented here which are doing nothing, or making the arguments public and supporting them going forward, both of which are less than ideal for reasons stated previously.

Doing nothing would perpetuate the confusion surrounding lifecycle hook arguments. While it might be argued that that ship has sailed, we prefer to think that it's never too late to provide users of the framework with clearer messaging regarding usage of certain features.

Making the arguments public and supported would mean supporting APIs that did not go through the RFC process, meaning they do not align with some of the current values of the framework, nor would iteration on them would be possible without introducing breakage. Additionally, there are some performance penalties to supporting these arguments, mentioned in the Motivation section.

Unresolved questions

None.

  • Start Date: 2016-12-24
  • RFC PR: https://github.com/emberjs/rfcs/pull/194
  • Ember Issue: https://github.com/emberjs/ember.js/issues/14754

Summary

Support for component eventMangers is a seldom used feature and should be deprecated.

Motivation

We should strive to simplify the Ember API and source code where possible. As the custom eventManager feature is rarely used in apps, we should deprecate it.

Detailed design

We'll introduce a deprecation warning which will be displayed when a component defines an eventManager property or when canDispatchToEventManager is set to true on EventDispatcher. The warning will have a target version of 3.0.

If required, we can create an addon which extends the EventDispatcher allowing for opt-in custom eventManagers in Ember apps.

How We Teach This

As this is a seldom used feature, we can simply note the deprecation in a future release blog post.

Drawbacks

This adds a little more churn for apps that rely on this feature.

Alternatives

This feature was recently made pay-as-you-go, so the immediate performance concerns have been addressed. We could decide to leave this in the framework as an opt-in feature.

  • Start Date: 2017-03-13
  • RFC PR: https://github.com/emberjs/rfcs/pull/213
  • Ember Issue: https://github.com/emberjs/ember.js/issues/16301

Summary

This RFC aims to expose a low-level primitive for defining custom components.

This API will allow addon authors to provide special-purpose component base classes that their users can subclass from in apps. These components are invokable in templates just like any other Ember components (descendants of Ember.Component) today.

Motivation

The ability to author reusable, composable components is a core feature of Ember.js. Despite being a last-minute addition to Ember 1.0, the Ember.Component API and programming model has proven itself to be an extremely versatile tool and has aged well over time into the primary unit of composition in Ember's view layer.

That being said, the current component API (hereinafter "classic components") does have some noticeable shortcomings. Over time, classic components have also accumulated some cruft due to backwards compatibility constraints.

These problems led to the original "angle bracket components" proposal (see RFC #15 and #60), which promised to address these problems via the angle bracket invocation opt-in (i.e. <foo-bar ...> instead of {{foo-bar ...}}).

Since the transition to the angle bracket invocation syntax was seen as a rare, once-in-a-lifetime opportunity, it became very tempting to debate every single shortcomings and missing features in the classic components API in the process and attempt to design solutions for all of them.

While that discussion was very helpful in capturing constraints and guiding the overall direction, designing that One True API™ in the abstract turned out to be extremely difficult. It also went against our philosophy that framework features should be extracted from applications and designed iteratively with feedback from real-world usage.

Since that original proposal, we have rewritten Ember's rendering engine from the ground up (the "Glimmer 2" project). One of the goals of the Glimmer 2 effort was to build first-class support for Ember's view-layer features into the rendering engine. As part of the process, we worked to rationalize these features and to re-think the role of components in Ember.js. This exercise has brought plenty of new ideas and constraints to the table.

The initial Glimmer 2 integration was completed in Ember 2.10. As of that version, classic components have been re-implemented using the new primitives provided by the rendering engine, and we are very happy with the results.

This approach yielded a number of very powerful and flexible primitives: in addition to classic components, we were able to implement Ember's {{mount}}, {{outlet}} and {{render}} helpers as "components" under the hood.

Based on our experience, we believe it would be beneficial to open up these new primitives to the wider community. Specifically, there are at least two clear benefits that comes to mind:

First, it provides addon authors fine-grained control over the exact behavior and semantics of their components in cases where the general-purpose components are a poor fit. For example, a low-overhead component designed to be used in performance hotspot can opt-out of certain convinence features using this API.

Second, it allows the community to experiment with and iterate on alternative component APIs outside of the core framework. Following the success of FastBoot and Engines, we believe the best way to design the new "Glimmer Components" API is to first stablize the underlying primitives in the core framework and experiment with the surface API through an addon.

Detailed design

This RFC introduces the concept of component managers. A component manager is an object that is responsible for coordinating the lifecycle events that occurs when invoking, rendering and re-rendering a component.

Registering component managers

Component managers are registered with the component-manger type in the application's registry. Similar to services, component managers are singleton objects (i.e. { singleton: true, instantiate: true }), meaning that Ember will create and maintain (at most) one instance of each unique component manager for every application instance.

To register a component manager, an addon will put it inside its app tree:

// ember-basic-component/app/component-managers/basic.js

import EmberObject from '@ember/object';

export default EmberObject.extend({
  // ...
});

(Typically, the convention is for addons to define classes like this in its addon tree and then re-export them from the app tree. For brevity, we will just inline them in the app tree directly for the examples in this RFC.)

This allows the component manager to participate in the DI system – receiving injections, using services, etc. Alternatively, component managers can also be registered with imperative API. This could be useful for testing or opt-ing out of the DI system. For example:

// ember-basic-component/app/initializers/register-basic-component-manager.js

const MANAGER = {
  // ...
};

export function initialize(application) {
  // We want to use a POJO here, so we are opt-ing out of instantiation
  application.register('component-manager:basic', MANAGER, { instantiate: false });
}

export default {
  name: 'register-basic-component-manager',
  initialize
};

Determining which component manager to use

For the purpose of this section, we will assume components with a JavaScript file (such as app/components/foo-bar.js or the equivilant in "pods" and Module Unification apps) and optionally a template file (app/templates/components/foo-bar.hbs or equivilant). The example section has additional information about how this relates to template-only components.

When invoking the component {{foo-bar ...}}, Ember will first resolve the component class (component:foo-bar, usually the default export from app/components/foo-bar.js). Next, it will determine the appropiate component manager to use based on the resolved component class.

Ember will provide a new API to assign the component manager for a component class:

// my-app/app/components/foo-bar.js

import EmberObject from '@ember/object';
import { setComponentManager } from '@ember/component';

export default setComponentManager('awesome', EmberObject.extend({
  // ...
}));

This tells Ember to use the awesome manager (component-manager:awesome) for the foo-bar component. setComponentManager function returns the class.

In the future, this function can also be invoked as a decorator:

// my-app/app/components/foo-bar.js

import EmberObject from '@ember/object';
import { componentManager } from '@ember/component';

export default @componentManager('awesome') EmberObject.extend({
  // ...
});

In reality, an app developer would never have to write this in their apps, since the component manager would already be assigned on a super-class provided by the framework or an addon. The setComponentManager function is essentially a low-level API designed for addon authors and not intended to be used by app developers.

For example, the Ember.Component class would have the classic component manager pre-assigned, therefore the following code will continue to work as intended:

// my-app/app/components/foo-bar.js

import Component from '@ember/component';

export default Component.extend({
  // ...
});

Similarly, an addon can provided the following super-class:

// ember-basic-component/addon/index.js

import EmberObject from '@ember/object';
import { componentManager } from '@ember/component';

export default setComponentManager('basic', EmberObject.extend({
  // ...
}));

With this, app developers can simply inherit from this in their app:

// my-app/app/components/foo-bar.js

import BasicComponent from 'ember-basic-component';

export default BasicComponent.extend({
  // ...
});

Here, the foo-bar component would automatically inherit the basic component manager from its super-class.

It is not advisable to override the component manager assigned by the framework or an addon. Attempting to reassign the component manager when one is already assinged on a super-class will be an error. If no component manager is set, it will also result in a runtime error when invoking the component.

Component Lifecycle

Back to the {{foo-bar ...}} example.

Once Ember has determined the component manager to use, it will be used to manage the component's lifecycle.

createComponent

The first step is to create an instance of the component. Ember will invoke the component manager's createComponent method:

// ember-basic-component/app/component-managers/basic.js

import EmberObject from '@ember/object';

export default EmberObject.extend({
  createComponent(factory, args) {
    return factory.create(args.named);
  },

  // ...
});

The createComponent method on the component manager is responsible for taking the component's factory and the arguments passed to the component (the ... in {{foo-bar ...}}) and return an instantiated component.

The first argument passed to createComponent is the result returned from the factoryFor API. It contains a class property, which gives you the the raw class (the default export from app/components/foo-bar.js) and a create function that can be used to instantiate the class with any registered injections, merging them with any additional properties that are passed.

The second argument is a snapshot of the arguments passed to the component in the template invocation, given in the following format:

{
  positional: [ ... ],
  named: { ... }
}

For example, given the following invocation:

{{blog-post (titleize post.title) post.body author=post.author excerpt=true}}

You will get the following as the second argument:

{
  positional: [
    "Rails Is Omakase",
    "Lorem ipsum dolor sit amet, consectetur adipiscing elit..."
  ],
  named: {
    "author": #<User name="David Heinemeier Hansson", ...>,
    "excerpt": true
  }
}

The arguments object should not be mutated (e.g. args.positional.pop() is no good). In development mode, it might be sealed/frozen to help prevent these kind of mistakes.

getContext

Once the component instance has been created, the next step is for Ember to determine the this context to use when rendering the component's template by calling the component manager's getContext method:

// ember-basic-component/app/component-managers/basic.js

import EmberObject from '@ember/object';

export default EmberObject.extend({
  createComponent(factory, args) {
    return factory.create(args.named);
  },

  getContext(component) {
    return component;
  },

  // ...
});

The getContext method gets passed the component instance returned from createComponent and should return the object that {{this}} should refer to in the component's template, as well as for any "fallback" property lookups such as {{foo}} where foo is neither a local variable or a helper (which resolves to {{this.foo}} where this is here is the object returned by getContext).

Typically, this method can simpliy return the component instance, as shown in the example above. The reason this exists as a separate method is to enable the so-called "state bucket" pattern which allows addon authors to attach extra book-keeping metadata to the component:

// ember-basic-component/app/component-managers/basic.js

import EmberObject from '@ember/object';

export default EmberObject.extend({
  createComponent(factory, args) {
    let metadata = { ... };
    let instance = factory.create(args.named);
    return { metadata, instance, ... };
  },

  getContext(bucket) {
    return bucket.instance;
  },

  // ...
});

Since the "state bucket", not the "context", is passed back to other hooks on the component manager, this allows the component manager to access the extra metadata but otherwise hide them from the app developers.

We will see an example that uses this pattern in a later section.

At this point, Ember will have gathered all the information it needs to render the component's template, which will be rendered with "Outer HTML" semantics.

In other words, the content of the template will be rendered as-is, without a wrapper element (e.g. <div id="ember1234" class="ember-view">...</div>), except for subclasses of Ember.Component, which will retain the current legacy behavior (the internal classic manager uses private capabilities to achieve that).

This API does not currently provide any way to fine-tune the rendering behavior (such as dynamically changing the component's template) besides getContext, but future iterations may introduce extra capabilities.

updateComponent

When it comes time to re-render a component's template (usually because an argument has changed), Ember will call the manager's updateComponent method to give the manager an opportunity to reflect those changes on the component instance, before performing the re-render:

// ember-basic-component/app/component-managers/basic.js

import EmberObject from '@ember/object';

export default EmberObject.extend({
  createComponent(factory, args) {
    return factory.create(args.named);
  },

  getContext(component) {
    return component;
  },

  updateComponent(component, args) {
    component.setProperties(args.named);
  },

  // ...
});

The first argument passed to this method is the component instance returned by createComponent. As mentioned above, using the "state bucket" pattern will allow this hook to access the extra metadata:

// ember-basic-component/app/component-managers/basic.js

import EmberObject from '@ember/object';

export default EmberObject.extend({
  createComponent(factory, args) {
    let metadata = { ... };
    let instance = factory.create(args.named);
    return { metadata, instance, ... };
  },

  getContext(bucket) {
    return bucket.instance;
  },

  updateComponent(bucket, args) {
    let { metadata, instance } = bucket;
    // do things with metadata
    instance.setProperties(args.named);
  },

  // ...
});

The second argument is a snapshot of the updated arguments, passed with the same format as in createComponent. Note that there is no guarentee that anything in the arguments object has actually changed when this method is called. For example, given:

{{blog-post title=(uppercase post.title) ...}}

Imagine if post.title changed from fOo BaR to FoO bAr. Since the value is passed through the uppercase helper, the component will see FOO BAR in both cases.

Generally speaking, Ember does not provide any guarentee on how it determines whether components need to be re-rendered, and the semantics may vary between releases – i.e. this method may be called more or less often as the internals changes. The only guarentee is that if something has changed, this method will definitely be called.

If it is important to your component's programming model to only notify the component when there are actual changes, the manager is responsible for doing the extra book-keeping.

For example:

// ember-basic-component/index.js

import EmberObject from '@ember/object';
import { setComponentManager } from '@ember/component';

function NOOP() {}

export default setComponentManager('basic', EmberObject.extend({
  // Users of BasicComponent can override this hook to be notified when an
  // argument will change
  argumentWillChange: NOOP,

  // Users of BasicComponent can override this hook to be notified when an
  // argument will change
  argumentDidChange: NOOP,

  // ...
}));
// ember-basic-component/app/component-managers/basic.js

import EmberObject from '@ember/object';

export default EmberObject.extend({
  createComponent(factory, args) {
    return {
      args: args.named,
      instance: factory.create(args.named)
    };
  },

  getContext(bucket) {
    return bucket.instance;
  },

  updateComponent(bucket, args) {
    let instance = bucket.instance;
    let oldArgs = bucket.args;
    let newArgs = args.named;
    let changed = false;

    // Since the arguments are coming from the template invocation, you can
    // generally assume that they have exactly the same keys. However, future
    // additions such as "splat arguments" in the template layer might change
    // that assumption.
    for (let key in oldArgs) {
      let oldValue = oldArgs[key];
      let newValue = newArgs[key];

      if (oldValue !== newValue) {
        instance.argumentWillChange(key, oldValue, newValue);
        instance.set(key, newValue);
        instance.argumentDidChange(key, oldValue, newValue);
      }
    }

    bucket.args = newArgs;
  },

  // ...
});

This example also shows when the "state bucket" pattern could be useful.

The return value of the updateComponent is ignored.

After calling the updateComponent method, Ember will update the component's template to reflect any changes.

Capabilities

In addition to the methods specified above, component managers are required to have a capabilities property. This property must be set to the result of calling the capabilities function provided by Ember.

Versioning

The first, mandatory, argument to the capabilities function is the component manager API, which is denoted in the ${major}.${minor} format, matching the minimum Ember version this manager is targeting. For example:

// ember-basic-component/app/component-managers/basic.js

import { capabilities } from '@ember/component';
import EmberObject from '@ember/object';

export default EmberObject.extend({
  capabilities: capabilities('3.2'),

  createComponent(factory, args) {
    return factory.create(args.named);
  },

  getContext(component) {
    return component;
  },

  updateComponent(component, args) {
    component.setProperties(args.named);
  }
});

This allows Ember to introduce new capabilities and make improvements to this API without breaking existing code.

Here is a hypothical scenario for such a change:

  1. Ember 3.2 implemented and shipped the component manager API as described in this RFC.

  2. The ember-basic-component addon released version 1.0 with the component manager shown above (notably, it declared capabilities('3.2')).

  3. In Ember 3.5, we determined that constructing the arguments object passed to the hooks is a major performance bottleneck, and changes the API to pass a "proxy" object with getter methods instead (e.g. args.getPositional(0) and args.getNamed('foo')).

    However, since Ember sees that the basic component manager is written to target the 3.2 API version, it will retain the old behavior and passes the old (more expensive) "reified" arguments object instead, to avoid breakage.

  4. The ember-basic-component addon author would like to take advantage of this performance optimization, so it updates its component manager code to work with the arguments proxy and changes its capabilities declaration to capabilities('3.5') in version 2.0.

This system allows us to rapidly improve the API and take advantage of underlying rendering engine features as soon as they become available.

Note that addon authors are not required to update to the newer API. Concretely, component manager APIs have the following support policy:

  • API versions will continue to be supported in the same major release of Ember. As shown in the example above, ember-basic-component 1.0 (which targets component manager API version 3.2), will continue to work on Ember 3.5. However, the reverse is not true – component manager API version 3.5 will (somewhat obviously) not work in Ember 3.2.

  • In addition, to ensure a smooth transition path for addon authors and app developers across major releases, each Ember version will support (at least) the previous LTS version as of the release was made. For example, if 3.16 is the last LTS release of the 3.x series, the component manager API version 3.16 will be supported by Ember 4.0 through 4.4, at minimum.

Addon authors can also choose to target multiple versions of the component manager API using ember-compatibility-helpers:

// ember-basic-component/app/component-managers/basic.js

import { gte } from 'ember-compatibility-helpers';

let ComponentManager;

if (gte('3.5')) {
  ComponentManager = EmberObject.extend({
    capabilities: capabilities('3.5'),

    // ...
  });
} else {
  ComponentManager = EmberObject.extend({
    capabilities: capabilities('3.2'),

    // ...
  });
}

export default ComponentManager;

Since the conditionals are resolved at build time, the irrevelant code will be stripped from production builds, avoiding any deprecation warnings.

Optional Features

The second, optional, argument to the capabilities function is an object enumerating the optional features requested by the component manager.

In the hypothical example above, while the "reified" arguments objects may be a little slower, they are certainly easier to work with, and the performance may not matter to but the most performance critical components. A component manager written for Ember 3.5 (again, only hypothically) and above would be able to explicitly opt back into the old behavior like so:

// ember-basic-component/app/component-managers/basic.js

import { capabilities } from '@ember/component';
import EmberObject from '@ember/object';

export default EmberObject.extend({
  capabilities: capabilities('3.5', {
    reifyArguments: true
  }),

  // ...
});

In general, we will aim to have the defaults set to as bare-bone as possible, and allow the component managers to opt into the features they need in a PAYGO (pay-as-you-go) manner, which aligns with the Glimmer VM philosophy. As the rendering engine evolves, more and more feature will become optional.

Optional Capabilities

The following optionally capabilities will be available with the first version of the component manager API. We expect future RFCs to propose additional capabilities within the framework provided by this initial RFC.

Async Lifecycle Callbacks

When the asyncLifecycleCallbacks capability is set to true, the component manager is expected to implement two additional methods: didCreateComponent and didUpdateComponent.

didCreateComponent will be called after the component has been rendered the first time, after the whole top-down rendering process is completed. Similarly, didUpdateComponent will be called after the component has been updated, after the whole top-down rendering process is completed. This would be the right time to invoke any user callbacks, such as didInsertElement and didRender in the classic components API.

These methods will be called with the component instance (the "state bucket" returned by createComponent) as the only argument. The return value is ignored.

These callbacks are called if and only if their synchronous APIs were invoked during rendering. For example, if updateComponent was called on during rendering (and it completed without errors), didUpdateComponent will always be called. Conversely, if didUpdateComponent is called, you can infer that the updateComponent was called on the same component instance during rendering.

This API provides no guarentee about ordering with respect to siblings or parent-child relationships.

Destructors

When the destructor capability is set to true, the component manager is expected to implement an additional method: destroyComponent.

destroyComponent will be called when the component is no longer needed. This is intended for performing object-model level cleanup.

Because this RFC does not provide ways to access or observe the component's DOM tree, the timing relative to DOM teardown is undefined (i.e. whether this is called before or after the component's DOM tree is removed from the document).

Therefore, this hook is not suitable for invoking user callbacks intended for performing DOM cleanup, such as willDestroyElement in the classic components API. We expect a subsequent RFC addressing DOM-related functionalities to clarify this issues or provide another specialized method for that purpose.

Similar to the other async lifecycle callbacks, this API provides no guarentee about ordering with respect to siblings or parent-child relationships. Further, the exact timing of the calls are also undefined. For example, the calls from several render loops might be batched together and deferred into a browser idle callback.

Examples

Basic Component Manager

Here is the simpliest end-to-end component manager example that uses a plain Ember.Object super-class (as opposed to Ember.Component) with "Outer HTML" semantics:

// ember-basic-component/app/component-managers/basic.js

import { capabilities } from '@ember/component';
import EmberObject from '@ember/object';

export default EmberObject.extend({
  capabilities: capabilities('3.2', {
    destructor: true
  }),

  createComponent(factory, args) {
    return factory.create(args.named);
  },

  getContext(component) {
    return component;
  },

  updateComponent(component, args) {
    component.setProperties(args.named);
  },

  destroyComponent(component) {
    component.destroy();
  }
});
// ember-basic-component/addon/index.js

import EmberObject from '@ember/object';
import { setComponentManager } from '@ember/component';

export default setComponentManager('basic', EmberObject.extend());

Usage

// my-app/app/components/x-counter.js

import BasicCompoment from 'ember-basic-component';

export default BasicCompoment.extend({
  init() {
    this.count = 0;
  },

  down() {
    this.decrementProperty('count');
  },

  up() {
    this.incrementProperty('count');
  }
});
{{!-- my-app/app/templates/components/x-counter.hbs --}}

<div>
  <button {{action this.down}}>🔽</button>
  {{this.count}}
  <button {{action this.up}}>🔼</button>
</div>

Template-only Components

This example implements a kind of component similar to what was proposed in the template-only components RFC.

Since the custom components API proposed in this RFC requires a JavaScript files, we cannot implement true "template-only" components. We will need to create a component JS file to export a dummy value, for the sole purpose of indicating the component manager we want to use.

In practice, there is no need for an addon to implement this API, since it is essentially re-implementing what the "template-only-glimmer-components" optional feature does. Nevertheless, this example is useful for illustrative purposes.

// ember-template-only-component/app/component-managers/template-only.js

import { capabilities } from '@ember/component';
import EmberObject from '@ember/object';

export default EmberObject.extend({
  capabilities: capabilities('3.2'),

  createComponent() {
    return null
  },

  getContext() {
    return null;
  },

  updateComponent() {
    return;
  }
});
// ember-template-only-component/addon/index.js

import { setComponentManager } from '@ember/component';

// Our `createComponent` method does not actually do anything with the factory,
// so we don't even need to export a class here, `{}` would work just fine.
export default setComponentManager('template-only', {});

Usage

// my-app/app/components/hello-world.js

import TemplateOnlyComponent from 'ember-template-only-component';

export default TemplateOnlyComponent;
Hello world! I have no backing class! {{this}} would be <code>null</code>.

Recycling Components

This example implements an API which maintain a pool of recycled component instances to avoid allocation costs.

This example also make use of the "state bucket" pattern.

// ember-component-pool/app/component-managers/pooled.js

import { capabilities } from '@ember/component';
import EmberObject from '@ember/object';

// How many instances to keep (per type/factory)
const LIMIT = 10;

export default EmberObject.extend({
  capabilities: capabilities('3.2', {
    destructor: true
  }),

  init() {
    this.pool = new Map();
  },

  createComponent(factory, args) {
    let instances = this.pool.get(factory);
    let instance;

    if (instances && instances.length > 0) {
      instance = instances.pop();
      instance.setProperties(args.named);
    } else {
      instance = factroy.create(args.named);
    }

    // We need to remember which factory does the instance belong to so we can
    // check it back into the pool later.
    return { factory, instance };
  },

  getContext({ instance }) {
    return instance;
  },

  updateComponent({ instance }, args) {
    instance.setProperties(args.named);
  },

  destroyComponent({ factory, instance }) {
    let instances;

    if (this.pool.has(factory)) {
      instances = this.pool.get(factory);
    } else {
      this.pool.set(factory, instances = []);
    }

    if (instances.length >= LIMIT) {
      instance.destroy();
    } else {
      // User hook to reset any state
      instance.willRecycle();
      instances.push(instance);
    }
  },

  // This is the `Ember.Object` lifecycle method, called when the component
  // manager instance _itself_ is being destroyed, not to be confused with
  // `destroyComponent`
  willDestroy() {
    for (let instances of this.pool.values()) {
      instances.forEach(instance => instance.destroy());
    }

    this.pool.clear();
  }
});
// ember-component-pool/addon/index.js

import EmberObject from '@ember/object';
import { setComponentManager } from '@ember/component';

function NOOP() {}

export default setComponentManager('pooled', EmberObject.extend({
  // Override this to implement reset any state on the instance
  willRecycle(): NOOP,

  // ...
}));

How We Teach This

What is proposed in this RFC is a low-level primitive. We do not expect most users to interact with this layer directly. Instead, most users will simply benefit from this feature by subclassing these special components provided by addons.

At present, the classic components APIs is still the primary, recommended path for almost all use cases. This is the API that we should teach new users, so we do not expect the guides need to be updated for this feature (at least not the components section).

For documentation purposes, each Ember.js release will only document the latest component manager API, along with the available optional capabilities for that realease. The documentation will also include the steps needed to upgrade from the previous version. Documentation for a specific version of the component manager API can be viewed from the versioned documentation site.

Drawbacks

In the long term, there is a risk of fragmentating the Ember ecosystem with many competing component APIs. However, given the Ember community's strong desire for conventions, this seems unlikely. We expect this to play out similar to the data-persistence story – there will be a primary way to do things (Ember Data), but there are also plenty of other alternatives catering to niche use cases that are underserved by Ember Data.

Also, because apps can mix and match component styles, it's possible for a library like smoke-and-mirrors or Liquid Fire to take advantage of the enhanced functionality internally without leaking those implementation details to applications.

Alternatives

Instead of focusing on exposing enough low-level primitives to build the new components API, we could just focus on building out the user-facing APIs without rationalizing or exposing the underlying primitives.

Appendix

Follow-up RFCs

We expect to rapidly iterate and improve the component manager API through the RFC process and in-the-field usage/implementation experience. Here are a few examples of additional capabilities that we hope to see proposed after this initial (and intentionally minimal) proposal is finalized:

  1. Expose a way to access to the component's DOM structure, such as its bounds. This RFC would also need to introduce a hook for DOM teardown and address how event handling/delegation would work.

  2. Expose a way to access to the reference-based APIs. This could include the ability to customize the component's "tag" (validator).

  3. Expose additional features that are used to implement classic components, {{outlet}} and other built-in components, such as layout customizations, and dynamic scope access.

  4. Angle bracket invocation.

Using ES6 Classes

Although this RFC uses Ember.Object in the examples, it is not a "hard" dependency.

Using ES6 Classes For Components

The main interaction between the Ember object model and the component class is through the DI system. Specifically, the factory function returned by factoryFor (factoryFor('component:foo-bar').create(...)), which is passed to the createComponent method on the component manager, assumes a static create method on the class that takes the "property bag" and returns the created instance.

Therefore, as long as your ES6 super-class provides such a function, it will work with the rest of the system:

// ember-basic-component/addon/index.js

import { setComponentManager } from '@ember/component';

class BasicComponent {
  static create(props) {
    return new this(props);
  }

  constructor(props) {
    // Do things with props, such as:
    Object.assign(this, props);
  }

  // ...
}

export default setComponentManager('basic', BasicComponent);
// ember-basic-component/app/component-managers/basic.js

import { capabilities } from '@ember/component';
import EmberObject from '@ember/object';

export default EmberObject.extend({
  capabilities: capabilities('3.2'),

  createComponent(factory, args) {
    // This Just Works™ since we have a static create method on the class
    return factory.create(args.named);
  },

  // ...
});
// my-app/app/components/foo-bar.js

import BasicCompoment from 'ember-basic-component';

export default class extends BasicCompoment {
  // ...
};

Alternatively, if you prefer not to add a static create method to your super-class, you can also instantiate them in the component manager without going through the DI system:

// ember-basic-component/app/component-managers/basic.js

import { capabilities } from '@ember/component';
import EmberObject from '@ember/object';

export default EmberObject.extend({
  capabilities: capabilities('3.2'),

  createComponent(factory, args) {
    // This does not use the factory function, thus no longer require a static
    // create method on the class
    return new factory.class(args.named);
  },

  // ...
});

However, doing do will prevent your components from receiving injections (as well as setting the appropiate owner, etc). Therefore, when possible, it is better to go through the DI system's factory function.

Using ES6 Classes For Component Managers

It is also possible to use ES6 classes for the component managers themselves. The main interaction here is that they are automatically instantiated by the DI system on-demand, which again assumes a static create method:

// ember-basic-component/app/component-managers/basic.js

import { capabilities } from '@ember/component';

export default class BasicComponentManager {
  static create(props) {
    return new this(props);
  }

  constructor(props) {
    // Do things with props, such as:
    Object.assign(this, props);
  }

  capabilities = capabilities('3.2');

  // ...
};

Alternatively, as shown above, you can also register the component manager with { instantiate: false }:

// ember-basic-component/app/initializers/register-basic-component-manager.js

import BasicComponentManager from 'ember-basic-component';

export function initialize(application) {
  application.register('component-manager:basic', new BasicComponentManager(), { instantiate: false });
}

export default {
  name: 'register-basic-component-manager',
  initialize
};

Note that this behaves a bit differently as the component manager instance is shared across all application instances and is never destroyed, which might affect stateful component managers such as the one shown in the "Recycling Components" example above.

  • Start Date: 2017-04-26
  • RFC PR: #15174
  • Ember Issue: (leave this empty)

Summary

This RFC proposes allowing parameters to be passed to the {{mount}} syntax.

Motivation

This will enable developers to pass contextual data into routeless engines at runtime, allowing individual engines to be used multiple times through a single application under different contexts.

An example could be a dashboard of charts where each chart is a routeless engine. Each chart could be of a different type and would require different data. This RFC would enable the following:

{{!-- app/templates/application.hbs --}}
{{#each charts as |chart|}}
  {{mount "chart" type=chart.type data=chart.data}}
{{/each}}

Detailed design

You can see the implementation for this RFC here.

Implementing this functionality turns out to be relatively straight forward. With routeless engines already supporting an application controller, we can use this as a means of providing access to the parameters.

Parameters would be passed to the {{mount}} syntax in the same way that they are currently passed to components and helpers.

{{mount "foo" bar="baz"}}

These parameters would then be set as the model property on the engines application controller; making them accessible from both a JS and HBS context.

// foo/controllers/application.js
import Ember from 'ember';

export default Ember.Controller.extend({

  actions: {
    exampleAction() {
      let barParam = this.get("model.bar");
    }
  }

});
{{!-- foo/templates/application.hbs --}}
The value of the bar param is: {{model.bar}}

How We Teach This

This RFC re-uses concepts that are already heavily used throughout other areas of the framework.

Updates will need to be made to the Ember API docs and ember-engines.com guides in order to explain that routeless engines can now accept parameters being passed via the {{mount}} syntax.

In addition, updates would need to include examples of both how to pass parameters to {{mount}} as well as how any passed parameters can be accessed from within the context of an engine.

Drawbacks

Increased risk of runtime dependencies [reference]

This RFC does increase the risk of introducing runtime dependencies.

Example:

import Ember from 'ember';

export default Ember.Component.extend({
  geo: Ember.inject.service('geolocation')
})
{{mount "blog" (hash geo=geo)}}

Alternatives

Application route model hook

This solution suggested that the parameters were provided to the engines application route via the model hooks params argument. While viable, this solution didn't feel quite right as you were using a route within a routeless engine.

Introduction of new routelessEngine primitive

This solution suggested that routeless engines should be given their own primitive similar to that of a component. The primitive would follow a construct to components and use the same hooks (e.g., didReceiveAttrs) for working with parameters.

This solution was decided against for following main reasons:

  1. The current public API is that we use application controller to back the application.hbs of a route-less engine. Adding a new conceptual primitive here would be a fairly difficult change without breakage.
  2. Introducing a new primitive (e.g. not controller and not a component) is a very heavy handed thing, and should not be taken lightly. We don't think this rises to that level of need.
  3. This is an ideal case for "just using a component", but that would be roughly akin to "routable components" and we should follow Ember's lead. When routeable components are introduced, we can refactor this in the same way with the same backwards compatibility guarantees.

Unresolved questions

Positional parameters

In addition to supporting named parameters, should the {{mount}} syntax also support positional parameters? If so, should this be covered in this RFC, or in a follow up RFC?

  • Start Date: 2017-05-05
  • Relevant Team(s): Ember.js
  • RFC PR: https://github.com/emberjs/rfcs/pull/226
  • Tracking: https://github.com/emberjs/rfc-tracking/issues/13

Summary

Introduce syntax for passing in multiple named template blocks into a component, and unify the rendering syntaxes / semantics for blocks/primitives/component-factories passed into a component.

This RFC is focused chiefly on bringing named blocks to Ember Components, but it was necessary to define a basic roadmap of functionality for Glimmer Components as well, but keep in mind that some of the Glimmer-centric details may change and should hence be considered non-normative.

Motivation

There are limitations to composition due to the inability to pass more than one block to a component (or 2 blocks if you include the inverse block).

The result of this is that Ember developers have this ultra-powerful, compositional API for overriding portions of a component, but they can only use it in one place in the component invocation; any remaining overrides/configuration needs to be expressed as data and passed in as attributes to the component when it'd be vastly preferable to just pass in a chunk of DOM.

Example:

{{#x-modal headerText=page.title as |c|}}
  <p>Modal Content {{foo}}</p>
  <button onclick={{c.close}}>
     Close modal
  </button>
{{/x-modal}}

This works, but the moment you need to render a component in the header (rather than just headerText), you end up having to add more config/data/attrs to x-modal just to support every one of those overrides, when really you just should be able to pass in another block of DOM to define what the header looks like. The API in this proposal would allow you to express this use case via:

{{#x-modal}}
  <@header as |c|>
    {{page.title}}
    {{status-indicator status=status}}
    {{close-button action=c.close}}
  </@header>

  <@main as |c|>
    <p>Modal Content {{foo}}</p>
    <button onclick={{c.close}}>
       Close modal
    </button>
  </@main>
{{/x-modal}}

and with Glimmer components:

<x-modal>
  <@header as |c|>
    {{page.title}}
    {{status-indicator status=status}}
    {{close-button action=c.close}}
  </@header>

  <@main as |c|>
    <p>Modal Content {{foo}}</p>
    <button onclick={{c.close}}>
       Close modal
    </button>
  </@main>
</x-modal>

Other RFCs/addons that have attempted to address this:

Detailed design

Invocation Syntax is separate from Component type

The features specified in this RFC require us to nail down some specifics as to how Ember and Glimmer components interop, which syntaxes can be used to render them, and the mental/teaching model behind how it all works.

  • Invocation Syntax (curly vs angle brackets) is conceptually separate from Component type (Ember vs Glimmer component)
  • "Curly Components" is a misnomer since curly syntax can render both Ember components AND Glimmer components
  • Angle-bracket syntax can only render Glimmer components
  • {{x-foo @k=v}} will remain invalid curly syntax due to the @k=v
  • The way KV pairs provided at invocation is handled depends on the component type:
    • Given {{x-foo k=v}}, Ember Component x-foo will assign/bind v to property k on the component instance, which can be rendered within the component layout template as {{k}} (same behavior as always). {{@k}} in an Ember component layout will remain a syntax error
    • Given {{x-foo k=v}}, Glimmer Component x-foo treats k=v as assigning/binding arg @k to v; it will assign/bind this.args.k, and expose the value as {{@k}} within the template. This example invocation is functionally equivalent to <x-foo @k={{v}} />
    • The mental model is that with curly syntax, k=v is the syntax for "passing data" to a component; Ember components receive/expose this data via the properties on the component instance, and Glimmer components receive the data as @args.
  • Curly syntax will not be enhanced with syntax for passing HTML attrs (at this time)
  • Angle-bracket syntax does not support passing positional params

Implementation-wise, these varying semantics will be defined/implemented via Component Managers.

Multi-block Syntax

Both curly and angle-bracket component invocation syntax will be enhanced with a nested syntax for passing multiple blocks into a component.

The syntax for curly invocation is as follows:

{{#x-foo}}
  <@header>
    Howdy.
  </@header>

  <@body as |foo|>
    Body {{foo}}
  </@body>
{{/x-foo}}

and for angle-bracket invocation:

<x-foo>
  <@header>
    Howdy.
  </@header>

  <@body as |foo|>
    Body {{foo}}
  </@body>
</x-modal>

As demonstrated above, the nested syntax for both curly and angle-bracket multi-block syntax has the format <@blockName>...</@blockName>.

This multi-block syntax cannot be mixed with other syntaxes; either ALL the nested "children" of a component invocation need to be <@blockName>...</@blockName> (multi-block syntax), or none of them do (classic single-block syntax). The presence of any non-whitespace character between or around <@blockName>...</@blockName>s is a compile-time error.

Passing two blocks with the same name is a compiler error (though this might be relaxed in a future RFC).

Blocks are just (opaque) data

This RFC introduces the concept that blocks are just opaque values passed into components as data, rather than living in what is essentially a separate namespace only accessible to {{yield}}.

In the above example (with curly syntax), Ember Component x-foo would have its header and body properties set on its instance. This means, among other things, that there's no need for a hasBlock API for JavaScript; you can just use normal property lookup / computed properties / etc to determine whether a block is provided. This means that blocks can be stashed on services and rendered elsewhere, e.g. the ember-elsewhere use case.

Unified Renderable Syntax

Rather than continuing to enhance the {{yield}} syntax, we should take this opportunity to unify the various syntaxes for rendering things, from blocks to primitive values to component factories.

We'll use the following example component invocation to explore what this syntax looks like: the following (curly syntax) invocation is valid syntax for rendering either an Ember.Component or a Glimmer Component named x-modal and passing it 3 named blocks: header, body, and footer:

{{#x-modal}}
  <@header as |title|>
    Header {{title}}
  </@header>

  <@body>
    Body
  </@body>

  <@footer>
    Footer
  </@footer>
{{/x-modal}}

Given the above invocation, here's how you could render these blocks:

{{! within ember-component layout }}
{{this.header "ima title"}}
{{this.body}}
{{this.footer}}

{{! within glimmer-component layout }}
{{@header "ima title"}}
{{@body}}
{{@footer}}

Both of these Ember/Glimmer layouts would render:

Header ima title
Body
Footer

The mental modal here is that is that for ECs, named blocks are set/bound as a properties on the instance, which we're rendering the same way we always rendering properties on the instance. For GCs, blocks are just args that we're rendering with the standard @arg syntax

Why {{this.header}} and not just {{header}}?

Unfortunately, we are constrained by the fact that {{foo}} means:

  • Try and find a helper named foo, and render it
  • If no such helper exists, fall back to rendering property foo on the template context

There is a risk that still exists today that any time you introduce a new helper to your codebase, for example, a time helper, you will break any templates that try to render a time property via {{time}}. This is an unfortunate hangover from the past, and we don't want to continue to expand the scope of this footgun with this RFC. Hence, to render blocks/component factories, you must use {{this.time}} in your Ember Component templates.

This is actually an extension to behavior introduced by the Contextual Component Lookup RFC; today, the following is supported:

<!-- invocation -->
{{x-foo header=(component 'x-header')}}
<!-- x-foo layout -->
{{this.header}}

In the above example, {{this.header}} will actually render the x-header component factory; this RFC merely proposes extending this syntax so that it can render blocks as well.

Since the potential for forgetting this rule is somewhat high, we should consider detecting when you use {{foo}} syntax when foo is a component factory or a block, and provide a useful warning or error message to use {{this.foo}} instead.

Rendering primitives

Consider the following example, which modifies above example by changing the footer block to a string:

{{#x-modal footer="ima footer"}}
  <@header as |title|>
    Header {{title}}
  </@header>
{{/x-modal}}

If x-modal renders footer via {{this.footer}}, then it'll just render the "ima footer" string just fine; this a nice benefit of having a Unified Renderable syntax and supports a common workflow where string args can be promoted to full-on blocks without having to rework the component code to support an alternative/parallel API.

Call syntax with primitives (or undefined)

If you try to render a block/component with args, e.g. {{this.foo 1 2 3}}, then foo MUST be a block or a component. If foo is any kind of non-callable primitive, including undefined, it will be an error.

Unified Renderable Syntax: component factories

The following invocation using component factories is also supported:

{{#x-modal
     header=(component 'my-modal-header')
     footer=(component 'my-modal-footer')}}
  <@main>
    Main
  </@main>
{{/x-modal}}

(It's worth mentioning that since we're only defining the main block here, this could also be expressed simply as:)

{{#x-modal
     header=(component 'my-modal-header')
     footer=(component 'my-modal-footer')}}
  Main
{{/x-modal}}

This demonstrates that the unified renderable syntax is also capable of rendering component factories (previously only renderable via {{component header}}).

Note since we're passing a positional param "ima title" to header, the my-modal-header component would only be able to access that param if it were using the positionalParams API with (reopenClass), which is a bit of a clunky / pro-user API.

As a component author, if you want to write your components to support passing data to both blocks (which accept positional params) and components (which accept KV pairs), you can pass in both formats of the same data, e.g.:

// Ember.component layout
{{this.header headerTitle title=headerTitle}}

// Glimmer Component layout
{{@header headerTitle title=headerTitle}}

Named block params

Prior to this RFC, there were only two ways to pass in overridable chunks of DOM to a component:

  • Passing in a block, which only accepts positional block params (or a (hash) object of named params)
  • Passing in a component factory, which only accepts KV args (unless you use the reopenClass-positionalParams api)

Given that we're introducing a Unified Renderable syntax, it would be unfortunate if we did nothing to address this impedance mismatch between named and positional params. The goal is for component consumers/invokers to be able to pass the most "convenient" kind of Renderable for their use case, be it a simple primitive string value, a block if they want the lexical scope + block params, or a component factory for rendering a shared component that might be used in many places throughout the app. Unfortunately, the component author will have to choose whether they want to pass positional params (which would push consumers towards only using blocks) or named params (which are presently only supported by component factories).

Hence, for this reason (among others), it makes sense to introduce a syntax for named block params; with this syntax, there will be an organic shift towards component authors using named KV pairs for passing data in most cases (while still allowing positional params in certain simpler cases were it only really makes sense to use a block, e.g. control flow components that wrap if or each, etc.)

Here is the syntax for named block params:

{{#x-modal}}
  <@header as |@title @data|>
    The title: {{@title}}
    The data: {{@data}}
  </@header>
{{/x-modal}}

There is also a "soaking" syntax which is useful in cases where nested blocks might introduce new named block params that clobber preexisting identifiers in the scope, as well as cases where spelling out each named block param consumes too much rightward space. The above example can be expressed using the soaking syntax as follows:

{{#x-modal}}
  <@header as |@...d|>
    The title: {{d.title}}
    The data: {{d.data}}
  </@header>
{{/x-modal}}

(The @ is not included as part of the identifier as that would suggest it was a KV arg rather than essentially a hash of args.)

Block form of Unified Renderable syntax

It should be possible to pass a block TO the block/component-factory that's been passed into a component. The common use cases for this are:

Passing a block to a component factory

Given the following invocation:

{{x-modal header=(component 'my-header')}}

It should be possible for x-modal to pass a block to the header renderable:

// ember-component x-modal layout
{{#this.header title="ima title"}}
  I'm a block provided by the component layout template.
{{/this.header}}

Assuming my-header had a layout of:

<div class="my-header-inner">
  title is {{title}}
  {{yield}}
</div>

This would render the following (assuming x-modal and my-header are Ember components with tagName: 'div' with classNames set):

<div class="x-modal">
  <div class="my-header">
    <div class="my-header-inner">
      title is ima title
      I'm a block provided by the component layout template.
    </div>
  </div>
</div>
Passing a block to a block (aka contextual components)

Given the following invocation:

{{#x-modal}}
  <@header as |@title @main|>
    <div class="header-block-content">
      title is {{@title}}
      {{@main}}
    </div>
  </header>
{{/x-modal}}

This would render the following (assuming the same x-modal layout as the previous example:

<div class="x-modal">
  <div class="header-block-content">
    title is ima title
    I'm a block provided by the component layout template.
  </div>
</div>

It would also be possible to pass a component factory to the header block from x-modal's layout:

// ember-component x-modal layout
{{this.header title="ima title"
         main=(component 'x-modal-inner-content')}}

The multi-block syntax can be used as well with Unified Renderable syntax:

{{#header}}
  <@main>
    I'm a block provided by the component layout template.
  </@main>

  <@title>
    ima title
  </@title>
{{/header}}
Block form of @-prefixed identifiers

The syntax for passing a block to an @-prefixed identifier (named block params and Glimmer @args) will be {{#@thing}} ... {{/@thing}}, e.g.:

{{#x-layout as |@widget|}}
  {{#@widget as |a b c|}}
    Hi.
  {{/@widget}}
{{/x-layout}}

Classic single-block syntax: main and else args

It would be unfortunate if component authors had to use different syntaxes for rendering named blocks vs the traditional "default" and "inverse" blocks provided by the classic single-block syntax.

Hence, the blocks provided in classic single-block syntax should also be exposed as properties (Ember) and args (Glimmer), and should have conventional, meaningful names: instead of "default" (which is a bit misleading) and "inverse", we standardize on main and else.

Glimmer Components: @main and @else

Given Glimmer component invocation:

{{#fancy-if cond=trueOrFalse}}
  True
{{else}}
  False
{{/fancy-component}}

The component layout could be:

{{#if cond}}
  {{@main}}
{{else}}
  {{@else}}
{{/if}}

Note that angle-bracket syntax doesn't support passing in an inverse/else block, but the block provided to angle-bracket invocation would be passed in as @main.

Ember Components: main and else

For Ember, we can't suddenly start setting main and else properties on the component instances as this would be a breaking change, and main in particular is not an uncommon property name.

We also shouldn't punt on this feature for Ember components for the following reasons/use cases:

  • ember-elsewhere (and other similar patterns) require having access to the opaque block so that it can be stashed on a service and rendered elsewhere
  • wrapper components that forward args/properties/blocks to another internal component; blocks need to be accessible as properties in order to pass them into another component (otherwise you'd have to use a combinatoric mess of block syntax + if hasBlock checks to forward blocks through to the inner component)

So we need an opt-in API; any Ember Component that wants main/else properties to be set on the component instance need to opt into this behavior via a mixin provided by Ember:

import { ImplicitBlockPropertyMixin } from "@ember/implicit-block-property-support";

export default Ember.Component.extend(ImplicitBlockPropertyMixin, {
  blockManager: inject.service(),
  init() {
    this._super();
    this.get('blockManager').registerBlock(this.get('main'));
  },
});

So if fancy-if were an Ember component that used this mixin, then given the component invocation:

{{#fancy-if cond=trueOrFalse}}
  True
{{else}}
  False
{{/fancy-if}}

The following ember component layout would work:

{{#if cond}}
  {{this.main}}
{{else}}
  {{this.else}}
{{/if}}

How We Teach This

We teach this as a followup to classic block syntax; once the user is comfortable with single-block syntax, we can introduce named block syntax for more complex use cases.

We teach that what <@blockName></@blockName> syntax really means is that we're just passing in an arg named @blockName, which is like any other arg we might pass into a component, but it happens to point to a template block than, say, some simple string value.

Drawbacks

Different from WC slot syntax

This isn't really anything like the WebComponents slot syntax that intends to address similar use cases, so there is some risk of introducing an API that doesn't fit in with what the rest of the world is doing.

Syntax highlighting changes

Some syntax highlighters might have trouble with this syntax; all the editors I've tried it on look reasonable, but GitHub's Handlebars parser isn't too kind (hence I've been using html snippets instead of handlebars snippets):

<x-modal>
  <@header as |c|>
    ...
  </@header>

  <@main as |c|>
    ...
  </@main>
</x-modal>

Conditionally passing blocks?

This RFC does NOT introduce any kind of facility for conditionally passing blocks, e.g.:

{{! this syntax is INVALID! }}
<x-layout>
  <@header>...</@header>
  <@main>...</@main>

  {{#if userCanProceed}}
    <@footer>
      {{submit-button}}
    </@footer>
  {{/if}}
</x-layout>

This might be desirable in the future, particularly for use cases involving flex-ish layouts where the component changes behavior / appearance based on whether blocks on passed in.

Alternatives

I'd proposed a JSX-y attr/component-centric syntax for passing what are essentially DOM lambdas, rendered with {{component}}. Perhaps we'll add something like that feature in the future, but it's a much less natural enhancement to Ember than named blocks.

Considerations for Future RFCs

Defining Default Blocks

There's not really a nice way defining default blocks inside your component layout (i.e. the block you render when known is provided at invocation time), but then again I believe the following would be a workable approach that is probably support by the features proposed in this RFC?

{{#with-blocks}}
  <@mainOrDefault>
    {{#if main}}
      {{main}}
    {{else}}
      I am the default main block when none is passed in.
    {{/if}}
  </@mainOrDefault>

  <@footerOrDefault>
    {{#if footer}}
      {{footer}}
    {{else}}
      I am the default footer block when none is passed in.
    {{/if}}
  </@footerOrDefault>

  <@render as |@mainOrDefault @footerOrDefault|>
    {{! this specially-named block gets passed all the other blocks above}}

    {{mainOrDefault}}
    {{footerOrDefault}}
  </@render>
{{/with-blocks}}

Either way, it feels hacky and weird and I would be surprised if we'd want/need a future RFC to define a nicer way to support default blocks.

Allow passing multiple blocks with the same name

e.g.

{{#power-select}}
  <@option value="foo">
    <em>Foo</em>
  </@option>
  <@option value="bar">
    <blink>Bar</blink>
  </@option>
{{/power-select}}

This RFC defines that passing multiple blocks with the same name is a syntax error, but it's something we might want to relax in the future for certain cases where you want to pass arrays of blocks, such as power-select or use cases involving tables.

  • Start Date: 2017-06-11
  • RFC PR: emberjs/rfcs#229
  • Ember Issue: (leave this empty)

Summary

In order to largely reduce the brittleness of tests, this RFC proposes to remove the concept of artificially restricting the resolver used under testing.

Motivation

Disabling the resolver while running tests leads to extremely brittle tests.

It is not possible for collaborators to be added to the object (or one of its dependencies) under test, without modifying the test itself (even if exactly the same API is exposed).

The ability to restrict the resolver is not actually a feature of Ember's container/registry/resolver system, and has posed as significant maintenance challenge throughout the lifetime of ember-test-helpers.

Removing this system of restriction will make choosing what kind of test to be used easier, simplify many of the blueprints, and enable much simpler refactoring of an applications components/controllers/routes/etc to use collaborating utilties and services.

Transition Path

Deprecate Functionality

Issue a deprecation if integration: true is not included in the specified options for the APIs listed below. This specifically includes specifying unit: true, needs: [], or specifying none of the "test type options" (unit, needs, or integration options) to the following ember-qunit and ember-mocha API's:

  • ember-qunit
    • moduleFor
    • moduleForComponent
    • moduleForModel
  • ember-mocha
    • setupTest
    • setupComponentTest
    • setupModelTest

Non Component Test APIs

The migration path for moduleFor, moduleForModel, setupTest, and setupModelTest is very simple:

// ember-qunit

// before
moduleFor('service:session');

moduleFor('service:session', {
  unit: true
});

moduleFor('service:session', {
  needs: ['type:thing']
});

// after
moduleFor('service:session', {
  integration: true
});
// ember-mocha

// before
describe('Session Service', function() {
  setupTest('service:session');
  
  // ...snip...
});

describe('Session Service', function() {
  setupTest('service:session', { unit: true });
  
  // ...snip...
});

describe('Session Service', function() {
  setupTest('service:session', { needs: [] });
  
  // ...snip...
});

// after

describe('Session Service', function() {
  setupTest('service:session', { integration: true });
  
  // ...snip...
});

The main change is adding integration: true to options (and removing unit or needs if present).

Component Test APIs

Implicitly relying on "unit test mode" has been deprecated for quite some time (introduced 2015-04-07), so all consumers of moduleForComponent and setupComponentTest are specifying one of the "test type options" (unit, needs, or integration).

This RFC proposes to deprecate completely using unit or needs options with moduleForComponent and setupComponentTest. The vast majority of component tests should be testing via moduleForComponent / setupComponentTest with the integration: true option set, but on some rare occaisions it is easier to use the "unit test" style is desired (e.g. non-rendering test) these tests should be migrated to using moduleFor / setupTest directly.

// ember-qunit

// before
moduleForComponent('display-page', {
  unit: true
});

moduleForComponent('display-page', {
  needs: ['type:thing']
});

// after

moduleFor('component:display-page', {
  integration: true
});
// ember-mocha
describe('DisplayPageComponent', function() {
  setupComponentTest('display-page', { unit: true });
  
  // ...snip...
});

describe('DisplayPageComponent', function() {
  setupComponentTest('display-page', { needs: [] });
  
  // ...snip...
});

// after

describe('DisplayPageComponent', function() {
  setupTest('component:display-page', { integration: true });
  
  // ...snip...
});

Ecosystem Updates

The blueprints in all official projects (and any provided by popular addons) will need to be updated to avoid triggering a deprecation.

This includes:

  • ember-source
  • ember-data
  • ember-cli-legacy-blueprints
  • Others?

Remove Deprecated unit / needs Options

Once the changes from this RFC are made, we will be able to remove support for the unit and needs options from ember-test-helpers, ember-qunit, and ember-mocha. This would be a "semver major" version bump for all of the related libraries to properly signal that functionality was removed.

Once the underlying libraries have done a major version bump, we will introduce a deprecation for using the integration option. This deprecation would be issued once for the entire test suite (not once per test module which has integration passed in). We will also update the blueprints to remove the extraneous integration option.

How We Teach This

This RFC would require an audit of the main Ember.js guides to ensure that all usages of the APIs in question continue to be non-deprecated valid usages.

Drawbacks

Churn

One drawback to this deprecation proposal is the churn associated with modifying the options passed for each test. This can almost certainly be mitigated by providing a codemod to enable automated updating.

There are additional changes being entertained that would require changes for the default testing blueprints, we should ensure that these RFCs do not conflict or cause undue churn/pain.

integration: true Confusion

Prior to this deprecation we had essentially 4 options for testing components:

  • moduleFor(..., { unit: true })
  • moduleFor(..., { integration: true })
  • moduleForComponent(..., { unit: true })
  • moduleForComponent(..., { integatrion: true })

With this RFC the option integration no longer provides value (we aren't talking about "unit" vs "integration" tests), and may be seen as confusing.

I believe that this concern is mitigated by the ultimate removal of the integration (it is only required in order to allow us a path forward that is compatible with todays ember-qunit/ember-mocha versions).

  • Start Date: 2017-06-13
  • RFC PR: emberjs/rfcs#232
  • Ember Issue: (leave this empty)

Summary

In order to embrace newer features being added by QUnit (our chosen default testing framework), we need to reduce the brittle coupling between ember-qunit and QUnit itself.

This RFC proposes a new testing syntax, that will expose QUnit API directly while also making tests much easier to understand.

Motivation

QUnit feature development has been accelerating since the ramp up to QUnit 2.0. A number of new features have been added that make testing our applications much easier, but the current structure of ember-qunit impedes our ability to take advantage of some of these features.

Developers are often confused by our moduleFor* APIs, questions like these are very common:

  • What "magic" is ember-qunit doing?
  • Where are the lines between QUnit and ember-qunit?
  • How can I use QUnit for plain JS objects?

The way that ember-qunit wraps QUnit functionality makes the division of responsiblity much harder to understand, and leads folks to believe that there is much more going on in ember-qunit than there is. It should be much clearer what ember-qunit is responsible for and what we rely on QUnit for.

This RFC also aims to remove a number of custom testing only APIs that exist today (largely because the container/registry system was completely private when the current tools were authored). Instead of things like this.subject, this.register, this.inject, or this.lookup we can rely on the standard way of performing these functions in Ember via the owner API.

When this RFC has been implemented and rolled out, these questions should all be addressed and our testing system will both: embrace QUnit much more and be much more framework agnostic, all the while dropping custom testing only APIs in favor of public APIs that work across tests and app code.

Sounds like a neat trick, huh?

Detailed design

The primary change being proposed in this RFC is to migrate to using the QUnit nested module syntax, and update our custom setup/teardown into a more functional API.

Lets look at a basic example:

// **** before ****

import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';

moduleForComponent('x-foo', {
  integration: true
});

test('renders', function(assert) {
  assert.expect(1);

  this.render(hbs`{{pretty-color name="red"}}`);

  assert.equal(this.$('.color-name').text(), 'red');
});

// **** after ****

import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';

module('x-foo', function(hooks) {
  setupRenderingTest(hooks);

  test('renders', async function(assert) {
    assert.expect(1);

    await this.render(hbs`{{pretty-color name="red"}}`);

    assert.equal(this.$('.color-name').text(), 'red');
  });
});

As you can see, this proposal leverages QUnit's nested module API in a way that makes it much clearer what is going on. It is quite obvious what QUnit is doing (acting like a general testing framework) and what ember-qunit is doing (setting up rendering functionality).

This API was heavily influenced by the work that Tobias Bieniek did in emberjs/ember-mocha#84.

QUnit Nested Modules API

Even though it is not a proposal of this RFC, the QUnit nested module syntax may seem foreign to some folks so lets briefly review.

With nested modules, a normal 1.x QUnit module setup changes from:

QUnit.module('some description', {
  before() {},
  beforeEach() {},
  afterEach() {},
  after() {}
});

QUnit.test('it blends', function(assert) {
  assert.ok(true, 'of course!');
});

Into:

QUnit.module('some description', function(hooks) {

  hooks.before(() => {});
  hooks.beforeEach(() => {});
  hooks.afterEach(() => {});
  hooks.after(() => {});

  QUnit.test('it blends', function(assert) {
    assert.ok(true, 'of course!');
  });
});

This makes it much simpler to support multiple before, beforeEach, afterEach, and after callbacks, and it also allows for arbitrary nesting of modules.

You can read more about QUnit nested modules here. The new APIs proposed in this RFC expect to be leveraging nested modules.

New APIs

The following new methods will be exposed from ember-qunit:

interface QUnitModuleHooks {
  before(callback: Function): void;
  beforeEach(callback: Function): void;
  afterEach(callback: Function): void;
  after(callback: Function): void;
}

declare module 'ember-qunit' {
  // ...snip... 
  export function setupTest(hooks: QUnitModuleHooks): void;
  export function setupRenderingTest(hooks: QUnitModuleHooks): void;
}

setupTest

This function will:

  • invoke ember-test-helpers setContext with the tests context
  • create an owner object and set it on the test context (e.g. this.owner)
  • setup this.set, this.setProperties, this.get, and this.getProperties to the test context
  • setup this.pauseTest and this.resumeTest methods to allow easy pausing/resuming of tests

setupRenderingTest

This function will:

  • run the setupTest implementation
  • setup this.$ method to run jQuery selectors rooted to the testing container
  • setup a getter for this.element which returns the DOM element representing the element that was rendered via this.render
  • setup Ember's renderer and create a this.render method which accepts a compiled template to render and returns a promise which resolves once rendering is completed
  • setup this.clearRender method which clears any previously rendered DOM ( also used during cleanup)

When invoked, this.render will render the provided template and return a promise that resolves when rendering is completed.

Changes from Current System

Here is a brief list of the more important but possibly understated changes being proposed here:

  • the various setup methods no longer need to know the name of the object under test
  • this.subject is removed in favor of using the standard public API for looking up and creating instances (this.owner.lookup and this.owner.factoryFor)
  • this.inject is removed in favor of using this.owner.lookup directly
  • this.register is removed in favor of using this.owner.register directly
  • this.render will begin being asynchronous to allow for further iteration in the underlying rendering engines ability to speed up render times (by yielding back to the browser and not blocking the main thread)
  • this.pauseTest and this.resumeTest are being added
  • this.element is being introduced as a public API for DOM assertions in a jQuery-less environment
  • QUnit nested modules are required

These changes generally do not affect our ability to write a codemod to aide in the migration.

Migration Examples

The migration can likely be largely automated (following the excellent codemod that Tobias Bieniek wrote for a similar ember-mocha the transition), but it is still useful to review concrete scenarios of tests before and after this RFC is implemented.

Component / Helper Integration Test

// **** before ****

import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';

moduleForComponent('x-foo', {
  integration: true
});

test('renders', function(assert) {
  assert.expect(1);

  this.render(hbs`{{pretty-color name="red"}}`);

  assert.equal(this.$('.color-name').text(), 'red');
});

// **** after ****

import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';

module('x-foo', function(hooks) {
  setupRenderingTest(hooks);

  test('renders', async function(assert) {
    assert.expect(1);

    await this.render(hbs`{{pretty-color name="red"}}`);

    assert.equal(this.$('.color-name').text(), 'red');
  });
});

Component Unit Test

// **** before ****

import { moduleForComponent, test } from 'ember-qunit';

moduleForComponent('x-foo', {
  unit: true
});

test('computes properly', function(assert) {
  assert.expect(1);

  let subject = this.subject({
    name: 'something'
  });

  let result = subject.get('someCP');
  assert.equal(result, 'expected value');
});

// **** after ****

import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';

module('x-foo', function(hooks) {
  setupTest(hooks);

  test('computed properly', function(assert) {
    assert.expect(1);

    let Factory = this.owner.factoryFor('component:x-foo');
    let subject = Factory.create({
      name: 'something'
    });

    let result = subject.get('someCP');
    assert.equal(result, 'expected value');
  });
});

Service/Route/Controller Test

// **** before ****

import { moduleFor, test } from 'ember-qunit';

moduleFor('service:flash', 'Unit | Service | Flash', {
  unit: true
});

test('should allow messages to be queued', function (assert) {
  assert.expect(4);
  
  let subject = this.subject();
  
  subject.show('some message here');
  
  let messages = subject.messages;
  
  assert.deepEqual(messages, [
    'some message here'
  ]);
});

// **** after ****

import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';

module('Unit | Service | Flash', function(hooks) {
  setupTest(hooks);
  
  test('should allow messages to be queued', function (assert) {
    assert.expect(4);
  
    let subject = this.owner.lookup('service:flash');
  
    subject.show('some message here');
  
    let messages = subject.messages;
  
    assert.deepEqual(messages, [
      'some message here'
    ]);
  });
});

Ecosystem Updates

The blueprints in all official projects (and any provided by popular addons) will need to be updated to detect ember-qunit version and emit the correct output.

This includes:

  • ember-source
  • ember-data
  • ember-cli-legacy-blueprints
  • others?

This exact process was done for ember-mocha's migration, making this a well trodden path.

Update Guides

The guides includes a section for testing, this section needs to be reviewed and revamped to match the proposal here.

Deprecate older APIs

Once this RFC is implemented, the older APIs will be deprecated and retained for a full LTS cycle (assuming speedy landing, this would mean the older APIs would be deprecated around Ember 2.20). After that timeframe, the older APIs will be removed from ember-qunit and ember-test-helpers and they will release with SemVer major version bumps.

Note that while the older moduleFor and moduleForComponent APIs will be deprecated, they will still be possible to use since the host application can pin to a version of ember-qunit / ember-test-helpers that support its own usage. This is a large benefit of migrating these testing features away from Ember's internals, and into the addon space.

Relationship to "Grand Testing Unification"

This RFC is a small stepping stone towards the future where all types of tests share a similar API. The API proposed here is much easier to extend to provide the functionality that is required for emberjs/rfcs#119.

How We Teach This

This change requires updates to the API documentation of ember-qunit and the main Ember guides' testing section. The changes are largely intended to reduce confusion, making it easier to teach and understand testing in Ember.

Drawbacks

Churn

As mentioned in emberjs/rfcs#229, test related churn is quite painful and annoying. In order to maintain the general goodwill of folks, we must ensure that we avoid needless churn.

This RFC should be implemented in conjunction with emberjs/rfcs#229 so that we can avoid multiple back to back changes in the blueprints.

qunitjs/qunit#977

Until very recently, the QUnit nested module API was only able to allow a single callback for each of the hooks per-nesting level. This means that the proposal in this RFC (which requires the hooks to be setup by ember-qunit) would disallow user-land beforeEach/afterEach hooks to be setup.

The work around is "simple" (if somewhat annoying), which is to "just nest another level". The good news is that Trent Willis fixed the underlying problem in qunitjs/qunit#1188, which should be released as 2.3.4 well before this RFC is merged.

Alternatives

The simplest alternative is to do nothing. This would loose all of the positive benefits mentioned in this RFC, but should still be considered a possibility...

Unanswered Questions

hooks argument

A few folks (e.g. @ebryn and @stefanpenner) have approached me with concerns around the hooks argument I have mentioned/used here. The concerns are generally an initial reaction to the QUnit nested modules API in general and not directly related to this RFC (other than it highlighting a new feature that they haven't used before).

The main concerns are:

  • Teaching folks what hooks means is a bit more difficult because it does not represent the "test environment", but rather just a way to invoke the callbacks for before / beforeEach / after / afterEach.
  • Passing only hooks to the helper functions proposed in the RFC means that if we ever need to thread more information through, we either have to use hooks as a transport or change our API to add more arguments.
  • It seems somewhat impossible to communicate across multiple helpers (again without using hooks as a state/transport mechanism).

I've kicked off a conversation over with the QUnit folks in https://github.com/qunitjs/qunit/issues/1200. If that PR were merged this proposal would be modified to the following syntax:

// current proposal
module('x-foo', function(hooks) {
  setupRenderingTest(hooks);
  // ....snip....
});

// after qunitjs/qunit#1200
module('x-foo', function(hooks) {
  setupRenderingTest(this);
  // ....snip....
});

Another possible solution is to rename the argument (here and in the blueprints) to module. This is more in line with what the QUnit folks view it as: the "module context" that is being created for that specific QUnit.module invocation.

  • Start Date: 2017-07-14
  • Relevant Team(s): Ember.js
  • RFC PR: https://github.com/emberjs/rfcs/pull/236
  • Tracking: https://github.com/emberjs/rfc-tracking/issues/26

Summary

This RFC proposes to deprecate the prototype extensions done by Ember.String, deprecate the loc method, and moving htmlSafe and isHTMLSafe to @ember/template.

Motivation

Much of the public API of Ember was designed and published some time ago, when the client-side landscape looked much different. It was a time without many utilities and methods that have been introduced to JavaScript since, without the current rich npm ecosystem, and without ES6 modules. On the Ember side, Ember CLI and the subsequent addons were still to be introduced. Global mode was the way to go, and extending native prototypes like Ember does for String, Array and Function was a common practice.

With the introduction of RFC #176, an opportunity to reduce and reorganize the API that is shipped by default with an Ember application appears. A lot of nice-to-have functionality that was added at that time can now be moved to optional packages and addons, where they can be maintained and evolved without being tied to the core of the framework.

In the specific case of Ember.String, our goal is that users that need these utility functions will include @ember/string in their dependencies, or rely on common utility packages like lodash.camelcase.

To achieve the above goal we will move the isHTMLSafe/htmlSafe pair into a new package, deprecate String.prototype extensions, and deprecate the utility functions under the Ember.String namespace.

The "Organize by Mental Model" section of RFC #176 mentions the concept of chunking. In the current setup, isHTMLSafe/htmlSafe make sense in the Ember.String namespace because they operate on strings, and they are available on the prototype, "myString".htmlSafe(). However, once prototype extensions are removed it becomes clearer that while this pair operates on strings, they don't transform them in the same way as capitalize or dasherize. They are instead a way for the user to communicate to the templating engine that this string should be safe to render. For this reason, moving to @ember/template seems appropriate.

Extending native prototypes, like we do for String with "myString".dasherize() and the rest of the API, has been falling out of favour more as time goes by. While the tradeoff might have been positive at the beginning, as it allowed users access to a richer API, prototype extensions blur the line between what is the framework and what is the language in a way that is not benefitial in the current module-driven and package-rich ecosystem.

Relatedly, deprecating Ember.String and requiring @ember/string as a dependency allows Ember to provide a leaner default core to all users, as well as iterate faster on the @ember/string package if desired. Doing this will also open a path to extract more packages in the future.

Transition Path

It is important to understand that the transition path will be done in the context of the new modules API defined in RFC #176, which is scheduled to land in 2.16. As this will likely be first of many packages to be extracted from the Ember source, the transition path arrived on needs to be clear and user-friendly.

What is happening for framework developers?

The order of operations will be as follows:

  1. Move htmlSafe and isHTMLSafe to @ember/template
    • Update https://github.com/ember-cli/ember-rfc176-data
  2. Create an @ember/string package with the remaining public API
  3. Create an ember-string-prototype-extensions package that introduces String prototype extensions to aid in transitioning
  4. Make ember-cli-babel aware of the @ember/string package so it tells babel-plugin-ember-modules-api-polyfill not to convert those imports to the global Ember namespace
  5. Update usages in Ember and Ember Data codebases so that the projects do not trigger deprecations
  6. Deprecate Ember.String
    • Write deprecation guide which mentions minimum version of ember-cli-babel, and how/when to use @ember/string and ember-string-prototype-extensions packages
  7. Deprecate loc in @ember/string

What is happening for framework users?

If you are using Ember.String.loc, you will be instructed to move to a dedicated localization solution, as this method will be completely deprecated.

If you are using Ember.String.htmlSafe or Ember.String.isHTMLSafe, you will be instructed to run the ember-modules-codemod and it will update to the correct imports from the @ember/template package.

If you are using one of the other Ember.String methods, like Ember.String.dasherize, you will receive a deprecation warning to inform you that you should run the ember-modules-codemod, update ember-cli-babel to a specific minor version, and add @ember/string to your application's or addon's dependencies.

If you are using the String prototype extensions, like 'myString'.dasherize(), on top of the previous instructions you will be instructed to install ember-string-prototype-extensions in case updating the code to dasherize('myString') is not trivial.

Timeline

  • Deprecations are introduced - Ember 2.x
    • String protoype extensions are deprecated
    • Ember.String functions are deprecated
    • loc is completely deprecated
    • isHTMLSafe and htmlSafe are moved to @ember/template
  • Transition packages are introduced - Ember 2.x
    • @ember/string, which replaced Ember.String
    • ember-string-prototype-extensions, which brings String prototype extensions back
  • Deprecations are removed - Ember 3.x, @ember/string 2.x
    • New major version of Ember is released
    • New major version of @ember/string is released

How We Teach This

Official code bases and documentation

The official documentation –website, Guides, API documentation– should be updated not to use String prototype extensions. This documentation should already use the new modules API from an effort to update it for Ember 2.16.

The Guides section on disabling prototype extension will need to be updated when String prototype extensions are removed from Ember.

Resources owned by the Ember teams, such and Ember and Ember Data code bases, the Super Rentals repository, or the builds app for the website, will be updated accordingly.

Ember.String.htmlSafe and Ember.String.isHTMLSafe

The move of htmlSafe and isHTMLSafe from Ember.String to @ember/template should be documented as part of the ember-rfc176-data and related codemods efforts, as that project is the source of truth for the mappings between the Ember global namespace and @ember-scoped modules.

Ember.String.loc and import { loc } from '@ember/string';, Ember.String to @ember/string, String prototype extensions

An entry to the Deprecation Guides will be added outlining the different recommended transition strategies.

Ember.String.loc, import { loc } from '@ember/string';

As this function is deprecated, users will be recommended to use a dedicated localization solution.

Ember.String to @ember/string

The way that @ember-scoped modules will work in 2.16 is that ember-cli-babel will convert something like import { dasherize } from '@ember/string'; to import Ember from 'Ember'; const dasherize = Ember.String.dasherize;. What this means is that import { dasherize } from '@ember/string'; will trigger a deprecation if you do not have the @ember/string package in your dependencies.

To address the above deprecation you will need to update ember-cli-babel to a a specific minor version or higher, to make sure it has the logic to detect @ember/string. The specific minor version will be known at the time the deprecation guide is written. You will also need to add @ember/string to your application's development dependencies, or your addon's dependencies.

String prototype extensions

If you are using 'myString'.dasherize() or one of the other functions added to String, you will be instructed to replace that usage with import { dasherize } from '@ember/string'; dasherize('myString'), in addition to the changes on the previous section.

In case your code base is complicated enough that migrating all these usages at the same time is not convenient, you will be able to add ember-string-prototype-extensions to your dependencies, which will bring back extensions, without deprecations.

Drawbacks

A lot of addons that deal with names depend on this behaviour, so they will need to install the addon. Also, Ember Data and some external serializers require these functions.

htmlSafe and isHTMLSafe would need to change packages, thus the reason to try and provide an Ember Watson recipe.

Another side-effect of this change is that certain users might be shipping duplicated code between Ember.String and @ember/string, but it is a necessary stepping stone and might be able to be addressed via svelting.

Alternatives

Leave things as they are.

Unresolved questions

None.

  • Start Date: 2017-07-20
  • RFC PR: https://github.com/emberjs/rfcs/pull/237
  • Ember Issue: (leave this empty)

Summary

This RFC proposes the deprecation of the following classes:

  • Ember.OrderedSet
  • Ember.Map
  • Ember.MapWithDefault

These classes need to be moved to an external addon given they are private classes and unused in Ember.js itself.

Motivation

These classes have not been used in Ember itself for a while now. They have always been private but they are used in a few addons, and in particular Ember Data is using them.

Transition Path

Ember.Map and Ember.MapWithDefault will be deprecated and not extracted, but not before the fix mentioned in the following paragraph is landed in Ember Data. There is already an addon with Ember.OrderedSet extracted (@ember/ordered-set).

Ember Data is quite likely the biggest project using these classes. There is already a PR that needs merging before deprecating Ember.Map and Ember.MapWithDefault https://github.com/emberjs/data/pull/5255. Ember Data still needs to migrate to @ember/ordered-set to its relationship logic.

Once Ember Data is updated to not use the classes from Ember, and that fix is released, the Ember.Map and Ember.MapWithDefault can be deprecated in Ember itself.

How We Teach This

These classes being private would make this simple than other deprecations. People were not supposed to be using a private API and the few that were, would just need to use a new addon.

This should not impact many codebases.

Drawbacks

This requires cooperation with Ember Data, the main user of these classes. It would be nice to have moved Ember Data to using the addon before releasing Ember with the deprecation so the average user does not see any deprecation warning.

Alternatives

Other option would be moving these classes to Ember Data itself or leaving things as they are now.

Unresolved questions

  • Start Date: 2017-07-28
  • RFC PR: #240
  • Ember Issue: (leave this empty)

Summary

This RFC aims to solidify the usage of ES2015 Classes as a public API of Ember so that users can begin building on them, and projects like ember-decorators can continue to push forward with experimental Javascript features. This includes:

  • Making the class constructor function a public API
  • Modifying some of the internals of Ember.Object to support existing features and make the usage of ES Classes cross-compatible with Ember.Object

It does not propose additions in the form of helpers or decorators, which should continue to be iterated on in the community as the spec itself is finalized. It also does not propose deprecating or removing existing functionality in Ember.Object.

Motivation

The Ember Object model has served its purpose well over the years, but now that ES Classes are becoming prevalent throughout the wider Javascript community it is beginning to show its age. With class properties at stage 3 and decorators at stage 2 in the TC39 process, classes are finally at a point where we can start integrating them into Ember.

The ember-decorators project has been experimenting with using ES Classes and filling out the Ember feature-set, allowing us to write Ember classes like so:

export default class MyComponent extends Ember.Component {
  didInsertElement() {
    // do stuff
  }

  @computed
  get foo() {
    // do stuff
  }

  @action
  bar() {
    // do stuff
  }
}

Using classes makes Ember easier to teach and understand by normalizing it with standard Javascript coding practices, and allows us to share code and solutions with other frameworks and libraries. It also brings with it all the benefits of ES Class syntax:

  • More aligned with the greater Javascript community
  • Ability to share code more easily with other libraries and frameworks
  • Easier to statically analyze
  • Cleaner and easier to read (subjective)

The Ember Object model already works extremely well with ES classes, as demonstrated above, but there several failure scenarios. Furthermore, because they are not officially supported as a public API, there is no guarantee that they will continue to work well. Thus, this RFC seeks to solidify the behavior of ES Classes so that the community can continue to experiment with new Javascript features and build on a stable API.

Detailed Design

Many of the standard features of Ember classes work out of the box today, either with vanilla ES Classes or through ember-decorators, including:

  • Inheritance
  • Lifecycle hooks
  • Computeds
  • Injections
  • Actions

However, the following features either do not exist or do not work as a user familiar with Ember.Object would expect:

  • Extending from ES Classes using extend
  • Class properties
  • Mixins
  • Observers and events
  • Merged and concatenated properties

These features will require changes to Ember.Object

Extend

Currently, once a class is defined using ES Classes it is not possible for users to extend it using the previous CoreObject style of writing and extending classes. This can limit the rate of adoption because ES Classes would become a trapdoor - once you begin using them, you must continue to use them. It would be a particularly thorny issue for addon developers, who may design components which their users expect to be able to extend and modify.

This RFC proposes that extend be fixed on ES Classes to make them fully cross-compatible with the existing syntax. There are two general approaches to making this work:

  1. Modify CoreObject to use prototypes/ES Classes internally. This would bring CoreObject more inline with ES Classes, but would be a significant internal change.

  2. Modify CoreObject to have different behavior if it is extending an ES Class using extend.

Both approaches should be explored and benchmarked to determine if there are an significant advantages to one over the other.

Class Properties

When using Ember.Object.extend, properties that are passed in on the object are assigned to the prototype of the class:

const Foo = Ember.Object.extend({ bar: 'baz' });
const foo = Foo.create();

console.log(Foo.prototype.bar) // 'baz'
foo.hasOwnProperty('bar') // false

This differs from the behavior of ES Class properties, which initialize their value on the instance of the class.

class Foo {
  bar = 'baz'
}

const foo = new Foo();

console.log(Foo.prototype.bar) // undefined
foo.hasOwnProperty('bar') // true

The above is essentially currently compiled down by Babel to the following:

class Foo {
  constructor() {
    this.bar = 'baz';
  }
}

Property assignments like this are always done at the end of the constructor, and given the requirement that super must always be called before properties are assigned it is unlikely that this will change as the spec progresses.

While one might intuitively expect class properties to function the same in ES Classes as they do with Ember Objects, this difference in behavior means that class properties will always be assigned after properties passed into create are initialized on the object, and thus will always win:

const Foo = Ember.Object.extend({ testProp: 'default value' });

class Bar extends Ember.Object {
  testProp = 'default value'
}

const foo = Foo.create({ testProp: 'new value' });
const bar = Bar.create({ testProp: 'new value' });

console.log(foo.get('testProp')); // 'new value'
console.log(bar.get('testProp')); // 'default value'

This behavior makes sense when you consider that it is equivalent to assigning values in init rather than on the object when it is defined. Rather than modify Ember.Object to treat class properties as default values, this RFC proposes that we accept the difference in behavior and utilize the constructor to allow users to set default values, as in the following example:

class Foo extends Ember.Object {
  constructor(props) {
    props.testProp = props.testProp || 'default value';

    super(props);
  }
}

This enforces a public API rather than allowing create to override values as it pleases, and is more inline with the behavior of components in Glimmer today - args that are passed into the class are distinguished from properties that are defined on the class.

Mixins

Mixins are a contentious part of both the Ember Object model and the wider Javascript community - some swear by the pattern, and others believe it fundamentally flawed. While Ember mixins are at the core of Ember Object, the fact is that no standard solution for them has arisen in the wider Javascript community as of yet.

Additionally, while concepts like computed properties, actions, and service injection are either unique to Ember or highly dependent on implementation, mixins can be implemented in a generic way which could be used across all of Javascript, independent of one's framework or library of choice. With that in mind, this RFC considers mixins out of scope and suggests that in the future Ember users can choose to use a mixin library if it suits their needs.

It should also be noted that existing classes which have used mixins can still be extended using ES Class syntax:

const Mix = Ember.Mixin.create({ bar: 'baz' });
const Foo = Ember.Object.extend(Mix, { /* ... */ });

class Bar extends Foo { /* ... */ }

const bar = Bar.create();

console.log(bar.get('bar')); // 'baz'

Observers and Events

Observers and events both fail to work properly when using ES Class syntax. The root of the issue here is how Ember.Object works at a fundamental level, and will require some refactoring to fix.

Currently, each time Ember.Object.extend is used, it stores the list of mixins and objects passed in on a list which also contains the superclass's properties and mixins, and so on. A class is then returned which has access to a closure variable, wasApplied:

makeCtor = function() {
  wasApplied = false;

  return class {
    constructor() {
      if (!wasApplied) {
        this.proto();
      }
    }
  }
}

The proto function walks the chain of stored mixins, collapsing them into a single object prototype the first time the class is created. It is during this walk that observers and events listeners are applied and finalized, as well as merged and concatenated properties applied (this will be touched on more in the next section).

Unfortunately, due to the nature of how observers and event listeners work, they cannot be applied at class definition time without a class decorator. For example:

const Foo = Ember.Object.extend({
  fooObserver: Ember.observer('foo', function() { /* ... */m })
});

class Bar extends Foo {
  fooObserver() { /* ... */ }
}

When proto walks the mixin chain for Foo, it will add an observer that triggers the fooObserver function whenever foo changes. Bar, however, overloads the fooObserver function with a function that is not observed, and thus should not trigger (this is analagous to how Ember Object's work today). Currently there is no time at which Bar can inspect undecorated properties to determine if the superclass has already defined them and if they are observed and thus should have the observer removed.

To fix this, the wasApplied state should be moved to the ember meta object on the class itself, so that both Ember Objects and ES Classes can track if they have had it applied. Additional logic will also need to be added to allow the current "squashing" behavior of proto to work with Prototypes instead of a list of mixins as well.

Merged and Concatenated Properties

Ember Objects currently have the ability to define special properties which are merged or concatenated with their superclass when extended. This is most commonly seen with actions and classNames among others.

As mentioned in the last section, merged and concatenated properties are also combined during the proto "squash" phase, and so it is also broken in ES Classes currently. This RFC proposes that their behavior also be fixed as part of the refactors to Ember.Object.

How We Teach This

The sole purpose of this RFC is to make the behavior of ES Classes within Ember a public API so that projects like ember-decorators can continue to build and experiment with confidence that the underlying behavior will not change. The Ember Object model will remain exactly the same as today, and will continue to be the recommended path for Ember users. Thus, we will not need to add new documentation for the time being.

Drawbacks

  • Making constructor a public API means we are solidifying the lifecycle of objects, locking us into a particular sequence of events (init occurs within the super() portion of the constructor).
  • Lack of mixin support may make it difficult for mixin heavy codebases to utilize ES Classes.
  • ES Class features/usage such as getters and setters may confuse users in general (getter functions will appear to work, but without a computed decorator will not update, etc.)

Alternatives

  • Class property initialization can be changed such that properties are initialized after the constructor runs entirely, allowing them to be overwritten by values passed to create

Topics for Future RFCs

While working on this RFC, some issues were brought into focus regarding existing features in CoreObject that are seen as problematic or unintuitive. In order to avoid bikeshedding these have been slated for discussion in future RFCs, but the discussion points have been included below.

Merged and Concatenated Properties

Merged and concatenated properties are pain points for new Ember developers, specifically because they give no lexical hint that they are special in any way. Developers must know that these particular properties will be merged with the superclass, and there is no way to opt out of this behavior.

With decorators, this same behavior can be accomplished in a much clearer and more straightforward way:

class FooComponent extends Ember.Component {
  @concatenated classNameBindings = ['foo']

  @computed
  get foo() { /* ... */ }

  @merged actions = {
    bar() { /* ... */ }
  }
}

They could also be accomplished more ergonomically with specialized decorators:

class FooComponent extends Ember.Component {
  @className
  @computed
  get foo() { /* ... */ }

  @action
  bar() { /* ... */ }
}

This approach has two distinct advantages over the existing behavior:

  1. It is less magical. The decorators indicate to new users that the properties are special in some way, and ultimately they are just plain decorators, which are compatible with ES Classes as a whole and can be reused anywhere.
  2. It provides a way to opt out of the behavior. Currently, there is no easy way to prevent properties which were marked to be merged from being merged, meaning subclasses are stuck with the values that their superclass provided.

Observers and Listeners

Observers and event listeners are a powerful pattern that saw a lot of usage in Ember 1. However, it is now widely accepted that they are problematic when overused, and using computed properties and lifecycle hooks are better patterns in most cases.

As such, rather than having events and observers turned on by default it may make more sense to have them be opt-in APIs. This could be accomplished by making new class decorators like so:

@evented
class Foo extends Ember.Object {
  @on('init')
  onInit() {
    // do something
  }
}

Or it could be accomplished with new base classes that include the functionality:

class Foo extends EventedObject {
  @on('init')
  onInit() {
    // do something
  }
}

Unresolved questions

None currently

Browser Support Changes

  • Start Date: 2017-09-25
  • RFC PR: (leave this empty)
  • Ember Issue: (leave this empty)

Summary

Solicit feedback on dropping support for IE9, IE10, and PhantomJS.

Motivation

As Ember heads towards version 3.0, it is a good time to evaluate our browser support matrix. Ember follows Semantic Versioning, and we consider browser compatibility to be under the umbrella of those guarantees. In other words, we will continue to support whatever browsers we officially support in Ember 3.0 until Ember 4.0.

We want to make this decision on the basis of the browsers that our community still needs to support, while weighing that against the costs we bear as a community to support older browsers. This RFC will lay out some of those costs, so we can decide what tradeoff is most appropriate. Members of the core team maintain many different kinds of apps across many different kinds of companies. Some of us work on applications with small, agile teams, while others work inside of large corporations with many engineers. When this topic came up amongst the team, we discovered that, across all these different companies and Ember apps, we did not generally support IE9, IE10, and PhantomJS.

Because of this, the core team's impression is that the costs support now far exceed the benefits, and we are considering dropping support for them in Ember 3.0. Before we make the decision, we want to hear from the rest of the community. Supporting IE9, IE10, and PhantomJS incurs significant cost, both in terms of features and maintenance, and we want the community to help us think through the cost-benefit analysis.

Ember is more than just the framework's code. When people use Ember, they expect to be able to use Ember's tooling, read Ember's documentation, find solutions to problems on Stack Overflow, and read tutorials produced by community members. All of these, including addons that follow Ember’s lead, are shackled to the limitations of these legacy browsers. By dropping support for them, people can begin to rely on the improved baseline of features.

Some of the features (unavailable in IE9, IE10, or PhantomJS) that addons will be able to freely take advantage of include:

Below, we’ve outlined several specific features we’re interested in using to improve the Ember framework itself. We’ve also included some other supporting arguments for this decision.

Vendor Support

Microsoft dropped most support and maintenance for IE9 and IE10 on 2016-01-16 (IE9 on Vista SP2 expired in April 2017).

With the advent of headless Chrome and Firefox, PhantomJS is now effectively unmaintained. The default testing boilerplate for Ember CLI-generated applications was changed to headless Chrome in Ember CLI 2.15.

WeakMap, Map, Set

From a framework perspective, being able to rely on native WeakMap support will allow us to remove a significant number of fallback paths that are used in browsers without WeakMap. Using WeakMap results in better developer ergonomics as it allows us to remove many of the random properties that we currently have to assign to an object which makes interacting with your objects in the devtools much less noisy. Minimal support for WeakMap was introduced in IE11.

Better ES Class Support

In order to support static class methods (with inheritance) transpilers (e.g. Babel) need to leverage the Object.setPrototypeOf / Object.getPrototypeOf APIs. Without the ability to rely on Object.setPrototypeOf we will not be able to continue iterating slowly towards leveraging ES classes as a replacement for the custom object model functionality that we have known and loved for so many years. Specifically, there is no replacement / capability to support proper inheritance with .reopenClass. There are several lower-fidelity hacks you might opt into, but none that we think satisfy the needs of the Ember community.

Generally this means IE11 is the oldest browser we can reliably transpile ES classes for reliably.

Typed Arrays

Typed arrays are not currently used in Ember, but experimentation is underway deep in the internals of Glimmer VM to be able to further reduce template size and the costs associated with expanding the wire format (currently a JSON structure) into a runnable program. Leveraging typed arrays would allow Ember and Glimmer apps to completely avoid the wire format to opcode compilation that currently happens before initial render. It also significantly reduces the resulting memory footprint for the same runnable program.

DOM API Improvements

Although IE9 introduced JavaScript engine with support for much of ES5, it was not until IE10 that the browser began to support much of what developers consider modern web platform APIs. Littered throughout the Ember and Glimmer VM codebase are many examples of IE9 workarounds (and PhantomJS workarounds, in fact). We’ve worked hard to make these fixes free at runtime for modern browsers, but some cost is unavoidable.

PhantomJS in particular is a weird environment. Users must often fix Phantom-specific browser bugs, which is wasted effort since real users never run your app in Phantom. And "how to debug in Phantom" is an entire extra skill people are forced to learn. Testing your app in PhantomJS is generally a form of “testing theater”, since it fails to execute your code in a realistic environment.

requestAnimationFrame

IE10 introduced support for requestAnimationFrame, an efficient way to schedule work in the browser environment. We’re interested in using this API to explore incremental rendering strategies, and as a way to improve Ember’s coordination with the browser when native promises are used in application code.

Detailed Design

When using Ember applications in IE9, IE10, or PhantomJS, Ember will cause an appropriate deprecation to be issued. The deprecation will be “until 3.0” and will reference an entry in the deprecation guide. The guide entry will describe For example:

Using Ember.js in IE9, IE10, or PhantomJS is deprecated and will be unsupported in Ember.js 3.0. We recommend using Ember’s 2.x LTS releases if your applications must support those browsers.

PhantomJS is often used for continuous integration testing. We strongly suggest adopting headless Chrome or Firefox to run CI tests.

Drawbacks

Many users have told us that they chose Ember because of the community's commitment to backwards compatibility. There will always be organizations using Ember that exist on the tail-end of browser adoption patterns. We risk alienating or upsetting those users by dropping support for a browser that, while on the way out, is not yet completely gone.

However, in many cases, the requirement for supporting these legacy browsers is driven by non-technical management who do not have a strong sense of the experience of using apps in IE9/IE10. In practice, many applications are not rigorously tested in older browsers, and the performance is so bad that applications written using any framework perform poorly. Techniques that framework and application developers use to make Chrome fast quite often have pathological characteristics on browsers with legacy DOM and JavaScript engines.

Still, some people make it work, and dropping support may prevent those teams from staying with the community as it migrates to Ember 3.0.

As a mitigation for these concerns, the final release of Ember 2.x will itself be made an LTS release. This will ensure a 2.x platform supporting IE9+ with critical bugfix for roughly 8 months following the 3.0 release and security fixes for roughly 14 months after 3.0 release.

Alternatives

Bring Your Own Compatibility

Some libraries attempt to thread the needle of compatibility by asking users to bring their own compatibility libraries. They write the internals of their framework as if these older browsers did not exist, and require end users to use polyfills to make the environment look equivalent to newer browsers.

We have spent considerable effort on first-class support in Ember 2.x, and we feel that users who require IE9 and IE10 support will have a better experience using Ember 2.x. (with the subset of the ecosystem that supports 2.x) than trying to cobble together a solution that works reliably in a version of Ember with second-class, bring-your-own-compatibility support.

  • Start Date: 2017-11-05
  • RFC PR: emberjs/rfcs#268
  • Ember Issue: (leave this empty)

Summary

The testing story in Ember today is better than it ever has been. It is now possible to test individual component/template combos, register your own mock components/services/etc, build complex acceptance tests, and almost anything else you would like.

Unfortunately, there is a massive disparity between different types of tests. In acceptance tests, you use well designed global helpers to deal with async related interactions; whereas in integration and unit tests you are forced to manually deal with this asynchrony. emberjs/rfcs#232 introduced us to QUnit's nested modules API, made integration and unit testing modular, and greatly simplified the concepts needed to learn how to write unit and integration tests. The goal of this RFC is to leverage what we have learned in prior RFCs and apply that knowledge to acceptance testing. Once this RFC has been implemented all test types in Ember will have a unified cohesive structure.

Motivation

Usage of rendering tests is becoming more and more common, but these tests often include manual event delegation (this.$('.foo').click() for example), and assumes most (if not all) interactions are synchronous. This is a major issue due to the fact that the vast majority of interactions will actually be asynchronous. There have been a few recent additions to @ember/test-helpers that have made dealing with asynchrony better (namely emberjs/rfcs#232) but forcing users to manually manage all interaction based async is a recipe for disaster.

Acceptance tests allow users to handle asynchrony with ease, but they rely on global helpers that automatically wrap a single global promise which makes testing of interleaved asynchronous things more difficult. There are a number of limitations in acceptance tests as compared to integration tests (cannot mock and/or stub services, cannot look up services to setup test context, etc).

We need a single unified way to teach and understand testing in Ember that leverages all the things we learned with the original acceptance testing helpers that were introduced in Ember 1.0.0. Instead of inventing our own syntax for dealing with the async (andThen) we should use new language features such as async / await.

Detailed design

The goal of this RFC is to introduce new system for acceptance tests that follows in the footsteps of emberjs/rfcs#232 and continues to enhance the system created in that RFC to share the same structure and helper system.

This new system for acceptance tests will be implemented in the @ember/test-helpers library so that we can iterate faster while supporting multiple Ember versions independently and easily support multiple testing frameworks build on top of the primitives in @ember/test-helpers. Ultimately, the existing ember-testing system will be deprecated but that deprecation will be added well after the new system has been released and adopted by the community.

Lets take a look at a basic example (lifted from the guides):

// **** before ****
import { test } from 'qunit';
import moduleForAcceptance from '../helpers/module-for-acceptance';

moduleForAcceptance('Acceptance | posts');

test('should add new post', function(assert) {
  visit('/posts/new');
  fillIn('input.title', 'My new post');
  click('button.submit');
  andThen(() => assert.equal(find('ul.posts li:first').text(), 'My new post'));
});

// **** after ****
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { visit, fillIn, click } from '@ember/test-helpers';

module('Acceptance | login', function(hooks) {
  setupApplicationTest(hooks);

  test('should add new post', async function(assert) {
    await visit('/posts/new');
    await fillIn('input.title', 'My new post');
    await click('button.submit');

    assert.equal(this.element.querySelectorAll('ul.posts li')[0].textContent, 'My new post');
  });
});

As you can see, this proposal unifies on Qunit's nested module syntax following in emberjs/rfcs#232's footsteps.

New APIs Proposed

The following new methods will be exposed from ember-qunit:

declare module 'ember-qunit' {
  // ...snip... 
  export function setupApplicationTest(hooks: QUnitModuleHooks): void;
}

DOM Interaction Helpers

New native DOM interaction helpers will be added to both setupRenderingTest and (proposed below) setupApplicationTest. The implementation for these helpers has been iterated on and is quite stable in the ember-native-dom-helpers addon.

The helpers will be migrated to @ember/test-helpers and eventually (once "the dust settles") ember-native-dom-helpers will be able to reexport the versions from @ember/test-helpers directly (which means apps that have already adopted will have very minimal changes to make).

The specific DOM helpers to be added to the @ember/test-helpers module are:

/**
  Clicks on the specified selector.
*/
export function click(selector: string | HTMLElement): Promise<void>;

/**
  Taps on the specified selector.
*/
export function tap(selector: string | HTMLElement): Promise<void>;

/**
  Triggers a keyboad event on the specified selector.
*/
export function triggerKeyEvent(
  selector: string | HTMLElement,
  eventType: 'keydown' | 'keypress' | 'keyup',
  keyCode: string,
  modifiers?: {
    ctrlKey: false,
    altKey: false,
    shiftKey: false,
    metaKey: false
  }
): Promise<void>;

/**
  Triggers an event on the specified selector.
*/
export function triggerEvent(
  selector: string | HTMLElement,
  eventType: string,
  eventOptions: any
): Promise<void>;

/**
  Fill in the specified selector's `value` property with the provided text.
*/
export function fillIn(selector: string | HTMLElement, text: string): Promise<void>;

/**
  Focus the specified selector.
*/
export function focus(selector: string | HTMLElement): Promise<void>;

/**
  Unfocus the specified selector.
*/
export function blur(selector: string | HTMLElement): Promise<void>;

/**
  Returns a promise which resolves when the provided callback returns a truthy value.
*/
export function waitUntil<T>(Function: Promise<T>, { timeout = 1000 }): Promise<T>;

/**
  Returns a promise which resolves when the provided selector (and count) becomes present.
*/
export function waitFor(selector: string, { count?: number, timeout = 1000 }): Promise<HTMLElement | HTMLElement[]>;

setupApplicationTest

This function will:

  • invoke ember-test-helpers setupContext with the tests context (which does the following):
    • create an owner object and set it on the test context (e.g. this.owner)
    • setup this.pauseTest and this.resumeTest methods to allow easy pausing/resuming of tests
  • add routing related helpers
    • setup importable visit method to visit the given url
    • setup importable currentRouteName method which returns the current route name
    • setup importable currentURL method which returns the current URL
  • add DOM interaction helpers (heavily influenced by @cibernox's lovely addon ember-native-dom-helpers)
    • setup a getter for this.element which returns the DOM element representing the applications root element
    • setup importable click helper method
    • setup importable tap helper method
    • setup importable triggerKeyEvent helper method
    • setup importable triggerEvent helper method
    • setup importable fillIn helper method
    • setup importable focus helper method
    • setup importable blur helper method
    • setup importable waitUntil helper method
    • setup importable waitFor helper method

setupRenderingTest

The setupRenderingTest function proposed in emberjs/rfcs#232 (and implemented in ember-qunit@3.0.0) will be modified to add the same DOM interaction helpers mentioned above:

  • setup importable click helper method
  • setup importable tap helper method
  • setup importable triggerKeyEvent helper method
  • setup importable triggerEvent helper method
  • setup importable fillIn helper method
  • setup importable focus helper method
  • setup importable blur helper method
  • setup importable waitUntil helper method
  • setup importable waitFor helper method

Once implemented, setupRenderingTest and setupApplicationTest will diverge from each other in very few ways.

Changes from Current System

Here is a brief list of the more important but possibly understated changes being proposed here:

  • The global test helpers that exist now, will no longer be present (e.g. click, visit, etc) and instead will be available on the test context as well as importable helpers.
  • this.owner will now be present and allow (for the first time 🎉) overriding items in the container/registry.
  • The new system will leverage the Ember.Application / Ember.ApplicationInstance split so that we can avoid creating an Ember.Application instance per-test, and instead leverage the same system that FastBoot itself uses to avoid running initializers for each acceptance test.
  • Implicit promise chaining will no longer be present. If your test needs to wait for a given promise, it should use await (which will wait for the system to "settle" in similar semantics to today's wait() helper).
  • The test helpers that are included by a new default ember-cli app will be no longer needed and will be removed from the new application blueprint. This includes:
    • tests/helpers/resolver.js
    • tests/helpers/start-app.js
    • tests/helpers/destroy-app.js
    • tests/helpers/module-for-acceptance.js

Examples

Test Helper

Assuming the following input:

import Ember from 'ember';

export function withFeature(app, featureName) {
  let featuresService = app.__container__.lookup('service:features');
  featuresService.enable(featureName);
}

Ember.Test.registerHelper('withFeature', withFeature);

In order for an addon to support both the existing acceptance testing system, and the new system it could replace that helper with the following:

import { registerAsyncHelper } from '@ember/test';

export function enableFeature(owner, featureName) {
  let featuresService = owner.lookup('service:features');
  featuresService.enable(featureName);
}

registerAsyncHelper('withFeature', function(app, featureName) {
  enableFeature(app.__container__, featureName);
});

This allows both the prior API (without modification) and the following:

// Option 2:
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { enableFeature } from 'addon-name-here/test-support';

module('asdf', function(hooks) {
  setupApplicationTest(hooks);

  test('awesome test title here', function(assert) {
    enableFeature(this.owner, 'feature-name-here');

    // ...snip...
  });
});

Registering Factory Overrides

Overriding a factory is generally done to allow the test to have more control over the thing being tested. This is sometimes used to prevent side effects that are not related to the test (i.e. to prevent network calls), other times it is used to allow the test to inject some known state to make asserting the results much easier.

It is currently possible to register custom factories in integration and unit tests, but not in acceptance tests (without using private API's that is).

As of emberjs/rfcs#232 the integration/unit test API for this registration is:

this.owner.register('service:stripe', MockService);

This RFC will allow this invocation syntax to work in all test types (acceptance, integration, and unit).

Migration

It is important that both the existing acceptance testing system, and the newly proposed system can co-exist together. This means that new tests can be generated in the new style while existing tests remain untouched.

However, it is likely that ember-qunit-codemod will be able to accurately rewrite acceptance tests into the new format.

How We Teach This

This change requires updates to the API documentation of ember-qunit and the main Ember guides' testing section. The changes are largely intended to reduce confusion, making it easier to teach and understand testing in Ember.

Drawbacks

  • This is a relatively large set of changes that are arguably not needed (things mostly work today).
  • One of the major hurdles in upgrading larger applications to newer Ember versions, is updating their tests to follow "new" patterns. This RFC introduces yet another "new" thing (and proposes to deprecate the old thing), and could therefore be considered "just more churn".

Alternatives

  • Do nothing?
  • Make ember-native-dom-helpers a default addon (removing the need for DOM interaction helpers proposed here).
  • Start Date: 2017-11-20
  • Relevant Team(s): Ember.js
  • RFC PR: https://github.com/emberjs/rfcs/pull/272
  • Tracking: https://github.com/emberjs/rfc-tracking/issues/12

Deprecate Function.prototype.on, Function.prototype.observes and Function.prototype.property

Summary

This RFC proposes to deprecate Function.prototype.on, Function.prototype.observes and Function.prototype.property

Motivation

Ember has been moving away from extending native prototypes due to the confusion that this causes users: is it specifically part of Ember, or JavaScript?

Continuing in that direction, we should consider recommending the usage of on (@ember/object/evented), observer (@ember/object) and computed (@ember/object) as opposed to their native prototype extension equivalents. We go from two ways to do something, to one.

eslint-plugin-ember already provides this as a rule.

Transition Path

The replacement functionality already exists in the form of on, observer, and computed.

We don't need to build anything new specifically, however, the bulk of the transition will be focused on deprecating the native prototype extensions.

A codemod for this deprecation has to take into consideration that while foo: function() { /* custom logic */ }.property('bar') is a Function.prototype extension, foo: observer(function () { /* some custom logic */ }).on('customEvent') is not.

How We Teach This

On the deprecation guide, we can showcase the same example as above. We can explain why the proposal was necessary, followed by a set of examples highlighting the deprecated vs current style.

Borrowing from the ESLint plugin example:

import { computed, observer } from '@ember/object';
import { on } from '@ember/object/evented';

export default Component.extend({
  // deprecated
  abc: function() { /* custom logic */ }.property('xyz'),
  def: function() { /* custom logic */ }.observe('xyz'),
  ghi: function() { /* custom logic */ }.on('didInsertElement'),
  jkl: function() { /* custom logic */ }.on('customEvent'),

  // current
  abc: computed('xyz', function() { /* custom logic */ }),
  def: observer('xyz', function() { /* custom logic */ }),
  didInsertElement() { /* custom logic */ }),
  jkl: on('customEvent', function() { /* custom logic */ }),
});

The official Guides currently discourage the use of Function.prototype extensions:

Function is extended with methods to annotate functions as computed properties, via the property() method, and as observers, via the observes() method. Use of these methods is now discouraged and not covered in recent versions of the Guides.

After the deprecated code is removed from Ember, we need to remove the section about Function prototypes altogether.

Alternatives

None.

  • Start Date: 2017-12-10
  • RFC PR: https://github.com/emberjs/rfcs/pull/276
  • Ember Issue: https://github.com/emberjs/ember.js/pull/15968

Summary

Introduce {{@foo}} in as a dedicated syntax for a component's template to refer to named arguments passed in by the caller.

For example, given the invocation {{hello-world name="Godfrey"}} and this component template in app/templates/components/hello-world.hbs:

Hello, {{@name}}

Ember will render "Hello, Godfrey".

Motivation

Currently, the way to access named arguments passed in from the caller is to reference {{name}} in the template. This works because when Ember creates the component instance, it automatically assigns all named arguments as properties on the component instance.

The first problem with this approach is that the {{name}} syntax is highly ambigious, as it could be referring to a local variable (block param), a helper or a named argument from the caller (which actually works by accessing auto-reflected {{this.name}}) or a property on the component class (such as a computed property).

This can often lead to confusion for readers of the template. Upon encountering {{foo}} in a component's template, the reader has to check all of these places: first you need to scan the surrounding lines for block params with that name; next you check in the helpers folder to see if there is a helper with that name (it could also be coming from an addon!); then you check if it is an argument provided by the caller; finally, you check the component's JavaScript class to look for a (computed) property. If you still did not find it, maybe it is a named arguments that is passed only sometimes, or perhaps it is just a leftover reference from a previous refactor?

Providing a dedicated syntax for referring to named arguments will resolve the ambiguity and greatly improve clarity, especially in big projects with a lot of files (and uses a lot of addons). (The existing {{this.name}} syntax can already be used to disambiguate component properties from helpers.)

As an aside, the ambiguity that causes confusion for human readers is also a problem for the compiler. While it is not the main goal of this proposal, resolving this ambiguity also helps the rendering system. Currently, the "runtime" template compiler has to perform a helper lookup for every {{name}} in each template. It will be able to skip this resolution process and perform other optimizations (such as reusing the internal reference object and caches) with this addition.

Another problem with the current approach of automatically "reflecting" named arguments on the instance is that they can unexpectedly overwrite other properties defined on the component's class. It also defeats performance optimizations in JavaScript engines as this approach creates many different polymorphic "shapes" for instances that otherwise belong to the same component class.

While this proposal does not directly solve this problem (we are not proposing that we deprecate or remove the "auto-reflection" on Ember.Component), it paves the way for a future world where components can work without them.

Notably, the current iteration of the Glimmer Components have adopted this design for over a year now and the experience has been very positive. This would be one of the first pieces (admittedly, only a tiny piece) of the Glimmer.js experiment to make its way into Ember. We think this feature is small, self-contained but useful enough to be the ideal candidate to kick off this process.

Detailed design

This feature was baked into the Glimmer VM very early on. In fact, the only thing that is stopping them from working in Ember is an AST transform that specifically disallows them. Therefore, "implementing" this feature is just a matter of deleting that file.

Additionally, the legacy {{attrs.foo}} syntax (which more or less tries to accomplish the same thing) has actually been implemented using {{@foo}} under-the-hood since Ember 2.10.

Reserved Names

We will reserve {{@args}}, {{@arguments}} and anything that does not start with a lowercase letter (such as @Foo, @0, @! etc) in the first version. This is purely speculative and the goal is to carve out some space for future features. If we don't end up needing them, we can always relax the restrictions down the road.

How We Teach This

{{@foo}} is the way to access the named arguments passed from the caller.

Since the {{foo}} syntax still works on Ember.Component (which is the only kind of components available today) via the auto-reflection mechanism, we are not really in a rush to migrate the community (and the guides, etc) to using the new syntax. In the meantime, this could be viewed as a tool to improve clarity in templates, similar to how the optional "explicit this" syntax ({{this.foo}}).

While we think writing {{@foo}} would be a best practice for new code going forward, the community can migrate at its own pace one component at a time.

We can also encourage the community to supplement this effort by wiring linting tools and code mods.

Drawbacks

This introduces a new piece of syntax that one would need to learn in order to understand Ember templates.

This mostly affects "casual" readers (as this should be very easy for an Ember developer to learn, understand and remember after encounting/learning it for the first time). However, since these casual readers are also among those who are most acutely affected by the ambiguity, we believe this is still a net improvement over the status-quo.

Alternatives

We have {{attrs.foo}} today. In React, there is this.props.foo.

Given how common this is, we think it deserves its own dedicated, succinct syntax. The other alternatives that involve reflecting them on the component instances also would not allow for the internal optimizations in the Glimmer VM.

  • Start Date: 2017-12-11
  • RFC PR: https://github.com/emberjs/rfcs/pull/278
  • Ember Issue: https://github.com/emberjs/ember.js/pull/15974

Summary

Introduce a low-level "flag" to remove the automatic wrapper <div> for template-only components (templates in the components folder that do not have a corresponding .js file).

In other words, given there is NO app/components/hello-world.js and there exists app/templates/components/hello-world.hbs which contains the following markup:

Hello world!

When this template-only component is invoked as {{hello-world}} with the flag unset or disabled (i.e. today's semantics), Ember will render:

<div id="ember123" class="ember-view">Hello world!</div>

When the flag is enabled, the same invocation will render:

Hello world!

Motivation

With today's component system (i.e. Ember.Component), a wrapper element (a div by default, along with an ID like ember123 and the ember-view class) is automatically added for every component.

Customizing this wrapper element (such as changing the tag name – or removing it altogether) requires making changes to the component's JavaScript class, such as:

import Component from "@ember/component";

export Component.extend({
  tagName: "footer",
  classNames: ["legalese"]
});

While we acknowledge this API is quite cumbersome, it is sufficient to "get things done" for regular components, and Glimmer Components will address the usability aspect once they land.

However, this API does not work for template-only components, as they do not have a component JavaScript class by definition. Therefore, in practice, template-only components always come with a <div> wrapper, along with the default id and class attributes, with no obvious ways to customize it.

This is quite problematic, as it is often desirable to use a template-only component to organize content that requires a certain markup structure. The most common workaround for this problem is to use a partial instead, which comes with a host of issues. I will discuss other workarounds in the section below.

This RFC proposes to add a global flag to remove this wrapper element around template-only components. This will allow the component author to specify the wrapper element in the component template, offering direct control over the tag name and other attributes. It would also allow the component to have more than one top-level element, or none at all.

In other words, this flag changes template-only components in the app to have "Outer HTML" semantics. What you type is what you get.

Notably, Glimmer Components have adopted the "Outer HTML" semantics long ago and the experience has been very positive. This would be one of the first pieces of the Glimmer.js experiment to make its way into Ember. We think this feature is small, self-contained but useful enough to be integrated back into Ember at this point.

If accepted, this RFC will fully subsume the Non-context-shifting partials RFC. We can therefore (at a later time, in a separate RFC) explore deprecating partials in favor of wrapper-free template-only components.

Detailed design

API Surface

We should not expose the flag directly as a public API. Instead, we should abstract the flag with a "privileged addon" whose only purpose is to enable the flag. Applications will enable the flag by installing this addon. This will allow for more flexibility in changing the flag's implementation (the location, naming, value, or even the existence of it) in the future. From the user's perspective, it is the addon that provides this functionality. The flag is simply an internal implementation detail.

We have done this before in other cases (such as the legacy view addon during the 2.0 transition period), and it has generally worked well.

When landing this feature, it will be entirely opt-in for existing apps, but the Ember CLI application blueprint should be updated to include the addon by default. At a later time, we should provide another addon that disables the flag explicitly (installing both addons would be an install-time error). At that time, we will issue a deprecation warning if the flag is not set, with a message that directs the user to install one of the two addons.

Single Global "Flag"

The proposed flag will be truly global in scope. That is, setting this flag will change the semantics of all template-only components in the entire app, even for components that were included by addons.

However, we believe this would not affect any addon components in practice, as the predominant pattern for addons to expose components currently necessitates a JavaScript class. Addon authors would create the component (with or without a JavaScript class) in the /addon folder, but exposing it for consumption in apps requires creating a corresponding JavaScript class in the /app folder to "re-export" the component. Therefore, in practice, it is not actually possible for addons to have a truly template-only component today (something to address in a future RFC).

Leakage Of Ember.Component Semantics

While the primary purpose of this flag is to remove the wrapper element from template-only components, there are a few other observable semantics changes that comes with it as well.

Currently, template-only components are "backed" by an instance of Ember.Component. That is, Ember will create an instance of Ember.Component and set it as the {{this}} context for the template.

With the flag enabled, there will be no component instance for the template and {{this}} will be set to undefined (or null, perhaps). This would improve performance for template-only components significantly.

Since there is no JavaScript file for the component, this is only observable in a few limited ways:

  1. The most noticable artifact is the component's arguments will not be auto-reflected on the component instance (as there is no component instance at all). Therefore, the only way to access the component's arguments is to use the {{@foo}} syntax proposed in RFC #276.

  2. Because of the named arguments auto-reflection, it is actually possible to configure the tagName and classes on the "hidden" component instance on the invocation (e.g. {{foo-bar tagName="footer" class="legalese"}}). This will obviously stop working, but it is also not necessary anymore as the component author can simply include the tag in the template. Alternatively, the component author can choose to leave out the tag and let the caller wrap it in their template.

  3. It is possible (but very rare) to configure global injections on the component type. Since no component is being instantiated here, those properties will not be accessible in the template.

    More broadly, {{this.foo}} or the shorthand {{foo}} (where it would have resolved into a this lookup) will always be undefined (or null, perhaps).

Migration Path

Given the subtle semantics differences enumerated above, it is not necessarily safe to simply turn on the flag in bigger applications as it is quite likely that some of the template-only components might be relying on one or more of these features. Further, removing the wrapper element might break the layout.

Therefore, the only safe, mechanical transformation is to generate a JavaScript file for each template-only component (turning them into non- template only components). We should supplement the change by providing a codemod that does this for you.

While this would mean that apps would not be able to immediately take advantage of the feature, it will open the door for new template-only components to be written in the new semantics.

The user can also audit the components we identified and decide to delete the JavaScript and migrate them on a case-by-case basis.

The codemod can also come with a more aggressive (and unsound) mode that simply wraps each template in a <div> (to avoid breaking layout in most cases). This might be acceptable for smaller apps.

For what it's worth, the Ember CLI component blueprint always generate a JavaScript and a template file, so it might not be that common to find existing template-only components in an average app.

Implementation Plan

Finally, for the actual implementation, this would be implemented using the internal Component Manager API that has already been available for a long time (and how Curly Components, outlets etc are implemented internally).

It should be very straightforward implementation – essentially just a Component Manager that requires no capabilities and returns null in getSelf.

How We Teach This

Going forward, the "Outer HTML" semantics will be the default for template-only components, Glimmer Components and other custom component types (when the Component Manager API is available), so over time it should feel quite natural. The experience from the Glimmer experiment has also proven that this is the more natural programming model for components.

In the mean time, we still have to deal with the consequence that existing Ember.Component comes with a wrapper element by default. The mental model for users to understand this is that the Ember.Component class is what is giving you the wrapper element (therefore, template-only components, which is not an Ember.Component does not get one of those).

This should feel quite natual, as the component class is where you configure the wrapper element (and where you would lookup the API documentation). You could imagine that the Ember.Component is doing something like this under-the-hood as a convenience feature (which turned out to be not very convenient after all, but that's a different story):

export const Component = Object.extend({
  tagName: "div",
  classNames: ["ember-view"],

  // This is not real code that exists in the implementation
  render(buffer, template) {
    buffer.append(`<${this.tagName} class="${this.classNames.join(' ')}">`);
    buffer.append(template(this));
    buffer.append(`</${this.tagName}>`);
  }
});

Drawbacks

In general, we avoid flags that puts Ember into very different "modes" as they causes complication across the whole addon ecosystem. However, as mentioned above, we don't believe this would be the case here.

Alternatives

We could keep the current semantics for template-only components. However, this is usually undesirable, and would only grow to feel more unnatural as Glimmer Components and friends adopt the "Outer HTML" semantics.

Alternatively, we can make this opt-in per template using a pragma or magic comment. However, this would be needed for a lot of templates and become very noisy, and the alternative strategy proposed here (by keeping around the Ember.Component JavaScript file as needed) would be able to accomplish the same goal with less noise.

  • Start Date: 2017-12-11
  • RFC PR: https://github.com/emberjs/rfcs/pull/280
  • Ember Issue: https://github.com/emberjs/ember.js/pull/15981

Summary

Introduce a low-level "flag" to remove the automatic wrapper <div> around Ember apps and tests.

Motivation

In Ember applications today, applications are anchored to some existing HTML element in the page. Usually, this element is the <body> of the document, but it can be configured to be a different one when the application is defined, passing a CSS selector to the rootElement property:

export default Ember.Application.extend({
  rootElement: '#app'
});

However, whatever the root is, the application adds another <div> wrapper that is not required anymore. It's a vestigial remainder of some implementation detail of how views worked in Ember 1.x. Some sort of wisdom tooth of the original rendering system that serves no purpose today.

Furthermore, much like a wisdom tooth, it can give us problems. In the past, this element was configurable using the ApplicationView, but when views were removed we lost that ability. Right now we are stuck with a wrapper element we can't remove nor customize, which is why some apps target the selector body > .ember-view to style this element.

Similarly, in testing there is another .ember-view wrapper inside the #ember-testing container for no good reason.

This RFC proposes to add a global flag to remove those wrapper elements, effectively making the application.hbs template have "Outer HTML" semantics, which aligns well with the changes recently proposed for template-only components, as well as the way Glimmer apps work.

The same flag will also remove the unnecessary extra wrapper inside the testing container.

Detailed design

API Surface

The proposed approach is identical to the one proposed in #278, quoted below:

We should not expose the flag directly as a public API. Instead, we should abstract the flag with a "privileged addon" whose only purpose is to enable the flag. Applications will enable the flag by installing this addon. This will allow for more flexibility in changing the flag's implementation (the location, naming, value, or even the existence of it) in the future. From the user's perspective, it is the addon that provides this functionality. The flag is simply an internal implementation detail.

We have done this before in other cases (such as the legacy view addon during the 2.0 transition period), and it has generally worked well.

When landing this feature, it will be entirely opt-in for existing apps, but the Ember CLI application blueprint should be updated to include the addon by default. At a later time, we should provide another addon that disables the flag explicitly (installing both addons would be an install-time error). At that time, we will issue a deprecation warning if the flag is not set, with a message that directs the user to install one of the two addons.

Migration Path

Given that this change only affects one single point in your application, I do not believe we need any specific strategy. If the users want to bring back the wrapper because it breaks their styles or some other reason, they can just add it manually on the application.hbs template, with any class or id they want.

How We Teach This

This addon will be opt-in, but at some point it will become part of the default blueprint. This change, rather than introducing a new concept, removes an old one. Users won't have to google what is the way to remove or customize the implicit application wrapper of the app (to sadly discover that is not even possible), but instead they will add a wrapper only if they want, and in the same way they would add a wrapper in any other point of their application, with regular Handlebars.

Drawbacks

There is a possibility that removing the wrapper can break styles for some apps, but since adding the wrapper back is just editing the application.hbs template, that is probably a minor drawback.

There is also a non-zero chance that some testing addon is relying on the #ember-testing > .ember-view HTML hierarchy for some reason, and those addons would have to be updated.

Alternatives

Leave things as they are today.

  • Start Date: 2017-12-12
  • RFC PR: https://github.com/emberjs/rfcs/pull/281
  • Ember Issue: (leave this empty)

Summary

Install ES5 getters for computed properties on object prototypes, thus eliminating the need to use this.get() or Ember.get() to access them.

Before:

import Object, { computed } from '@ember/object';

const Person = Object.extend({
  fullName: computed('firstName', 'lastName', function() {
    return `${this.get('firstName')} ${this.get('lastName')}`;
  })
});

let chancancode = Person.create({ firstName: 'Godfrey', lastName: 'Chan' });

chancancode.get('fullName'); // => 'Godfrey Chan'

chancancode.set('firstName', 'ʎǝɹɟpo⅁');

chancancode.get('fullName'); // => 'ʎǝɹɟpo⅁ Chan'

let { firstName, lastName, fullName } = chancancode.getProperties('firstName', 'lastName', 'fullName');

After:

import Object, { computed } from "@ember/object";

const Person = Object.extend({
  fullName: computed('firstName', 'lastName', function() {
    return `${this.firstName} ${this.lastName}`;
  })
});

let chancancode = Person.create({ firstName: 'Godfrey', lastName: 'Chan' });

chancancode.fullName; // => 'Godfrey Chan'

chancancode.set('firstName', 'ʎǝɹɟpo⅁');

chancancode.fullName; // => 'ʎǝɹɟpo⅁ Chan'

let { firstName, lastName, fullName } = chancancode; // No problem!

Motivation

Ember inherited its computed properties functionality from SproutCore. The feature was designed at a time before ES5 getters were widely available. This necessitated using a special function such as this.get() or Ember.get() to access the values of computed properties.

Since all of our target browsers support ES5 getters now, we can drop the need of this special function, improving developer ergonomics and interoperability between other libraries and tooling (such as TypeScript).

Note that at present, using this.set() or Ember.set() is still mandatory for the property to recompute properly. In the future, we might be able to loosen this requirement, perhaps with the help of ES5 setters. However, that would require more design and is out-of-scope for this RFC.

this.get() and Ember.get() will still work. This RFC does not propose removing or deprecating them in the near term. They support other use cases that ES5 getters do not, such as "safe" path chaining (get('foo.bar.baz')) and unknownProperty (and Proxies by extension), so any future plans to deprecate them would have to take these features into account.

Addon authors would likely need to continue using Ember.get() for at least another two LTS cycles (8 releases) to support older versions of Ember (and possibly longer to support proxies). It is, however, very unlikely that the everyday user would need to use this.

Detailed design

The computed property function, along with any caches, can be stored in the object's "meta". We will then define a getter on the object's prototype to compute the value.

One caveat is that the computed property function is currently stored on the instances for implementation reasons that are no longer relevant. However, it is possible that some developers have observed their existance and have accidentally relied on these private semantics (e.g. chancancode.fullName.get() or chancancode.fullName.isDescriptor).

Before landing this change, we should turn the property into an assertion so that in these unlikely scenarios, developers will at least receive some warning.

Another thing to consider is that there is this Little Known Trick™ to add Computed Properties to POJOs:

import { computed, get } from "@ember/object";

let foo = {
  bar: computed(function() { return 'bar'; })
};

get(foo, 'bar'); // => 'bar'

In this case, there is no opportunity for us to install an ES5 getter, and Ember.get is the only solution. This is very rare in practice and is more or less just a party trick. We should deprecate this use case (in Ember.get) and suggest the alternative:

import Object, { computed } from "@ember/object";

let foo = Object.extend({
  bar: computed(function() { return 'bar'; })
}).create();

foo.bar; // => 'bar'

Or simply...

let foo = {
  get bar() {
    return 'bar';
  }
};

foo.bar; // => 'bar'

How We Teach This

For the most part, this RFC removes a thing that we need to teach new users.

It might, however, come across as slightly strange that set() is still required. However, many other libraries share the same model, and empricially, this does not appear to be an issue. For example, in React, you can freely access this.state.foo but must use this.setState('foo', ...) to update it. Even Vue has the same API for some cases.

The mental model for this is that you must use the set() in order for Ember to notice your mutations, so that it can update the caches, rerender things on the screen, etc.

As for users who already learned to use get() everywhere, that would continue to work. Ideally, this would be a Cool Trick™ they pick up some day (as in "Oh, I don't have to do that anymore? Cool."), at which point the old habit would quickly die. If this turned out to be too confusing, we could always explore deprecating this.get(); we will just have to weigh the cost-benefits of the confusion (if any) versus churn.

Drawbacks

As mentioned, not removing set() at the same time might be a source of confusion. However, removing set() would require significantly more upfront design work, and it might not even be possible to completely remove the need of set() (as the system is designed today) in all cases (see Vue.set()).

Since removing get() would unlock so many benefits, and since there are plenty of other libraries that uses the same model, the case for decoupling the two seems overwhemlingly positive.

Alternatives

  • Hold off until we also remove set
  • Hold off until we transition to something like Glimmer's @tracked

In my opinion, these alternatives do not make a lot of sense, as neither of these hypothetical systems appear to require (or would benefit from) having a user-land getter system.

  • Start Date: 2017-12-21
  • RFC PR: https://github.com/emberjs/rfcs/pull/286
  • Ember Issue: https://github.com/emberjs/ember.js/pull/16076

Block let template helper

Summary

Introduce the let template helper in block form.

Motivation

The goal of this RFC is to introduce a let template helper that allows to create new bindings in templates. The design of this helper is similar to with, but without the conditional rendering of the block depending on the values passed into the helper.

While the conditional semantics of with are coherent with the other built-in helpers like each and if, users often find this unexpected. The fact that only the first positional parameter of with controls whether the block is rendered might also add to the confusion.

Taking an example from RFC #200, let's consider we have the following template:

Welcome back {{concat (capitalize person.firstName) ' ' (capitalize person.lastName)}}

Account Details:
First Name: {{capitalize person.firstName}}
Last Name: {{capitalize person.lastName}}

Because you have to know to capitalize every time you want to display a name, errors might be introduced if we forget to do it when adding the name somewhere else in the template. Using the let helper, this could be done like so:

{{#let (capitalize person.firstName) (capitalize person.lastName)
  as |firstName lastName|
}}
  Welcome back {{concat firstName ' ' lastName}}

  Account Details:
  First Name: {{firstName}}
  Last Name: {{lastName}}
{{/let}}

Now you can use firstName and lastName inside the let block with the knowledge that that logic is in a single place.

With the introduction of template-only components in RFC #278, having the capability to create additional bindings in the template would prove useful. Another aspect to consider is related to the Named Blocks RFC. In both the case of named blocks and block let, you can achieve most of the same functionality by using components. The components approach has its own drawbacks, which are explored in Alternatives below.

Detailed design

The let helper should be implemented as a built-in helper, with the following semantics:

  • Only the block form is available
  • The block is always rendered
  • It should support however many positional arguments are passed to the helper
  • Positional arguments passed to the helper should be yielded back out in the same order
  • Inline form issues an error, linking users to documentation

There already exists an implementation in the codebase that can be used as a basis.

How We Teach This

The introduction of the let helper brings no new concepts. It touches on the concepts of block helpers, how to pass arguments to them, and how to use block parameters (as |foo|), which should already be introduced in the literature.

Current Ember developers should find it familiar to use let, as it is very similar to with.

JavaScript developers should also be familiar with let bindings, as recent specifications of the language introduced that keyword.

The Guides already possess a section dedicated to Templates, with multiple mentions of helpers. let would likely be documented in the Built-in Helpers guide alongside the others.

If this RFC is approved, the let will initially only support the block form. This means that only the following form is available for users:

{{#let 1 2 3 as |one two three|}}
  A, B, C, easy as {{one}}, {{two}}, {{three}}
{{/let}}

This could also be enforced by issuing a helpful error when let is used in the inline form.

Drawbacks

As is the case when adding any sort of API, we will be increasing the cognitive load of learners and users, as it is one more piece of information to obtain and retain.

The cost of learning this API is mitigated by the fact that its effects are very localized. It is a template helper, so it will only affect templates. It is not required for general usage of Ember, unlike something like link-to, so you can learn the helper at your own pace.

And lastly, if you do use it or encounter it in code, only the markup inside the {{#let}}{{/let}} block is affected, making it easier to reason about.

Alternatives

Inline form

At the moment, the only way to introduce a new binding in a template is through block params. For example, if you are iterating over an array with each, you introduce a binding named item for the item currently being iterated:

{{#each myArray as |item|}}
  I am item {{item}}.
{{/each}}

The inline form of let would be an additional way of introducing bindings in templates. Using the names example from the RFC, it would look like the following in inline form:

{{let
  firstName=(capitalize person.firstName)
  lastName=(capitalize person.lastName)
}}

Welcome back {{concat firstName ' ' lastName}}

Account Details:
First Name: {{firstName}}
Last Name: {{lastName}}

This syntax raises questions about the semantics of the inline form, such as what is the scope of the binding, that are better left to a subsequent RFC.

Using components

In a similar situation to Named Blocks RFC, it is also possible to replicate some of the behavior of the proposed let helper using components. However, using components also presents some drawbacks.

You can extract the template and do:

// app/templates/components/person-tile.hbs
Welcome back {{concat firstName ' ' lastName)}}

Account Details:
First Name: {{firstName}}
Last Name: {{lastName}}
{{person-tile firstName=(capitalize person.firstName) lastName=(capitalize person.lastName)}}

This addresses not having to repeat capitalize wherever the names are used, but splits the content into multiple files for the sake of it. While module unification mitigates the locality problem by putting related files in the same folder, there is still the overhead of having to consult multiple files.

You can instead use a block version of the component as a wrapper to the content. Some variations are possible: you can pass data into the component as either positional or named arguments; you can export either an object with the arguments as keys, or export multiple block parameters.

Passing positional arguments to components is onerous, and necessitates having a JavaScript file to define which positional arguments it accepts.

Passing named arguments to components would be the closest to let, but it would still require a componente template file which would yield them as block parameters.

Yielding out the values is where it gets tricky in components, regardless of returning a hash or multiple block parameters, due to the lack of a "splat" operator in Handlebars.

Since you cannot do something like this at the moment:

// app/templates/components/person-tile.hbs
{{yield ...arguments}}

You would have to explicitly encode all of the arguments:

// app/templates/components/person-tile.hbs
{{yield firstName lastName}}

Or

// app/templates/components/person-tile.hbs
{{yield args=(hash firstName=firstName lastName=lastName)}}

Leading to some repetition of names.

This makes the solution of using components brittle to changes, as typos or ordering mistakes can introduce silent errors in your application.

Adding named arguments to with

RFC #202 proposes to add named arguments to with.

I feel it is less practical to add a new mode to the helper where it always renders, when its semantics are already confusing to users. The RFC #202 proposal also presents the problem of bringing back context-switching helpers, as it proposes omitting block arguments (as |bar| in {{#with foo as |bar|}}).

Remove the conditional behavior of with

Making the with helper unconditionally render the block would be a major breaking change of its semantics, and would likely affect existing applications in insidious ways. For this reason, I reject this alternative out of the gate.

Support let via the ember-let addon

There is an ember-let addon which implements both the block and the inline forms of let. To implement the necessary functionality, the addon had to resort to private API usage, which is brittle and subject to breakage.

Having let available from Ember itself would make sure that it would not be subject to breakage the same way, and the end user would not have to worry about version compatibility.

Unresolved questions

None.

Future work

Deprecating with

With the introduction of the let helper, with should likely be deprecated.

if-let, let* and others

RFC #200 also proposes the if-let and let* helpers.

if-let mimics the behaviour of with, enabling the user to introduce bindings and conditionally rendering the block. The advantage of introducing if-let over using with would be to define its semantics without worrying about making breaking changes to with.

let* would allow bindings to happen sequentially, that is, let ({{let* a=1 b=(sum a 5)}} would be valid instead of throwing an error about a in (sum a 5).

These could also be addressed in subsequent RFCs, focused on the specificities of each proposal.

  • Start Date: 2017-12-22
  • Relevant Team(s): Ember.js
  • RFC PR: https://github.com/emberjs/rfcs/pull/287
  • Tracking: https://github.com/emberjs/rfc-tracking/issues/25

Summary

Promote the private API {{-in-element}} to public API as {{in-element}}.

Motivation

Sometimes developers need to render content out of the regular HTML flow. This concept is often also called "portals". Some components like dropdowns and modals use this technique to render stuff close to the root of the page to bypass CSS overflow rules. Some apps that are embedded into static pages even use this technique to update parts of the page outside the app itself.

This need need has been covered by solutions developed in the user space but it was so common that glimmer baked it into the VM in the form of {{-in-element}}, but it remains private (or intimate) API. People is usually wary of using private APIs (and for good reason) as they may get removed at any time.

If the core team and the community is happy with the current behavior of {{-in-element}} it's time to make it public.

Detailed design

The existing API of {{-in-element}} is very simple:

  • It takes a single positional param destinationElement that is a DOM element, and a block.
  • The given block is rendered not where it is located, but inside the given destination element, at the end of it if there is any other content on the destination element.
  • If destinationElement is null/undefined then it doesn't render anything but it doesn't error.
  • If destinationElement is false/0/"" it raises an assertion in development but fails silently in production.
  • If destinationElement changes the block is removed from the previous destination and added to the new one. This process tears down the rendered content on the initial destination and renders it again on the new one, meaning that any component withing the block will be destroyed and instantiated again (calling the appropiate lifecycle hooks), so transient HTML state like the value of an input will be lost unless manually preserved somewhere else, like a service.
  • If the destination element is an invalid value (a string, a number ...) it throws an parent.insertBefore is not a function error. I think that throwing an error is correct but the error message could be improved.
  • If the destination element has a different context (like SVG) the content will be appended normally by the glimmer VM, which doesn't try to validate the correctness of the generated HTML. This is normal behavior in Glimmer, not an exception, and users must be aware that rendering invalid markup might be interpreted or auto-corrected in unexpected ways by the browser when in SSR mode.
  • Rendering into a foreign object (an element within an <iframe>) should be disallowed initially. If someone asks for this feature it would require an RFC to explore the consequences.

Example usage:

{{#-in-element destinationElement}}
  <div>Some content</div>
{{/-in-element}}

The current implementation only suggests creating a new {{in-element}} construct that is a simple alias of {{-in-element}} with the exact same params and behavior, and then, after a while, remove the private one.

Although {{-in-element}} is technically private, there there is enough people using it to deserve a deprecation. I suggest keeping the deprecated private API will until the first LTS release of the 3.X cycle (3.4) to be finally removed in the next one (3.5).

Small proposed changes

There is however one part of the behavior that the core team wants to make explicit before promoting the private API to public, and that is how the content is added to the destination when there is other content already there.

The desired behavior is that, by default, the rendered content will replace all the content of the destination, effectively becoming the its innerHTML. In the current behaviour the rendered content is appended as the end of any existing content. This will still be supported by passing insertBefore=null, but it will not be the default anymore. Any other value passed to insertBefore must produce an error.

How We Teach This

This will be a new build-in helper and must be added to the guides and the API. For most usages, it will replace some community solution created with the same goal, like ember-wormhole or ember-elsewhere. It would be for the best to let the authors of those addons know about this feature so they can deprecate their packages if they feel there is no longer a need for them, or at least update their Readme files to let their users know that there is a built-in solution in Ember that might cover their needs.

Drawbacks

By augmenting the public API of the framework, the framework is committing to support it for the lifespan of an entire mayor version (Ember 4.0).

Alternatives

We can decide that the framework does not want to make public and support this feature, and continue to rely on community-built addons like we've done until today.

Unresolved questions

Do we want to make any improvement to {{-in-element}} before making it public API?

Some possible ideas:

  • Allow to conditionally render the block in place. See https://github.com/DockYard/ember-maybe-in-element
  • Allow to receive not only DOM elements as first argument, but also strings, representing the ID of other CSS selector.
  • Modify or improve the way it behaves during SSR using ember-fastboot.
  • Start Date: 2018-01-10
  • Relevant Team(s): Ember Data
  • RFC PR: github.com/emberjs/rfcs/pull/293
  • Tracking: https://github.com/emberjs/rfc-tracking/issues/24

Summary

Currently, incrementally experimenting with Ember Data internals is hard both for addon authors and Ember Data contributors. This RFC rationalizes the internals and establishes clear boundaries for record data storage and manipulation allowing us to expose a public api for addon authors to experiment with.

Motivation

Externally, addons can customize how apps communicate with the server by implementing the Adapter/Serializer APIs but changing how ED deals with relationships, attribute buckets, rollbacks, dirtyness and similar issues is extremely challenging and impossible without extremely internal hacks. One can look at popular addons like EmberDataModelFragments and see how many private APIs they had to override and hack to implement their funcionality.

Internally, while ED is reasonably well factored between data coming into the system through Adapter/Serializers/IdentityMap/Store and data going out through DS.Model/Snapshots/Adapters/Serializers , internal handling of the data including relationships and attributes has extremely fuzzy and unclear boundaries.

Data currently lives in internalModels, relationship state objects, computed property caches, relationship payload caches, etc.

before

image

This RFC proposes rationalizing and extracting ED's core record data handling layer into a RecordData class.

after

image

This will allow us to rationalize internal ED APIs, establish clearer internal boundaries, allow experimentation by addon authors, and create a path for internal ED experimentation.

You can think of Record Data as a layer that can receive JSON api payloads for a record, apply local changes to it, and can be queried for the current state of the data.

Examples of things this would enable:

  1. By shipping a custom RecordData, EmberDataModelFragments can implement a large part of their funcionality without relying on private apis. Spike at model fragments

  2. A spike of Ember Data backed by Orbit, can be implemented as an addon, where most of the work is in implementing a Record Data backed by Orbit. Spike at data-orbit

  3. By using an ES6 class for Record Data implementation, this brings us closer to an Emberless Ember Data running.

  4. If you needed to implement a GraphQL like projection API, Adapters and Serializers would be enough for the loading data, but currently there is no good place to handle client side data interactions. RecordData would make it much easier to have a GraphQL ED addon

  5. Certain apps and models have a large amount of read only data, which is currently very performance heavy to implement in ED. They could use a read only fast record data addon, which would enable a large perf win.

  6. Experimenting with schemaless approaches is currently very hard in ED, because internal models encode assumptions of how attributes and relationships work. Having a swappable RecordData would make it easier for us to implement schemaless approaches in addons.

  7. By having Record Data fully expressed in JSON API apis, the current state of the store becomes serializable.

By designing a public interface for RecordData that dosen't rely on any other part of EDs current system, we can use RecordData as the main building block around which we can refactor the rest of ED.

Detailed design

High level design

Ember Data would define a RecordData interface, and ship a default implementation. Addons would be able to swap their own implementation of the RecordData interface.

RecordData is an interface defining the api for how the store and DS.Models store and apply changes to data. RecordDatas hold the backing data for each record, and act as a bridge between the Store, DS.Model, and Snapshots. It is per record, and defines apis that respond to store api calls like pushData, adapterDidCommit and DS.Model updates like setAttribute. RecordData represents the bucket of state that is backing a particular DS.Model.

The store instantiates the RecordData, feeds it JSON API data coming from the server and tells it about state changes. DS.Model queries the RecordData for the attribute and relationship values and sends back the updates the user has made.

Other than the storeApisWrapper passed to it, RecordData does not assume existence of any other Ember or Ember Data object. It is a fully self contained system, that might serve as a basic building block of non Ember/ED data libraries and could be extracted into a separate library.

Interface

The interface for RecordData is:

export default class RecordData {
  constructor(modelName: string, clientId?: string, id?: string, storeApisWrapper: StoreApisWrapper) {
    /*
      Exposing the entire store api to the RecordData seems very risky and would 
      limit the kind of refactors we can do in the future. We would provide a wrapper
      to the RecordData that would enable funcionality MD absolutely needs 
    */
  }


  /*
    Hooks through which the store tells the Record Data about the data
    changes. They all take JSON API and return a list of keys that the 
    record will need to update
  */

  pushData(data: JsonApi, shouldCalculateChanges: boolean/* if false, don't need to return changed keys*/) {
  }

  adapterDidCommit(data: JsonApi) {
  }

  didCreateLocally(properties) {
  }

  /*
    Hooks through which the store tells RecordData about the lifecycle of the data,
    allowing it to keep track of dirtyness
  */

  adapterWillCommit(modelName: string, id?: string, clientId?: string) {
  }

  saveWasRejected(modelName: string, id?: string, clientId?: string) {
  }

  adapterDidDelete(modelName: string, id?: string, clientId?: string) {
  }

  recordUnloaded(modelName: string, id?: string, clientId?: string) {
  }


  /*
   Rollback handling
  */

  rollbackAttributes(modelName: string, id?: string, clientId?: string) {
  }

  rollbackAttribute(modelName: string, id?: string, clientId?: string, attribute: string) {
  }

  changedAttributes(modelName: string, id?: string, clientId?: string) {
  }

  hasChangedAttributes(modelName: string, id?: string, clientId?: string) {
  }


  /*
    Methods through which DS.Model interacts with RecordData, by setting and getting local state
  */

  setAttr(modelName: string, id?: string, clientId?: string, key: string, value: string) {
  }

  getAttr(modelName: string, id?: string, clientId?: string, key: string) {
  }

  hasAttr(modelName: string, id?: string, clientId?: string, key: string) {
  }

  /*
    Relationships take and return json api resource objects
    The store takes those references and decides whether it needs to load them, or
    it can serve them from the cache
  */

  getHasMany(modelName: string, id?: string, clientId?: string, key: string) {
  }
  
  addToHasMany(modelName: string, id?: string, clientId?: string, key: string, jsonApiResources, idx: number) {
  }

  removeFromHasMany(modelName: string, id?: string, clientId?: string, key: string, jsonApiResources) {
  }

  setHasMany(modelName: string, id?: string, clientId?: string, key: string, jsonApiResources) {
  }

  getBelongsTo(modelName: string, id?: string, clientId?: string, key: string) {
  }

  setBelongsTo(modelName: string, id?: string, clientId?: string, key: string, jsonApiResource) {
  }


export default class StoreApiWrapper {
  /* clientId is used as a fallback in the case of client side creation */
  createRecordDataFor(modelName, id, clientId)
  notifyPropertyChanges(modelName, id, clientId, keys)
  /* 
  in order to not expose ModelClasses to RecordData, we need to supply it with
  model schema information. Because a schema design is out of scope for this RFC,
  for now we expose these two methods we intend to deprecate once we have a schema
  interpretation
   */
  attributesDefinitionFor(modelName, id)
  relationshipsDefinitionFor(modelName, id)

}

ED's usage of RecordData

We would refactor internal models, DS.Models and Snapshots to use RecordData's apis.

Reimplementation of ED current internals on top of RecordData apis would consist of the store pushing the json api payload to the backing record data and the record data setting up internal data tracking, as well as storing relationship data on any additional needed recordDatas.

let data = {
  data: {
    id:1,
    type: 'user',
    attributes: { name: 'Clemens' },
    relationships: { houses: { data: [{ id: 5, type: 'house' }], links: { related: '/houses' } } }
  }
};

store.push(data);

// internal store method
_internalMethod() {
  let recordData = store.recordDataFor('user', 1, this._storeWrapperApi)
  recordData.pushData(data, false)
}

->

// model-data.js
pushData(data, shouldCalculateChanges) {
  this._data = this.data.attributes;
  this._setupRelationships(data);
}
->
// model-data.js
_setupRelationships(data) {
  this.storeWrapperApi.recordDataFor('house', 1);
  ....
}

The DS.Model interactions would look like:

let user = store.peekRecord('user', 1);
user.get('name');
->
// DS.Model
get(key) {
  let recordData = _internalMethodForGettingTheCorrespondingRecordData(this);
  return recordData.getAttr('name');
}

Relationships

Basic loading of relationships

RecordData's relationship hooks would receive and return json api relationship objects with additional metadata meaningful to Ember Data.

Lets say that we started off with the same user data as above

let data = {
  data: {
    id:1,
    type: 'user',
    attributes: { name: 'Clemens' },
    relationships: { houses: { data: [{ id: 5, type: 'house' }], links: { related: '/houses' } } }
  }
};
let clemens = store.push(data);

Getting a relationships from Clemens would trace a path from the DS.Model to backing record data, which would then give the store a json api object, and the store would instantiate a ManyArray with the records

clemens.get('houses');
// DS.Model
get() {
  let clemensRecordData = _internalApiGetsUsTheRecordDataFromIDMMAP();
  return clemens.getHasMany('houses');
}
->
// Record Data returns
{[ 
  data: { id: 5, type: 'house'},
  links: { related: '/houses' },
  meta: { realMetaFromServer: 'hi', _ED: { hasAllIds: true, needToLoadLink: false } }
}
-> //store takes the above, figures out that it needs to fetch house with id 5
  // and returns a promise which resolves into a ManyArray

ED extends the relationship payload with a custom meta, which gives the store information about whether we have information about the entire relationship (we couldn't be sure we have all the ids if we loaded from the belongsTo side) and whether the link should be refetched (we might need to refetch the link in the case it potentially changed)

Setting relationship data locally

Similarly to the attributes, changing relationships locally tells record data to update the backing data store

let anotherHouse = store.push({data: { type: 'house', id: '5' }});
clemens.get('houses').then((houses) => {
  houses.pushObject(anotherHouse);
  -> 
  // internally
  clemensRecordData.addToHasMany('houses', { data: { type: 'house', id: '5' } })
});
Dealing with newly created records in relationships

Unfortunately, because ED does not have first class clientId support, we need a special case for handling locally created records, and pushing them to relationships.

We extend JSON API resource object with a clientId meta field. A locally created record, will also have a ED specific internal client id, which will take preference;

let newHouse = store.createRecord('house');
clemens.get('houses').then((houses) => {
  houses.pushObject(newHouse);
  ->
  // internally
  clemensRecordData.addToHasMany('houses', { data: { type: 'house', id: null, { meta: _ED: { clientId: 1}} } })
});
clemens.get('houses') ->
{ data: 
  [ { id: 5, type: 'house'}, 
    { id: null, type: 'house', meta: { _ED: { clientId: 1 } } }],
  links: { related: '/hi' },
  meta: { realMetaFromServer: 'hi', _ED: { loaded: true, needToLoadLink: false } }
}

ED internals would keep a separate cache of client ID and resolve the correct record

Addon usage

The Store provides a public api for looking up a recordData which the store has not seen before.

recordDataFor(modelName, id, options) {

}

If an Addon wanted to implement custom data handling functionality, it would subclass the store and implement their own RecordData handler.

There are three main reasons to do this.

  1. Full replacement of Ember Data's data handling mechanisms

Best example would be the Ember Data backed by Orbit.js experiment. EmberDataOrbit Addon replaces Ember Data's backing data implementation with Orbit.js. Most of this work can be done by EmberDataOrbit replacing ED's Record Data implementation

recordDataFor(modelName, id, options, storeWrapper) {
  return new OrbitRecordData(modelName, id, storeApisWrapper) 
}
  1. Per Model replacement of Ember Data's data handling

If a large app was loading thousands of instances of a particular record type, which was read-only, it could use a read only ED addon, which implemented a simplified RecordData without any change tracking.

The addon would implement a recordDataFor on the store as

recordDataFor(modelName, id, options, storeWrapper) {
  if (addonDecidesIfReadOnly(modelName))  {
    return new ReadOnlyRecordData(modelName, id, storeApisWrapper) 
  }
  return this._super(modelName, id, options, storeWrapper);
}
  1. Adding common funcionality to all ED models

Ember Data Model Fragments Addon adds support for handling of embedded data fragments. In order to manage the handling of fragments, Model Fragments would compose ED's default RecordData with it's own for handling fragments.

recordDataFor(modelName, id, options, storeWrapper) {
  let EDRecordData = this._super(modelName, id, options, storeWrapper);
  return new ModelFragmentsRecordData(modelName, id, options, storeWrapper, EDRecordData);
}

When receiving a payload, ModelFragments would handle the fragment part and delegate the rest to ED's implementation

pushData(data, shouldCalculateChanges) {
  let keysThatChanged = this.extractAndHandleFragments(data);
  return keysThatChanged.concat(this.EDRecordData.pushData(data, shouldCalculateChanges))
}

How we teach this

These APIs are not meant to be used by most users, or app level code, and should be hidden away and described in an api/guides section meant for ED addon authors. Currently there are a few widely used addons which would greatly benefit from this, so we can also reach out in person. I have already implemented a spike of ModelFragments using RecordData. Having couple addons implement different RecordDatas would be a great way to teach new addon authors about the purpose and implementation of the API.

Drawbacks

Defines a bigger API surface area

This change would increase the public API surface area, in a codebase that is already pretty complex. However, this would codify and simplifyA APIs addon authors have already had to interact with, while creating a path for future simplification of the codebase.

It allows people to do very non-standard changes that will complexify their app needlessly

The main mitigation, is only giving RecordData access to a small amount of knowledge of the external world, and keeping most APIs pull only thus discouraging trying to do innapropriate work in the RecordData layer

The new JSON api interaction might preclude performance improvements, or reduce current performance

Alternatives

We could do this work as an internal refactor, and not expose it to public.

I believe that this approach is valid as an internal architecture, so would like to do it even if we did not expose any of it to addons/apps.

Make RecordData's looked up from the resolver

Currently RecordData is a dumb ES6 class and does not live in the Ember resolver system, for performance and simplicity reasons. We could alternatively look it up from the resolver, allowing people to mock it and inject into it easier.

Don't expect a per record Record Data

Currently, the MD layer semantics mimics current ED's data storage, where data is stored per record in internalModels. You could alternatively do this using an app wide cache, like Orbit.js does, or using any number of other approaches. This approach while valid, would be harder to implement and it's apis would not map as well to ED behavior.

Open Questions

Versioning and stability

Our current implementation of internalModel is deeply monkeypatched by at least few addons. I think we have to consider it as an semi-intimate api, even though it literally has internal in the name(I've been told adding couple undescores to the name would have helped). Because the number of addons monkeypatching it is limited, we can manually migrate them onto the new apis. However this requires us to make the new apis public from the get go, and doesn't allow for a long period of api evolution.

The following options are available, none of them great:

  1. Feature flag RecordData work. The scope of this refactor is large enough, that doing a full feature flagging would be an enourmous burden to bear, and I would advise against it. We can proxy some basic things, to allow for simpler changes and as a way of warning/deprecating

  2. Move from the internals to public RecordData in a single release cycle, and hope public apis we created make sense, and will not be performance issues in the future. I am reasonably confident having implemented several addons using RecordData that the basic design works, but things can always come up.

  3. Move from private internals to private RecordData, and then feature flag the public apis over couple versions. In this case the addons monkeypatching the internals, would monkeypatch the new nicer apis for a while, and then easily switch to the public api. This feel a bit like SemVer cheating.

ClientID passing to store api methods

We use recordDataFor(modelName, id, clientId) as the api to look up recordDatas. Passing an often null clientId seems annoying. Orbit.js uses an identity object instead, and if we did the api would look like recordDataFor(identityObject), where identityObject would look like { type, id, meta: { _ED: { clientId }}}. This seem a bit more correct, but doesn't look like any existing ED api, and could create a lot of allocations.

RecordDatas might need to do some global setup/communication, how does that work?

Normally you would do this in an initializer, but becasue MDs aren't resolved, the only way would be to do it in RecordDataFor or by using a singleton import. Some ceremony being required to using RecordData isn't super bad, because it will discourage app authors from customizing it for trivial/innapropriate things.

What do we do with the record state management?

Currently RecordData has no interaction with the state machine. I think we should punt on this for now.

{ meta: { _ED: { props here } } } alternatives?

We could put the ED internal data outside of meta, and keep meta only for actual meta that comes from the server.

Naming of everything

Please help with better names for things if you have ideas

Snapshot interface

How does a Snapshot ask Record Data for it's attributes

Real life perf impact

Need benchmarks

  • Start Date: 2018-01-11
  • RFC PR: github.com/emberjs/rfcs/pull/294
  • Ember Issue: (leave this empty)

Make jQuery optional

Summary

For the past Ember has been relying and depending on jQuery. This RFC proposes making jQuery optional and having a well defined way for users to opt-out of bundling jQuery.

Motivation

Why we don't need jQuery any more

One of the early goals of jQuery was cross-browser normalization, at a time where browser support for web standards was incomplete and inconsistent, and Internet Explorer 6 was the dominating browser. It provided a useful and convenient API for DOM traversal and manipulation as well as event handling, that hid the various browser differences and bugs from the user. For example document.querySelector wasn't a thing at that time, and browsers were using very different event models (DOM Level 0, DOM Level 2 and IE's own proprietary model).

But this level of browser normalization is not required anymore, as today's browsers all support the basic DOM APIs well enough. Even more so that the upcoming Ember 3.0 will drop support for all versions of Internet Explorer except 11.

Furthermore Ember users will need to directly traverse and modify the DOM or manually attach event listeners in very special cases only. Most of these low level interactions are taken care of by Ember's templates and its underlying Glimmer rendering engine, as well as action helpers or the component's event handler methods.

So having jQuery included by default does not provide that much value to users most of the time, and Ember itself is expected to be fully functional and tested without jQuery, presumably for the upcoming 3.0 stable release.

What are the drawbacks of bundling jQuery

The major drawback is the increased bundle size, which amounts to ~29KB (minified and gzipped). This not only increases the loading time, but also parse and compile times, thus increasing the total time to interactive. This is especially true for mobile devices, where slow connectivity and weak CPU performance is not uncommon.

Having jQuery not included will improve the suitability of Ember for mobile applications considerably. Even if the raw number is not that huge, it all adds up. And it plays together with other efforts to make leaner Ember builds possible, like enabling tree shaking with the new Module API, moving code from core to addons (e.g. the Ember.String deprecation) or the "Explode RFC". In that regard removing the dependency on jQuery is a rather low hanging fruit with an high impact.

But this is already possible, why this RFC?

There is indeed a somewhat quirky way to build an app without jQuery even today. Although this happens to work, it is not sufficient to consider this officially supported for these reasons:

  • Ember itself must be fully tested to work without jQuery
  • the public APIs that depend on and/or expose jQuery need to have some well defined behavior when jQuery is not available
  • there should be a way to technically opt-out (other than fiddling with vendorFiles) that is easier to use, understand and maintain
  • addons should mostly default to not use jQuery, to make removing jQuery practically possible for their consuming apps

Detailed design

Remove internal jQuery usage

As of writing this, there are major efforts underway to remove and cleanup the Ember codebase and especially its tests from jQuery usage. Having a way to fully test Ember without jQuery is a prerequisite to officially support jQuery being optional. When this is done, it will enable a "no jQuery" mode, that will make it not use jQuery anymore, but only native DOM APIs.

Add an opt-out flag

There should be a global flag that will toggle the optional jQuery integration (true by default). When this is disabled, it will make Ember CLI's build process not include jQuery into the vendor.js bundle, and it will explicitly put Ember itself into its "no jQuery" mode.

The flag itself will not be made a public API. Rather it will be handled by a privileged addon, that will allow to disable the integration flag, thus to opt out from jQuery integration. This approach is in line with RFC 278 and RFC 280, to allow for some better implementation flexibility.

Introduce @ember/jquery package

Currently Ember CLI itself is importing jQuery into the app's vendor.js file. To decouple it from this task, and to allow for some better flexibility in the future, the responsibility for importing jQuery is moved to a dedicated @ember/jquery addon.

To not create any breaking changes, Ember CLI will have to check the app's dependencies for the presence of this addon. If it is not present, it will continue importing jQuery unless the jQuery integration flag is disabled. If it is present, it will stop importing jQuery at all, and delegate this responsibility to the addon.

To nudge users to install @ember/jquery when they need jQuery, some warning/deprecation messages should be issued when the addon is not installed and the integration flag is either not specified or is set to true. To ease migration the addon should be placed in the default blueprint (until an eventual more aggressive deprecation of jQuery). Only in the case the app is actively opting out of jQuery integration the addon is not needed.

The addon itself has to make sure the Ember CLI version in use is at least the one that introduced the above mentioned logic, to prevent importing jQuery twice.

Assertions for jQuery based APIs

Apart from testing (see below), Ember features some APIs that directly expose jQuery, which naturally cannot continue to work without it. For these APIs some assertions have to be added when running in "no jQuery" mode (and not in production), that provide some useful error messages for the developer:

  • Ember.$() should throw an assertion stating that jQuery is not available.
  • this.$() in components should throw an assertion stating that jQuery is not available and that this.element and native DOM APIs should be used instead.

Introducing ember-jquery-legacy and deprecating jQuery.Event usage

Event handler methods in components will usually receive an instance of jquery.Event as an argument, which is very similar to native event objects, but not exactly the same. To name a few differences, not all properties of the native event are mapped to the jQuery event, on the other hand a jquery event has a originalEvent property referencing the native event.

The updated event dispatcher in Ember 3.0 is capable of working without jQuery (similar to what ember-native-dom-event-dispatcher provided for Ember 2.x). When jQuery is not available, it will naturally not be able to pass a jquery.Event instance but a native event instead. This creates some ambiguity for addons, as they cannot know in advance how the consuming app is built (with or without jQuery).

For code that does not rely on any jQuery.Event specific API, there is no need to change anything as it will continue to work with native DOM events.

But there are cases where jQuery specific properties have to be used (when jQuery events are passed). This is especially true for the originalEvent property, for example to access TouchEvent properties that are not exposed on the jQuery.Event instance itself. So there has to be a way to make the code work with either jQuery events or native events being passed to the event handler (especially important for addons). Moreover this should be done in a way that uses native DOM APIs only, to support the migration away from jQuery coupled code.

To solve this issue another addon ember-jquery-legacy will be introduced, which for now will only expose a single normalizeEvent function. This function will accept a native event as well as a jQuery event (possibly distinguishing between those two modes at build time, based on the jQuery integration flag), but will always return a native event only.

This will allow addon authors to work with both event types, but start to only use native DOM APIs:

import Component from '@ember/component';
import { normalizeEvent } from 'ember-jquery-legacy';

export default Component.extend({
  click(event) {
    let nativeEvent = normalizeEvent(event);
    // from here on use only native DOM APIs...
  }
})

To encourage addon authors to refactor their jQuery coupled event code, the use of jQuery.Event specific APIs used for jQuery events passed to component event handlers should be deprecated and a deprecation message be shown when accessing them (e.g. event.originalEvent). Care must be taken though that this warning will not be issued when normalizeEvent has to access originalEvent.

Also for apps that do not want to transition away from jQuery and would be overloaded with unnecessary warnings, the deprecations should be silenced when the jQuery integration flag is explicitly set to true (and not just true by default). By doing so users effectively state their desire to continue using jQuery, thus any needless churn should be avoided for them.

Testing

Ember's test harness has been based on jQuery for a long time. Most global acceptance test helpers like find or click rely on jQuery. For integration tests the direct use of jQuery like this.$('button').click() to trigger events or assert the state of the DOM is still the standard, based on this.$() returning a jQuery object representing the rendered result of the tests render call.

To be able to reliably run tests in a jQuery-less world, we need to run our tests without jQuery being included, so our test harness has to work without jQuery as well.

Fortunately this is well underway already. ember-native-dom-helpers introduced native DOM test helpers for integration and acceptance tests as an user space addon. The recent acceptance testing RFC 268 provides similar test helpers, implemented in the @ember/test-helpers package, and envisages deprecating the global test helpers.

However while the existing jQuery based APIs are still available, when these are used without jQuery they have to throw an assertion with some meaningful error message:

  • global acceptance test helpers that expect jQuery selectors (which are a potentially incompatible superset of standard CSS selectors)

  • this.$() in component tests, provided currently by @ember/test-helpers in moduleForComponent and setupRenderingTest

In both cases the error message should state that jQuery is not available and that the native DOM based test helpers of the @ember/test-helpers package should be used instead.

The transitioning to these new test helpers can be eased through a codemod. For ember-native-dom-helpers there already exists ember-native-dom-helpers-codemod, which could be adapted to the very similar RFC 268 based interaction helpers in @ember/test-helpers.

Implementation outline

The following outlines how a possible implementation of the jQuery integration flag could look like. This is just to provide some additional context, but is intentionally not meant to be normative, to allow some flexibility for the actual implementation.

The addon that will handle the flag is expected to be ember-optional-features, which will read from and write to a config/optional-features.{js,json} file. This will hold the jquery-integration flag (amongst others). This flag in turn will be added to the EmberENV hash, which will make Ember go into its "no jQuery" mode when set to false.

Ember CLI and the @ember/jquery addon will also look for jquery-integration in this configuration file, and will opt-out of importing jQuery when this file is present and the flag is set to false.

How we teach this

Guides

The existing "Managing Dependencies" chapters in the Ember Guides as well as on ember-cli.com provide a good place to explain users how to set the jQuery integration flag by means of the mentioned privileged addon that handles this flag.

The section on components should be updated to remove any eventually remaining references to this.$, to not let users fall into the trap of creating an implicit dependency on jQuery by "accidental" use of it. These should be changed to refer to their native DOM counterparts like this.element or this.element.querySelector().

The section on acceptance tests will have been updated as per RFC 268 to use the new @ember/test-helpers based test helpers instead of the jQuery based global helpers.

The section on component tests should not use this.$() anymore as well, and instead also according to RFC 268 use this.element to refer to the component's root element, and use the new DOM interaction helpers instead of jQuery events triggered through this.$().

Deprecation guide

The deprecation warnings introduced for using jQuery.Event specific APIs should explain the use of the normalizeEvent helper function to migrate towards native DOM APIs on the one side, and on the other side the effect of setting the jQuery integration flag to explicitly opt into jQuery usage thus suppressing the warnings.

Addon migration

One of the biggest problems to easily opt-out of jQuery is that many addons still depend on it. Many of these usages seem to be rather "accidental", in that the full power of jQuery is not really needed for the given task, and could be fairly easily refactored to use only native DOM APIs.

For this reason this RFC encourages addon authors to not use jQuery anymore and to refactor existing usages whenever possible! This certainly does not apply categorically to all addons, e.g. those that wrap jQuery plugins as components and as such cannot drop this dependency.

ember-try

ember-try, which is used to test addons in different scenarios with different dependencies, should provide some means to define scenarios without jQuery, based on the jQuery integration flag introduced in this RFC.

Furthermore the Ember CLI blueprint for addons should be extended to include no-jQuery scenarios by default, to make sure addons don't cause errors when jQuery is not present.

emberobserver.com

It would be very helpful to have a clear indication on emberobserver.com which addons depend on jQuery and which not. This would benefit users as to know which addons they can use without jQuery, but also serve as an incentive for authors to make their addons work without it.

Given the jQuery integration flag introduced in this RFC, this paves the way to automatically detect addons that are basically declaring their independence from jQuery by having this flag set to false (in their own repository).

Drawbacks

Churn

A vast amount of addons still depend on jQuery. While as far as this RFC is concerned no jQuery based APIs will be deprecated and the default will still be to include jQuery, addons are nevertheless encouraged to remove their dependency on jQuery, which will add some considerable churn to the addon ecosystem. As of writing this, there are:

Among these are still some very essential addons like ember-data, which still relies on $.ajax, see #5320.

A good amount of that churn can be mitigated by having a codemod that migrates tests (see "Testing" above).

Alternatives

Continue to depend on jQuery.

Unresolved questions

None so far.

  • 2018-01-17
  • RFC PR: 0297
  • Ember Issue: https://github.com/emberjs/ember.js/issues/16231

Deprecation of Ember.Logger

Summary

This RFC recommends the deprecation and eventual removal of Ember.Logger.

Motivation

There are a variety of features of Ember designed to support old browsers, features that are no longer needed. Ember.Logger came into being because the browser support for the console was inconsistent. In some browsers, like Internet Explorer 9, the console only existed when the developer tools panel was open, which caused null references and program crashes when run with the console closed. Ember.Logger provided methods that would route to the console when it was available.

With Ember 3.x, Ember no longer supports these older browsers, and hence this feature no longer serves a purpose. Removing it will make Ember smaller and lighter.

Detailed design

For the most part, this is a 1:1 substitution of the global console object for Ember.Logger.

Node only added support for console.debug in Node version 9. Where we wish to support earlier versions of Node, we will need to use console.log, rather than console.debug, as the replacement for Logger.debug. Apps and addons which don't care about Node or are specifying Node version 9 as their minimum can use console.debug.

Internet Explorer 11 and Edge both require console methods to be bound to the console object when the developer tools are not showing. This diverges from the expectations of other browsers. Direct calls to console methods will work correctly, but constructs which involve explicitly or implicitly binding the console methods to other objects or using them unbound will fail. This is straightforward to work around.

You can address the issue by binding the method to the console object:

// Before - assigning raw method to a variable for later use
var print = Logger.log; // assigning method to variable
print('Message');

// After - assigning console-bound method to variable for later use
var print = console.log.bind(console);
print('Message');

In some cases, you can use rest parameter syntax to avoid the issue entirely:

// Before
Logger.info.apply(undefined, arguments); // or
Logger.info.apply(null, arguments); // or
Logger.info.apply(this, arguments); // or

// After
console.info(...arguments);

Within the framework

Remove the following direct uses of Ember.Logger from the ember.js and ember-data projects:

  • ember-debug:
    • deprecate (ember-debug\lib\deprecate.js) - Logger.warn
    • debug (ember-debug\lib\index.js) - Logger.info
    • warn (ember-debug\lib\warn.js) - Logger.warn
  • ember-routing (ember-routing\lib\system\router.js):
    • transitioned to - Logger.log
    • preparing to transition to - Logger.log
    • intermediate-transitioned to - Logger.log
  • ember-testing:
    • Testing paused (ember-testing\lib\helpers\pause_test.js) - Logger.info
    • Catch-all handler (ember-testing\lib\test\adapter.js) - Logger.error
  • ember-data:
    • tests\test-helper.js- Logger.log

Adjust all test code that redirects logging and sets it back:

  • ember\tests\routing\basic_test.js (adjust)
  • ember-application\tests\system\dependency_injection\default_resolver_test.js (adjust)
  • ember-application\tests\system\logging_test.js (remove?)
  • ember-glimmer\tests\integration\helpers\log-test.js (remove?)

Note: None of the uses of Ember.Logger in ember.js or ember-data involve Ember.debug, so that issue doesn't affect the Ember.js code directly.

Add deprecation warnings to the implementation: ember-console\lib\index.js. Bear in mind that Ember.deprecate in ember-debug currently calls Logger.warn, so the ember-debug code should be changed first or adding the deprecation warning will create a deep recursion.

The Ember.assert, Ember.warn, Ember.info, Ember.debug, and Ember.deprecate methods suppress their output on production builds. However, they are suppressing them in the ember-debug module, which currently consumes Ember.Logger, not by Ember.Logger itself. Hence, replacing calls to Ember.Logger with direct calls to the console will not affect this behavior.

Add-On Developers

The following high-impact add-ons (9 or 10 or a * on EmberObserver) use Ember.Logger and should probably be given an early heads-up to adjust their code to use console before this RFC is implemented. This will limit the level of pain that their users experience when the deprecation is released.

Add-ons that need to also support Ember 2.x will need to make their console references conditional on console being "truthy", of course, to support Internet Explorer 9.

In the order of their number of references to Ember.Logger:

  • ember-concurrency (15)
  • ember-cli-deprecation-workflow (9)
  • ember-stripe-service (9)
  • semantic-ui-ember (7)
  • ember-resolver (6)
  • ember-cli-page-object (4)
  • ember-cli-sentry (3)
  • ember-islands (3)
  • ember-states (3)
  • ember-cli-pagination (2)
  • ember-cli-clipboard (1)
  • ember-cli-fastboot (1)
  • ember-elsewhere (1)
  • ember-i18n (1)
  • ember-simple-auth-token (1)
  • ember-svg-jar (1)
  • liquid-fire (1)

For details, see https://emberobserver.com/code-search?codeQuery=Ember.Logger.

How we teach this

Communication of change

We need to inform users that Ember.Logger will be deprecated and in what release it will occur.

Official code bases and documentation

We do not currently actively teach the use of Ember.Logger. We will need to remove any passing references to Ember.Logger from the Ember guides from the Super Rentals tutorial, and anywhere else it appears on the website.

Once it is gone from the code, we also need to verify it no longer appears in the API listings.

We must provide an entry in the deprecation guide for this change:

  • describing relevant divergences remaining in the handling of the console in Internet Explorer 11 and Edge browsers.
  • describing the issue with using console.debug on node versions earlier than Node 9.
  • describing alternative ways of dealing with eslint's no-console messages.

Drawbacks

191 add-ons in Ember Inspector are using Ember.Logger. It has been there and documented for a long time. So this deprecation will cause some level of change on many projects.

This, of course, can be said for almost any deprecation, and Ember's disciplined approach to deprecation has been repeatedly shown to ease things. These particular changes are proving easy to locate and replace by hand. Also, only twenty of those add-ons have more than six references to Ember.Logger. If this is characteristic of the user base, the level of effort to make the change, even by hand, should be very small for most users.

Those using Logger.debug as something different from Logger.log may have at least a theoretical concern. Under the covers Logger.debug only calls console.debug if it exists, calling console.log otherwise. The only platform where the difference between the two is visible in the console is on Safari. We can encourage folks with a tangible, practical concern about this to speak up during the comment period, but I don't anticipate this will have much impact.

Alternatives

  1. Leave things as they are, perhaps providing an @ember/console module interface.

  2. Extract Ember.Logger into its own (tiny) @ember/console package as a shim for users.

Unresolved questions

None at this point. The answers from prior drafts have been promoted into the text.

  • Start Date: 2018-02-04
  • Relevant Team(s): Steering
  • RFC PR: https://github.com/emberjs/rfcs/pull/300
  • Tracking: https://github.com/emberjs/rfc-tracking/issues/1

RFC (Request for Comments) Process Update

Summary

Refine the Ember RFC process and have it apply to all Ember teams.

Motivation

The Ember community has been using the RFC process to great effect over the last few years. Proposals by both Core and community members are discussed and refined with the result coming out much stronger.

During this time, the community and the core teams have identified shortcomings of the RFC process as well as new requirements, which this RFC intends to address:

Confusion between emberjs/rfcs and ember-cli/rfcs

The Ember project currently has two separate RFC processes for Ember.js and Ember CLI.

This leads to confusion because the community needs to keep track of two different repositories. For contributors there is the overhead of having to decide where to file their RFC if the proposal involves both projects, as well as being aware of the differences in the processes.

The process does not cover the entire project

RFCs to emberjs/rfcs and ember-cli/rfcs have traditionally concerned themselves with features or deprecations to Ember.js and Ember CLI respectfully, with some Ember Data proposals in emberjs/rfcs.

We have already begun to use emberjs/rfcs for other initiatives, such as the project-wide Ember.js 2018 Roadmap but have not codified or updated the process to make it clear that it should be used for efforts such as a website redesign, information architecture suggestions, SEO suggestions, and the like.

Lingering RFCs

Both the emberjs/rfcs and the ember-cli/rfcs repositories have many open issues and pull-requests. A percentage of these have not been active in the recent past.

We have kept PRs and issues open so people could more easily find the discussions, but this has instead given a negative impression of staleness, as RFCs linger open without new feedback.

The process for an RFC after it has been accepted

At the moment the process does not specify what happens when an RFC is accepted and merged. This has led to many questions about the status of merged RFCs.

Detailed design

One RFC Process for all of Ember

Ember is organized into teams, with each team being responsible for certain projects. The RFC process will be a useful tool for all of those projects. The header of the RFC template will be updated to include a spot to specify the relevant team(s). The header will have "Ember Issue:" removed.

A list of the teams and respective projects will be added to the instructions, possibly with the addition of per-team instructions on specifics of the project. Additional templates might be created as well, such a design work template.

Each team will be responsible for reviewing new RFCs and, if an RFC requires work from their team, ensuring that the RFC reflects that. As it is with the wider community, the RFC process is the time for teams and team members to push back on, encourage, refine, or otherwise comment on proposals.

Require a Core Champion

To make sure that RFCs receive adequate support from the team, Ember CLI has implemented the idea of a champion associated with each RFC. One goal is that in seeking a champion from the team, the RFC author starts a dialogue with the team and gets some early feedback. That champion is then responsible for representing the RFC in team meetings, and for shepherding its progress. We will import a version of this process to emberjs/rfcs:

Each RFC will require a champion from the primary core team to which the RFC has been marked relevant. The champion must be found by the opener of the RFC or other community member. They are not assigned by the core teams. The champion will assign themselves on the RFC on Github. The champion will be responsible for:

  • achieving consensus from the team(s) to move the RFC through the stages of the RFC process.
  • ensuring the RFC follows the RFC process.
  • shepherding the planning and implementation of the RFC. Before the RFC is accepted, the champion may remove themselves. The champion may find a replacement champion at any time.

A section on 'Finding a champion' will be added to the instructions on proposing an RFC.

Introduce the concept of "FCP to close"

To address the problem of RFC triage and inactivity, this RFC introduces the concept of FCP to close.

Closing an RFC should be viewed as another triage tool, not as a rejection of the RFC. Sometimes a rewrite of an RFC would be so fundamental that it would benefit of a fresh discussion in a new thread. Sometimes the original author is no longer active (Champions should help here as well), and someone else might want to take over the work in a new RFC. Sometimes the timing might not be right, or the feature might have been addressed some other way, and yes, sometimes it might be something that is not aligned with the team's values for the project.

A good example of this is the Named Blocks RFC, which lists in the motivation section previous attempts at similar ideas.

Like the FCP to merge process, once an RFC is marked as FCP to close there will be a period of one week where people can raise new concerns. After that period of one week, the respective team will review and close the RFC or extend the period for another week.

Merge ember-cli/rfcs into emberjs/rfcs

We will have a single repository for all Ember Project RFCs.

To achieve merging ember-cli/rfcs into emberjs/rfcs the following will be done:

  • Add to the RFC header to indicate it applies to ember-cli
  • Copy both active and completed RFC files into text of emberjs/rfcs
  • Transfer active PRs and Issues to emberjs/rfcs
  • Archive ember-cli/rfcs

There are some concerns about links breaking when we move the files to emberjs/rfcs, but given the fact that ember-cli/rfcs had the concept of active/completed by moving the files into different folders, links were already being broken.

The ember-cli/rfcs do not need name or numbering changes, as there is currently no duplicated name. Going forward, the numbering should be unified by virtue of having a single repository.

Track RFCs after they are accepted

At the moment it is not clear what happens to an RFC after it has been merged.

This RFC proposes that after an RFC is merged, the relevant teams, guided by the champion, will plan implementation by creating tracking issues in the relevant projects.

This RFC proposes having a single place to track the implementation of each RFC. Each RFC will have a header Tracking: that will be filled out with a link. At that link all issues related to that RFC, across all projects and organizations, will be enumerated.

How We Teach This

To ensure that contributors are updated on the RFC process and the process is clear, the documentation should be improved in a couple of ways.

The README will be updated to reflect process changes described in this RFC. We will add checklists to the instructions for each stage of the RFC process to make it very clear what needs to happen.

Drawbacks

Adjustment period

There are active RFCs in ember-cli/rfcs. Moving these discussions would be onerous, so they should be kept there until completion, and no new RFCs accepted.

Permalinks to ember-cli/rfcs proposals

Moving the RFC files from ember-cli/rfcs (active or completed) to emberjs/rfcs can be seen as a breaking change, and could lead to someone linking to ember-cli/rfcs and then the RFC being updated in emberjs/rfcs. However, ember-cli/rfcs already suffers from a linking problem due to the active/completed folders, as RFCs need to be moved from one to the other even after being accepted. This could be mitigated by introducing a warning in the RFC text directing people to the new source.

Alternatives

None at the moment.

Unresolved questions

None at the moment.


Glossary

  • RFC: Request For Comments. The process by which a proposal is discussed by the community and then approved by an Ember team.
  • FCP: Final Comment Period. Period of one week at the end of which an RFC is to be accepted or rejected by an Ember team. Extended in periods of one week if new concerns are raised.
  • Start Date: 2018-02-15
  • RFC PR: https://github.com/emberjs/rfcs/pull/308
  • Ember Issue:

Summary

Beginning the transition to deprecate the fallback behavior of resolving {{foo}} by requiring the usage of {{this.foo}} as syntax to refer to properties of the templates' backing component. This would be the default behavior in Glimmer Components.

For example, given the following component class:

import Component from '@ember/component';
export default Component.extends({
  init() {
    super(...arguments);
    this.set('greeting', 'Hello');
  }
});

One would refer to the greeting property as such:

<h1>{{this.greeting}}, Chad</h1>

Ember will render "Hello, Chad".

To make this deprecation tractable, we will provide a codemod for migrating templates.

Motivation

Currently, the way to access properties on a components class is {{greeting}} from a template. This works because the component class is one of the objects we resolve against during the evaluation of the expression.

The first problem with this approach is that the {{greeting}} syntax is ambiguous, as it could be referring to a local variable (block param), a helper with no arguments, a closed over component, or a property on the component class.

Exemplar

Consider the following example where the ambiguity can cause issues:

You have a component class that looks like the following component and template:

import Component from '@ember/component';
import computed from '@ember/computed';

export default Component.extend({
  formatName: computed('firstName', 'lastName', function() {
    return `${this.firstName} ${this.lastName}`;
  });
});
<h1>Hello {{formatName}}!</h1>

Given { firstName: 'Chad', lastName: 'Hietala' }, Ember will render the following:

<h1>Hello Chad Hietala!</h1>

Now some time goes on and someone adds a formatName helper at app/helpers/fortmatName.js that looks like the following:

export default function formatName([firstName, lastName]) {
  return `${firstName} ${lastName}`;
}

Due to the fact that helpers take precedence over property lookups, our {{formatName}} now resolves to a helper. When the helper runs it doesn't have any arguments so our template now renders the following:

<h1>Hello !</h1>

This can be a refactoring hazard and can often lead to confusion for readers of the template. Upon encountering {{greeting}} in a component's template, the reader has to check all of these places: first, you need to scan the surrounding lines for block params with that name; next, you check in the helpers folder to see if there is a helper with that name (it could also be coming from an addon!); finally, you check the component's JavaScript class to look for a (computed) property.

Like RFC#0276 made argument usage explicit through the @ prefix, the this prefix will resolve the ambiguity and greatly improve clarity, especially in big projects with a lot of files (and uses a lot of addons).

As an aside, the ambiguity that causes confusion for human readers is also a problem for the compiler. While it is not the main goal of this proposal, resolving this ambiguity also helps the rendering system. Currently, the "runtime" template compiler has to perform a helper lookup for every {{greeting}} in each template. It will be able to skip this resolution process and perform other optimizations (such as reusing the internal reference object and caches) with this addition.

Furthermore, by enforcing the this prefix, tooling like the Ember Language Server does not need to know about fallback resolution rules. This makes common features like "Go To Definition" much easier to implement since we have semantics that mean "property on class".

Transition Path

We intend this to be a very slow process as we understand it is a large change. Because of this we will be doing a phased rollout to help guide people in transtion. Below is an outline of how we plan to roll this change out.

Phase 1:

Phase 2:

  • Add the lint rule by default in the apps .template-lintrc.js
  • Complete doc migration to use this.

Phase 3:

  • Enable the lint rule by default in the recommended config

Phase 4:

  • Introduce deprecation app only fallbacks

Phase 5:

  • Introduce deprecation for any fallbacks

Phase 6:

  • Rev major to 4.0.0
  • Add assert for fallback behavior

Phase 7:

  • Remove fallback functionality in 4.5, post 4.4.0 LTS

How We Teach This

{{this.foo}} is the way to access the properties on the component class. This also aligns with property access in JavaScript.

Since the {{this.foo}} syntax has worked in Ember.Component (which is the only kind of component available today) since the 1.0 series, we are not really in a rush to migrate the community (and the guides, etc) to using the new syntax. In the meantime, this could be viewed as a tool to improve clarity in templates.

While we think writing {{this.foo}} would be a best practice for new code going forward, the community can migrate at its own pace one component at a time. However, once the fallback functionality is eventually removed this will result in a "Helper not found" error.

Syntax Breakdown

The follow is a breakdown of the different forms and what they mean:

  • {{@foo}} is an argument passed to the component
  • {{this.foo}} is a property on the component class
  • {{#with this.foo as |foo|}} {{foo}} {{/with}} the {{foo}} is a local
  • {{foo}} is a helper

Drawbacks

The largest downside of this proposal is that it makes templates more verbose, causing developers to type a bit more. This will also create a decent amount of deprecation noise, although we feel like tools like ember-cli-deprecation-workflow can help mitigate this.

Alternatives

This pattern of having programming model constructs to distinguish between the backing class and arguments passed to the component is not unique to Ember.

What Other Frameworks Do

React has used this.props to talk about values passed to you and this.state to mean data owned by the backing component class since it was released. However, this approach of creating a specific object on the component class to mean "properties available to the template", would likely be even more an invasive change and goes against the mental model that the context for the template is the class.

Vue requires enumeration of props passed to a component, but the values in the template suffer from the ambiguity that we are trying to solve.

Angular relies heavily on the dependency injection e.g. @Input to enumerate the bindings that were passed to the component and relies heavily on TypeScript to hide or expose values to templating layer with public and private fields. Like Vue, Angular does not disambiguate.

Introduce Yet Another Sigil

We could introduce another sigil to remove ambiguity. This would address the concern about verbosity, however it is now another thing we would have to teach.

Change Resolution Order

The other option is to reverse the resolution order to prefer properties over helpers. However this has the reverse problem as described in the exemplar.

Do Nothing

I personally don't think this is an option, since the goal is to provide clarity for applications as they evolve over time and to provide a more concise mental model.

Unresolved questions

TBD

  • Start Date: 2018-03-09
  • RFC PR: https://github.com/emberjs/rfcs/pull/311
  • Ember Issue: (leave this empty)

Angle Bracket Invocation

Summary

This RFC introduces an alternative syntax to invoke components in templates.

Examples using the classic invocation syntax:

{{site-header user=this.user class=(if this.user.isAdmin "admin")}}

{{#super-select selected=this.user.country as |s|}}
  {{#each this.availableCountries as |country|}}
    {{#s.option value=country}}{{country.name}}{{/s.option}}
  {{/each}}
{{/super-select}}

Examples using the angle bracket invocation syntax:

<SiteHeader @user={{this.user}} class={{if this.user.isAdmin "admin"}} />

<SuperSelect @selected={{this.user.country}} as |Option|>
  {{#each this.availableCountries as |country|}}
    <Option @value={{country}}>{{country.name}}</Option>
  {{/each}}
</SuperSelect>

Motivation

The original angle bracket components RFC focused on capitalizing on the opportunity of switching to the new syntax as an opt-in to the "new-world" components programming model.

Since then, we have switched to a more iterative approach, favoring smaller RFCs focusing on one area of improvement at a time. Collectively, these RFCs have largely accomplished the goals in the original RFC without the angle bracket opt-in.

Still, separate from other programming model improvements, there is still a strong desire from the Ember community for the previously proposed angle bracket invocation syntax.

The main advantage of the angle bracket syntax is clarity. Because component invocation are often encapsulating important pieces of UI, a dedicated syntax would help visually distinguish them from other handlebars constructs, such as control flow and dynamic values. This can be seen in the example shown above – the angle bracket syntax made it very easy to see the component invocations as well as the {{#each}} loop, especially with syntax highlight:

<SuperSelect @selected={{this.user.country}} as |Option|>
  {{#each this.availableCountries as |country|}}
    <Option @value={{country}}>{{country.name}}</Option>
  {{/each}}
</SuperSelect>

This RFC proposes that we adopt the angle bracket invocation syntax to Ember as an alternative to the classic ("curlies") invocation syntax.

Unlike the original RFC, the angle bracket invocation syntax proposed here is purely syntactical and does not affect the semantics. The invocation style is largely transparent to the invokee and can be used to invoke both classic components as well as custom components.

Since the original angle bracket RFC, we have worked on a few experimental implementation of the feature, both and in Ember and Glimmer. These experiments allowed us to attempt using the feature in real apps, and we have learned some valuable insights throughout these usage.

The original RFC proposed using the <foo-bar ...> syntax, which is the same syntax used by web components (custom elements). While Ember components and web components share a few similarities, in practice, we find that there are enough differences that causes the overload to be quite confusing for developers.

In addition, the code needed to render Ember components is quite different from what is needed to render web components. If they share the same syntax, the Glimmer template compiler will not be able to differentiate between the two at build time, thus requiring a lot of extra runtime code to support the "fallback" scenario.

In conclusion, the ideal syntax should be similar to HTML syntax so it doesn't feel out of place, but different enough that developers and the compiler can easier tell that they are not just regular HTML elements at a glance.

Detailed design

Tag Name

The first part of the angle bracket invocation syntax is the tag name. While web components use the "dash rule" to distinguish from regular HTML elements, we propose to use capital letters to distinguish Ember components from regular HTML elements and web components.

The invocation <FooBar /> is equivalent to {{foo-bar}}. The tag name will be normalized using the dasherize function, which is the same rules used by existing use cases, such as service injections. This allows existing components to be invoked by the new syntax.

Another benefit of the capital letter rule is that we can now support component names with a single word, such as <Button>, <Modal> and <Tab>.

Note: Some day, we may want to explore a file system migration to remove the need for the normalization rule (i.e. also use capital case in filenames). However, that is out-of-scope for this RFC, as it would require taking into consideration existing code (like services), transition paths and codemods.

Arguments

The next part of the invocation is passing arguments to the invoked component. We propose to use the @ syntax for this purpose. For example, the invocation <FooBar @foo=... @bar=... /> is equivalent to {{foo-bar foo=... bar=...}}. This matches the named arguments syntax in the component template.

If the argument value is a constant string, it can appear verbatim after the equal sign, i.e. <FooBar @foo="some constant string" />. Other values should be enclosed in curlies, i.e. <FooBar @foo={{123}} @bar={{this.bar}} />. Helpers can also be used, as in <FooBar @foo={{capitalize this.bar}} />.

Reserved Names

@args, @arguments and anything that does not start with a lowercase letter (such as @Foo, @0, @! etc) are reserved names and cannot be used. These restrictions may be relaxed in the future.

Positional Arguments

Positional arguments ({{foo-bar "first" "second"}}) are not supported.

HTML Attributes

HTML attributes can be passed to the component using the regular HTML syntax. For example, <FooBar class="btn btn-large" role="button" />. HTML attributes can be interleaved with named arguments (it does not make any difference). This is a new feature that is not available in the classic invocation style.

These attributes can be accessed from the component template with the new ...attributes syntax, which is available only in element positions, e.g. <div ...attributes />. Using ...attributes in any other positions, e.g. <div>{{...attributes}}</div>, would be a syntax error. It can also be used on multiple elements in the same template. If attributes are passed but the component template does not contain ...attributes (i.e. the invoker passed some attributes, but the invokee does not take them), it will be a development mode error.

It could be thought of that the attributes in the invocation side is stored in an internal block, and ...attributes is the syntax for yielding to this internal block. Since the yield keyword is not available in element position, a dedicated syntax is needed.

Classic components (Ember.Component) will implicitly have an ...attributes added to the end of the wrapper element (if tagName is not an empty string), after any attributes added by the component itself (using attributeBindings, classNames etc). This means that attributes provided by the caller will override (replace) those added by the component (except for class, which is merged).

Block

A block can be passed to the invokee using the angle bracket invocation syntax. For example, the invocation <FooBar>some content</FooBar> is equivalent to {{#foo-bar}}some content{{/foo-bar}}. As with the classic invocation style, this block will be accessible using the {{yield}} keyword, or the @main named argument per the named blocks RFC.

Block params are supported as well, i.e. <FooBar as |foo bar|>...</FooBar>.

There is no dedicated syntax for passing an "else" block directly. If needed, that can be passed using the named blocks syntax.

Closing Tag

The last piece of the angle bracket invocation syntax is the closing tag, which is mandatory. The closing tag should match the tag name portion of the opening tag exactly. If no block is passed, the self-closing tag syntax <FooBar /> can also be used (in which case {{has-block}} will be false).

Dynamic Invocations

In additional to the static invocation described above (where the tag name is a statically known component name), it is also possible to use the angle bracket invocation syntax for dynamic invocations.

The most common use case is for invoking "contextual components", as shown in the first example:

<SuperSelect @selected={{this.user.country}} as |Option|>
  {{#each this.availableCountries as |country|}}
    <Option @value={{country}}>{{country.name}}</Option>
  {{/each}}
</SuperSelect>

Because Option is the name of a local variable (block param), the <Option> invocation will invoke the yielded value instead of looking for a component named "option".

Similar to curly invocations, most valid Handlebars path expressions are invokable in this manner:

{{!-- LOCAL VARIABLES --}}

{{#form-for model=user as |f|}}
  {{f.fieldset}}
    {{f.input name="username" type="text"}}
    {{f.input name="password" type="password" }}
  {{/f.fieldset}}

  {{!-- is equivilant to --}}

  <f.fieldset>
    <f.input @name="username" @type="text" />
    <f.input @name="password" @type="text" />
  </f.fieldset>
{{/form-for}}

{{!-- NAMED BLOCKS OR CURRIED COMPONENTS --}}

{{@content}}

{{!-- is equivilant to --}}

<@content />

{{!-- THIS LOOKUP --}}

{{#this.container}}
  {{this.child}}
{{/this.container}}

<this.container>
  <this.child />
</this.container>

Note: The named blocks RFC proposed to use the <@foo>...</@foo> syntax on the invocation side to mean providing a block named @foo, which creates a conflict with this proposal. RFC #317 propose to change the block-passing syntax to <@foo=>...</@foo> to avoid this conflict.

Notably, based on the rules laid out above, the following is perfectly legal:

{{!-- DON'T DO THIS --}}

{{#let (component "my-div") as |div|}}
  {{!-- here, <div /> referes to the local variable, not the HTML tag! --}}
  <div id="my-div" class="lol" />
{{/let}}

From a programming language's perspective, the semantics here is quite clear. A local variable is allowed to override ("shadow") another variable on the outer scope (the "global" scope, in this case), similar to what is possible in JavaScript:

let console = {
  log() {
    alert("I win!");
  }
};

console.log("Hello!"); // shows alert dialog instead of logging to the console

While this is semantically unambiguous, it is obviously very confusing to the human reader, and we don't recommend anyone actually doing this.

A previous version of this RFC recommended statically disallowing these cases. However, after giving it more thoughts, we realized it should not be the programming language's job to dictate what are considered "good" programming patterns. By statically disallowing arbitrary expressions, it actually makes it more difficult to learn and understand the underlying programming model.

Instead, we recommend including a template linter in the default stack and defer to the linter to make such recommendations. At minimum, we recommend linting against invoking local variables with lowercase names without a path segment, regardless of whether the name actually collide with a known HTML tag – human readers of an Ember template should be able to safely assume lowercase tags refer to HTML.

Eventually, we might want to provide stronger guidance with via the linter. For example, we may want to recommend capitalizing invokable local variables, as in <F.Input />. We will let the community experiment and coalesce around these conventions before recommending them by default.

Finally, there are two exceptions to the general rule where certain technically valid Handlebars path expressions are not supported for dynamic invocations:

  • Implicit this lookups (a.k.a. "property fallback" in RFC #308
  • Slash lookups

First, while {{foo}} or {{Foo}} can normally refer to {{this.foo}} or {{this.Foo}} normally, allowing this implicitly lookup will mean any tag in the template (i.e. <foo /> or <Foo />) can possibly refer to a property on the current this context.

This ambiguity is highly undesirable for both human readers and the compiler, therefore implicitly this lookup is not allowed in angle bracket invocations. This explicit form, <this.foo /> and <this.Foo /> is required.

This requirement aligns well with RFC #308 and the current curly invocation semantics, due to the "dot rule" that requires a dot in the path. Note that this is actually more restrictive than the proposed angle bracket invocation semantics, since it is not possible to invoke a local variable without a dot:

{{#super-select selected={{this.user.country}} as |option|>
  {{#each this.availableCountries as |country|}}
    {{!-- this is not legal today, since `option` does not contain a dot --}}
    {{#option value=country}}{{country.name}}{{/option}}
  {{/each}}
{{/super-select}}

We propose to relax that rule to match the proposed angle bracket invocation semantics (i.e. allowing local variables without a dot, as well as @names, but disallowing implicit this lookup).

Second, while Handlebars technically allows {{foo/bar}} as an equivalent alternative to the {{foo.bar}} path lookup (and therefore foo/bar is technically a valid Handlebars path expression), it will not be supported in angle bracket invocation. This is both because the / conflicts with the HTML closing tag syntax, and the fact that Ember overrides that syntax with a different semantic.

In today's semantics, {{foo/bar}} does not try to lookup this.foo.bar and invoke it as a component. Instead, it is used as a filesystem scoping syntax. Since this feature will be rendered unnecessary with Module Unification, we recommend apps using "slash components" to migrate to alternatives provided by Module Unification (or, alternatively, keep using curly invocations for this purpose).

How we teach this

Over time, we will switch to teaching angle bracket invocation as the primary invocation style for components. The HTML-like syntax should make them feel more familiar for new developers.

Classic invocation is here to stay – the ability to accept positional arguments and "else" blocks makes them ideal for control-flow like components such as {{liquid-if}}.

Drawbacks

Because angle bracket invocation is designed for the future in mind, allowing angle bracket invocations on classic components might introduce some temporary incoherence (such as the interaction between the attributes passing feature and the "inner HTML" semantics). However, in our opinion, the upside of allowing incremental migration outweighs the cons.

Alternatives

We could just stick with the classic invocation syntax.

  • Start Date: 2018-03-24
  • Relevant Team(s): Ember.js
  • RFC PR: https://github.com/emberjs/rfcs/pull/318
  • Tracking: https://github.com/emberjs/rfc-tracking/issues/23

array helper

Summary

This RFC proposes to add an array template helper for creating arrays in templates.

The helper would be invoked as (array arg1 ... argN) and return the value [arg1, ..., argN]. For example, (array 'a' 'b' 'c') would return the value ['a', 'b', 'c'].

Motivation

Objects (or hashes) and arrays are the two main data structures in JavaScript. Ember already has a hash helper for building objects, so it makes sense to also include an array helper for building arrays.

Detailed design

The design is straightforward and mirrors the design of the hash helper. In particular, the important thing to note is that if any of the arguments to the array helper change then an entirely new array will be returned, rather than updating the existing array in place.

The implementation would also mirror the implementation of the hash helper and would simply capture the positional arguments instead.

How we teach this

This helper is not an important part of the programming model and can just be mentioned in the API docs like its sibling the hash helper.

Drawbacks

As usual, adding new helpers increases the surface area of the API and file size but in this case it is justified because the file size change is extremely small and its actually filling an existing hole in the API.

Alternatives

This helper could be left to addons, and indeed there are addons that include this helper. It's also trivial to generate your own array helper with ember generate helper array. Humorously, the default helper blueprint generates a helper that already acts like the array helper ;)

Nevertheless, I believe it's preferable to include this helper in Ember to fill the hole in Ember's API.

  • 2018-03-24
  • RFC PR: 322
  • Ember Issue: (leave this empty)

Deprecation of Ember.copy and Ember.Copyable

Summary

This RFC recommends the deprecation and eventual removal of Ember.copy and the Ember.Copyable mixin.

Motivation

A deep-copy mechanism is certainly useful, but it is a general JavaScript problem. Ember itself doesn't need to offer one, especially one that Ember itself isn't using internally. This function and its accompanying mixin arrived with SproutCore, a long time ago, and are not used by Ember itself, even though they currently reside in @ember/object/internals.

ember-data uses Ember.copy to do deep-copies. However, the ember-data team finds its needs would be better served by a private deep-copy mechanism that doesn't flow inadvertently through external interfaces into the Ember.copy methods of user-supplied objects. These interfaces are not designed to support deep copies of user-supplied data, and it can raise havoc in the form of hard-to-diagnose bugs, especially in test scenarios.

Since ember and ember-data do not intend to use this mechanism going forward, it would be better to remove it from the Ember codebase and extract it into an add-on for those who wish to continue to use it.

Detailed design

There are four steps to deprecating any function:

  • logging the deprecation in the call
  • removal of calls to the function from ember and any add-ons that ship with ember-cli
  • extraction to an add-on
  • eventual removal of the feature in the stated release (in this case 4.0.0).

This RFC deprecates the copy function and Copyable mixin of @ember/object/internals.

Shallow copies of the form copy(x) or copy(x, false) can be replaced mechanically with Object.assign({}, x). The simplest way to deal with deep copies in any situation depends upon the nature of the data involved.

Current internal uses

ember-source

This following modules in packages/ember-runtime/lib implement the code being deprecated:

  • copy.js contains the copy() function that will log the deprecation before executing,
  • mixins/copyable.js provides the Copyable mixin, but it contains no executable code to deprecate.
  • mixins/array.js - The NativeArray mixin extends the Copyable mixin and implements copy().

The following tests in packages/ember-runtime/tests use the implementation above:

  • core/copy_test.js tests the copy() method itself.
  • copyable-array/copy-test.js tests the copy() method of a NativeArray for identical results.
  • helpers/array.js provides the arrays used by the NativeArray test above.
  • system/native_array/copyable_suite_test.js tests the independence of the results of deep copying a NativeArray

The route packages/ember-routing/lib/system/route.js has one shallow copy, but the test packages/ember/tests/routing/decoupled_basic_test is using deep copy.

The copy() methods in packages/ember-metal/lib/map.js and chains.js and their use in meta.js, and map_test.js are unrelated.

At present, the handling of arrays in Ember.copy is inconsistent. NativeArray uses the Copyable mixin and implements a copy method. When calling Ember.copy, passing a NativeArray, it will note that the passed parameter uses Copyable and call the copy method inside NativeArray. However, the recursive _copy method that Ember.copy calls for other objects has its own generic mechanism for copying arrays. If copy is passed a non-Copyable object that contains a NativeArray as a member, when the recursion gets to that member, it will use the generic mechanism rather than delegating to the copy method within the NativeArray.

The recursive _copy method also has an assertion that will fail if it is called with any EmberObject that is not also Copyable. This assertion occurs before (and hence affects) the code which handles arrays, even though, for arrays, the object's copy method isn't then used.

During the deprecation period, the Ember.copy method and the NativeArray.copy methods will carry a deprecation warning. We will remove Copyable from NativeArray and change Ember.copy to consistently use the common array copy mechanism to copy arrays rather than sometimes delegating. We will move the assertion that an EmberObject must be Copyable to the clause that handles non-array objects.

We need a way to deprecate use of the Copyable mixin. If the penalty for adding code in such a common place isn't too high, we could have core_object.extend() check for Copyable and deprecate accordingly. We will also supply a new eslint warning that flags the deprecated use of Copyable. (This may be our first eslint check for deprecations. We may want to consider adding others at the same time.)

Those using the add-on will need to mechanically adjust any uses of myArray.copy(deep) to copy(myArray, deep) in order to avoid the deprecation message.

At the end of this period, we will remove the deprecated copy() method, the Copyable mixin, and the deprecated NativeArray.copy() method.

ember-data

The following code in ember-data uses copy(), but only for shallow copies:

  • addon/-private/system/model/internal-model.js - one use
  • addon/-private/system/snapshot.js - two uses
  • addon/-private/system/store.js - one use

All of the following uses in tests perform deep copies:

  • tests/integration/adapter/build-url-mixin-test.js - two uses
  • tests/integration/adapter/rest-adapter-test.js - two uses
  • tests/integration/store-test.js - two uses
  • tests/unit/system/relationships/polymorphic-relationship-payloads-test.js - four uses

The copy() methods referenced in addon/-private/system.map.js and addon/-private/system/relationships/state/relationship.js are unrelated.

It would appear that deep copy is used within these packages only during testing, and generally to ensure fresh test data without side-effects.

Current external uses

The key considerations for add-ons or apps looking for an alternative to copy() and Copyable are:

  • Do they call copy() to do shallow copies or deep copies?
  • If deep copies are being performed, are the objects involved POJOs or are they derived from EmberObject?
  • Do they provide objects that use the Copyable mixin with copy() methods intended for use in deep copies by other classes?
  • Is the data you are copying the sort of thing where you can do the copy in its behalf, or does it require collaboration from the object itself? Or are the contents so open-ended that you can't possibly know?

Shallow copies are directly supported by ES6. It's easy to perform recursive deep copies for most simple POJOs without delegating work to the object you are copying. For more complex data, you may need some kind of recursive delegation. Copyable is a delegation mechanism, and apps and add-ons that require delegation will probably want to use the proposed add-on.

The Code Search capabilities of emberobserver are a wonderful way to get a glimpse of how code in the wild is using particular features.

A quick search of the top-scoring add-on packages revealed that most, but by no means all, of the uses of copy() in the modules were for shallow copies that can be accomplished using Object.assign, so a lot of the code affected by this deprecation can rely on a simple substitution.

Very few packages used Copyable - only 9 across the whole set - and most used the feature for only one class. ember-data-copyable is probably most wedded to the mechanism: it delivers a Copyable-based mixin for asynchronous copying. ember-data-model-fragments has pretty open-ended properties. These add-ons would be likely to use the proposed add-on moving forward. ember-restless, and ember-calendar appear more bounded. Any deep copy mechanism for POJOs may meet their needs.

Add-on

The add-on will supply the copy() function and the Copyable mixin based on the existing code, modified as indicated above for handling of arrays.

We could treat the add-on as the extraction of a feature from the monolithic ember-source, as was recently done for strings. If we choose to frame it in that way, the naming should follow the conventions set out for extracting elements of Ember into their own packages. If we choose not to frame it that way, then naming is one of the things this section should specify clearly.

How we teach this

Communication of change

We need to inform users that Ember.copy and Ember.Copyable will be dprecated and in what release it will occur. This notification should also point them to the add-on for those who need it.

Official code bases and documentation

We do not actively teach the use of Ember.copy. It doesn't appear anywhere in our guides, website, or tutorial. Once it is gone from the code, we also need to verify it no longer appears in the API listings.

We must provide an entry in the deprecation guide for this change:

  • describing the use of to = Object.assign({},from) for shallow copies.
  • pointing out viable alternatives for deep copies.
  • directing heavy users of deep copies to the addon.

Drawbacks

The primary drawback is the API churn of people pulling it out of their code. However, for most uses, the change will be straightforward, and the add-on will be available for the foreseeable future for those who want to continue with the implementation.

Alternatives

We could simply leave it in place as a utility for others to use. Even then, it would make sense to split it out into its own module, as has already been done for strings, so the work would be much the same.

Unresolved questions

None at the moment...

  • Start Date: 2018-03-28
  • Relevant Team(s): Ember.js
  • RFC PR: https://github.com/emberjs/rfcs/pull/324
  • Tracking: https://github.com/emberjs/rfc-tracking/issues/22

Summary

The aim of this RFC is to deprecate the component's isVisible property. It is not used by Ember internally and left undefined unless manually set. It's poorly documented and component visibility it better managed in template space rather than JS.

Motivation

Setting the isVisible property on a component instance as a way to toggle the visibility of the component is confusing. The majority of its usage predates even Ember 1.0.0, and modern Ember applications already completely avoid using isVisible in favor of simpler conditionals in the template space.

In addition, when isVisible is used today it often introduces subtle (and difficult to track down) bugs due to its interaction with the style attribute (toggling isVisible clobbers any existing content in style).

Simply put, removing isVisible will reduce confusion amongst users.

Transition Path

Whenever isVisible is used a deprecation will be issued with a link to the deprecation guide explaining the deprecation and how to refactor in order to avoid it.

Given that Component#isVisible is a public API, deprecating now would schedule for removal in the next major version release (4.0).

There are several options available to hiding elements such as <div hidden={{boolean}}></div>(hidden is valid for all elements and is semantically correct) or wrapping the component in a template conditional {{#if}} statement. Components classNames and classNameBindings could also be used to add hidden classes.

How We Teach This

The isVisible property is rarely used, the deprecation along with a mention in a future blog post would be sufficient.

We should consider adding documentation on hiding components to the Ember guides with the conditional handlebar helper or via the widely supported hidden attribute.

{{#if showComponent}}
  {{component}}
{{/if}}

{{! or }}
<div hidden={{isHidden}}></div>

Alternatives

An alternative option would be to to keep isVisible.

  • Start Date: (fill me in with today's date, 2018-04-18)
  • RFC PR: #326
  • Ember Issue: (leave this empty)

Ember Data Filter Deprecation

Summary

Deprecate the store.filter API. This API was previously gated behind a private ENV variable that was enabled by the addon ember-data-filter.

Motivation

The filter API was a "memory leak by design". Patterns exist with no-worse ergonomics that have better performance and do not incur memory leak penalties.

While the change in ergonomics for end consumers in minimal, the change to ember-data is substantial. The code for this feature required significant amounts of confusing internal plumbing to ensure that filters were rerun every time any form of mutation (update, addition, deletion) occurred to any record.

In addition to maintenance costs, this plumbing negatively affects the performance of all RecordArrays, and slow any operations that count as mutations (such as pushing new records into the store).

By removing this feature, we significantly simplify and streamline the core of Ember Data.

Detailed design

We will provide 3 new deprecations with links to a guide on how to refactor. These deprecations will target 3.5, meaning that the ember-data-filter addon will continue to work and be supported through the release of ember-data 3.4.

Deprecation: ember-data-filter:filter

Deprecate the primary case (store.filter('posts', filterFn)). Instead, users can combine store.peekAll with a computed property.

Deprecation: ember-data-filter:query-for-filter

This deprecation is specific to folks providing a query to be requested the first time a filter is run. To do this better, users can separate their usage of filter from their usage of query.

Deprecation: ember-data-filter:empty-filter

In the case that users were creating a filter with no method for filtering by, a deprecation is printed letting them know that the easiest path forward is to use peekAll, which would return the same record result set.

How we teach this

The filter API is rarely used, having been discouraged for many years. A simple post alerting users to it's deprecation should be sufficient. The refactoring guide is sufficiently simple that teaching folks a better way should not be much of a hurdle.

Drawbacks

Minor churn for folks that did use this API; however, the end result will improve the performance of apps using filters more so than anyone else.

Alternatives

There's been some talk of an API for local querying; however, said alternative RFC would only result in deprecating this API as well.

Unresolved questions

None

  • Start Date: 2018-05-01
  • Relevant Team(s): Ember Data
  • RFC PR: https://github.com/emberjs/rfcs/pull/329
  • Tracking: https://github.com/emberjs/rfc-tracking/issues/21

Deprecate Usage of Ember Evented in Ember Data

Summary

Ember.Evented functionality on DS.Model, DS.ManyArray, DS.Errors, DS.RecordArray, and DS.PromiseManyArray will be deprecated and eventually removed in a future release. This includes the following methods from the Ember.Evented class: has, off, on, one, and trigger. Additionally the following lifecycle methods on DS.Model will also be deprecated: becameError, becameInvalid, didCreate, didDelete, didLoad, didUpdate, ready, rolledBack.

Motivation

The use of Ember.Evented is mostly a legacy from the pre 1.0 days of Ember Data when events were a core part of the Ember Data programming model. Today there are better ways to do everything that once needed events. Removing the usage of the Ember.Evented mixin will make it easier for Ember Data to eventually transition to using native ES2015 JavaScript classes and will reduce the surface area of APIs that Ember Data must support in the long term.

Detailed design

Ember.Evented mixin will be scheduled to be removed from the following classes in a future Ember Data release: DS.Model, DS.ManyArray, DS.Errors, DS.RecordArray, and DS.PromiseManyArray.

The has, off, on, one, and trigger methods will be trigger a deprecation warning when called and will be completly in a future Ember Data release.

A special deprecation will be logged when users of a DS.adapterPopulatedRecordArray attempt to listen to the didLoad event. This depecations will prompt users to use a computed property instead of the didLoad event.

DS.Model will also recieve deprecation warnings when a model is defined with the following methods: becameError, becameInvalid, didCreate, didDelete, didLoad, didUpdate, ready, rolledBack.

When a model is instantiated for the first time with any of these methods a deprecation warning will be logged notifiying the user that this method will be deprecated and the user should use an computed or overide the model's init method instead.

How we teach this

Today we do not teach the use of any of the Ember Data lifecycle events in the guides. They are referenced in the API docs but they will be updated to mark the APIs as deprecated and show alternative examples of how to achieve the same functionality using a non event pattern.

The deprecation guide app will be updated with examples showing how to migrate away from an evented pattern to using a computed or imperative method to achieve the same results.

Drawbacks

The drawback to making this change is existing code that takes advantage of the Ember Data lifecycle events will need to be updated to use a different pattern.

Alternatives

We could leave the Ember.Evented mixin on all of the Ember Data objects that currently support it and continue to support this interface for the foreseeable future. However, Ember Data itself doesn't require these events internally. There is only one place in the DS.Error code that takes advantage of the Ember.Evented system and that code can be easilly re-written to avoid Ember.Evented APIs.

Unresolved questions

None

  • Start Date: 2018-05-08
  • Relevant Team(s): Ember.js
  • RFC PR: https://github.com/emberjs/rfcs/pull/331
  • Tracking: https://github.com/emberjs/rfc-tracking/issues/20

Summary

Deprecate all use of:

  • Ember Globals Resolver (looks up a class via a global namespace such as "App")
  • Creation of a Global Namespace (var App = Ember.Namespace.create();)
  • Ember.TEMPLATES array
  • <script type="text/handlebars" data-template-name="path/to/template">

Use of any of the above should trigger a deprecation warning, with a target of version 4.0

Motivation

Over the past years we have transitioned to using Ember-CLI as the main way to compile Ember apps. The globals resolver is a holdover and primarily facilitates use of Ember without Ember-CLI.

The Globals Resolver

For those who are not aware, the globals resolver is available via @ember/globals-resolver or Ember.DefaultResolver. For more information, see the api. Using it looks like the following:

// app.js
var App = Ember.Application.create();

App.Router.map(function() {
  this.route('about');
});

App.AboutRoute = Ember.Route.extend({
  model: function() {
    return ['red', 'yellow', 'blue'];
  }
});
// index.html
<script type="text/x-handlebars" data-template-name="about">
  <ul>
    {{#each model as |item|}}
      <li>{{item}}</li>
    {{/each}}
  </ul>
</script>

Implementation Details

One small detail required to implement this RFC: ember-cli's own default resolver, ember-resolver currently still extends from the globals resolver. In order to implement this RFC, the ember-cli resolver will need to be changed so that it does not extend from the globals resolver, or otherwise ember-cli users will get a deprecation warning as well. However, changing the base class of the ember cli classic resolver is a breaking change, so prior to ember/ember-cli version 4.0 we need to take another step. In the ember-cli classic resolver, deprecate any runtime calls where there is fallback to the globals mode resolver. This would be a deprecation in ember-cli's resolver. We could bump a major version of ember-cli-resolver removing the base class and release it in ember-cli after an LTS of ember-cli.

Transition Path

Primarily, the transition path is to recommend using Ember-CLI.

During the 3.x timeframe, it MAY become increasingly difficult to use this old functionality. For example, with the release of 3.0, we already stopped publishing builds that support globals mode. Here are some of the changes that have impacted or may soon impact users of globals mode:

Impact of ES6 modules

Users of ES6 modules must use their own build tooling to convert them to named AMD modules via Babel. No support is provided for <script type="module"> at this time, although that may change.

Impact of New Module Imports

Globals based apps are only able to use new module imports via the polyfill available at https://github.com/ember-cli/babel-plugin-ember-modules-api-polyfill No build support for this is provided.

Impact of not publishing globals builds

It is necessary to get a globals build of Ember.js from the npm package now that globals builds are no longer published to S3, builds.emberjs.com, and CDNs.

Impact of not Generating a Globals Build in Ember.js Package

At some point during the 3.x cycle, it may be that we no longer publish a globals build in the npm package. At that point, it may become necessary to use Ember-CLI to generate a globals build of Ember.js

Impact of Package Splitting

Work has started on package splitting. It is likely that the globals resolver may not be included in a default partial build of Ember.js and may be moved to its own package for easy removal.

Impact of Tree Shaking

If the globals resolver is moved to a separate package, it will likely not be included in a build of Ember.js by default unless tree shaking is turned off.

How We Teach This

We already do teach this and don't teach the globals resolver. No changes required here.

Deprecation Guide

A draft deprecation guide has been pull requested at https://github.com/ember-learn/deprecation-app/pull/155

Drawbacks

A drawback is that people may want alternate build tooling to Ember-CLI. We have mitigated this by openly publishing the ember-cli resolver and all parts of the ember-cli ecosystem under the MIT license. Alternate build tooling may simply use this open source code to build a competing infrastructure to ember-cli.

Alternatives

Without doing this, we will have to continue to ship and maintain this rarely used functionality. We don't believe this is a reasonable alternative.

Unresolved questions

There has never been a transition guide for transitioning an old codebase to Ember-CLI. Do we want to create one at this late date?

  • Start Date: 2018-10-24
  • Relevant Team(s): Ember Data
  • RFC PR: https://github.com/emberjs/rfcs/pull/332
  • Tracking: https://github.com/emberjs/rfc-tracking/issues/19

Ember Data Record Links & Meta

Summary

Enable users to associate links and meta information with individual records in a manner accessible via the template.

Motivation

Sometimes users have meta or links information to associate with a specific record. Users of the json-api specification will commonly understand this information as belonging to an individual resource.

While ember-data allows for this information to exist on relationships, it does not allow for it to exist on records, which has to this point been a glaring omission for users of json-api and similar specifications.

Detailed design

In keeping with the current design of the store.push API which expects the json-api format, users would include optional meta and links information as member properties of a resource.

store.push({
  data: {
    type: 'contributor',
    id: '1',
    attributes: {},
    relationships: {},
    meta: {
      // ... <any>
    },
    links: {
      self: './person/1', // ... <String>
    }
  }
});

links and meta will be accepted anywhere a resource may be encountered in a payload.

store.push({
  data: [
    {
      type: 'contributor',
      id: '1',
      attributes: {},
      relationships: {
        projects: {
          data: [
            { type: 'project', id: '1' }
          ]
        }
      },
      meta: {
        // ... <any>
      },
      links: {
        self: './person/1', // ... <String>
      }
    }
  ],
  included: [
    {
      type: 'project',
      id: '1',
      attributes: {},
      relationships: {
        contributors: {
          data: [
            { type: 'contributor', id: '1' }
          ]
        }
      },
      meta: {
        // ... <any>
      },
      links: {
        self: './github-projects/1', // ... <String>
      }
    }
  ]
})

Links & Meta on objects used as ResourceIdentifiers (e.g. to link to another resource within a relationship) will not be used for the associated resource and will be silently ignored.

let record = store.push({
  data: {
    type: 'contributor',
    id: '1',
    attributes: {},
    relationships: {
      projects: {
        data: [
          {
            type: 'project',
            id: '1',         
            meta: {}, // ignored
            links: {} // ignored
          }
        ]
      }
    },
  }
});

Links & Meta on objects provided for Relationships will continue to work (as they do today).

let record = store.push({
  data: {
    type: 'contributor',
    id: '1',
    attributes: {},
    relationships: {
      projects: {
        data: [
          {
            type: 'project',
            id: '1',         
          }
        ],
        meta: {}, // available on the Record's hasMany relationship
        links: {} // available on the Record's hasMany relationship
      }
    },
  }
});

links and meta properties will be exposed as getters on instances of DS.Model and will default to null if no meta or links have been provided.

let record = store.push({
  data: {
    type: 'person',
    id: '1',
    attributes: { name: '@runspired' },
    meta: {
      expiresDate: '2018-05-10'
    },
    links: {
      self: './people/runspired'
    }
  }
});

record.meta.expiresDate; // '2018-05-10'
record.links.self; // './people/runspired'

links and meta will similarly be exposed as on instances of Snapshot given to adapter and serializer methods. In keeping with Snapshot#attributes(), they will be exposed as methods. Should users desire to reload a record via link, they could achieve such by utilizing the links() method to check for a link when making a request.

class Snapshot {
  links() {}
  meta() {}
}

The shared namespace problem and interop with existing workaround for links and meta.

The json-api spec places type, id, and all members of attributes and relationships into a single shared flattened namespace. This flattened namespace is what records expose.

The spec does not put links and meta into this namespace, and it is valid to have links and meta as member names of either attributes or relationships.

Some apps have taken advantage of this to move links and meta into attributes on their serializer and to expose them via DS.attr on their records.

The getter we are proposing adding to DS.Model would be overwriteable. In the case that there is a conflict, the version defined by the end user model would win. It would be up to consuming apps to decide whether they wish to avoid this conflict by renaming the non-resource links and meta either in their serializer or in their API responses.

How we teach this

Documentation for DS.Model should be updated to reflect these properties, the potential conflict (and the default conflict resolution) explained in said documentation, and guides on working with Models should reflect this capability.

Drawbacks

Users may sometimes encounter confusion when links or meta is a member of attributes or relationships.

Alternatives

  • Rename links and meta to a name less likely to collide and which we fully reserve, such as recordLinks and recordMeta. We felt this would be confusing.

  • Enforce accessing links and meta via some other object such as the Reference API. In addition to being cumbersome and confusing, this would lack discoverability and be unergonomic in templates.

  • Enforce accessing links and meta via some imported helper, e.g. recordMetaFor(record) or recordLinksFor(record). We felt this would be confusing and unergonomic for templates.

Unresolved questions

None

  • Start Date: 2018-05-29
  • RFC PR: https://github.com/emberjs/rfcs/pull/335
  • Ember Issue: (leave this empty)

Deprecate .sendAction

Summary

In old versions of Ember (< 1.13) component#sendAction was the only way for a component to call an action on a parent scope. In 1.13 with the so called closure actions a more intuitive and flexible way of calling actions was introduced, yielding the old way redundant.

Motivation

With the new closure actions being the recommended way, component#sendAction is not even mentioned in the guides. With the goal of simplifying the framework I think we should remove what is not considered the current best practice. Closure actions have been available since 1.13. That is 3 years ago, so deprecating sendAction should not cause too much pain and yet addons can support still support the last version of the 1.X cycle if they really want to.

It is out of the scope of this RFC to enumerate the reasons why closure actions are preferred over sendAction but you can find an in depth explanation of closure actions in this blog post from 2016.

Detailed design

A deprecation message will appear when sendAction is invoked. The feature will be removed in Ember 4.0. The deprecation message will use the arguments passed to sendAction to generate a dynamic explanation that will make super-easy for developers to migrate to closure actions.

As it is mandatory with new deprecations, a new entry in the deprecation guides will be added explaining the migration path in depth.

To refresh what the migration path would look like in the typical use case.

BEFORE

// parent-component.js
export default Component.extend({
  actions: {
    sayHi() {
      alert('Hello user!');
    }
  }
})
{{!-- parent-component.hbs --}}
{{child-component salute="sayHi"}}
// child-component.js
export default Component.extend({
  actions: {
    sendSalute() {
      this.sendAction('salute');
    }
  }
});
{{!-- child-component.hbs --}}
<button {{action "sendSalute"}}>Send salute</button>

AFTER

// parent-component.js
export default Component.extend({
  actions: {
    sayHi() {
      alert('Hello user!');
    }
  }
})
{{!-- parent-component.hbs --}}
{{child-component salute=(action "sayHi")}}
// child-component.js
export default Component.extend({
  actions: {
    sendSalute() {
      this.salute();
      // if the salute action is optional you'll have to guard in case it's undefined:
      // if (this.salute) {
      //   this.salute()
      // }
      // 
      // Alternatively, you can also define a noop salute function:
      // salute() {}
      //
      // This allows you to remove the guard while provinding an obvious place to add
      // docs for that action.
    }
  }
});
{{!-- child-component.hbs --}}
<button {{action "sendSalute"}}>Send salute</button>

However closure actions allow to be less verbose, so the same behavior could be attained using less intermediate calls

// parent-component.js
export default Component.extend({
  actions: {
    sayHi() {
      alert('Hello user!');
    }
  }
})
{{!-- parent-component.hbs --}}
{{child-component salute=(action "sayHi")}}
{{!-- child-component.hbs --}}
<button onclick={{@salute}}>Send salute</button>

How we teach this

There are no new concepts to teach, but the removal of an old concept now considered outdated.

Drawbacks

There might be some churn following the deprecation, specially comming from addons that haven't been updated in a while. Addons that want to support the latest versions of Ember without deprecation messages and still work past Ember 1.13 will have to do some gymnastics to do so.

Alternatives

Wait longer to deprecate it and keep sendAction undocumented until it's usage is yet more minoritary than it is today, to lower the churn.

  • Start Date: 2018-06-14
  • RFC PR: https://github.com/emberjs/rfcs/pull/337
  • Ember Issue: https://github.com/emberjs/ember.js/pull/16795

Native Class Constructor Update

Summary

Update the behavior of EmberObject's constructor to defer object initialization.

Motivation

Using native class syntax with EmberObject has almost reached full feature parity, meaning soon we'll be able to ship native classes and begin recommending them. This will do wonders for the Ember learning story, and will bring us in line with the wider Javascript community.

However, early adopters of native classes have experienced some serious ergonomic issues due to the current behavior of the class constructor. The issue is caused by the fact that properties passed to EmberObject.create are assigned to the instance in the root class constructor. Due to the way that native class fields work, this means that they are assigned before any subclasses' fields are assigned, causing subclass fields to overwrite any value passed to create:

class Foo extends EmberObject {
  bar = 'baz';
}

let foo = Foo.create({ bar: 'something different' });

console.log(foo.bar); // 'baz'

This has made adoption very difficult, and is a consistent stumbling block for new users of native class syntax in Ember. Worse yet, it makes writing a codemod for converting to native class syntax very difficult because we don't have a clear target.

For instance, given the above class, how would we convert the class field? Let's go through the various options:

class Foo extends EmberObject {
  // Does not work, for the reasons described above
  bar = 'baz';

  // Does not cover all cases. If we did `Foo.create({ bar: false })` it would
  // still assign the default.
  bar = this.bar || 'baz';

  // This works, but is very verbose and not ideal
  bar = this.hasOwnProperty('bar') ? this.bar : 'baz';

  // This is one of the community accepted solutions, but it requires lodash
  bar = _.defaultTo(this.bar, 'baz');

  // This is another community accepted solution, but it requires
  // @ember-decorators/argument, which is a separate library
  @argument foo = 'bar';
}

None of these is ideal. Instead, we can change the behavior of the constructor and the create method to circumvent this issue.

This change would be a breaking change to the behavior of native classes today, and a change from the previous class RFC. This will impact early adopters and should be made with that in mind. It would not be a change that breaks the behavior of the community solutions to class fields mentioned above, and all other changes would be relatively easy to create a safe codemod for (essentially converting constructor -> init in affected classes), so the impact should be minimal.

Because native classes never officially shipped as part of Ember's public API (an announcement was not made, docs have not been written, etc), this RFC proposes that the change would not be considered a breaking change for the purposes of semver. This would allow us to ship the change during the Ember v3 release cycle, and prevent more code from being built on top of the previous behavior.

Detailed design

One very important design constraint to making this change is that we cannot break the behavior of EmberObject when used without native classes. To do this, we will leverage the fact that the static create method is the only public way to create an instance of EmberObject.

Currently, the behavior of EmberObject is the following (simplified):

class EmberObject {
  constructor(props) {
    // ..class setup things

    Object.assign(this, props);
    this.init();
  }

  static create(props) {
    let instance = new this(props);

    return instance;
  }
}

We can change it to the following (simplified):

class EmberObject {
  constructor(props) {
    // ..class setup things
  }

  static create(props) {
    let instance = new this(props);

    Object.assign(instance, props);
    instance.init();

    return instance;
  }
}

This would assign the properties after all of the class fields for any subclasses have been assigned. Revisiting our previous example, the following two class declarations would effectively be equivalent:

const Foo = EmberObject.extend({
  bar: 'baz'
});

class Foo extends EmberObject {
  bar = 'baz';
}

Much easier to codemod! There are other subtle differences between native class fields and EmberObject properties, such as the fact that class fields are assigned each time a class is initialized, but these are easier to work around.

Injections and the init hook

One side effect of this change is that injections will not be available on the class instance during the constructor phase. This behavior is not very commonly used - based on an informal community survey we found only a few usages - but it does exist and have its use cases.

Figuring out the ideal behavior of injections during the constructor phase is outside of the scope of this RFC, and is something that should be discussed in future RFCs. For the time being, users can still rely on the init hook, which will continue to be called after all injections and properties have been assigned to the instance.

new EmberObject()

It was previously possible to use new syntax with EmberObject. While this was not considered public API, it has technically worked and been under test since the early days of Ember, and may fall under the category of intimate API. Ideally, we would deprecate this usage as a private/intimate API, which would mean supporting it through the next LTS version, and dropping support after (currently, this would mean dropping it at v3.5.0).

We can continue to support this behavior in a backwards compatible way while deprecating it with one final tweak to the change above:

const DEFER_INIT = new Symbol();

function initialize(instance, props) {
  Object.assign(instance, props);
  instance.init();
}

class EmberObject {
  constructor(props, maybeDefer) {
    // ..class setup things

    if (maybeDefer === DEFER_INIT) {
       return this;
    }

    deprecate('using `new` with EmberObject has been deprecated. Please use `create` instead.', false, {
      id: 'object.new-constructor',
      until: '3.5.0'
    });

    initialize(this, props);
  }

  static create(props) {
    let instance = new this(props, DEFER_INIT);
    initialize(instance, props);

    return instance;
  }
}

How we teach this

If this PR is accepted, most of the major issues with classes will have been resolved. We can begin working on a codemod to make converting easier, and move toward officially making native classes a finalized part of the public API of Ember. Pending decorators and class fields moving to a late enough stage in the TC39 process, we can also begin converting the guides to use native class syntax.

We can document the exact behavior of the new constructor in the API docs for EmberObject. Most details won't have to change since this change only affects native class syntax, which has not been documented much officially. We can also demonstrate the behaviors of classes throughout the guides and API docs.

One thing we should make clear is that EmberObject will likely be deprecated in the near future, and that ideally for non-Ember classes (things that aren't Components, Services, etc.) users should drop EmberObject altogether and use native classes only.

Drawbacks

This would be a breaking change that could negatively affect early adopters.

Alternatives

  • We could leave the behavior as is, and choose a method for defaulting to standardize on.

  • We could make this change behind a feature flag and require users to opt-in to the new behavior, like optional features that currently exist. This would have to be a build time feature flag, since the area is very performance sensitive. Given native classes are not yet public API, if we were to do this we should probably still default to enabling the new behavior and recommending it as the preferred path.

  • We could not deprecate new EmberObject altogether, and instead only deprecate passing properties to the constructor. While this would work as a temporary solution, it may also encourage users to continue using EmberObject instead of switching to native classes, which is ultimately the long term goal.

Unresolved questions

How do we handle DI during the construction phase?

  • Start Date: 2018-06-19
  • RFC PR: https://github.com/emberjs/rfcs/pull/340
  • Ember Issue: (leave this empty)

Deprecate Ember.merge in favor of Ember.assign

Summary

The goal of this RFC is to remove Ember.merge in favor of using Ember.assign.

Motivation

Ember.assign has been around quite awhile, and has the same functionality as Ember.merge. With that in mind, we should remove the old Ember.merge, in favor of just having a single function.

Detailed design

Ember will start logging deprecation messages that tell you to use Ember.assign instead of Ember.merge.

The exact deprecation message will be decided later, but something along the lines of:

Using `Ember.merge` is deprecated. Please use `Ember.assign` instead. If you are using a version of
Ember <= 2.4 you can use [ember-assign-polyfill](https://github.com/shipshapecode/ember-assign-polyfill) to make `Ember.assign`
available to you.

How we teach this

This should be a simple 1 to 1 conversion, and the deprecation message should be clear enough for all to understand what they need to do, and convert all usages of Ember.merge to Ember.assign.

Deprecation Guide

An entry to the Deprecation Guides will be added outlining the conversion from Ember.merge to Ember.assign.

Ember.merge predates Ember.assign, but since Ember.assign has been released, Ember.merge has been mostly unnecessary. To cut down on duplication, we are now recommending using Ember.assign instead of Ember.merge. If you are using a version of Ember <= 2.4 you can use ember-assign-polyfill to make Ember.assign available to you.

Before:

import { merge } from '@ember/polyfills';

var a = { first: 'Yehuda' };
var b = { last: 'Katz' };
merge(a, b); // a == { first: 'Yehuda', last: 'Katz' }, b == { last: 'Katz' }

After:

import { assign } from '@ember/polyfills';

var a = { first: 'Yehuda' };
var b = { last: 'Katz' };
assign(a, b); // a == { first: 'Yehuda', last: 'Katz' }, b == { last: 'Katz' }

Codemod

A codemod will be provided to allow automatic conversion of Ember.merge to Ember.assign.

Drawbacks

The only drawback, that I can think of, is people would need to convert Ember.merge to Ember.assign, but this would be a very easy change and could easily be done via codemod.

Alternatives

The impact of not doing this, is we continue to have two functions that do basically the same thing, which we need to maintain.

Another alternative, could be to remove both Ember.merge and Ember.assign, in favor of Object.assign or something similar.

Unresolved questions

None, that I can think of.

  • Start Date: 07/11/2018
  • RFC PR: https://github.com/emberjs/rfcs/pull/345
  • Ember Issue: (leave this empty)

RFC to move the Ember community chat to Discord

Summary

Encourage the Ember community to adopt Discord for real-time chat (vs Slack or other options).

Motivation

Real-time chat is essential to the function of online communities, particularly in open source. Chat fosters an informal, interactive style of communication that is important for building relationships, sharing community norms, coordinating on projects, and brainstorming ideas.

The Ember community predominantly uses a Slack instance as the gathering place of choice. While we have benefited enormously from Slack, there are significant downsides as well.

Loss of History

Because we use Slack's free plan, the entire instance is limited to 10,000 messages in history at any time. Because of this hard cap, the amount of time messages persist continues to shrink as the community grows.

It's hard to quantify exactly how painful this limitation is, as it means that new community members can't search for the answer to a question that was likely answered in the past. We can never go back to reference how or when a decision was made, which can mean decision-making feels less transparent that it should be.

This limit applies not just to chat messages, but direct messages between community members as well. This leads to annoyance, as people have to ask for the same information over again if they forgot to save it, or data loss, as useful things like code snippets vanish into the ether.

Performance

The architecture of the Slack native application relies on running a separate web application per Slack instance the user is signed into. For users who need to be in multiple Slack instances, this can add up to a significant tax on computer resources, particularly as the application starts up.

Once Slack is up and running, most people generally find the performance reasonable, but a solution that offers better startup and runtime performance would be ideal.

Privacy Concerns

Slack is very clear that their target audience is companies, who often have strict compliance rules that they must follow. Unfortunately, those needs are often at odds with concerns about privacy in an open source community.

In particular, Slack recently added a feature called Corporate Export that theoretically allows administrators to export all messages, including private messages, without notifying users.

Now, the odds of this feature being abused are extremely low. It is only available on Slack's Plus plan, which means a malicious actor would need to be granted administrator priveleges, pony up at least $150,000 to upgrade our Slack plan for that month, apply for the the Corporate Export feature, have it granted by Slack, and then perform an export without anyone noticing.

Because the difficulty of exploiting this feature for evil is so remote, it's not a primary concern driving this change. But all things being equal, we prefer a solution that doesn't offer export of private messages at all, so it's never a concern at the back of someone's mind.

Better Communication & Transparency

Out of frustration with Slack's disappearing messages, the Ember.js core team set up a Discord server to evaluate if it might be a better fit for open source communities.

While this was a public Discord server that anyone could sign up for, its existence was not widely publicized because we were unsure if Discord was the right solution.

Over time, the core team and many contributors gravitated towards the Discord, finding that it served our needs better. Because of how valuable the Slack instance is, no one wanted to propose a move to Discord until a plan (like, say, this RFC) could be put in place.

Unfortunately, this state of affairs has had several undesirable outcomes.

First, it has caused many of the most prolific contributors to be less active in Slack. This may give the appearance of stagnation or disinterest, when momentum on Ember has never been higher. It robs lurkers of the ability to become contributors if a good opportunity to help pops up. And it prevents some of the most experienced members of the community from being around to help answer questions they might have an off-hand answer to.

Second, and perhaps worst of all, it undermines the transparency and open governance that we have worked hard to create. Our bar is higher than just making it possible to contribute—we go out of our way to actively welcome and encourage everyone to participate, learn and contribute.

Finally, this is not intended to replace the forum, and that should be made clear. The forum is still the preferred place for asyncronous, threaded conversations where in-depth discussion is desired.

Detailed design

Transition Plan

We will need these things to transition the community smoothly:

  • a period of time when we use both chat platforms during the transition, put the equivalent Discord channel information in the Slack channel topic
  • a clear guide (with illustrations)
  • once all of the setup is complete, the Discord server invites can be distributed.

Note: the current Discord chat will be closed while this RFC is under consideration. If the RFC is accepted, then a detailed implementation plan (mostly role/channel/server setup) & invitation strategy will be carried out.

Initial Setup

Because Discord has fine-grained controls, we will be able to implement categories for chats.

We intend to have the "welcome" channel as the initial channel for everyone who joins the Discord server. This channel will be read-only and will list the rules for the Discord server.

We also intend to have a "setup" channel. This channel will give you a complete guide of how to take advantage of the personalization, privacy and security, and notification controls in Discord.

Verification Level Initially, we will be implementing the "low" verification level, which means users will need to have a verified email on their Discord account. If this proves to be too easy of a target for spammers, we will implement a higher level of verification (levels include amount of time a user has to be a verified member of the server before they can post).

Explicit Content Filter Since this is a public Discord server, we will be setting an explicit content filter- it will scan messages from all members without a role. Email-verified members will be given a community member role to start, and other roles may be added to users over time.

Categories and Channels Community members will then have the option of visiting the "setup" channel and learning more about fine-grained controls, such as:

  • notifications
  • muting a channel
  • muting a category

Because our goal is transparency, all of the channels that exist will be visible in the channel list. A lock icon will display if the user does not have the role necessary to join that channel. (FWIW, the alternative is to not display locked channels at all, which we felt would be less ideal- it is better to know that there are channels where private conversations are necessary and see what they are.)

The following proposed initial category and channel list was chosen based on the current channel needs and evaluation of the channels with the most members on Slack. Additional channels may be requested in the Admin/community-feedback channel.

Category/Channel List:

  • (No Category)
    • welcome (community guidelines are posted here) <--readonly & the server invite puts users in this channel first.
    • setup-profile (how to setup your profile) <--readonly
  • Admin
    • community-feedback (questions, comments, concerns, requests)
    • security
    • steering-committee 🔒 (locked to role “steering-committee”)
    • news & announcements
    • ember-jobs
    • bots
  • Core Teams
    • ember-js 🔒 (locked to role “core-js”)
    • ember-data 🔒 (locked to role “core-data”)
    • ember-cli 🔒 (locked to role “core-cli”)
    • ember-learning 🔒 (locked to role “core-learning”)
  • Working on Ember
    • ember-cli
    • ember-data
    • ember-engines
    • ember-js
    • glimmer-vm
    • triage
    • st-* (as needed)
  • Using Ember
    • general-help
    • learning-ember
    • a11y
    • backend
    • internationalization
    • jsonapi
    • mobile
    • ember-js
    • ember-data
    • ember-cli
    • ember-engines
    • fastboot
    • ember-twiddle
    • e-*
  • Supporting Ember
    • documentation
    • website
    • marketing-and-advocacy
    • infrastructure
  • Event-Chat
    • EmberConf
    • EmberCamps
    • EmberFest
    • Talks
    • Other Conferences
    • Meetup organizers
  • Social
    • Water-cooler (random)
    • Local-*
    • Media (livestreams, videos, podcasts)
    • Pets
    • Women in Ember 🔒

Integrations Discord's integration game is strong. Discord has a very detailed API and many integrations already exist, and with no limitation (compared to free Slack instances, that have limited numbers of integrations).

How do we teach this?

In addition to having a setup channel available upon login (with illustrated instructions), here are some links where community members can read more:

Drawbacks

Supporting Learning vs Supporting Development

There is some concern that there is already some confusion on Slack about where to get help learning/using Ember, and where to coordinate working on Ember. We need to have a clear delineation so that the folks who are spending their volunteer time to ship Ember features can continue to concentrate and do that.

Losing Community Members

There is some concern that we may lose some community members due to this move. This could happen for a variety of reasons- the nature of OSS work means that some are not always active on the chat community, or the user doesn't want a different chat app, etc. We believe that the former is probably more likely than the latter, since many of us are on at least 2-3 chat apps already.

Alternatives

The alternative to this would be to temporarily remain on Slack until we are able to evaluate and choose another viable option. However, we believe that staying on Slack is not desirable.

List of Slack alternatives:

  • riot.io
  • mattermost.org
  • rocket.chat
  • spectrum.chat

Unresolved questions & FAQ

  • When will there be conversation threads? We have been told that it is in the works, but there is no ETA.
  • Disqus, Discord, Discuss? Which is which? For clarity, we will encourage the use of the terms chat (Discord), the forums (Discuss), and blog comments (Disqus)- mostly so no one has to try to remember.
  • Start Date: 2018-08-24
  • RFC PR: https://github.com/emberjs/rfcs/pull/364
  • Tracking: https://github.com/emberjs/rfc-tracking/issues/28

Ember 2018 Roadmap RFC

Summary

This RFC sets the Ember 2018 Roadmap. This year’s goals are to:

  • Improve communication and streamline decision-making, and empower new leaders.
  • Finish the major initiatives that we’ve already started.
  • Ship a new edition, Ember Octane, focused on performance and productivity.

Motivation

This document is a distillation of multiple sources:

  1. The 2018 Community Survey.
  2. Community #EmberJS2018 blog posts, authored in response to our call for posts.
  3. Discussion on https://discuss.emberjs.com
  4. Deliberations among the Ember core teams.

The goal of the RFC is to align the Ember community around a set of shared, achievable goals that balance the needs of existing users with the need to grow and support new use cases.

Detailed design

This year is primarily about finishing initiatives that we’ve already started, fine-tuning our communication channels, and getting the world excited about Ember.

  • Improve communication and streamline decision-making. We will expand and refine the core team structure, to ensure decisions are made quickly, communication is clear, and users feel empowered to become contributors. We will invest in mentoring new leaders, and cross-pollinating knowledge between teams. As a community, we will share our excitement about Ember with the wider web development world.
  • Finish what we started. We need to focus on stabilizing and polishing the work that we’ve already started in 2018. We will add extension points to allow popular new tools to be quickly adopted in Ember apps. We will standardize around ES modules and npm packages, better enabling the sharing of Ember tools with the wider JavaScript community.
  • Ship Ember Octane. We will ship a new edition of Ember, emphasizing its modern productivity and performance. We will polish our compatibility with new JavaScript language features like native classes, decorators, and async functions. We will continue efforts like optional jQuery and treeshaking that reduce file size. We will overhaul the Ember homepage to align with Octane and tell the story of modern Ember.

To help us deliver a polished, cohesive experience, we will focus on two end-to-end, real world use cases. Having concrete use cases in mind helps us improve our marketing as well as prioritize feature development. In 2018, our two use cases are:

  • Productivity apps. Ember’s historical strength: sophisticated, highly interactive apps that users spend a lot of time in, getting things done.
  • Content apps, where pages are text-heavy and where the first load is critical. In performance-constrained environments, Ember’s strong conventions can help developers build faster apps by default.

Improve communication and streamline decision-making

Silence is the only thing that cause developers to lose trust in Ember. And overcommunication is the cure to silence. —Ryan Toronto and Sam Selikoff

Technical leadership seems to me to be about 10% technical brilliance and 90% clear communication. We have loads of technical brilliance; we need more communication! —Chris Krycho

Communication is well, not stellar. Newsletters do a great job at communicating what already happened, but future plans are largely unknown to public. —V. Lascik

The Core Team is in a unique position to add external-facing commentary on the framework's vision. Our RFC process and release posts are awesome, and they have done great things internally, so I would like to encourage Core to look outwards next. —Jen Weber

My hope is that Ember will continue to be an investment worth making. I see a growing, diverse community with lots of fresh faces as an essential part of that. —Matt McManus

Finding how and where I can help feels scattered. Issues do not receive effective labeling. This has translated into me not contributing to varying projects. —Eli Flanagan

What I’d wish for Ember’s 2018 Roadmap though is to find ways to lower the entry barriers for newcomers to get started in their attempt to advocate Ember and to be creative on how to encourage a sense of empowerment in the wider community regarding outreach efforts. —Jessica Jordan

My hope is that we will continue to hand off the baton of the community values to developers who are new to Ember. —Bill Heaton

A good idea would be to continue creating quests for small things like documentation, code-cleaning… And maybe add a place where this quest can be found —Benjamin Jegard

There has never been more time and energy going into Ember, but we’ve heard loud and clear that this momentum is not as visible as it needs to be. We are going to prioritize sharing work as it happens, making planning and status updates more discoverable, and making it easier for would-be contributors to get involved.

We also need to double down on making Ember as friendly and inclusive as possible, particularly for folks who have never participated in an open source project before. As we bring in new community members, we will make changes to ensure that individuals can have a meaningful impact, no matter what time zone they live in.

Lastly, we need to make sure that our core teams are not so bogged down that they become a bottleneck for decision-making. Core teams and strike teams decentralize planning, empower new contributors to take ownership of community initiatives, and help to build and strengthen relationships among community members. We will invest in improving the organization and structure of these teams this year.

To accomplish these goals, this year we will:

  • Expand and refine our team structure, breaking up work and delegating it to strike teams or new core teams as appropriate.
  • Move to discoverable communication tools, such as our Discourse forum, which is visible to search engines, and Discord chat, which doesn't lose history.
  • Invest in mentoring. This includes direct mentorship relationships, as well as written guides like quest issues that are helpful even for people in different time zones or who have difficulty with spoken English.
  • Track RFC implementation via GitHub issues, so it's clear what the next steps are after an RFC is merged.
  • Automate communication and status updates. For example, we will improve the Statusboard to automatically pull from RFCs and RFC tracking issues.
  • Document “best practices” for core teams, spreading knowledge about what works and what doesn’t for building an active community.
  • Unify the RFC process to ensure a consistent experience across all of Ember's sub-projects.

Finish what we started

The last few years have seen the Ember team do a lot of really important exploratory work, including projects like Glimmer.js; and we have landed some of the initiatives we have started. But I think it’s fair to say that focus has not been our strong suit. It’s time for a year of shipping.—Chris Krycho

I think the goal of being able to just npm install or yarn install any package and having it "just work" should be high on the TODO list. —Andrew Callahan

When Yehuda Katz closed that RFC, I think a bit of that dream died, but at the same time I was happy. Not because it wasn't going to happen but because there was clear communication, finally. —Ilya Radchenko

I firmly believe that Ember needs to deliver all the great new features that are currently in flight before taking more to its plate. —Josemar Luedke

This year, we need a strong focus on shipping. Huge improvements to Ember have either already landed or are in the pipeline. We need to cross the finish line on these before moving on to new initiatives, however important or exciting they might seem.

“Done” doesn’t mean behind a feature flag on canary. Finishing what we started means ensuring that features are discoverable, on by default, and that the guides and other documentation have been revised to take them into account. It means making sure they work well with the entire Ember ecosystem so that new developers get a seamless experience.

This year, we are going to ship:

  • Broccoli 2.0 in Ember CLI, as well as significant investment into Broccoli documentation, marketing and advocacy.
  • Module Unification as the default file system layout.
  • Glimmer Components as the default component API.
  • Native JavaScript classes as the default object model.
  • Native JavaScript modules, including:
    • Exposing modules in the build pipeline and allowing addons to integrate tools like Parcel, Rollup or Webpack.
    • Publishing Ember as npm packages.
    • Importing npm packages into your Ember apps with zero additional configuration. (This was, far and away, the most-mentioned feature request in all of the #EmberJS2018 blog posts.)

Ember Octane

The homepage looks a bit outdated and does not a very compelling job at selling Ember to new users, IMHO. This needs to change. —Simon Ihmig

When you generate a project with ember new, you get a project that is almost “legacy” by standards of the wider JavaScript community. —Gaurav Munjal

Ember's custom object model isn't hard to learn, but it's a big reason people are turned off before learning why Ember is such a great choce. I'd like to see ES classes support finished and adopted in the Guides ASAP, followed by decorators. —Michael Kaiser-Nyman

ES6 syntax, the new file layout, new templating etc. — the new features will land in 3.x releases as non-breaking changes, but let’s prepare to show off the sum of all those amazing parts. Sell the vision, right now! A ‘relaunch’ of Ember in the minds of those who dismiss it. —Will Viles

Ember releases a new, stable version every six weeks. For existing users, this drumbeat of incremental improvement is easier to keep up with than splashy, big-bang releases.

However, for people not following Ember closely, it’s easy to miss the significant improvements that happen over time. As detailed in the forthcoming Ember Editions RFC (being worked on by Dave Wasmer), every year or so we will release a new edition of Ember, focused on a particular theme. The set of improvements related to that theme, taken together, mark a meaningful change to how people should think about Ember.

In 2018, we will release the first edition of Ember, called Ember Octane. Octane will focus on the themes of productivity and performance. We’ll talk about how Ember excels in performance-constrained environments, particularly on mobile devices, as well as the productivity benefits of modern JavaScript features like classes, decorators, and async functions when paired with Ember’s strong conventions and community.

This is also a good time for us to review the new application blueprint, to ensure that it is up-to-date with the latest Ember Octane idioms and includes the right set of addons to help new users achieve our goals of productivity and performance.

Ember Octane is about doing more with less. Not only does this make Ember simpler to learn, it makes the framework smaller and faster, too. These are some of the highlights of Ember Octane:

  • No jQuery. Currently available as an optional feature, we will enable this by default.
  • Svelte builds, where deprecated features are stripped out of framework code. We will get more aggressive about deprecating code that is not widely used.
  • Native JavaScript classes perform better and require less code, and integrate better with tools like TypeScript and ESLint.
  • Glimmer components offer a greatly simplified API and remove common slow paths.
  • Incremental rendering and rehydration that keeps even low-end devices responsive as the application boots.
  • Treeshaking to automatically remove code unused by the application.
  • Eliminating the runloop from the programming model, replaced by async and await in tests.
  • Stabilizing Ember Data by streamlining internals and providing more extension points for applications and addons to customize behavior.

The final timeline and feature set of Ember Octane will be determined by the core teams and are not set in stone in this RFC.

In keeping with our commitment to finishing what we’ve started, these are all features that are either finished or being implemented now. We should not plan for Octane to have any features that are not already close to being done today, so that we have adequate time to make sure they all work well together as part a cohesive programming model.

The process of releasing a new edition also gives us an opportunity to evaluate what it’s like to use Ember end-to-end. We will overhaul the Ember homepage, focusing on Ember Octane and how it helps solve targeted use cases.

This is also a good time to perform a holistic review of the guides, making sure that examples use the latest idioms and set new learners on a good path.

Non-goals

One of our most important goals this year is to focus on shipping. Focus means saying “no” to ideas that we really like.

  • Significant work on Glimmer.js. We will instead focus on our efforts on incorporating the lessons of Glimmer.js into work that enables a smaller core in Ember.
  • Further Glimmer VM optimizations. Glimmer performance is industry leading and not a bottleneck in most Ember.js apps. At this point, the Ember.js payload is the primary performance bottleneck, and we should turn our attention to enabling better performance there.
  • Brand new language features in either Handlebars templates or Ember’s JavaScript files. There is already a full pipeline of features, such as Glimmer components, JavaScript classes with decorators, and module unification that we need to finish before starting any new major design.
  • Start Date: 2018-08-30
  • Relevant Team(s): Ember.js
  • RFC PR: https://github.com/emberjs/rfcs/pull/369
  • Tracking: https://github.com/emberjs/rfc-tracking/issues/18

Summary

Deprecate computed overridability and computed().readOnly() in favor of read-only computeds as the default.

Motivation

Computed properties have existed in Ember long before class syntax and native accessors (getters and setters) were readily available, and as such they have a few notable behavioral differences. As we move toward adopting native class syntax and using a decorator-based form of computeds, it makes sense to reconcile these differences so that users can expect them to work the same as their native counterparts.

The main and most notable difference this RFC seeks to deprecate is computed overridability (colloquially known as "clobbering"). There are some other notable differences, including the caching behavior of the return value of setter functions, which may be addressed in future RFCs.

Overridability

When defining a native getter without a setter, attempting to set the value will throw a hard error (in strict mode):

function makeFoo() {
  'use strict';

  class Foo {
    get bar() {
      return this._value;
    }
  }

  let foo = new Foo();

  foo.bar; // undefined
  foo.bar = 'baz'; // throws an error in strict mode
}

By constrast, computed properties without setters will be overridden when they are set, meaning the computed property is removed from the object and replaced with the set value:

const Foo = EmberObject.extend({
  bar: computed('_value', {
    get() {
      return this._value;
    },
  }),
});

let foo = Foo.create();

foo.bar; // undefined
foo.set('bar', 'baz'); // Overwrites the getter
foo.bar; // 'baz'
foo.set('_value', 123);
foo.bar; // 'baz'

This behavior is confusing to newcomers, and oftentimes unexpected. Common best practice is to opt-out of it by declaring the property as readOnly, which prevents this overridability.

Transition Path

This RFC proposes that readOnly properties become the default, and that in order to override users must opt in by defining their own setters:

class Foo {
  get bar() {
    if (this._bar) {
      return this._bar;
    }

    return this._value
  }

  set bar(value) {
    this._bar = value
  }
}

Macros

Most computed macros are overridable by default, the exception being readOnly. This RFC proposes that all computed macros with the exception of reads would become read only by default. The purpose of reads is to be overridable, so its behavior would remain the same.

Decorator Interop

It may be somewhat cumbersome to write overriding functionality or add proxy properties when overriding is needed. In an ideal world, computed properties would modify accessors transparently so that they could be composed with other decorators, such as an @overridable decorator:

class Foo {
  @overridable
  @computed('_value')
  get bar() {
    return this._value;
  }

  @overridable
  @and('baz', 'qux')
  quux;
}

Currently this is not possible as computed properties store their getter/setter functions elsewhere and replace them with a proxy getter and the mandatory setter assertion, respectively. In the long term, making computeds more transparent in this way would be ideal, but it is out of scope for this RFC.

Deprecation Timeline

This change will be a breaking change, which means we will not be able to change the behavior of computed until Ember v4.0.0. Additionally, users will likely want to continue using .readOnly() up until overriding has been fully removed to ensure they are using properties safely. With that in mind, the ordering of events should be:

  1. Ember v3
    • Deprecate the default override-setter behavior immediately. This means that a deprecation warning will be thrown if a user attempts to set a non-readOnly property which does not have a setter. Users will still be able to declare a property is readOnly without a deprecation warning.
    • Add optional feature to change the deprecation to an assertion after the deprecation has been released, and to show a deprecation when using the .readOnly() modifier.
    • After the deprecation and optional feature have been available for a reasonable amount of time, enable the optional feature by default in new apps and addons. The main reason we want to delay this is to give addons a chance to address deprecations, since enabling this feature will affect both apps and the addons they consume.
  2. Ember v4
    • Remove the override-setter entirely, making non-overrideable properties the default.
    • Make the readOnly modifier a no-op, and show a deprecation warning when it is used.

The warnings should explain the deprecation, and recommend that users do not rely on setter behavior or opting-in to read only behavior.

How We Teach This

In general, we can teach that computed properties are essentially cached native getters/setters (with a few more bells and whistles). Once we have official decorators in the framework, we can make this connection even more solid.

We should add notes on overridability, and we should scrub the guides of any examples that make use of overriding directly and indirectly via .readOnly().

Drawbacks

Overriding is not a completely uncommonly used feature, and developers who have become used to it may feel like it makes their code more complicated, especially without any easy way to opt back in.

Alternatives

We could convert .readOnly() into .overridable(), forcing users to opt-in to overriding. Given the long timeline of this deprecation, it would likely be better to work on making getters/setters transparent to decoration, and provide a @overridable decorator either in Ember or as an independent package.

  • Start Date: 2018-08-31
  • Relevant Team(s): Ember.js
  • RFC PR: https://github.com/emberjs/rfcs/pull/370
  • Tracking: https://github.com/emberjs/rfc-tracking/issues/17

Summary

Deprecate computed().volatile() in favor of undecorated native getters and setters.

Motivation

computed().volatile() is a commonly misunderstood API. On its surface, declaring a computed as volatile causes the computed to recalculate every time it is called. This actually works much like native, undecorated accessors do on classes, with one key difference.

Volatile properties are meant to respresent fundamentally unobservable values. This means that they swallow notification changes, and will not notify under any circumstances, and that when setting a volatile value the user must notify manually:

const Foo = EmberObject.extend({
  bar: computed({
    get() {
      return this._value;
    }

    set(key, value) {
      return this._value = value;
    }
  }).volatile(),

  baz: computed('bar', {
    get() {
      return this.bar;
    }
  }),
});

let foo = Foo.create();

foo.set('bar', 123);
foo.baz; // 123, it's the initial get so nothing cached yet

foo.set('bar', 456);
foo.baz; // 123, no property changes were made so the cache was not cleared

This behavior is useful at times for framework code, but is generally not what users are expecting. By constrast, when using native accessors with set and get, Ember treats them just like any other property. From its perspective, they are standard properties, so it'll continue to notify as expected.

class Foo {
  get bar() {
    return this._value;
  }

  set bar(value) {
    this._value = value;
  }

  @computed('bar')
  get baz() {
    return this.bar;
  }
});

let foo = new Foo();

set(foo, 'bar', 123);
foo.baz; // 123, it's the initial get so nothing cached yet

set(foo, 'bar', 456);
foo.baz; // 456, cache was cleared and value was updated

The most common use case for volatile computeds was when users wanted a computed to behave like a native getter/setter. Now that we (almost) have those in a easy to use form, it makes more sense to deprecate the volatile API and rely directly on native functionality.

Transition Path

Native getters and setters will only work on native classes, due to how the internals of the old object model work. To ensure that users do not accidentally try to replace volatile with getters/setters on non-native classes, we should provide 2 deprecation warnings:

  1. Deprecation when users use volatile on a computed which tells them that the API has been deprecated, and that they'll need to update native class syntax to remove the volatile property.

  2. Deprecation when users use volatile on a computed decorator (to be RFC'd) which tells them to remove the computed decorator entirely from the getter.

Volatile properties will be removed once native classes are the default.

How We Teach This

In general documentation should be updated to use native getters and setters wherever volatile was used. This will have to happen after docs are updated to use native classes, because native getters and setters do not work with the older object model.

Drawbacks

Volatility is useful for framework level concerns, for instance if developing an API or decorator that already handles notification. Addon authors may be able to use this functionality.

Not having an alternative for old style classes or mixins could be problematic for users who aren't ready to update to native class syntax.

Alternatives

We could keep volatile() around for any potential addons that may want to use it, but teach native getters/setters as the preferred path for most use cases.

We could provide volatile as a separate API/decorator to distinguish it from computed properties, and discourage use for users.

  • Start Date: 2018-09-06
  • Relevant Team(s): Ember Data
  • RFC PR: https://github.com/emberjs/rfcs/pull/372
  • Tracking: https://github.com/emberjs/rfc-tracking/issues/16

ember-data | modelFactoryFor

Summary

Promote the private store._modelFactoryFor to public API as store.modelFactoryFor.

Motivation

This RFC is a follow-up RFC for #293 RecordData.

Ember differentiates between klass and factory for classes registered with the container. At times, ember-data needs the klass, at other times, it needs the factory. For this reason, ember-data has carried two APIs for accessing one or the other for some time. The public modelFor provides access to the klass where schema information is stored, while the private _modelFactoryFor provides access to the factory for instantiation.

We provide access to the class with modelFor roughly implemented as store._modelFactoryFor(modelName).klass. We instantiate records from this class roughly implemented as store._modelFactoryFor(modelName).create({ ...args }).

For symmetry, both of these APIs should be public. Making modelFactoryFor public would provide a hook that consumers can override should they desire to provide a custom ModelClass as an alternative to DS.Model.

Detailed design

Due to previous complexity in the lookup of models in ember-data, we previously had both modelFactoryFor and _modelFactoryFor. Despite the naming, both of these methods were private. During a recent cleanup phase, we unified the methods into _modelFactoryFor and left a deprecation in modelFactoryFor. This RFC proposes un-deprecating the modelFactoryFor method and making it public, while deprecating the private _modelFactoryFor.

More precisely:

  • store._modelFactoryFor becomes deprecated and calls store.modelFactoryFor.
  • store.modelFactoryFor becomes un-deprecated.

The contract for modelFactoryFor

The return value of modelFactoryFor MUST be the result of a call to applicationInstance.factoryFor where applicationInstance is the owner returned by using getOwner(this) to access the owner of the store instance.

interface Klass {}

interface Factory {
  klass: Klass,
  create(): Klass
}

interface FactoryMap {
    [factoryName: string]: Factory
}

declare function factoryFor<K extends keyof FactoryMap>(factoryName: K): FactoryMap[K];

interface Store {
  modelFactoryFor(modelName: string): ReturnType<typeof factoryFor>;
}

Users interested in providing a custom class for their records and who override modelFactoryFor, would not need to also change modelFor, as this would be the klass accessible via the factory.

Users wishing to extend the behavior of modelFactoryFor could do so in the following manner:

Example 1:

services/store.js

import { getOwner } from '@ember/application';
import Store from 'ember-data/store';

export default Store.extend({
  modelFactoryFor(modelName) {
    if (someCustomCondition) {
      return getOwner(this).factoryFor(someFactoryName);
    }
    
    return this._super(modelName);
  }
});

Model.modelName

ember-data currently sets modelName onto the klass accessible via the factory. For classes that do not inherit from DS.Model this would not be done, although end users may do so themselves in their implementations if so desired.

What is a valid factory?

The default export of a custom ModelClass MUST conform to the requirements of Ember.factoryFor. The requirements of factoryFor are currently underspecified; however, in practice, this means that the default export is an instantiable class with a static create method and an instance destroy method or that inherits from EmberObject (which provides such methods).

Example 2:

import { assign } from '@ember/polyfills';

export default class CustomModel {
  constructor(createArgs) {
    assign(this, createArgs);
  }
  destroy() {
    // ... do teardown
  }
  static create(createArgs) {
    return new this(createArgs);
  }
}

Example 3:

import EmberObject from '@ember/object';

export default class CustomModel extends EmberObject {
  constructor(createArgs) {
    super(createArgs);
  }
}

Custom classes for models should expect their constructor to receive a single argument: an object with at least the following.

  • A recordData instance accessible via getRecordData (see below)
  • Any properties passed as the second arg to createRecord
  • An owner accessible via Ember.getOwner
  • Any DI injections
  • any other properties that Ember chooses to pass to a class instantiated via factory.create (currently none)

getRecordData

Every record (instance of the class returned by modelFactoryFor) will have an associated RecordData which contains the backing data for the id, type, attributes and relationships of that record.

This backing data can be accessed by using the getRecordData util on the record (or on the createArgs passed to a record). Using getRecordData on a record is only guaranteed after the record has been instantiated. During instantiation, this call should be made on the createArgs object passed into the record.

Example 4

import { getRecordData } from 'ember-data';

export default class CustomModel {
  constructor(createArgs) {
    // during instantiation, `recordData` is available by calling `getRecordData` on createArgs
    let recordData = getRecordData(createArgs);
  }
  someMethod() {
    // post instantiation, `recordData` is available by calling `getRecordData` on the instance
    let recordData = getRecordData(this);
  }
  destroy() {
    // ... do teardown
  }
  static create(createArgs) {
    return new this(createArgs);
  }
}

How we teach this

This API would be intended for addon-authors and power users. It is not expected that most apps would implement custom models, much as it is not expected that most apps would implement custom RecordData. The teaching story would be limited to documenting the nature and purpose of modelFactoryFor.

Drawbacks

  • Users may try to use the hook to instantiate records on their own. Ultimately, the store should still do the instantiating.

Alternatives

Users could define models in models/*.js that utilize a custom ModelClass. However, such an API for custom classes would exclude the ability to dynamically generate classes.

Unresolved questions

None

  • Start Date: 2018-09-10
  • RFC PR: https://github.com/emberjs/rfcs/pull/373
  • Ember Issue: (leave this empty)

Element Modifier Manager

Summary

This RFC proposes a low-level primitive for defining element modifiers. It is a parent to the Modifiers RFC.

Motivation

Ever since Ember 1.0 we have had the concept of element modifiers, however Ember only exposes one modifier; {{action}}. We also do not provide a mechanism for defining your own modifiers and managing their life cycles.

As pointed out in the Element Modifiers RFC we should expose the underlying infrastructure that makes element modifiers possible. Based on our experience, we believe it would be beneficial to open up these new primitives to the wider community. The largest benefit is that it allows the community to experiment with and iterate on APIs outside of the core framework.

This RFC is in the same spirit as the custom components RFC.

Detailed design

This RFC introduces the concept of modifier managers. A modifier manager is an object that is responsible for coordinating the lifecycle events that occurs when invoking, installing and updating an element modifier.

Registering modifier managers

Modifier managers are registered with the modifier-manager type in the application's registry. Similar to services, modifier managers are singleton objects (i.e. { singleton: true, instantiate: true }), meaning that Ember will create and maintain (at most) one instance of each unique modifier manager for every application instance.

To register a modifier manager, an addon will put it inside its app tree:

// ember-basic-component/app/modifier-managers/basic.js

import EmberObject from '@ember/object';

export default EmberObject.extend({
  // ...
});

(Typically, the convention is for addons to define classes like this in its addon tree and then re-export them from the app tree. For brevity, we will just inline them in the app tree directly for the examples in this RFC.)

This allows the modifier manager to participate in the DI system – receiving injections, using services, etc. Alternatively, modifier managers can also be registered with imperative API. This could be useful for testing or opt-ing out of the DI system. For example:

// ember-basic-modifier/app/initializers/register-basic-modifier-manager.js

const MANAGER = {
  // ...
};

export function initialize(application) {
  // We want to use a POJO here, so we are opt-ing out of instantiation
  application.register('modifier-manager:basic', MANAGER, { instantiate: false });
}

export default {
  name: 'register-basic-modifier-manager',
  initialize
};

Determining which modifier manager to use

When invoking the modifier <p {{foo baz bar=bar}} />, Ember will first resolve the modifier class (modifier:foo, usually the default export from app/modifiers/foo.js). Next, it will determine the appropiate modifier manager to use based on the resolved modifier class.

Ember will provide a new API to assign the modifier manager for a element modifier class:

// my-app/app/modifier/foo.js

import EmberObject from '@ember/object';
import { createManager } from './basic-manager';
import { setModifierManager } from '@ember/modifier';

export default setModifierManager(createManager, EmberObject.extend({
  // ...
}));
// my-app/app/modifier/basic-manager.js

// ...

export function createManager(owner) {
  return new BasicManager(owner);
}

setModifierManager takes two parameters. The first parameter is a function that takes an Owner and returns an instance of a manager. The second parameter is the base class that applications would extend from.

In reality, an app developer would never have to write this in their apps, since the modifier manager would already be assigned on a super-class provided by the framework or an addon. The setModifierManager function is essentially a low-level API designed for addon authors and not intended to be used by app developers. Attempting to reassign the modifier manager when one is already assinged on a super-class will be an error. If no modifier manager is set, it will also result in a runtime error when invoking the modifier.

Modifier Lifecycle

Back to the <p {{foo baz bar=bar}}></p> example.

Once Ember has determined the modifier manager to use, it will be used to manage the modifiers's lifecycle.

createModifier

The first step is to create an instance of the modifier. Ember will invoke the modifier manager's createModifier method:

// ember-basic-component/app/modifier-managers/basic.js

import EmberObject from '@ember/object';

export default EmberObject.extend({
  createModifier(factory, args) {
    return factory.create(args);
  },
});

The createModifier method on the modifier manager is responsible for taking the modifier's factory and the arguments passed to the modifier (the ... in {{foo ...}}) and return an instantiated modifier.

The first argument passed to createModifier is the result returned from the factoryFor API. It contains a class property, which gives you the the raw class (the default export from app/modifiers/foo.js) and a create function that can be used to instantiate the class with any registered injections, merging them with any additional properties that are passed.

The second argument is a snapshot of the arguments passed to the modifier in the template invocation, given in the following format:

{
  positional: [ ... ],
  named: { ... }
}

For example, given the following invocation:

<p {{foo baz bar=bar}}></p>

You will get the following as the second argument:

{
  positional: [true],
  named: {
    "bar": "Another RFC by Chad"
  }
}

The arguments object should not be mutated (e.g. args.positional.pop() is no good). In development mode, it might be sealed/frozen to help prevent these kind of mistakes.

This hook has the following timing semantics:

Always

  • called as discovered during DOM construction
  • called in defintion order in template

installModifier

Once the modifier instance has been created, the next step is to install the modifier on to the underlying element.

// ember-basic-component/app/modifier-managers/basic.js

import EmberObject from '@ember/object';

export default EmberObject.extend({
  createModifier(factory, args) {
    return factory.create(args.named);
  },

  installModifier(instance, element, args) {
    instance.element = element;
    if (instance.wasInstalled !== undefined) {
      instance.wasInstalled(args.positional, args.named);
    }
  },

  // ...
});

installModifer is responsible for giving access to the underlying element and arguments to the modifier instance.

The first argument passed to installModifer is the result of createModifier. The second argument is the element the modifier was defined on. The third argument is the same snapshot of the arguments passed to the modifier in the template invocation that createModifier recieved.

This hook has the following timing semantics:

Always

  • called after all children modifier managers installModifer hook are called
  • called after DOM insertion

May or May Not

  • be called in the same tick as DOM insertion
  • have the sibling nodes fully initialized in DOM

updateModifier

Modifiers are only updated when one of its arguments is changed. In this case Ember will call the manager's updateModifier method to give the manager the oppurtunity to reflect those changes on the modifier instance, before re-rendering.

// ember-basic-component/app/modifier-managers/basic.js

import EmberObject from '@ember/object';

export default EmberObject.extend({
  createModifier(factory, args) {
    return factory.create(args.named);
  },

  installModifier(instance, element, args) {
    instance.element = element;
    if (instance.wasInstalled !== undefined) {
      instance.wasInstalled(args.positional, args.named);
    }
  },

  updateModifier(instance, args) {
    if (instance.didUpdateArguments !== undefined) {
      instance.didUpdateArguments(args.positional, args.named);
    }
  }

  // ...
});

updateModifier recieves the modifier instance and also the the updated snapshot of arguments.

This hook has the following timing semantics:

Always

  • called after the arguments to the modifier have changed

Never

  • called if the arguments to the modifier are constants

destroyModifier

destroyModifier will be called when the modifier is no longer needed. This is intended for performing object-model level cleanup.

// ember-basic-component/app/modifier-managers/basic.js

import EmberObject from '@ember/object';

export default EmberObject.extend({
  createModifier(factory, args) {
    return factory.create(args.named);
  },

  installModifier(instance, element, args) {
    instance.element = element;
    if (instance.wasInstalled !== undefined) {
      instance.wasInstalled(args.positional, args.named);
    }
  },

  destroyModifier(instance, args) {
    if (instance.willDestroyDOM !== undefined) {
      instance.willDestroyDOM();
    }
  }
});

This hook has the following timing semantics:

Always

  • called after all children modifier manager's destroyModifier hook is called

May or May Not

  • be called in the same tick as DOM removal

Capabilities

In addition to the methods specified above, modifier managers are required to have a capabilities property. This property must be set to the result of calling the capabilities function provided by Ember.

Versioning

The first, mandatory, argument to the capabilities function is the modifier manager API, which is denoted in the ${major}.${minor} format, matching the minimum Ember version this manager is targeting. For example:

// ember-basic-component/app/component-managers/basic.js

import { capabilities } from '@ember/modifier';
import EmberObject from '@ember/object';

export default EmberObject.extend({
  capabilities: capabilities('3.6'),

  createModifier(factory, args) {
    return factory.create(args.named);
  },

  installModifier(instance, element, args) {
    instance.element = element;
    if (instance.wasInstall