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.
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 isdeprecation-template.md
. Copy the template file totext/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 Team | Project/Topics |
---|---|
Ember.js | Ember.js |
Ember Data | Ember Data |
Ember CLI | Ember CLI |
Learning | Documentation, Website, learning experiences |
Steering | Governance |
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
-
Find one via 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. -
Ask for a champion via an issue, or in the RFC itself. 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: 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
- Passing the information from the server, which is a poor solution.
- 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 undocumentedkeywords
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.
- 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
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: https://github.com/ember-cli/rfcs/pull/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:
- Allows developers to submit system level information in pull requests, so that bugs can be filed and potentially replicated.
- 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 ofapp.js
- defines theEngine
class and loads its initializers. -
routes.js
instead ofrouter.js
- defines an engine's routing map in aRoutes
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
xorset
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: https://github.com/ember-cli/rfcs/pull/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 asmodel.propName
. - Similarly, computed properties that move to your component will need
to depend on
model.propName
if they are migrated from anObjectController
. - In both cases, the short version is that you can no longer rely on the
proxying behavior of
ObjectController
orArrayController
, but you can remedy the situation by prefixingmodel.
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 aattrs
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 beapp/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: https://github.com/ember-cli/rfcs/pull/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 onfingerprint.prepend
assets - Applications with
fingerprint.prepend
andorigin
specified and matching get aSRI.crossorigin
of anonymous onfingerprint.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
orSRI.crossorigin =
?
Start Date: 2015-08-18 Relevant Team(s): Ember CLI RFC PR: https://github.com/ember-cli/rfcs/pull/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 atype
property those will be accumulated for every cli-command in the cli-command chain and suggested onsome-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.
Start Date: 2014-11-26 RFC PR: https://github.com/emberjs/rfcs/pull/24
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.
- If the element has an SVG namespace, use
setAttribute
. Setting SVG attributes as properties is not supported. - If the attribute name is
style
, usesetAttribute
. - 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 withsetAttribute
.
propertyNameFor
is a normalization setup for attribute names that takes the element
and attribute name as input.
- Build a list of normalized properties for the passed element
normalizedAttrs[element.tagName][elementAttrName.toLowerCase()] = elementAttrName
- Fetch the normalized property name from this list
normalizedAttr = normalizedAttrs[element.tagName][passedAttrName.toLowerCase()]
- Return this normalized attr. If an
attrName
is did not normalize to a property (for exampleclass
), 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: https://github.com/ember-cli/rfcs/pull/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
becomesassets/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 intoassets/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
inapp.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: https://github.com/ember-cli/rfcs/pull/29
Summary
It should be possible to white- and/or blacklist addons in EmberApp
.
Motivation
If there are two (or more) EmberApp
s, 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 likeEmber 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
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
(viajQuery.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: https://github.com/ember-cli/rfcs/pull/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 themaster
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 thanrelease
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 examplev2.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
- Ember's Post-1.0 Release Cycle
- Ember RFC #56 - Improved Release Cycle
- Announcing Ember's First LTS Release
- ember-cli Release Instructions
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 toRegistry#options
registerOptionsForType
- mapped toRegistry#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 currentneeds
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: https://github.com/ember-cli/rfcs/pull/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
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 thesubmit
function off the current scope'sactions.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 themodel.name
value.{{action (mut model.name)}}
tests the passed object for a property with the keyINVOKE
(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 theattrs
.
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 torerender
). - 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 params | has layout (shadow DOM) | can yield template | has lifecycle, instance | can control rerender | |
---|---|---|---|---|---|
components | Yes | Yes | Yes | Yes | Yes |
helpers | Yes | No | No | Yes | Yes |
shorthand helpers | Yes | No | No | No | No |
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 anattr
tomy-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 anattr
tomy-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
orhash
) - 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: https://github.com/ember-cli/rfcs/pull/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
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:
- 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.
- 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:
- 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.
- Ember CLI will support "svelte builds", which strip out deprecated features.
- 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.
- 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 tostore.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
andstore.pushPayload
to take promises - APIs like
record.inverseFor(relationship)
,record.typeFor(relationship)
, etc. - APIs like
record.idsFor(relationship)
,record.metadataFor(relationship)
, andstore.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 (withregisterHelper
) 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
orEmber.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:
- Waits to resolve until the data is fetched from the server, on the initial request.
- 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 RecordArray
s 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
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
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: https://github.com/ember-cli/rfcs/pull/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:
-
Setting the response headers
-
serving the files and ending the response
-
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:
-
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 thatbroccoli-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. -
ember-cli:broccoli:serve-files
: This addon will always run afterember-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: https://github.com/ember-cli/rfcs/pull/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: https://github.com/ember-cli/rfcs/pull/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 callingtreeFor
for the same tree name. TheAddon.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). IfAddon.prototype.cacheKeyForTree
returnsnull
/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 callingmergeTrees([treeA, treeA]);
simply returnstreeA
, andmergeTrees([treeA, treeB, treeA])
removes the duplicatedtreeA
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 frompackage.json
).this.pkg
- This builds a checksum accounting for the addon'spackage.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)
- The addon implements a custom
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 implementtreeFor*
hooks understand how they can properly implementAddon.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: https://github.com/ember-cli/rfcs/pull/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:
- Easily discover which portions of their build are costly;
- Be able to summarize and report build information in an addon;
- 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:
- To make the concept of experiments supported and explicit
- 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 duringinit
summary.platform.name
The value ofprocess.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 thedist
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 ofprocess.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 duringinit
summary.platform.name
The value ofprocess.platform
summary.name
The name of the command that was runsummary.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 duringinit
summary.platform.name
The value ofprocess.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: https://github.com/emberjs/ember.js/pull/12224 / https://github.com/emberjs/ember.js/pull/12990 / https://github.com/emberjs/ember.js/pull/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: https://github.com/ember-cli/rfcs/pull/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: https://github.com/ember-cli/rfcs/pull/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'sEmber.Router
. -
codifying and expanding the supported public API for the
transition
object that is currently passed toRoute
hooks. -
introducing the
get-route-info
template helper -
introducing the
#with-route-info
template keyword -
introducing the
readsRouteInfo
static property onComponent
andHelper
.
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 inember-routing/system/router
and available publicly asEmber.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:
- current route is A
- user initiates a transition to B
- routeWillChange fires
from
Ato
B. - B redirects to C
- routeWillChange fires
from
Ato
C. - routeDidChange fires
from
Ato
C.
Abort example:
- current route is A
- user initiates a transition to B
- routeWillChange fires
from
Ato
B. - in response to the previous routeWillChange event, the transition is aborted.
- routeWillChange fires
from
Ato
A. - routeDidChange fires
from
Ato
A.
Error example:
- current route is A
- user initiates a transition to B.index
- routeWillChange fires
from
Ato
B. - B throws an exception, and the router discovers a "B-error" template.
- routeWillChange fires
from
Ato
B-error - routeDidChange fires
from
Ato
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 nottitle
is the default value ofsortBy
. -
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
'smodel
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 useservice: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:
- 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 therouteInfo
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.
-
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. -
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}}
- Prerendering of non-current routes becomes possible. You can use
recognizeAndLoad
to obtain aRouteInfoWithAttributes
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: https://github.com/ember-cli/rfcs/pull/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
withyarn.lock
present will use Yarnember install ember-cli-mirage
withoutyarn.lock
present will use npmember install ember-cli-mirage --yarn
will use Yarnember 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 npmember 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.AdapterError
s. 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:
- "Ember Data Error"
- Request Method & URI
- Response Status
- Response Content Type
- 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-04-23 Relevant Team(s): Ember CLI RFC PR: https://github.com/ember-cli/rfcs/pull/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: https://github.com/ember-cli/rfcs/pull/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: https://github.com/ember-cli/rfcs/pull/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 fromaddons/
) - 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: https://github.com/ember-cli/rfcs/pull/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
- Move ember-cli-template-lint to the ember-cli org (better for contributing and getting work off one person, @rwjblue)
- Add the dependency to the app blueprint here: https://github.com/ember-cli/ember-cli/blob/master/blueprints/app/files/package.json#L19
- 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: https://github.com/ember-cli/rfcs/pull/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 likeember-mocha
is only a second-class citizen. Since we add it to the defaultpackage.json
file it is easy to opt-out though and can be replaced withchai-jquery
orchai-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 usingqunit-dom
. Another advantage is thatqunit-dom
generates a useful assertion description by default, whileassert.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 detectqunit-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: https://github.com/ember-cli/rfcs/pull/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:
- 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 - Power users - developers who make their own configurations to the build pipeline
- Beginner addon authors - those who are looking to build simple shared UI components, methods, or wrappers for existing npm libraries
- 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 likeember 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
- CLI Commands: Explain how to use the
- 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:
- 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.
- 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.
- 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: https://github.com/emberjs/rfcs/pull/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: https://github.com/ember-cli/rfcs/pull/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
:
- Integration with editors
- Utilize precommit hooks with
eslint
- Support a standard way to run
eslint
(such asyarn lint:js
)
We can also discuss configuring testem
to automatically run eslint
as part
of yarn test
Motivation
- Improve our build speed
- Simplicity.
eslint
is common among JS stack, and integrations with editors / precommit-hooks are ubiquitous. Removing this layer of abstraction will simplify howeslint
is used throughoutember-cli
. Most editors have plugins available foreslint
, and as long as the.eslint.rc
is not removed, we should still see the benefits ofeslint
in our Ember projects. - Hacks required to support features such as PR #122 broccoli-lint-eslint
Detailed design
- Change blueprint to pull in
eslint
as opposed toember-cli-eslint
underdevDependencies
. - 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
- No console warnings during builds
- lint failures are no longer included in browser tests
Alternatives
- 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 anincludes
method onEmber.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 tocontains
, and giving migration examples - Indicate in api docs that this is a polyfill
Drawbacks
- May break existing apps
- Was considered before but was too early
Alternatives
Keep current methods
Unresolved questions
None
Start Date: 2016-04-18 RFC PR: https://github.com/emberjs/rfcs/pull/139 Ember Issue: https://github.com/emberjs/ember.js/pull/13666
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:
- 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.
- No slashes in component names. The existing system allows this, but we don't want to support invocation of nested components in Glimmer Components.
- Addons need to participate in the naming scheme, most likely with namespace prefix and double-colon separator.
- Subtrees should be relocatable. If you move a directory to a new place in the tree, its internal structure should all still work.
- There should be no cognitive overhead when adding a new thing. The right way should be obvious and not impose unnecessary decisions.
- 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.
- Ideally we will have a place to put tests and styles alongside corresponding components.
- Local relative lookup for components and helpers needs to work.
- Avoid the "titlebar problem", in which many files are all named "component.js" and you can't tell them apart in your editor.
- 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.
- Module structures must be statically analyzable at build time to enable efficiency optimizations.
- 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 ofsrc/router.js
- An engine would require a
dummy
app withintests
- An engine should export an
Engine
instead of anApplication
fromsrc/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:
src/${type}
- typed modules namedmain
(explained further below), in which default exports match the type specified by the file name.src/${collection}/${namespace}/${name}/${type}
- expanded collection modules, in which default exports match the type specified by the file name.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:
- Each collection can contain one or more types. The types allowed in a particular collection MUST be explicitly declared.
- Each type MAY exist in any number of collections.
- 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.
- 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.
- 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.
- Top-level collections may be grouped for organization purposes. No resolvable modules must be placed in a group directory.
- 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, templateinitializers
- INITIALIZERinstance-initializers
- INSTANCE-INITIALIZERmodels
- MODEL, ADAPTER, SERIALIZERpartials
- PARTIALroutes
- ROUTE, CONTROLLER, templateservices
- SERVICEtransforms
- TRANSFORMutils
- 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
- componentinitializer
- initializerinstance-initializers
- instance-initializermodels
- modelpartials
- partialroutes
- routeservices
- servicetransforms
- transformutils
- util
The following private collections are allowed within collections (rule 5):
components
- utilsmodels
- utilsinitializers
- utilsinstance-initializers
- utilsroutes
- components, utilsservices
- utilstransforms
- utils
The following groups are proposed for collections (rule 6):
data
- models, transformsinit
- initializers, instance-initializersui
- 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:
- 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.
- 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.
- 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.
- 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 ofpower-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 ananimations
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: https://github.com/emberjs/rfcs/pull/150 Ember Issue: https://github.com/emberjs/ember.js/pull/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:
- ember-cart uses the functionality to create model objects without tying them to the store example a, example b
- Ember-Data's
modelFactoryFor
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.
- 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 besidescreate
orclass
is accessed.class
must return the registered factory, not the double extended factory. - 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). - In 2.13 drop
_lookupFactory
and migrate thefactoryFor
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 acreate
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()
andfactoryFor(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.
- 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
- 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, infactoryFor
that CP cache will be on the class registered as a factory.
- 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
- 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
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:- Increased download time, particularly noticeable on slower connections.
- 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
@
, likeimport 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:
- "Official" packages are clearly differentiated from community packages.
- 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, includingEmber.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 byMap
andWeakMap
in ES2015.@ember/polyfills
- Polyfills forObject.keys
,Object.assign
andObject.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":
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:
- Because they are uncommon, developers may be confused by the syntax.
- 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:
- Package modules in such a way that private modules cannot be accessed.
- 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
Before | After |
---|---|
Ember.$ | import $ from "jquery" |
Ember.A | import { A } from "@ember/array" |
Ember.Application | import Application from "@ember/application" |
Ember.Array | import EmberArray from "@ember/array" |
Ember.ArrayProxy | import ArrayProxy from "@ember/array/proxy" |
Ember.AutoLocation | import AutoLocation from "@ember/routing/auto-location" |
Ember.Checkbox | import Checkbox from "@ember/component/checkbox" |
Ember.Component | import Component from "@ember/component" |
Ember.ContainerDebugAdapter | import ContainerDebugAdapter from "@ember/debug/container-debug-adapter" |
Ember.Controller | import Controller from "@ember/controller" |
Ember.DataAdapter | import DataAdapter from "@ember/debug/data-adapter" |
Ember.DefaultResolver | import GlobalsResolver from "@ember/application/globals-resolver" |
Ember.Enumerable | import Enumerable from "@ember/enumerable" |
Ember.Evented | import Evented from "@ember/object/evented" |
Ember.HashLocation | import HashLocation from "@ember/routing/hash-location" |
Ember.Helper | import Helper from "@ember/component/helper" |
Ember.Helper.helper | import { helper } from "@ember/component/helper" |
Ember.HistoryLocation | import HistoryLocation from "@ember/routing/history-location" |
Ember.LinkComponent | import LinkComponent from "@ember/routing/link-component" |
Ember.Location | import Location from "@ember/routing/location" |
Ember.Map | import EmberMap from "@ember/map" |
Ember.MapWithDefault | import MapWithDefault from "@ember/map/with-default" |
Ember.Mixin | import Mixin from "@ember/object/mixin" |
Ember.MutableArray | import MutableArray from "@ember/array/mutable" |
Ember.NoneLocation | import NoneLocation from "@ember/routing/none-location" |
Ember.Object | import EmberObject from "@ember/object" |
Ember.RSVP | import RSVP from "rsvp" |
Ember.Resolver | import Resolver from "@ember/application/resolver" |
Ember.Route | import Route from "@ember/routing/route" |
Ember.Router | import Router from "@ember/routing/router" |
Ember.Service | import Service from "@ember/service" |
Ember.String.camelize | import { camelize } from "@ember/string" |
Ember.String.capitalize | import { capitalize } from "@ember/string" |
Ember.String.classify | import { classify } from "@ember/string" |
Ember.String.dasherize | import { dasherize } from "@ember/string" |
Ember.String.decamelize | import { decamelize } from "@ember/string" |
Ember.String.fmt | import { fmt } from "@ember/string" |
Ember.String.htmlSafe | import { htmlSafe } from "@ember/string" |
Ember.String.loc | import { loc } from "@ember/string" |
Ember.String.underscore | import { underscore } from "@ember/string" |
Ember.String.w | import { w } from "@ember/string" |
Ember.TextArea | import TextArea from "@ember/component/text-area" |
Ember.TextField | import TextField from "@ember/component/text-field" |
Ember.addListener | import { addListener } from "@ember/object/events" |
Ember.addObserver | import { addObserver } from "@ember/object/observers" |
Ember.aliasMethod | import { aliasMethod } from "@ember/object" |
Ember.assert | import { assert } from "@ember/debug" |
Ember.assign | import { assign } from "@ember/polyfills" |
Ember.cacheFor | import { cacheFor } from "@ember/object/internals" |
Ember.compare | import { compare } from "@ember/utils" |
Ember.computed | import { computed } from "@ember/object" |
Ember.computed.alias | import { alias } from "@ember/object/computed" |
Ember.computed.and | import { and } from "@ember/object/computed" |
Ember.computed.bool | import { bool } from "@ember/object/computed" |
Ember.computed.collect | import { collect } from "@ember/object/computed" |
Ember.computed.deprecatingAlias | import { deprecatingAlias } from "@ember/object/computed" |
Ember.computed.empty | import { empty } from "@ember/object/computed" |
Ember.computed.equal | import { equal } from "@ember/object/computed" |
Ember.computed.filter | import { filter } from "@ember/object/computed" |
Ember.computed.filterBy | import { filterBy } from "@ember/object/computed" |
Ember.computed.filterProperty | import { filterProperty } from "@ember/object/computed" |
Ember.computed.gt | import { gt } from "@ember/object/computed" |
Ember.computed.gte | import { gte } from "@ember/object/computed" |
Ember.computed.intersect | import { intersect } from "@ember/object/computed" |
Ember.computed.lt | import { lt } from "@ember/object/computed" |
Ember.computed.lte | import { lte } from "@ember/object/computed" |
Ember.computed.map | import { map } from "@ember/object/computed" |
Ember.computed.mapBy | import { mapBy } from "@ember/object/computed" |
Ember.computed.mapProperty | import { mapProperty } from "@ember/object/computed" |
Ember.computed.match | import { match } from "@ember/object/computed" |
Ember.computed.max | import { max } from "@ember/object/computed" |
Ember.computed.min | import { min } from "@ember/object/computed" |
Ember.computed.none | import { none } from "@ember/object/computed" |
Ember.computed.not | import { not } from "@ember/object/computed" |
Ember.computed.notEmpty | import { notEmpty } from "@ember/object/computed" |
Ember.computed.oneWay | import { oneWay } from "@ember/object/computed" |
Ember.computed.or | import { or } from "@ember/object/computed" |
Ember.computed.readOnly | import { readOnly } from "@ember/object/computed" |
Ember.computed.reads | import { reads } from "@ember/object/computed" |
Ember.computed.setDiff | import { setDiff } from "@ember/object/computed" |
Ember.computed.sort | import { sort } from "@ember/object/computed" |
Ember.computed.sum | import { sum } from "@ember/object/computed" |
Ember.computed.union | import { union } from "@ember/object/computed" |
Ember.computed.uniq | import { uniq } from "@ember/object/computed" |
Ember.copy | import { copy } from "@ember/object/internals" |
Ember.create | import { create } from "@ember/polyfills" |
Ember.debug | import { debug } from "@ember/debug" |
Ember.deprecate | import { deprecate } from "@ember/application/deprecations" |
Ember.deprecateFunc | import { deprecateFunc } from "@ember/application/deprecations" |
Ember.get | import { get } from "@ember/object" |
Ember.getOwner | import { getOwner } from "@ember/application" |
Ember.getProperties | import { getProperties } from "@ember/object" |
Ember.guidFor | import { guidFor } from "@ember/object/internals" |
Ember.inject.controller | import { inject } from "@ember/controller" |
Ember.inject.service | import { inject } from "@ember/service" |
Ember.inspect | import { inspect } from "@ember/debug" |
Ember.instrument | import { instrument } from "@ember/instrumentation" |
Ember.isArray | import { isArray } from "@ember/array" |
Ember.isBlank | import { isBlank } from "@ember/utils" |
Ember.isEmpty | import { isEmpty } from "@ember/utils" |
Ember.isEqual | import { isEqual } from "@ember/utils" |
Ember.isNone | import { isNone } from "@ember/utils" |
Ember.isPresent | import { isPresent } from "@ember/utils" |
Ember.keys | import { keys } from "@ember/polyfills" |
Ember.makeArray | import { makeArray } from "@ember/array" |
Ember.observer | import { observer } from "@ember/object" |
Ember.on | import { on } from "@ember/object/evented" |
Ember.onLoad | import { onLoad } from "@ember/application" |
Ember.platform.defineProperty | import { defineProperty } from "@ember/polyfills" |
Ember.platform.hasPropertyAccessors | import { hasPropertyAccessors } from "@ember/polyfills" |
Ember.removeListener | import { removeListener } from "@ember/object/events" |
Ember.removeObserver | import { removeObserver } from "@ember/object/observers" |
Ember.reset | import { reset } from "@ember/instrumentation" |
Ember.run | import { run } from "@ember/runloop" |
Ember.run.begin | import { begin } from "@ember/runloop" |
Ember.run.bind | import { bind } from "@ember/runloop" |
Ember.run.cancel | import { cancel } from "@ember/runloop" |
Ember.run.debounce | import { debounce } from "@ember/runloop" |
Ember.run.end | import { end } from "@ember/runloop" |
Ember.run.join | import { join } from "@ember/runloop" |
Ember.run.later | import { later } from "@ember/runloop" |
Ember.run.next | import { next } from "@ember/runloop" |
Ember.run.once | import { once } from "@ember/runloop" |
Ember.run.schedule | import { schedule } from "@ember/runloop" |
Ember.run.scheduleOnce | import { scheduleOnce } from "@ember/runloop" |
Ember.run.throttle | import { throttle } from "@ember/runloop" |
Ember.runInDebug | import { runInDebug } from "@ember/debug" |
Ember.runLoadHooks | import { runLoadHooks } from "@ember/application" |
Ember.sendEvent | import { sendEvent } from "@ember/object/events" |
Ember.set | import { set } from "@ember/object" |
Ember.setOwner | import { setOwner } from "@ember/application" |
Ember.setProperties | import { setProperties } from "@ember/object" |
Ember.subscribe | import { subscribe } from "@ember/instrumentation" |
Ember.tryInvoke | import { tryInvoke } from "@ember/utils" |
Ember.trySet | import { trySet } from "@ember/object" |
Ember.typeOf | import { typeOf } from "@ember/utils" |
Ember.unsubscribe | import { unsubscribe } from "@ember/instrumentation" |
Ember.warn | import { 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
Module | Global |
---|---|
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
Module | Global |
---|---|
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
Module | Global |
---|---|
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
Module | Global |
---|---|
import Controller from "@ember/controller" | Ember.Controller |
import { inject } from "@ember/controller" | Ember.inject.controller |
@ember/debug
Module | Global |
---|---|
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
Module | Global |
---|---|
import Enumerable from "@ember/enumerable" | Ember.Enumerable |
@ember/instrumentation
Module | Global |
---|---|
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
Module | Global |
---|---|
import EmberMap from "@ember/map" | Ember.Map |
import MapWithDefault from "@ember/map/with-default" | Ember.MapWithDefault |
@ember/object
Module | Global |
---|---|
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
Module | Global |
---|---|
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
Module | Global |
---|---|
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
Module | Global |
---|---|
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
Module | Global |
---|---|
import Service from "@ember/service" | Ember.Service |
import { inject } from "@ember/service" | Ember.inject.service |
@ember/string
Module | Global |
---|---|
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
Module | Global |
---|---|
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
Module | Global |
---|---|
import $ from "jquery" | Ember.$ |
rsvp
Module | Global |
---|---|
import RSVP from "rsvp" | Ember.RSVP |
Addendum 3 - Table of Modules with Side Effects
Module | Description |
---|---|
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: https://github.com/emberjs/rfcs/pull/178 Ember Issue: https://github.com/emberjs/ember.js/issues/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
replacesEmber.K
with an empty function. This leads to the most idiomatic and intention-revealing code, but does not allow chaining, like the originalEmber.K
did. Despite of that, chainingEmber.K
was such an uncommon patterns that we thing virtually everybody can use this option.--return-this
replacesEmber.K
with a function that just returnsthis
. 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: https://github.com/emberjs/rfcs/pull/181
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
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: https://github.com/emberjs/rfcs/pull/191 Ember Issue: https://github.com/emberjs/ember.js/pull/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 eventManger
s 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 eventManager
s 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:
-
Ember 3.2 implemented and shipped the component manager API as described in this RFC.
-
The
ember-basic-component
addon released version 1.0 with the component manager shown above (notably, it declaredcapabilities('3.2')
). -
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)
andargs.getNamed('foo')
).However, since Ember sees that the
basic
component manager is written to target the3.2
API version, it will retain the old behavior and passes the old (more expensive) "reified" arguments object instead, to avoid breakage. -
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 tocapabilities('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:
-
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.
-
Expose a way to access to the reference-based APIs. This could include the ability to customize the component's "tag" (validator).
-
Expose additional features that are used to implement classic components,
{{outlet}}
and other built-in components, such as layout customizations, and dynamic scope access. -
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: https://github.com/emberjs/rfcs/pull/225 Ember Issue: https://github.com/emberjs/ember.js/pull/15174
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:
- The current public API is that we use
application
controller to back theapplication.hbs
of a route-less engine. Adding a new conceptual primitive here would be a fairly difficult change without breakage. - 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.
- 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:
- named yields
- multiple yields
- yet another named yields rfc
- ember-block-slots
- ember-named-yields
- local template blocks
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 Componentx-foo
will assign/bindv
to propertyk
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 Componentx-foo
treatsk=v
as assigning/binding arg@k
tov
; it will assign/bindthis.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@arg
s.
- Given
- 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 @arg
s) 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: https://github.com/emberjs/rfcs/pull/229
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: https://github.com/emberjs/rfcs/pull/232
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-helper
ssetContext
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
, andthis.getProperties
to the test context - setup
this.pauseTest
andthis.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 viathis.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
andthis.owner.factoryFor
)this.inject
is removed in favor of usingthis.owner.lookup
directlythis.register
is removed in favor of usingthis.owner.register
directlythis.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
andthis.resumeTest
are being addedthis.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 forbefore
/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 usehooks
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:
- Move
htmlSafe
andisHTMLSafe
to@ember/template
- Update https://github.com/ember-cli/ember-rfc176-data
- Create an
@ember/string
package with the remaining public API - Create an
ember-string-prototype-extensions
package that introducesString
prototype extensions to aid in transitioning - Make
ember-cli-babel
aware of the@ember/string
package so it tellsbabel-plugin-ember-modules-api-polyfill
not to convert those imports to the globalEmber
namespace - Update usages in Ember and Ember Data codebases so that the projects do not trigger deprecations
- Deprecate
Ember.String
- Write deprecation guide which mentions minimum version of
ember-cli-babel
, and how/when to use@ember/string
andember-string-prototype-extensions
packages
- Write deprecation guide which mentions minimum version of
- 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 deprecatedEmber.String
functions are deprecatedloc
is completely deprecatedisHTMLSafe
andhtmlSafe
are moved to@ember/template
- Transition packages are introduced - Ember 2.x
@ember/string
, which replacedEmber.String
ember-string-prototype-extensions
, which bringsString
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
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: https://github.com/emberjs/rfcs/pull/240
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 withEmber.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:
-
Modify CoreObject to use prototypes/ES Classes internally. This would bring CoreObject more inline with ES Classes, but would be a significant internal change.
-
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 thesuper()
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:
- 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.
- 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
Start Date: 2017-09-25 RFC PR: https://github.com/emberjs/rfcs/pull/252
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:
- requestAnimationFrame (caniuse, MDN)
- CSS flexbox (caniuse, MDN)
- Websockets (caniuse, MDN)
- let (caniuse, MDN)
- const (caniuse, MDN)
- TypedArray (caniuse, MDN)
- Geolocation API (caniuse, MDN)
- Online/offline API (caniuse, MDN)
- XHR advanced features (caniuse, specification)
- HTTP2 (caniuse, wikipedia)
- Web Workers (caniuse, MDN)
- IndexedDB (caniuse, MDN)
- WebGL (caniuse, MDN)
- File API (caniuse, MDN)
- PageTransitionEvent (caniuse, MDN)
- SVG filters (caniuse, MDN)
- MutationObserver (caniuse, MDN)
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: https://github.com/emberjs/rfcs/pull/268
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-helper
ssetupContext
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
andthis.resumeTest
methods to allow easy pausing/resuming of tests
- create an owner object and set it on the test context (e.g.
- 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
- setup importable
- 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
- setup a getter for
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 anEmber.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'swait()
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:
-
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. -
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. -
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 athis
lookup) will always beundefined
(ornull
, 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
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: https://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
This RFC proposes rationalizing and extracting ED's core record data handling layer into a RecordData class.
after
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:
-
By shipping a custom RecordData, EmberDataModelFragments can implement a large part of their funcionality without relying on private apis. Spike at model fragments
-
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
-
By using an ES6 class for Record Data implementation, this brings us closer to an Emberless Ember Data running.
-
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
-
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.
-
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.
-
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.
- 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)
}
- 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);
}
- 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:
-
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
-
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.
-
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: https://github.com/emberjs/rfcs/pull/294
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 thatthis.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
inmoduleForComponent
andsetupRenderingTest
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:
- 475 addons using
Ember.$
- 479 addons using
this.$
in components - 994 addons using
this.$
in tests
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.
Start Date: 2018-01-17 RFC PR: https://github.com/emberjs/rfcs/pull/297 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
- deprecate (
ember-routing
(ember-routing\lib\system\router.js
):- transitioned to -
Logger.log
- preparing to transition to -
Logger.log
- intermediate-transitioned to -
Logger.log
- transitioned to -
ember-testing
:- Testing paused (
ember-testing\lib\helpers\pause_test.js
) -Logger.info
- Catch-all handler (
ember-testing\lib\test\adapter.js
) -Logger.error
- Testing paused (
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
-
Leave things as they are, perhaps providing an
@ember/console
module interface. -
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
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:
- Add template lint rule to ember-template-lint as an opt-in rule
- Document the codemod infrastructure and codemod. Make it available for early adopters
- Start updating docs to use
this.
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
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.
Start Date: 2018-03-24 RFC PR: https://github.com/emberjs/rfcs/pull/322
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 thecopy()
function that will log the deprecation before executing,mixins/copyable.js
provides theCopyable
mixin, but it contains no executable code to deprecate.mixins/array.js
- TheNativeArray
mixin extends theCopyable
mixin and implementscopy()
.
The following tests in packages/ember-runtime/tests
use the implementation above:
core/copy_test.js
tests thecopy()
method itself.copyable-array/copy-test.js
tests thecopy()
method of aNativeArray
for identical results.helpers/array.js
provides the arrays used by theNativeArray
test above.system/native_array/copyable_suite_test.js
tests the independence of the results of deep copying aNativeArray
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 useaddon/-private/system/snapshot.js
- two usesaddon/-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 usestests/integration/adapter/rest-adapter-test.js
- two usestests/integration/store-test.js
- two usestests/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 withcopy()
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: 2018-04-18 RFC PR: https://github.com/emberjs/rfcs/pull/326
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 RecordArray
s,
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
andmeta
to a name less likely to collide and which we fully reserve, such asrecordLinks
andrecordMeta
. We felt this would be confusing. -
Enforce accessing
links
andmeta
via some other object such as theReference
API. In addition to being cumbersome and confusing, this would lack discoverability and be unergonomic in templates. -
Enforce accessing
links
andmeta
via some imported helper, e.g.recordMetaFor(record)
orrecordLinksFor(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
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
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: 2019-07-11 RFC PR: https://github.com/emberjs/rfcs/pull/345
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:
- The 2018 Community Survey.
- Community #EmberJS2018 blog posts, authored in response to our call for posts.
- Discussion on https://discuss.emberjs.com
- 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
andawait
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:
- 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 isreadOnly
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.
- Deprecate the default override-setter behavior immediately. This means that
a deprecation warning will be thrown if a user attempts to set a
non-
- 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:
-
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.
-
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 callsstore.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 viagetRecordData
(see below) - Any properties passed as the second arg to
createRecord
- An
owner
accessible viaEmber.getOwner
- Any DI injections
- any other properties that
Ember
chooses to pass to a class instantiated viafactory.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
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.wasInstalled !== undefined) {
instance.wasInstalled(args.positional, args.named);
}
},
destroyModifier(instance, args) {
if (instance.willDestroyDOM !== undefined) {
instance.willDestroyDOM();
}
}
});
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:
-
Ember 3.6 implemented and shipped the modifier manager API as described in this RFC.
-
The
ember-basic-modifier
addon released version 1.0 with the modifier manager shown above (notably, it declaredcapabilities('3.6')
). -
In Ember 3.8, 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)
andargs.getNamed('foo')
).However, since Ember sees that the
basic
modifier manager is written to target the3.6
API version, it will retain the old behavior and passes the old (more expensive) "reified" arguments object instead, to avoid breakage. -
The
ember-basic-modifier
addon author would like to take advantage of this performance optimization, so it updates its modifier manager code to work with the arguments proxy and changes its capabilities declaration tocapabilities('3.8')
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, modifier 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-modifier
1.0 (which targets modifier manager API version 3.6), will continue to work on Ember 3.8. However, the reverse is not true – modifier manager API version 3.8 will (somewhat obviously) not work in Ember 3.6. -
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 modifier 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 modifier manager API using ember-compatibility-helpers:
// ember-basic-modifier/app/modifier-managers/basic.js
import { gte } from 'ember-compatibility-helpers';
let ComponentManager;
if (gte('3.5')) {
ComponentManager = EmberObject.extend({
capabilities: capabilities('3.8'),
// ...
});
} else {
ComponentManager = EmberObject.extend({
capabilities: capabilities('3.6'),
// ...
});
}
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 modifier 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 modifiers. A modifier manager written for Ember 3.8 (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.8', {
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.
At this time this RFC does not specify any optional capabilties for the initial release.
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 subc