From 2dc6d2699ddd2e5a313892bd37c96f2aa40d9c8e Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Fri, 21 Feb 2020 12:30:36 -0700 Subject: [PATCH] @ember/routing: correct types for `transitionTo` (#42544) - add types to support query-params-only transitions in both the `Route` and `RouterService` classes - supply updated docs for each API as well --- types/ember__routing/index.d.ts | 3 + types/ember__routing/route.d.ts | 211 ++++++++++++++++++-- types/ember__routing/router-service.d.ts | 35 +++- types/ember__routing/test/route.ts | 21 ++ types/ember__routing/test/router-service.ts | 20 ++ types/ember__routing/tsconfig.json | 5 +- 6 files changed, 272 insertions(+), 23 deletions(-) create mode 100644 types/ember__routing/test/router-service.ts diff --git a/types/ember__routing/index.d.ts b/types/ember__routing/index.d.ts index c90b477697..cc061a4054 100644 --- a/types/ember__routing/index.d.ts +++ b/types/ember__routing/index.d.ts @@ -1,6 +1,9 @@ // Type definitions for non-npm package @ember/routing 3.0 // Project: https://emberjs.com/api/ember/3.4/modules/@ember%2Frouting // Definitions by: Mike North +// Chris Krycho +// Dan Freeman +// James C. Davis // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped // TypeScript Version: 2.8 diff --git a/types/ember__routing/route.d.ts b/types/ember__routing/route.d.ts index e455a4c6cd..4eaed9841f 100644 --- a/types/ember__routing/route.d.ts +++ b/types/ember__routing/route.d.ts @@ -1,8 +1,8 @@ -import EmberObject from "@ember/object"; -import ActionHandler from "@ember/object/-private/action-handler"; -import Transition from "@ember/routing/-private/transition"; -import Evented from "@ember/object/evented"; -import { RenderOptions, RouteQueryParam } from "@ember/routing/types"; +import EmberObject from '@ember/object'; +import ActionHandler from '@ember/object/-private/action-handler'; +import Transition from '@ember/routing/-private/transition'; +import Evented from '@ember/object/evented'; +import { RenderOptions, RouteQueryParam } from '@ember/routing/types'; import Controller, { Registry as ControllerRegistry } from '@ember/controller'; /** @@ -189,19 +189,196 @@ export default class Route extends EmberObject.extend(ActionHandler, Evented) { /** * Transition the application into another route. The route may - * be either a single route or route path + * be either a single route or route path: + * + * ```javascript + * this.transitionTo('blogPosts'); + * this.transitionTo('blogPosts.recentEntries'); + * ``` + * + * Optionally supply a model for the route in question. The model + * will be serialized into the URL using the `serialize` hook of + * the route: + * + * ```javascript + * this.transitionTo('blogPost', aPost); + * ``` + * + * If a literal is passed (such as a number or a string), it will + * be treated as an identifier instead. In this case, the `model` + * hook of the route will be triggered: + * + * ```javascript + * this.transitionTo('blogPost', 1); + * ``` + * + * Multiple models will be applied last to first recursively up the + * route tree. + * + * ```app/routes.js + * // ... + * + * Router.map(function() { + * this.route('blogPost', { path:':blogPostId' }, function() { + * this.route('blogComment', { path: ':blogCommentId' }); + * }); + * }); + * + * export default Router; + * ``` + * + * ```javascript + * this.transitionTo('blogComment', aPost, aComment); + * this.transitionTo('blogComment', 1, 13); + * ``` + * + * It is also possible to pass a URL (a string that starts with a + * `/`). + * + * ```javascript + * this.transitionTo('/'); + * this.transitionTo('/blog/post/1/comment/13'); + * this.transitionTo('/blog/posts?sort=title'); + * ``` + * + * An options hash with a `queryParams` property may be provided as + * the final argument to add query parameters to the destination URL. + * + * ```javascript + * this.transitionTo('blogPost', 1, { + * queryParams: { showComments: 'true' } + * }); + * + * // if you just want to transition the query parameters without changing the route + * this.transitionTo({ queryParams: { sort: 'date' } }); + * ``` + * + * See also [replaceWith](#method_replaceWith). + * + * Simple Transition Example + * + * ```app/routes.js + * // ... + * + * Router.map(function() { + * this.route('index'); + * this.route('secret'); + * this.route('fourOhFour', { path: '*:' }); + * }); + * + * export default Router; + * ``` + * + * ```app/routes/index.js + * import Route from '@ember/routing/route'; + * import { action } from '@ember/object'; + * + * export default class IndexRoute extends Route { + * @action + * moveToSecret(context) { + * if (authorized()) { + * this.transitionTo('secret', context); + * } else { + * this.transitionTo('fourOhFour'); + * } + * } + * } + * ``` + * + * Transition to a nested route + * + * ```app/router.js + * // ... + * + * Router.map(function() { + * this.route('articles', { path: '/articles' }, function() { + * this.route('new'); + * }); + * }); + * + * export default Router; + * ``` + * + * ```app/routes/index.js + * import Route from '@ember/routing/route'; + * import { action } from '@ember/object'; + * + * export default class IndexRoute extends Route { + * @action + * transitionToNewArticle() { + * this.transitionTo('articles.new'); + * } + * } + * ``` + * + * Multiple Models Example + * + * ```app/router.js + * // ... + * + * Router.map(function() { + * this.route('index'); + * + * this.route('breakfast', { path: ':breakfastId' }, function() { + * this.route('cereal', { path: ':cerealId' }); + * }); + * }); + * + * export default Router; + * ``` + * + * ```app/routes/index.js + * import Route from '@ember/routing/route'; + * import { action } from '@ember/object'; + * + * export default class IndexRoute extends Route { + * @action + * moveToChocolateCereal() { + * let cereal = { cerealId: 'ChocolateYumminess' }; + * let breakfast = { breakfastId: 'CerealAndMilk' }; + * + * this.transitionTo('breakfast.cereal', breakfast, cereal); + * } + * } + * ``` + * + * Nested Route with Query String Example + * + * ```app/routes.js + * // ... + * + * Router.map(function() { + * this.route('fruits', function() { + * this.route('apples'); + * }); + * }); + * + * export default Router; + * ``` + * + * ```app/routes/index.js + * import Route from '@ember/routing/route'; + * + * export default class IndexRoute extends Route { + * @action + * transitionToApples() { + * this.transitionTo('fruits.apples', { queryParams: { color: 'red' } }); + * } + * } + * ``` + * + * @param name the name of the route or a URL. + * @param models the model(s) or identifier(s) to be used while + * transitioning to the route. + * @param options optional hash with a queryParams property + * containing a mapping of query parameters. May be supplied + * as the only parameter to trigger a query-parameter-only + * transition. + * @returns the Transition object associated with this attempted + * transition */ - transitionTo(name: string, ...object: any[]): Transition; - - /** - * The name of the view to use by default when rendering this routes template. - * When rendering a template, the route will, by default, determine the - * template and view to use from the name of the route itself. If you need to - * define a specific view, set this property. - * This is useful when multiple routes would benefit from using the same view - * because it doesn't require a custom `renderTemplate` method. - */ - transitionTo(name: string, ...object: any[]): Transition; + transitionTo(name: string, ...modelsOrOptions: object[]): Transition; + transitionTo(options: { queryParams: object }): Transition; // https://emberjs.com/api/ember/3.2/classes/Route/methods/intermediateTransitionTo?anchor=intermediateTransitionTo /** diff --git a/types/ember__routing/router-service.d.ts b/types/ember__routing/router-service.d.ts index 07349dc765..6c6bd55cdd 100644 --- a/types/ember__routing/router-service.d.ts +++ b/types/ember__routing/router-service.d.ts @@ -138,16 +138,42 @@ export default class RouterService extends Service { options?: { queryParams: object } ): Transition; - // https://emberjs.com/api/ember/2.18/classes/RouterService/methods/isActive?anchor=transitionTo + // https://emberjs.com/api/ember/release/classes/RouterService/methods/isActive?anchor=transitionTo /** - * Transition the application into another route. The route may be - * either a single route or route path + * Transition the application into another route. The route may + * be either a single route or route path: + * + * See [transitionTo](https://api.emberjs.com/ember/release/classes/Route/methods/transitionTo?anchor=transitionTo) for more info. + * + * Calling `transitionTo` from the Router service will cause default query parameter values to be included in the URL. + * This behavior is different from calling `transitionTo` on a route or `transitionToRoute` on a controller. + * See the [Router Service RFC](https://github.com/emberjs/rfcs/blob/master/text/0095-router-service.md#query-parameter-semantics) for more info. + * + * In the following example we use the Router service to navigate to a route with a + * specific model from a Component. + * + * ```app/components/example.js + * import Component from '@glimmer/component'; + * import { action } from '@ember/object'; + * import { inject as service } from '@ember/service'; + * + * export default class extends Component { + * @service router; + * + * @action + * goToComments(post) { + * this.router.transitionTo('comments', post); + * } + * } + * ``` * * @param routeNameOrUrl the name of the route or a URL * @param models the model(s) or identifier(s) to be used while * transitioning to the route. * @param options optional hash with a queryParams property - * containing a mapping of query parameters + * containing a mapping of query parameters. May be + * supplied as the only parameter to trigger a + * query-parameter-only transition. * @returns the Transition object associated with this attempted transition */ transitionTo( @@ -180,6 +206,7 @@ export default class RouterService extends Service { modelsD: RouteModel, options?: { queryParams: object } ): Transition; + transitionTo(options: { queryParams: object }): Transition; // https://emberjs.com/api/ember/2.18/classes/RouterService/methods/isActive?anchor=urlFor /** diff --git a/types/ember__routing/test/route.ts b/types/ember__routing/test/route.ts index c34d0bac63..755a981ab2 100755 --- a/types/ember__routing/test/route.ts +++ b/types/ember__routing/test/route.ts @@ -104,6 +104,27 @@ class InvalidRedirect extends Route { } } +class TransitionToExamples extends Route { + // NOTE: this one won't check that `queryParams` has the right shape, + // because the overload for the version where `models` are passed + // necessarily includes all objects. + transitionToModelAndQP() { + this.transitionTo('somewhere', { queryParams: { neat: true } }); + } + + transitionToJustQP() { + this.transitionTo({ queryParams: { neat: 'true' }}); + } + + transitionToNonsense() { + this.transitionTo({ cannotDoModelHere: true }); // $ExpectError + } + + transitionToBadQP() { + this.transitionTo({ queryParams: 12 }); // $ExpectError + } +} + class ApplicationController extends Controller {} declare module '@ember/controller' { interface Registry { diff --git a/types/ember__routing/test/router-service.ts b/types/ember__routing/test/router-service.ts new file mode 100644 index 0000000000..02d203f26a --- /dev/null +++ b/types/ember__routing/test/router-service.ts @@ -0,0 +1,20 @@ +import RouterService from '@ember/routing/router-service'; + +declare let router: RouterService; + +router.transitionTo('someRoute'); +router.transitionTo('someRoute', { withModel: true }); +router.transitionTo('someRoute', { withModel: true }, { queryParams: {} }); +router.transitionTo( + 'someRoute', + { withModel: true }, + { withMultipleModels: 'still checks correctly' }, + { queryParams: {} }, +); +// NOTE: we cannot check the validity of invocations with just route name and +// query params beyond that the second argument is an object of some sort, +// because TS will always resolve it to the `models` variant if the +// `queryParams` variant fails. +router.transitionTo('someRoute', { queryParams: { shouldWork: true } }); +router.transitionTo({ queryParams: { areSupported: true } }); +router.transitionTo({ queryParams: 'potato' }); // $ExpectError diff --git a/types/ember__routing/tsconfig.json b/types/ember__routing/tsconfig.json index 987e508d31..29f8fbc447 100644 --- a/types/ember__routing/tsconfig.json +++ b/types/ember__routing/tsconfig.json @@ -57,6 +57,7 @@ "index.d.ts", "test/lib/assert.ts", "test/route.ts", - "test/router.ts" + "test/router.ts", + "test/router-service.ts" ] -} \ No newline at end of file +}