ember-source
Version:
A JavaScript framework for creating ambitious web applications
1,366 lines (1,301 loc) • 59.1 kB
JavaScript
import { p as privatize } from '../../shared-chunks/registry-B8WARvkP.js';
import { g as get, I as flushAsyncObservers, b as descriptorForProperty, d as defineProperty, v as addObserver, c as computed } from '../../shared-chunks/cache-DORQczuy.js';
import '../-internals/meta/lib/meta.js';
import { info } from '../debug/index.js';
import '../../@glimmer/validator/index.js';
import { l as lookupDescriptor } from '../../shared-chunks/mandatory-setter-BiXq-dpN.js';
import { isDevelopingApp } from '@embroider/macros';
import { i as isProxy } from '../../shared-chunks/is_proxy-DjvCKvd5.js';
import '../../@glimmer/destroyable/index.js';
import '../../@glimmer/manager/index.js';
import { s as set } from '../../shared-chunks/property_set-4etrFh8A.js';
import { g as getProperties, s as setProperties } from '../../shared-chunks/set_properties-BQFOCF2x.js';
import { E as ENV } from '../../shared-chunks/env-mInZ1DuF.js';
import { getOwner } from '../-internals/owner/index.js';
import EmberObject from '../object/index.js';
import Evented from '../object/evented.js';
import { A } from '../array/index.js';
import '../-internals/runtime/lib/mixins/registry_proxy.js';
import '../-internals/runtime/lib/mixins/container_proxy.js';
import '../-internals/runtime/lib/mixins/comparable.js';
import ActionHandler from '../-internals/runtime/lib/mixins/action_handler.js';
import '../-internals/runtime/lib/mixins/-proxy.js';
import '../enumerable/mutable.js';
import '../-internals/runtime/lib/mixins/target_action_support.js';
import '../-internals/runtime/lib/ext/rsvp.js';
import typeOf from '../utils/lib/type-of.js';
import Controller from '../controller/index.js';
import { deprecateUntil, DEPRECATIONS } from '../-internals/deprecations/index.js';
import EngineInstance from '../engine/instance.js';
import { dependentKeyCompat } from '../object/compat.js';
import { once } from '../runloop/index.js';
import '../../route-recognizer/index.js';
import '../../shared-chunks/rsvp-DaQAFb0W.js';
import { S as STATE_SYMBOL, P as PARAMS_SYMBOL } from '../../shared-chunks/unrecognized-url-error-zpz-JEoG.js';
import generateController from './lib/generate_controller.js';
import { prefixRouteNameArg, stashParamNames, calculateCacheKey, normalizeControllerQueryParams } from './lib/utils.js';
import { a as decorateMethodV2 } from '../../shared-chunks/chunk-3SQBS3Y5-Cj4eryg1.js';
import { assert } from '../debug/lib/assert.js';
import { isTesting } from '../debug/lib/testing.js';
function isStoreLike(store) {
return typeof store === 'object' && store !== null && typeof store.find === 'function';
}
const RENDER = Symbol('render');
const RENDER_STATE = Symbol('render-state');
/**
@module @ember/routing/route
*/
/**
The `Route` class is used to define individual routes. Refer to
the [routing guide](https://guides.emberjs.com/release/routing/) for documentation.
@class Route
@extends EmberObject
@uses ActionHandler
@uses Evented
@since 1.0.0
@public
*/
class Route extends EmberObject.extend(ActionHandler, Evented) {
static isRouteFactory = true;
// These properties will end up appearing in the public interface because we
// `implements IRoute` from `router.js`, which has them as part of *its*
// public contract. We mark them as `@internal` so they at least signal to
// people subclassing `Route` that they should not use them.
/** @internal */
context = {};
/** @internal */
/** @internal */
_bucketCache;
/** @internal */
_internalName;
_names;
_router;
constructor(owner) {
super(owner);
if (owner) {
let router = owner.lookup('router:main');
let bucketCache = owner.lookup(privatize`-bucket-cache:main`);
this._router = router;
this._bucketCache = bucketCache;
this._topLevelViewTemplate = owner.lookup('template:-outlet');
this._environment = owner.lookup('-environment:main');
}
}
/**
A hook you can implement to convert the route's model into parameters
for the URL.
```app/router.js
// ...
Router.map(function() {
this.route('post', { path: '/posts/:post_id' });
});
```
```app/routes/post.js
import Route from '@ember/routing/route';
export default class PostRoute extends Route {
model({ post_id }) {
// the server returns `{ id: 12 }`
return fetch(`/posts/${post_id}`;
}
serialize(model) {
// this will make the URL `/posts/12`
return { post_id: model.id };
}
}
```
The default `serialize` method will insert the model's `id` into the
route's dynamic segment (in this case, `:post_id`) if the segment contains '_id'.
If the route has multiple dynamic segments or does not contain '_id', `serialize`
will return `getProperties(model, params)`
This method is called when `transitionTo` is called with a context
in order to populate the URL.
@method serialize
@param {Object} model the routes model
@param {Array} params an Array of parameter names for the current
route (in the example, `['post_id']`.
@return {Object} the serialized parameters
@since 1.0.0
@public
*/
serialize(model, params) {
if (params.length < 1 || !model) {
return;
}
let object = {};
if (params.length === 1) {
let [name] = params;
(isDevelopingApp() && !(name) && assert('has name', name));
if (typeof model === 'object' && name in model) {
object[name] = get(model, name);
} else if (/_id$/.test(name)) {
object[name] = get(model, 'id');
} else if (isProxy(model)) {
object[name] = get(model, name);
}
} else {
object = getProperties(model, params);
}
return object;
}
/**
Configuration hash for this route's queryParams. The possible
configuration options and their defaults are as follows
(assuming a query param whose controller property is `page`):
```javascript
queryParams = {
page: {
// By default, controller query param properties don't
// cause a full transition when they are changed, but
// rather only cause the URL to update. Setting
// `refreshModel` to true will cause an "in-place"
// transition to occur, whereby the model hooks for
// this route (and any child routes) will re-fire, allowing
// you to reload models (e.g., from the server) using the
// updated query param values.
refreshModel: false,
// By default, changes to controller query param properties
// cause the URL to update via `pushState`, which means an
// item will be added to the browser's history, allowing
// you to use the back button to restore the app to the
// previous state before the query param property was changed.
// Setting `replace` to true will use `replaceState` (or its
// hash location equivalent), which causes no browser history
// item to be added. This options name and default value are
// the same as the `link-to` helper's `replace` option.
replace: false,
// By default, the query param URL key is the same name as
// the controller property name. Use `as` to specify a
// different URL key.
as: 'page'
}
};
```
@property queryParams
@for Route
@type Object
@since 1.6.0
@public
*/
// Set in reopen so it can be overriden with extend
/**
The name of the template to use by default when rendering this route's
template.
```app/routes/posts/list.js
import Route from '@ember/routing/route';
export default class PostsListRoute extends Route {
templateName = 'posts/list';
}
```
```app/routes/posts/index.js
import PostsListRoute from '../posts/list';
export default class PostsIndexRoute extends PostsListRoute {};
```
```app/routes/posts/archived.js
import PostsListRoute from '../posts/list';
export default class PostsArchivedRoute extends PostsListRoute {};
```
@property templateName
@type String
@default null
@since 1.4.0
@public
*/
// Set in reopen so it can be overriden with extend
/**
The name of the controller to associate with this route.
By default, Ember will lookup a route's controller that matches the name
of the route (i.e. `posts.new`). However,
if you would like to define a specific controller to use, you can do so
using this property.
This is useful in many ways, as the controller specified will be:
* passed to the `setupController` method.
* used as the controller for the template being rendered by the route.
* returned from a call to `controllerFor` for the route.
@property controllerName
@type String
@default null
@since 1.4.0
@public
*/
// Set in reopen so it can be overriden with extend
/**
The controller associated with this route.
Example
```app/routes/form.js
import Route from '@ember/routing/route';
import { action } from '@ember/object';
export default class FormRoute extends Route {
@action
willTransition(transition) {
if (this.controller.get('userHasEnteredData') &&
!confirm('Are you sure you want to abandon progress?')) {
transition.abort();
} else {
// Bubble the `willTransition` action so that
// parent routes can decide whether or not to abort.
return true;
}
}
}
```
@property controller
@type Controller
@since 1.6.0
@public
*/
/**
The name of the route, dot-delimited.
For example, a route found at `app/routes/posts/post.js` will have
a `routeName` of `posts.post`.
@property routeName
@for Route
@type String
@since 1.0.0
@public
*/
/**
The name of the route, dot-delimited, including the engine prefix
if applicable.
For example, a route found at `addon/routes/posts/post.js` within an
engine named `admin` will have a `fullRouteName` of `admin.posts.post`.
@property fullRouteName
@for Route
@type String
@since 2.10.0
@public
*/
/**
Sets the name for this route, including a fully resolved name for routes
inside engines.
@private
@method _setRouteName
@param {String} name
*/
_setRouteName(name) {
this.routeName = name;
let owner = getOwner(this);
(isDevelopingApp() && !(owner instanceof EngineInstance) && assert('Expected route to have EngineInstance as owner', owner instanceof EngineInstance));
this.fullRouteName = getEngineRouteName(owner, name);
}
/**
@private
@method _stashNames
*/
_stashNames(routeInfo, dynamicParent) {
if (this._names) {
return;
}
let names = this._names = routeInfo['_names'];
if (!names.length) {
routeInfo = dynamicParent;
names = routeInfo && routeInfo['_names'] || [];
}
// SAFETY: Since `_qp` is protected we can't infer the type
let qps = get(this, '_qp').qps;
let namePaths = new Array(names.length);
for (let a = 0; a < names.length; ++a) {
namePaths[a] = `${routeInfo.name}.${names[a]}`;
}
for (let qp of qps) {
if (qp.scope === 'model') {
qp.parts = namePaths;
}
}
}
/**
@private
@property _activeQPChanged
*/
_activeQPChanged(qp, value) {
this._router._activeQPChanged(qp.scopedPropertyName, value);
}
/**
@private
@method _updatingQPChanged
*/
_updatingQPChanged(qp) {
this._router._updatingQPChanged(qp.urlKey);
}
/**
Returns a hash containing the parameters of an ancestor route.
You may notice that `this.paramsFor` sometimes works when referring to a
child route, but this behavior should not be relied upon as only ancestor
routes are certain to be loaded in time.
Example
```app/router.js
// ...
Router.map(function() {
this.route('member', { path: ':name' }, function() {
this.route('interest', { path: ':interest' });
});
});
```
```app/routes/member.js
import Route from '@ember/routing/route';
export default class MemberRoute extends Route {
queryParams = {
memberQp: { refreshModel: true }
}
}
```
```app/routes/member/interest.js
import Route from '@ember/routing/route';
export default class MemberInterestRoute extends Route {
queryParams = {
interestQp: { refreshModel: true }
}
model() {
return this.paramsFor('member');
}
}
```
If we visit `/turing/maths?memberQp=member&interestQp=interest` the model for
the `member.interest` route is a hash with:
* `name`: `turing`
* `memberQp`: `member`
@method paramsFor
@param {String} name
@return {Object} hash containing the parameters of the route `name`
@since 1.4.0
@public
*/
paramsFor(name) {
let owner = getOwner(this);
(isDevelopingApp() && !(owner) && assert('Route is unexpectedly missing an owner', owner));
let route = owner.lookup(`route:${name}`);
if (route === undefined) {
return {};
}
let transition = this._router._routerMicrolib.activeTransition;
let state = transition ? transition[STATE_SYMBOL] : this._router._routerMicrolib.state;
let fullName = route.fullRouteName;
let params = {
...state.params[fullName]
};
let queryParams = getQueryParamsFor(route, state);
return Object.entries(queryParams).reduce((params, [key, value]) => {
(isDevelopingApp() && !(!params[key]) && assert(`The route '${this.routeName}' has both a dynamic segment and query param with name '${key}'. Please rename one to avoid collisions.`, !params[key]));
params[key] = value;
return params;
}, params);
}
/**
Serializes the query parameter key
@method serializeQueryParamKey
@param {String} controllerPropertyName
@private
*/
serializeQueryParamKey(controllerPropertyName) {
return controllerPropertyName;
}
/**
Serializes value of the query parameter based on defaultValueType
@method serializeQueryParam
@param {Object} value
@param {String} urlKey
@param {String} defaultValueType
@private
*/
serializeQueryParam(value, _urlKey, defaultValueType) {
// urlKey isn't used here, but anyone overriding
// can use it to provide serialization specific
// to a certain query param.
return this._router._serializeQueryParam(value, defaultValueType);
}
/**
Deserializes value of the query parameter based on defaultValueType
@method deserializeQueryParam
@param {Object} value
@param {String} urlKey
@param {String} defaultValueType
@private
*/
deserializeQueryParam(value, _urlKey, defaultValueType) {
// urlKey isn't used here, but anyone overriding
// can use it to provide deserialization specific
// to a certain query param.
return this._router._deserializeQueryParam(value, defaultValueType);
}
/**
@private
@property _optionsForQueryParam
*/
_optionsForQueryParam(qp) {
const queryParams = get(this, 'queryParams');
return get(queryParams, qp.urlKey) || get(queryParams, qp.prop) || queryParams[qp.urlKey] || queryParams[qp.prop] || {};
}
/**
A hook you can use to reset controller values either when the model
changes or the route is exiting.
```app/routes/articles.js
import Route from '@ember/routing/route';
export default class ArticlesRoute extends Route {
resetController(controller, isExiting, transition) {
if (isExiting && transition.targetName !== 'error') {
controller.set('page', 1);
}
}
}
```
@method resetController
@param {Controller} controller instance
@param {Boolean} isExiting
@param {Object} transition
@since 1.7.0
@public
*/
resetController(_controller, _isExiting, _transition) {
// We document that subclasses do not have to return *anything* and in fact
// do not even have to call super, so whiel we *do* return `this`, we need
// to be explicit in the types that our return type is *effectively* `void`.
return this;
}
/**
@private
@method exit
*/
exit(transition) {
this.deactivate(transition);
this.trigger('deactivate', transition);
this.teardownViews();
}
/**
@private
@method _internalReset
@since 3.6.0
*/
_internalReset(isExiting, transition) {
let controller = this.controller;
// SAFETY: Since `_qp` is protected we can't infer the type
controller['_qpDelegate'] = get(this, '_qp').states.inactive;
this.resetController(controller, isExiting, transition);
}
/**
@private
@method enter
*/
enter(transition) {
this[RENDER_STATE] = undefined;
this.activate(transition);
this.trigger('activate', transition);
}
/**
This event is triggered when the router enters the route. It is
not executed when the model for the route changes.
```app/routes/application.js
import { on } from '@ember/object/evented';
import Route from '@ember/routing/route';
export default Route.extend({
collectAnalytics: on('activate', function(){
collectAnalytics();
})
});
```
@event activate
@since 1.9.0
@public
*/
/**
This event is triggered when the router completely exits this
route. It is not executed when the model for the route changes.
```app/routes/index.js
import { on } from '@ember/object/evented';
import Route from '@ember/routing/route';
export default Route.extend({
trackPageLeaveAnalytics: on('deactivate', function(){
trackPageLeaveAnalytics();
})
});
```
@event deactivate
@since 1.9.0
@public
*/
/**
This hook is executed when the router completely exits this route. It is
not executed when the model for the route changes.
@method deactivate
@param {Transition} transition
@since 1.0.0
@public
*/
deactivate(_transition) {}
/**
This hook is executed when the router enters the route. It is not executed
when the model for the route changes.
@method activate
@param {Transition} transition
@since 1.0.0
@public
*/
activate(_transition) {}
/**
Perform a synchronous transition into another route without attempting
to resolve promises, update the URL, or abort any currently active
asynchronous transitions (i.e. regular transitions caused by
`transitionTo` or URL changes).
This method is handy for performing intermediate transitions on the
way to a final destination route, and is called internally by the
default implementations of the `error` and `loading` handlers.
@method intermediateTransitionTo
@param {String} name the name of the route
@param {...Object} models the model(s) to be used while transitioning
to the route.
@since 1.2.0
@public
*/
intermediateTransitionTo(...args) {
let [name, ...preparedArgs] = prefixRouteNameArg(this, args);
this._router.intermediateTransitionTo(name, ...preparedArgs);
}
/**
Refresh the model on this route and any child routes, firing the
`beforeModel`, `model`, and `afterModel` hooks in a similar fashion
to how routes are entered when transitioning in from other route.
The current route params (e.g. `article_id`) will be passed in
to the respective model hooks, and if a different model is returned,
`setupController` and associated route hooks will re-fire as well.
An example usage of this method is re-querying the server for the
latest information using the same parameters as when the route
was first entered.
Note that this will cause `model` hooks to fire even on routes
that were provided a model object when the route was initially
entered.
@method refresh
@return {Transition} the transition object associated with this
attempted transition
@since 1.4.0
@public
*/
refresh() {
return this._router._routerMicrolib.refresh(this);
}
/**
This hook is the entry point for router.js
@private
@method setup
*/
setup(context, transition) {
let controllerName = this.controllerName || this.routeName;
let definedController = this.controllerFor(controllerName, true);
let controller = definedController ?? this.generateController(controllerName);
// SAFETY: Since `_qp` is protected we can't infer the type
let queryParams = get(this, '_qp');
// Assign the route's controller so that it can more easily be
// referenced in action handlers. Side effects. Side effects everywhere.
if (!this.controller) {
let propNames = queryParams.propertyNames;
addQueryParamsObservers(controller, propNames);
this.controller = controller;
}
let states = queryParams.states;
controller._qpDelegate = states.allowOverrides;
if (transition) {
// Update the model dep values used to calculate cache keys.
stashParamNames(this._router, transition[STATE_SYMBOL].routeInfos);
let cache = this._bucketCache;
let params = transition[PARAMS_SYMBOL];
let allParams = queryParams.propertyNames;
allParams.forEach(prop => {
let aQp = queryParams.map[prop];
(isDevelopingApp() && !(aQp) && assert('expected aQp', aQp));
aQp.values = params;
let cacheKey = calculateCacheKey(aQp.route.fullRouteName, aQp.parts, aQp.values);
let value = cache.lookup(cacheKey, prop, aQp.undecoratedDefaultValue);
set(controller, prop, value);
});
let qpValues = getQueryParamsFor(this, transition[STATE_SYMBOL]);
setProperties(controller, qpValues);
}
this.setupController(controller, context, transition);
if (this._environment.options.shouldRender) {
this[RENDER]();
}
// Setup can cause changes to QPs which need to be propogated immediately in
// some situations. Eventually, we should work on making these async somehow.
flushAsyncObservers(false);
}
/*
Called when a query parameter for this route changes, regardless of whether the route
is currently part of the active route hierarchy. This will update the query parameter's
value in the cache so if this route becomes active, the cache value has been updated.
*/
_qpChanged(prop, value, qp) {
if (!qp) {
return;
}
// Update model-dep cache
let cache = this._bucketCache;
let cacheKey = calculateCacheKey(qp.route.fullRouteName, qp.parts, qp.values);
cache.stash(cacheKey, prop, value);
}
/**
This hook is the first of the route entry validation hooks
called when an attempt is made to transition into a route
or one of its children. It is called before `model` and
`afterModel`, and is appropriate for cases when:
1) A decision can be made to redirect elsewhere without
needing to resolve the model first.
2) Any async operations need to occur first before the
model is attempted to be resolved.
This hook is provided the current `transition` attempt
as a parameter, which can be used to `.abort()` the transition,
save it for a later `.retry()`, or retrieve values set
on it from a previous hook. You can also just call
`router.transitionTo` to another route to implicitly
abort the `transition`.
You can return a promise from this hook to pause the
transition until the promise resolves (or rejects). This could
be useful, for instance, for retrieving async code from
the server that is required to enter a route.
@method beforeModel
@param {Transition} transition
@return {any | Promise<any>} if the value returned from this hook is
a promise, the transition will pause until the transition
resolves. Otherwise, non-promise return values are not
utilized in any way.
@since 1.0.0
@public
*/
beforeModel(_transition) {}
/**
This hook is called after this route's model has resolved.
It follows identical async/promise semantics to `beforeModel`
but is provided the route's resolved model in addition to
the `transition`, and is therefore suited to performing
logic that can only take place after the model has already
resolved.
```app/routes/posts.js
import Route from '@ember/routing/route';
import { service } from '@ember/service';
export default class PostsRoute extends Route {
@service router;
afterModel(posts, transition) {
if (posts.get('length') === 1) {
this.router.transitionTo('post.show', posts.get('firstObject'));
}
}
}
```
Refer to documentation for `beforeModel` for a description
of transition-pausing semantics when a promise is returned
from this hook.
@method afterModel
@param {Object} resolvedModel the value returned from `model`,
or its resolved value if it was a promise
@param {Transition} transition
@return {any | Promise<any>} if the value returned from this hook is
a promise, the transition will pause until the transition
resolves. Otherwise, non-promise return values are not
utilized in any way.
@since 1.0.0
@public
*/
afterModel(_resolvedModel, _transition) {}
/**
A hook you can implement to optionally redirect to another route.
Calling `this.router.transitionTo` from inside of the `redirect` hook will
abort the current transition (into the route that has implemented `redirect`).
`redirect` and `afterModel` behave very similarly and are
called almost at the same time, but they have an important
distinction when calling `this.router.transitionTo` to a child route
of the current route. From `afterModel`, this new transition
invalidates the current transition, causing `beforeModel`,
`model`, and `afterModel` hooks to be called again. But the
same transition started from `redirect` does _not_ invalidate
the current transition. In other words, by the time the `redirect`
hook has been called, both the resolved model and the attempted
entry into this route are considered fully validated.
@method redirect
@param {Object} model the model for this route
@param {Transition} transition the transition object associated with the current transition
@since 1.0.0
@public
*/
redirect(_model, _transition) {}
/**
Called when the context is changed by router.js.
@private
@method contextDidChange
*/
contextDidChange() {
this.currentModel = this.context;
}
/**
A hook you can implement to convert the URL into the model for
this route.
```app/router.js
// ...
Router.map(function() {
this.route('post', { path: '/posts/:post_id' });
});
export default Router;
```
Note that for routes with dynamic segments, this hook is not always
executed. If the route is entered through a transition (e.g. when
using the `link-to` Handlebars helper or the `transitionTo` method
of routes), and a model context is already provided this hook
is not called.
A model context does not include a primitive string or number,
which does cause the model hook to be called.
Routes without dynamic segments will always execute the model hook.
```javascript
// no dynamic segment, model hook always called
this.router.transitionTo('posts');
// model passed in, so model hook not called
thePost = store.findRecord('post', 1);
this.router.transitionTo('post', thePost);
// integer passed in, model hook is called
this.router.transitionTo('post', 1);
// model id passed in, model hook is called
// useful for forcing the hook to execute
thePost = store.findRecord('post', 1);
this.router.transitionTo('post', thePost.id);
```
This hook follows the asynchronous/promise semantics
described in the documentation for `beforeModel`. In particular,
if a promise returned from `model` fails, the error will be
handled by the `error` hook on `Route`.
Note that the legacy behavior of automatically defining a model
hook when a dynamic segment ending in `_id` is present is
[deprecated](https://deprecations.emberjs.com/v5.x#toc_deprecate-implicit-route-model).
You should explicitly define a model hook whenever any segments are
present.
Example
```app/routes/post.js
import Route from '@ember/routing/route';
import { service } from '@ember/service';
export default class PostRoute extends Route {
@service store;
model(params) {
return this.store.findRecord('post', params.post_id);
}
}
```
@method model
@param {Object} params the parameters extracted from the URL
@param {Transition} transition
@return {any | Promise<any>} the model for this route. If
a promise is returned, the transition will pause until
the promise resolves, and the resolved value of the promise
will be used as the model for this route.
@since 1.0.0
@public
*/
model(params, transition) {
let name, sawParams, value;
// SAFETY: Since `_qp` is protected we can't infer the type
let queryParams = get(this, '_qp').map;
for (let prop in params) {
if (prop === 'queryParams' || queryParams && prop in queryParams) {
continue;
}
let match = prop.match(/^(.*)_id$/);
if (match !== null) {
name = match[1];
value = params[prop];
}
sawParams = true;
}
if (!name) {
if (sawParams) {
// SAFETY: This should be equivalent
return Object.assign({}, params);
} else {
if (transition.resolveIndex < 1) {
return;
}
// SAFETY: This should be correct, but TS is unable to infer this.
return transition[STATE_SYMBOL].routeInfos[transition.resolveIndex - 1].context;
}
}
return this.findModel(name, value);
}
/**
@private
@method deserialize
@param {Object} params the parameters extracted from the URL
@param {Transition} transition
@return {any | Promise<any>} the model for this route.
Router.js hook.
*/
deserialize(_params, transition) {
return this.model(this._paramsFor(this.routeName, _params), transition);
}
/**
@method findModel
@param {String} type the model type
@param {Object} value the value passed to find
@private
*/
findModel(type, value) {
if (ENV._NO_IMPLICIT_ROUTE_MODEL) {
return;
}
deprecateUntil(`The implicit model loading behavior for routes is deprecated. ` + `Please define an explicit model hook for ${this.fullRouteName}.`, DEPRECATIONS.DEPRECATE_IMPLICIT_ROUTE_MODEL);
const store = 'store' in this ? this.store : get(this, '_store');
(isDevelopingApp() && !(isStoreLike(store)) && assert('Expected route to have a store with a find method', isStoreLike(store))); // SAFETY: We don't actually know it will return this, but this code path is also deprecated.
return store.find(type, value);
}
/**
A hook you can use to setup the controller for the current route.
This method is called with the controller for the current route and the
model supplied by the `model` hook.
By default, the `setupController` hook sets the `model` property of
the controller to the specified `model` when it is not `undefined`.
If you implement the `setupController` hook in your Route, it will
prevent this default behavior. If you want to preserve that behavior
when implementing your `setupController` function, make sure to call
`super`:
```app/routes/photos.js
import Route from '@ember/routing/route';
import { service } from '@ember/service';
export default class PhotosRoute extends Route {
@service store;
model() {
return this.store.findAll('photo');
}
setupController(controller, model) {
super.setupController(controller, model);
this.controllerFor('application').set('showingPhotos', true);
}
}
```
The provided controller will be one resolved based on the name
of this route.
If no explicit controller is defined, Ember will automatically create one.
As an example, consider the router:
```app/router.js
// ...
Router.map(function() {
this.route('post', { path: '/posts/:post_id' });
});
export default Router;
```
If you have defined a file for the post controller,
the framework will use it.
If it is not defined, a basic `Controller` instance would be used.
@example Behavior of a basic Controller
```app/routes/post.js
import Route from '@ember/routing/route';
export default class PostRoute extends Route {
setupController(controller, model) {
controller.set('model', model);
}
});
```
@method setupController
@param {Controller} controller instance
@param {Object} model
@param {Transition} [transition]
@since 1.0.0
@public
*/
setupController(controller, context, _transition) {
if (controller && context !== undefined) {
set(controller, 'model', context);
}
}
/**
Returns the controller of the current route, or a parent (or any ancestor)
route in a route hierarchy.
The controller instance must already have been created, either through entering the
associated route or using `generateController`.
```app/routes/post.js
import Route from '@ember/routing/route';
export default class PostRoute extends Route {
setupController(controller, post) {
super.setupController(controller, post);
this.controllerFor('posts').set('currentPost', post);
}
}
```
@method controllerFor
@param {String} name the name of the route or controller
@return {Controller | undefined}
@since 1.0.0
@public
*/
controllerFor(name, _skipAssert = false) {
let owner = getOwner(this);
(isDevelopingApp() && !(owner) && assert('Route is unexpectedly missing an owner', owner));
let route = owner.lookup(`route:${name}`);
if (route && route.controllerName) {
name = route.controllerName;
}
let controller = owner.lookup(`controller:${name}`);
// NOTE: We're specifically checking that skipAssert is true, because according
// to the old API the second parameter was model. We do not want people who
// passed a model to skip the assertion.
(isDevelopingApp() && !(controller !== undefined || _skipAssert === true) && assert(`The controller named '${name}' could not be found. Make sure that this route exists and has already been entered at least once. If you are accessing a controller not associated with a route, make sure the controller class is explicitly defined.`, controller !== undefined || _skipAssert === true));
(isDevelopingApp() && !(controller === undefined || controller instanceof Controller) && assert(`Expected controller:${name} to be an instance of Controller`, controller === undefined || controller instanceof Controller));
return controller;
}
/**
Generates a controller for a route.
Example
```app/routes/post.js
import Route from '@ember/routing/route';
export default class Post extends Route {
setupController(controller, post) {
super.setupController(controller, post);
this.generateController('posts');
}
}
```
@method generateController
@param {String} name the name of the controller
@private
*/
generateController(name) {
let owner = getOwner(this);
(isDevelopingApp() && !(owner) && assert('Route is unexpectedly missing an owner', owner));
return generateController(owner, name);
}
/**
Returns the resolved model of a parent (or any ancestor) route
in a route hierarchy. During a transition, all routes
must resolve a model object, and if a route
needs access to a parent route's model in order to
resolve a model (or just reuse the model from a parent),
it can call `this.modelFor(theNameOfParentRoute)` to
retrieve it. If the ancestor route's model was a promise,
its resolved result is returned.
Example
```app/router.js
// ...
Router.map(function() {
this.route('post', { path: '/posts/:post_id' }, function() {
this.route('comments');
});
});
export default Router;
```
```app/routes/post/comments.js
import Route from '@ember/routing/route';
export default class PostCommentsRoute extends Route {
model() {
let post = this.modelFor('post');
return post.comments;
}
}
```
@method modelFor
@param {String} name the name of the route
@return {Object} the model object
@since 1.0.0
@public
*/
modelFor(_name) {
let name;
let owner = getOwner(this);
(isDevelopingApp() && !(owner instanceof EngineInstance) && assert('Expected router owner to be an EngineInstance', owner instanceof EngineInstance));
let transition = this._router && this._router._routerMicrolib ? this._router._routerMicrolib.activeTransition : undefined;
// Only change the route name when there is an active transition.
// Otherwise, use the passed in route name.
if (owner.routable && transition !== undefined) {
name = getEngineRouteName(owner, _name);
} else {
name = _name;
}
let route = owner.lookup(`route:${name}`);
// If we are mid-transition, we want to try and look up
// resolved parent contexts on the current transitionEvent.
if (transition !== undefined && transition !== null) {
let modelLookupName = route && route.routeName || name;
if (Object.prototype.hasOwnProperty.call(transition.resolvedModels, modelLookupName)) {
return transition.resolvedModels[modelLookupName];
}
}
return route?.currentModel;
}
[RENDER_STATE] = undefined;
/**
`this[RENDER]` is used to set up the rendering option for the outlet state.
@method this[RENDER]
@private
*/
[RENDER]() {
this[RENDER_STATE] = buildRenderState(this);
once(this._router, '_setOutlets');
}
willDestroy() {
this.teardownViews();
}
/**
@private
@method teardownViews
*/
teardownViews() {
if (this[RENDER_STATE]) {
this[RENDER_STATE] = undefined;
once(this._router, '_setOutlets');
}
}
/**
Allows you to produce custom metadata for the route.
The return value of this method will be attached to
its corresponding RouteInfoWithAttributes object.
Example
```app/routes/posts/index.js
import Route from '@ember/routing/route';
export default class PostsIndexRoute extends Route {
buildRouteInfoMetadata() {
return { title: 'Posts Page' }
}
}
```
```app/routes/application.js
import Route from '@ember/routing/route';
import { service } from '@ember/service';
export default class ApplicationRoute extends Route {
@service router
constructor() {
super(...arguments);
this.router.on('routeDidChange', transition => {
document.title = transition.to.metadata.title;
// would update document's title to "Posts Page"
});
}
}
```
@method buildRouteInfoMetadata
@return any
@since 3.10.0
@public
*/
buildRouteInfoMetadata() {}
_paramsFor(routeName, params) {
let transition = this._router._routerMicrolib.activeTransition;
if (transition !== undefined) {
return this.paramsFor(routeName);
}
return params;
}
/** @deprecated Manually define your own store, such as with `@service store` */
get _store() {
const owner = getOwner(this);
(isDevelopingApp() && !(owner) && assert('Route is unexpectedly missing an owner', owner));
let routeName = this.routeName;
return {
find(name, value) {
let modelClass = owner.factoryFor(`model:${name}`);
(isDevelopingApp() && !(Boolean(modelClass)) && assert(`You used the dynamic segment \`${name}_id\` in your route ` + `\`${routeName}\` for which Ember requires you provide a ` + `data-loading implementation. Commonly, that is done by ` + `adding a model hook implementation on the route ` + `(\`model({${name}_id}) {\`) or by injecting an implemention of ` + `a data store: \`@service store;\`.`, Boolean(modelClass)));
if (!modelClass) {
return;
}
modelClass = modelClass.class;
(isDevelopingApp() && !(typeof modelClass.find === 'function') && assert(`You used the dynamic segment \`${name}_id\` in your route ` + `\`${routeName}\` for which Ember requires you provide a ` + `data-loading implementation. Commonly, that is done by ` + `adding a model hook implementation on the route ` + `(\`model({${name}_id}) {\`) or by injecting an implemention of ` + `a data store: \`@service store;\`.\n\n` + `Rarely, applications may attempt to use a legacy behavior where ` + `the model class (in this case \`${name}\`) is resolved and the ` + `\`find\` method on that class is invoked to load data. In this ` + `application, a model of \`${name}\` was found but it did not ` + `provide a \`find\` method. You should not add a \`find\` ` + `method to your model. Instead, please implement an appropriate ` + `\`model\` hook on the \`${routeName}\` route.`, typeof modelClass.find === 'function'));
return modelClass.find(value);
}
};
}
/**
@private
@property _qp
*/
static {
decorateMethodV2(this.prototype, "_store", [computed]);
}
get _qp() {
let combinedQueryParameterConfiguration = {};
let controllerName = this.controllerName || this.routeName;
let owner = getOwner(this);
(isDevelopingApp() && !(owner) && assert('Route is unexpectedly missing an owner', owner));
let controller = owner.lookup(`controller:${controllerName}`);
let queryParameterConfiguraton = get(this, 'queryParams');
let hasRouterDefinedQueryParams = Object.keys(queryParameterConfiguraton).length > 0;
if (controller) {
(isDevelopingApp() && !(controller instanceof Controller) && assert('Expected an instance of controller', controller instanceof Controller)); // the developer has authored a controller class in their application for
// this route find its query params and normalize their object shape them
// merge in the query params for the route. As a mergedProperty,
// Route#queryParams is always at least `{}`
let controllerDefinedQueryParameterConfiguration = get(controller, 'queryParams') || [];
let normalizedControllerQueryParameterConfiguration = normalizeControllerQueryParams(controllerDefinedQueryParameterConfiguration);
combinedQueryParameterConfiguration = mergeEachQueryParams(normalizedControllerQueryParameterConfiguration, queryParameterConfiguraton);
} else if (hasRouterDefinedQueryParams) {
// the developer has not defined a controller but *has* supplied route query params.
// Generate a class for them so we can later insert default values
controller = generateController(owner, controllerName);
combinedQueryParameterConfiguration = queryParameterConfiguraton;
}
let qps = [];
let map = {};
let propertyNames = [];
for (let propName in combinedQueryParameterConfiguration) {
if (!Object.prototype.hasOwnProperty.call(combinedQueryParameterConfiguration, propName)) {
continue;
}
// to support the dubious feature of using unknownProperty
// on queryParams configuration
if (propName === 'unknownProperty' || propName === '_super') {
// possible todo: issue deprecation warning?
continue;
}
let desc = combinedQueryParameterConfiguration[propName];
(isDevelopingApp() && !(desc) && assert(`[BUG] missing query parameter configuration for ${propName}`, desc));
let scope = desc.scope || 'model';
let parts = undefined;
if (scope === 'controller') {
parts = [];
}
let urlKey = desc.as || this.serializeQueryParamKey(propName);
let defaultValue = get(controller, propName);
defaultValue = copyDefaultValue(defaultValue);
let type = desc.type || typeOf(defaultValue);
let defaultValueSerialized = this.serializeQueryParam(defaultValue, urlKey, type);
let scopedPropertyName = `${controllerName}:${propName}`;
let qp = {
undecoratedDefaultValue: get(controller, propName),
defaultValue,
serializedDefaultValue: defaultValueSerialized,
serializedValue: defaultValueSerialized,
type,
urlKey,
prop: propName,
scopedPropertyName,
controllerName,
route: this,
parts,
// provided later when stashNames is called if 'model' scope
values: null,
// provided later when setup is called. no idea why.
scope
};
map[propName] = map[urlKey] = map[scopedPropertyName] = qp;
qps.push(qp);
propertyNames.push(propName);
}
return {
qps,
map,
propertyNames,
states: {
/*
Called when a query parameter changes in the URL, this route cares
about that query parameter, but the route is not currently
in the active route hierarchy.
*/
inactive: (prop, value) => {
let qp = map[prop];
(isDevelopingApp() && !(qp) && assert('expected inactive callback to only be called for registered qps', qp));
this._qpChanged(prop, value, qp);
},
/*
Called when a query parameter changes in the URL, this route cares
about that query parameter, and the route is currently
in the active route hierarchy.
*/
active: (prop, value) => {
let qp = map[prop];
(isDevelopingApp() && !(qp) && assert('expected active callback to only be called for registered qps', qp));
this._qpChanged(prop, value, qp);
return this._activeQPChanged(qp, value);
},
/*
Called when a value of a query parameter this route handles changes in a controller
and the route is currently in the active route hierarchy.
*/
allowOverrides: (prop, value) => {
let qp = map[prop];
(isDevelopingApp() && !(qp) && assert('expected allowOverrides callback to only be called for registered qps', qp));
this._qpChanged(prop, value, qp);
return this._updatingQPChanged(qp);
}
}
};
}
// Set in reopen
static {
decorateMethodV2(this.prototype, "_qp", [computed]);
}
/**
Sends an action to the router, which will delegate it to the currently
active route hierarchy per the bubbling rules explained under `actions`.
Example
```app/router.js
// ...
Router.map(function() {
this.route('index');
});
export default Router;
```
```app/routes/application.js
import Route from '@ember/routing/route';
import { action } from '@ember/object';
export default class ApplicationRoute extends Route {
@action
track(arg) {
console.log(arg, 'was clicked');
}
}
```
```app/routes/index.js
import Route from '@ember/routing/route';
import { action } from '@ember/object';
export default class IndexRoute extends Route {
@action
trackIfDebug(arg) {
if (debug) {
this.send('track', arg);
}
}
}
```
@method send
@param {String} name the name of the action to trigger
@param {...*} args
@since 1.0.0
@public
*/
// Set with reopen to override parent behavior
}
function getRenderState(route) {
return route[RENDER_STATE];
}
function buildRenderState(route) {
let owner = getOwner(route);
(isDevelopingApp() && !(owner) && assert('Route is unexpectedly missing an owner', owner));
let name = route.routeName;
let controller = owner.lookup(`controller:${route.controllerName || name}`);
(isDevelopingApp() && !(controller instanceof Controller) && assert('Expected an instance of controller', controller instanceof Controller));
let model = route.currentModel;
let template = owner.lookup(`template:${route.templateName || name}`);
let render = {
owner,
into: undefined,
outlet: 'main',
name,
controller,
model,
template: template?.(owner) ?? route._topLevelViewTemplate(owner)
};
if (isDevelopingApp()) {
let LOG_VIEW_LOOKUPS = get(route._router, 'namespace.LOG_VIEW_LOOKUPS');
if (LOG_VIEW_LOOKUPS && !template) {
info(`Could not find "${name}" template. Nothing will be rendered`, {
fullName: `template:${name}`
});
}
}
return render;
}
function getFullQueryParams(router, state) {
if (state.fullQueryParams) {
return state.fullQueryParams;
}
let haveAllRouteInfosResolved = state.routeInfos.every(routeInfo => routeInfo.route);
let fullQueryParamsState = {
...state.queryParams
};
router._deserializeQueryParams(state.routeInfos, fullQueryParamsState);
// only cache query params state if all routeinfos have resolved; it's possible
// for lazy routes to not have resolved when `getFullQueryParams` is called, so
// we wait until all routes have resolved prior to caching query params state
if (haveAllRouteInfosResolved) {
state.fullQueryParams = fullQueryParamsState;
}
return fullQueryParamsState;
}
function getQueryParamsFor(route, state) {
state.queryParamsFor = state.query