Magic Resolver adds support for named exports to the Ember CLI module system, allowing developers to group related classes into larger modules. It also lets filenames themselves represent additional levels of nesting, obviating the need for subdirectories whose sole purpose is to hold a few template files.
As Ember transitions to a module-based system, the framework will generally enforce a one class per file parity for automatic route-based lookups.
The bundled resolver — responsible for locating classes and templates — is only interested in the default export of each module. Consequently, a single module must directly correspond to a single class, and the module's location in the source tree will determine its location in the route hierarchy.
Magic Resolver provides an alternative for developers who prefer to keep related logic together and avoid a proliferation of tiny files and subdirectories. It does this by making the class lookups aware of named exports and by searching every level of the full route path.
Let's say we have a nested route foo.bar.baz
.
In the past (pre-Ember-CLI), the associated classes would be registered on a global application object under a full name like App.FooBarBazController
.
In the module-based system (introduced with Ember CLI), the standard resolver will look for that same class under the module /controllers/foo/bar/baz.js
and its pod-based equivalent /foo/bar/baz/controller.js
but nowhere else.
By using Magic Resolver, developers can choose to place the class on either of the parent levels (foo
and foo/bar
) as well as the standard location foo/bar/baz
. For example, a dozen subroutes in /routes/users/*.js
could be consolidated into a single module /routes/users.js
.
This approach, therefore, combines the best of both worlds. It preserves the namespacing benefits of the ES6 module system while adding support for arbitrarily large modules.
-
Install addon:
npm install --save-dev ember-magic-resolver
-
In
app.js
, switch to Magic Resolver by declaring:import Resolver from 'magic-resolver';
Classes that are placed above their standard module location are identified by their names. If a class is moved up two levels so that bar/baz
is dropped from the module path, Baz
or BarBaz
would be added to the class name. This compound naming scheme is much like the pre-CLI system, except that you don't use the full path, just the omitted segment.
In more general terms, you can remove a class from a subdirectory, place it in an upper level module, and incorporate the omitted subpath in the name of the export. The class that is the default export of the consolidated module may remain so, or it can be named along with the others.
Modules are searched in a descending order of specificity. The most deeply nested module /controllers/foo/bar/baz
would be looked up first, meaning that Magic Resolver is directly compatible with the bundled resolver and simply extends its lookup logic when nothing is found in the standard location.
/routes/posts/comments.js
export default Ember.Route.extend({ ... }); // posts.comments
/routes/posts/comments/new.js
export default Ember.Route.extend({ ... }); // posts.comments.new
/routes/posts/comments.js
var BaseRoute = Ember.Route.extend({ ... }); // posts.comments
var NewRoute = Ember.Route.extend({ ... }); // posts.comments.new
export {BaseRoute, NewRoute}
Alternatively, if you don't like Base...
, the base name may be prefixed to the class names:
var CommentsRoute = Ember.Route.extend({ ... }); // posts.comments
var CommentsNewRoute = Ember.Route.extend({ ... }); // posts.comments.new
export {CommentsRoute, CommentsNewRoute}
A third option is to leave the base class as a default export and only treat the nested routes as named exports:
export default Ember.Route.extend({ ... }); // posts.comments
var NewRoute = Ember.Route.extend({ ... }); // posts.comments.new
export {NewRoute}
Nesting can extend to an arbitrary level. Continuing with the example, you might do away with the 'posts' directory as well and put everything in a top-level module:
/routes/posts.js
var BaseRoute = Ember.Route.extend({ ... }); // posts
var CommentsRoute = Ember.Route.extend({ ... }); // posts.comments
var CommentsNewRoute = Ember.Route.extend({ ... }); // posts.comments.new
export {BaseRoute, CommentsRoute, CommentsNewRoute}
Again, BaseRoute
may alternatively be exported as PostsRoute
or default
, CommentsRoute
may be named PostsCommentsRoute
, and so on.
The exact same procedure works in a pod structure.
/pods/posts/comments/route.js
export default Ember.Route.extend({ ... }); // posts.comments
/pods/posts/comments/new/route.js
export default Ember.Route.extend({ ... }); // posts.comments.new
/pods/posts/comments/route.js
var BaseRoute = Ember.Route.extend({ ... }); // posts.comments
var NewRoute = Ember.Route.extend({ ... }); // posts.comments.new
export {BaseRoute, NewRoute}
Prefixing the base name:
var CommentsRoute = Ember.Route.extend({ ... }); // posts.comments
var CommentsNewRoute = Ember.Route.extend({ ... }); // posts.comments.new
export {CommentsRoute, CommentsNewRoute}
Using a default export:
export default Ember.Route.extend({ ... }); // posts.comments
var NewRoute = Ember.Route.extend({ ... }); // posts.comments.new
export {NewRoute}
/pods/posts/route.js
var BaseRoute = Ember.Route.extend({ ... }); // or 'PostsRoute' or a default export
var CommentsRoute = Ember.Route.extend({ ... }); // or 'PostsCommentsRoute'
var CommentsNewRoute = Ember.Route.extend({ ... }); // or 'PostsCommentsNewRoute'
export {BaseRoute, CommentsRoute, CommentsNewRoute}
Since multiple templates cannot be combined in a single file, there's an alternative method of storing a template on its parent level in the filesystem: express the path segments in the filename separated by dots.
/templates/posts/comments.hbs
/templates/posts/comments/new.hbs
/templates/posts/comments.hbs
/templates/posts/comments.new.hbs
/templates/posts.hbs
/templates/posts.comments.hbs
/templates/posts.comments.new.hbs
/pods/posts/comments/template.hbs
/pods/posts/comments/new/template.hbs
/pods/posts/comments/template.hbs
/pods/posts/comments/template.new.hbs
Observe that in the pod variant the nested segments appear after "template" in order to keep the filenames grouped together.
/pods/posts/template.hbs
/pods/posts/template.comments.hbs
/pods/posts/template.comments.new.hbs
Magic Resolver does not undo the namespacing capabilities of ES6 modules. When the route one.two.three
is requested, the resolver will look for modules and exports in the following order (export names in brackets):
/{podDir}/one/two/three/route.js [default]
/{podDir}/one/two/three/route.js [BaseRoute]
/{podDir}/one/two/three/route.js [ThreeRoute]
/routes/one/two/three.js [default]
/routes/one/two/three.js [BaseRoute]
/routes/one/two/three.js [ThreeRoute]
/{podDir}/one/two/route.js [ThreeRoute]
/{podDir}/one/two/route.js [TwoThreeRoute]
/routes/one/two.js [ThreeRoute]
/routes/one/two.js [TwoThreeRoute]
/{podDir}/one/route.js [TwoThreeRoute]
/{podDir}/one/route.js [OneTwoThreeRoute]
/routes/one.js [TwoThreeRoute]
/routes/one.js [OneTwoThreeRoute]
A module's path will have to exactly match the beginning portion of the requested route for the module to be included in the search. For example, a class named OneTwoThree
might exist in both /routes/one.js
and /routes/one-two.js
. There is no naming conflict because only one of the modules will be searched, depending on if the resolution request begins with route:one/
or route:one-two/
.
There is a caveat, though. In case your application contains routes ending in "-error" or "-loading" (including the hyphen), beware of using named exports for the resulting classes. Ember implicitly looks for routes with the names "error" and "loading", and as a result a route like foo.bar-loading
may cause a conflict if defined as BarLoading
in module foo
. This is because the class would inadvertently match a lookup for foo.bar.loading
if no actual loading route has been defined under foo/bar
.
To see what's going on, set ENV.APP.LOG_RESOLVER = true
in
{application root}/config/environment.js
.
Interpretation:
[ ] route:foo/bar ....... app/routes/foo/bar did not find module
/routes/foo/bar
[ ] route:foo/bar ....... app/routes/foo ✓ found module /routes/foo
but no matching export
[✓] route:foo/bar ....... app/routes/foo/bar[default] found matching default export
in module /routes/foo/bar
[✓] route:foo/bar ....... app/routes/foo[Bar] found matching export 'Bar'
in module /routes/foo
The addon has been tested on Ember versions 1.7.0–1.9.0 (Ember CLI 0.1.2–0.1.4). However, it is at an experimental stage. If you find a problem, please file an issue.