diff --git a/packages/@ember/-internals/glimmer/lib/components/link-to.ts b/packages/@ember/-internals/glimmer/lib/components/link-to.ts index 5b34abd73e8..b3442b0473a 100644 --- a/packages/@ember/-internals/glimmer/lib/components/link-to.ts +++ b/packages/@ember/-internals/glimmer/lib/components/link-to.ts @@ -3,8 +3,11 @@ */ import { alias, computed } from '@ember/-internals/metal'; +import { getOwner } from '@ember/-internals/owner'; +import RouterState from '@ember/-internals/routing/lib/system/router_state'; import { isSimpleClick } from '@ember/-internals/views'; import { assert, warn } from '@ember/debug'; +import { EngineInstance, getEngineParent } from '@ember/engine'; import { flaggedInstrument } from '@ember/instrumentation'; import { inject as injectService } from '@ember/service'; import { DEBUG } from '@glimmer/env'; @@ -487,6 +490,13 @@ const LinkComponent = EmberComponent.extend({ init() { this._super(...arguments); + assert( + 'You attempted to use the component within a routeless engine, this is not supported. ' + + 'If you are using the ember-engines addon, use the component instead. ' + + 'See https://ember-engines.com/docs/links for more info.', + !this._isEngine || this._engineMountPoint !== undefined + ); + // Map desired event name to invoke function let { eventName } = this; this.on(eventName, this, this._invoke); @@ -497,9 +507,17 @@ const LinkComponent = EmberComponent.extend({ _currentRouterState: alias('_routing.currentState'), _targetRouterState: alias('_routing.targetState'), + _isEngine: computed(function (this: any) { + return getEngineParent(getOwner(this) as EngineInstance) !== undefined; + }), + + _engineMountPoint: computed(function (this: any) { + return (getOwner(this) as EngineInstance).mountPoint; + }), + _route: computed('route', '_currentRouterState', function computeLinkToComponentRoute(this: any) { let { route } = this; - return route === UNDEFINED ? this._currentRoute : route; + return this._namespaceRoute(route === UNDEFINED ? this._currentRoute : route); }), _models: computed('model', 'models', function computeLinkToComponentModels(this: any) { @@ -608,7 +626,7 @@ const LinkComponent = EmberComponent.extend({ } ), - _isActive(routerState: any) { + _isActive(routerState: RouterState): boolean { if (this.loading) { return false; } @@ -619,25 +637,17 @@ const LinkComponent = EmberComponent.extend({ return currentWhen; } - let isCurrentWhenSpecified = Boolean(currentWhen); + let { _models: models, _routing: routing } = this; - if (isCurrentWhenSpecified) { - currentWhen = currentWhen.split(' '); + if (typeof currentWhen === 'string') { + return currentWhen + .split(' ') + .some((route) => + routing.isActiveForRoute(models, undefined, this._namespaceRoute(route), routerState) + ); } else { - currentWhen = [this._route]; - } - - let { _models: models, _query: query, _routing: routing } = this; - - for (let i = 0; i < currentWhen.length; i++) { - if ( - routing.isActiveForRoute(models, query, currentWhen[i], routerState, isCurrentWhenSpecified) - ) { - return true; - } + return routing.isActiveForRoute(models, this._query, this._route, routerState); } - - return false; }, transitioningIn: computed( @@ -664,6 +674,18 @@ const LinkComponent = EmberComponent.extend({ } ), + _namespaceRoute(route: string): string { + let { _engineMountPoint: mountPoint } = this; + + if (mountPoint === undefined) { + return route; + } else if (route === 'application') { + return mountPoint; + } else { + return `${mountPoint}.${route}`; + } + }, + /** Event handler that invokes the link, activating the associated route. diff --git a/packages/@ember/-internals/glimmer/tests/integration/application/engine-test.js b/packages/@ember/-internals/glimmer/tests/integration/application/engine-test.js index 8869f0129a2..5104cc5591d 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/application/engine-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/application/engine-test.js @@ -875,7 +875,7 @@ moduleFor( ["@test query params don't have stickiness by default between model"](assert) { assert.expect(1); - let tmpl = '{{#link-to "blog.category" 1337}}Category 1337{{/link-to}}'; + let tmpl = '{{#link-to "category" 1337}}Category 1337{{/link-to}}'; this.setupAppAndRoutableEngine(); this.additionalEngineRegistrations(function () { this.register('template:category', compile(tmpl)); @@ -895,7 +895,7 @@ moduleFor( ) { assert.expect(2); let tmpl = - '{{#link-to "blog.author" 1337 class="author-1337"}}Author 1337{{/link-to}}{{#link-to "blog.author" 1 class="author-1"}}Author 1{{/link-to}}'; + '{{#link-to "author" 1337 class="author-1337"}}Author 1337{{/link-to}}{{#link-to "author" 1 class="author-1"}}Author 1{{/link-to}}'; this.setupAppAndRoutableEngine(); this.additionalEngineRegistrations(function () { this.register('template:author', compile(tmpl)); diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-angle-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-angle-test.js index 8bec47c3f3c..1f26d1f4211 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-angle-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-angle-test.js @@ -1,11 +1,19 @@ -import { moduleFor, ApplicationTestCase, runLoopSettled, runTask } from 'internal-test-helpers'; +import { + ApplicationTestCase, + ModuleBasedTestResolver, + moduleFor, + runLoopSettled, + runTask, +} from 'internal-test-helpers'; import Controller, { inject as injectController } from '@ember/controller'; import { A as emberA, RSVP } from '@ember/-internals/runtime'; import { alias } from '@ember/-internals/metal'; import { subscribe, reset } from '@ember/instrumentation'; import { Route, NoneLocation } from '@ember/-internals/routing'; import { EMBER_IMPROVED_INSTRUMENTATION } from '@ember/canary-features'; +import Engine from '@ember/engine'; import { DEBUG } from '@glimmer/env'; +import { compile } from '../../../utils/helpers'; // IE includes the host name function normalizeUrl(url) { @@ -348,6 +356,209 @@ moduleFor( ); }); } + + async ['@test Using inside a non-routable engine errors'](assert) { + this.add( + 'engine:not-routable', + class NotRoutableEngine extends Engine { + Resolver = ModuleBasedTestResolver; + + init() { + super.init(...arguments); + this.register( + 'template:application', + compile(`About`, { + moduleName: 'non-routable/templates/application.hbs', + }) + ); + } + } + ); + + this.addTemplate('index', `{{mount 'not-routable'}}`); + + await assert.rejectsAssertion( + this.visit('/'), + 'You attempted to use the component within a routeless engine, this is not supported. ' + + 'If you are using the ember-engines addon, use the component instead. ' + + 'See https://ember-engines.com/docs/links for more info.' + ); + } + + async ['@test Using inside a routable engine link within the engine'](assert) { + this.add( + 'engine:routable', + class RoutableEngine extends Engine { + Resolver = ModuleBasedTestResolver; + + init() { + super.init(...arguments); + this.register( + 'template:application', + compile( + ` +

Routable Engine

+ {{outlet}} + Engine Appliction + `, + { + moduleName: 'routable/templates/application.hbs', + } + ) + ); + this.register( + 'template:index', + compile( + ` +

Engine Home

+ Engine About + Engine Self + `, + { + moduleName: 'routable/templates/index.hbs', + } + ) + ); + this.register( + 'template:about', + compile( + ` +

Engine About

+ Engine Home + Engine Self + `, + { + moduleName: 'routable/templates/about.hbs', + } + ) + ); + } + } + ); + + this.router.map(function () { + this.mount('routable'); + }); + + this.add('route-map:routable', function () { + this.route('about'); + }); + + this.addTemplate( + 'application', + ` +

Application

+ {{outlet}} + Appliction + Engine + ` + ); + + await this.visit('/'); + + assert.equal(this.$('#application-layout').length, 1, 'The application layout was rendered'); + assert.strictEqual(this.$('#engine-layout').length, 0, 'The engine layout was not rendered'); + assert.equal(this.$('#application-link.active').length, 1, 'The application link is active'); + assert.equal(this.$('#engine-link:not(.active)').length, 1, 'The engine link is not active'); + + assert.equal(this.$('h3.home').length, 1, 'The application index page is rendered'); + assert.equal(this.$('#self-link.active').length, 1, 'The application index link is active'); + assert.equal( + this.$('#about-link:not(.active)').length, + 1, + 'The application about link is not active' + ); + + await this.click('#about-link'); + + assert.equal(this.$('#application-layout').length, 1, 'The application layout was rendered'); + assert.strictEqual(this.$('#engine-layout').length, 0, 'The engine layout was not rendered'); + assert.equal(this.$('#application-link.active').length, 1, 'The application link is active'); + assert.equal(this.$('#engine-link:not(.active)').length, 1, 'The engine link is not active'); + + assert.equal(this.$('h3.about').length, 1, 'The application about page is rendered'); + assert.equal(this.$('#self-link.active').length, 1, 'The application about link is active'); + assert.equal( + this.$('#home-link:not(.active)').length, + 1, + 'The application home link is not active' + ); + + await this.click('#engine-link'); + + assert.equal(this.$('#application-layout').length, 1, 'The application layout was rendered'); + assert.equal(this.$('#engine-layout').length, 1, 'The engine layout was rendered'); + assert.equal(this.$('#application-link.active').length, 1, 'The application link is active'); + assert.equal(this.$('#engine-link.active').length, 1, 'The engine link is active'); + assert.equal( + this.$('#engine-application-link.active').length, + 1, + 'The engine application link is active' + ); + + assert.equal(this.$('h3.engine-home').length, 1, 'The engine index page is rendered'); + assert.equal(this.$('#engine-self-link.active').length, 1, 'The engine index link is active'); + assert.equal( + this.$('#engine-about-link:not(.active)').length, + 1, + 'The engine about link is not active' + ); + + await this.click('#engine-about-link'); + + assert.equal(this.$('#application-layout').length, 1, 'The application layout was rendered'); + assert.equal(this.$('#engine-layout').length, 1, 'The engine layout was rendered'); + assert.equal(this.$('#application-link.active').length, 1, 'The application link is active'); + assert.equal(this.$('#engine-link.active').length, 1, 'The engine link is active'); + assert.equal( + this.$('#engine-application-link.active').length, + 1, + 'The engine application link is active' + ); + + assert.equal(this.$('h3.engine-about').length, 1, 'The engine about page is rendered'); + assert.equal(this.$('#engine-self-link.active').length, 1, 'The engine about link is active'); + assert.equal( + this.$('#engine-home-link:not(.active)').length, + 1, + 'The engine home link is not active' + ); + + await this.click('#engine-application-link'); + + assert.equal(this.$('#application-layout').length, 1, 'The application layout was rendered'); + assert.equal(this.$('#engine-layout').length, 1, 'The engine layout was rendered'); + assert.equal(this.$('#application-link.active').length, 1, 'The application link is active'); + assert.equal(this.$('#engine-link.active').length, 1, 'The engine link is active'); + assert.equal( + this.$('#engine-application-link.active').length, + 1, + 'The engine application link is active' + ); + + assert.equal(this.$('h3.engine-home').length, 1, 'The engine index page is rendered'); + assert.equal(this.$('#engine-self-link.active').length, 1, 'The engine index link is active'); + assert.equal( + this.$('#engine-about-link:not(.active)').length, + 1, + 'The engine about link is not active' + ); + + await this.click('#application-link'); + + assert.equal(this.$('#application-layout').length, 1, 'The application layout was rendered'); + assert.strictEqual(this.$('#engine-layout').length, 0, 'The engine layout was not rendered'); + assert.equal(this.$('#application-link.active').length, 1, 'The application link is active'); + assert.equal(this.$('#engine-link:not(.active)').length, 1, 'The engine link is not active'); + + assert.equal(this.$('h3.home').length, 1, 'The application index page is rendered'); + assert.equal(this.$('#self-link.active').length, 1, 'The application index link is active'); + assert.equal( + this.$('#about-link:not(.active)').length, + 1, + 'The application about link is not active' + ); + } } ); diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-curly-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-curly-test.js index 48d800e0a46..542736dd08d 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-curly-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-curly-test.js @@ -1,11 +1,19 @@ -import { moduleFor, ApplicationTestCase, runLoopSettled, runTask } from 'internal-test-helpers'; +import { + ApplicationTestCase, + ModuleBasedTestResolver, + moduleFor, + runLoopSettled, + runTask, +} from 'internal-test-helpers'; import Controller, { inject as injectController } from '@ember/controller'; import { A as emberA, RSVP } from '@ember/-internals/runtime'; import { alias } from '@ember/-internals/metal'; import { subscribe, reset } from '@ember/instrumentation'; import { Route, NoneLocation } from '@ember/-internals/routing'; import { EMBER_IMPROVED_INSTRUMENTATION } from '@ember/canary-features'; +import Engine from '@ember/engine'; import { DEBUG } from '@glimmer/env'; +import { compile } from '../../../utils/helpers'; // IE includes the host name function normalizeUrl(url) { @@ -349,6 +357,209 @@ moduleFor( ); }); } + + async ['@test Using {{link-to}} inside a non-routable engine errors'](assert) { + this.add( + 'engine:not-routable', + class NotRoutableEngine extends Engine { + Resolver = ModuleBasedTestResolver; + + init() { + super.init(...arguments); + this.register( + 'template:application', + compile(`{{#link-to 'about'}}About{{/link-to}}`, { + moduleName: 'non-routable/templates/application.hbs', + }) + ); + } + } + ); + + this.addTemplate('index', `{{mount "not-routable"}}`); + + await assert.rejectsAssertion( + this.visit('/'), + 'You attempted to use the component within a routeless engine, this is not supported. ' + + 'If you are using the ember-engines addon, use the component instead. ' + + 'See https://ember-engines.com/docs/links for more info.' + ); + } + + async ['@test Using {{link-to}} inside a routable engine link within the engine'](assert) { + this.add( + 'engine:routable', + class RoutableEngine extends Engine { + Resolver = ModuleBasedTestResolver; + + init() { + super.init(...arguments); + this.register( + 'template:application', + compile( + ` +

Routable Engine

+ {{outlet}} + {{#link-to 'application' id='engine-application-link'}}Engine Appliction{{/link-to}} + `, + { + moduleName: 'routable/templates/application.hbs', + } + ) + ); + this.register( + 'template:index', + compile( + ` +

Engine Home

+ {{#link-to 'about' id='engine-about-link'}}Engine About{{/link-to}} + {{#link-to 'index' id='engine-self-link'}}Engine Self{{/link-to}} + `, + { + moduleName: 'routable/templates/index.hbs', + } + ) + ); + this.register( + 'template:about', + compile( + ` +

Engine About

+ {{#link-to 'index' id='engine-home-link'}}Engine Home{{/link-to}} + {{#link-to 'about' id='engine-self-link'}}Engine Self{{/link-to}} + `, + { + moduleName: 'routable/templates/about.hbs', + } + ) + ); + } + } + ); + + this.router.map(function () { + this.mount('routable'); + }); + + this.add('route-map:routable', function () { + this.route('about'); + }); + + this.addTemplate( + 'application', + ` +

Application

+ {{outlet}} + {{#link-to 'application' id='application-link'}}Appliction{{/link-to}} + {{#link-to 'routable' id='engine-link'}}Engine{{/link-to}} + ` + ); + + await this.visit('/'); + + assert.equal(this.$('#application-layout').length, 1, 'The application layout was rendered'); + assert.strictEqual(this.$('#engine-layout').length, 0, 'The engine layout was not rendered'); + assert.equal(this.$('#application-link.active').length, 1, 'The application link is active'); + assert.equal(this.$('#engine-link:not(.active)').length, 1, 'The engine link is not active'); + + assert.equal(this.$('h3.home').length, 1, 'The application index page is rendered'); + assert.equal(this.$('#self-link.active').length, 1, 'The application index link is active'); + assert.equal( + this.$('#about-link:not(.active)').length, + 1, + 'The application about link is not active' + ); + + await this.click('#about-link'); + + assert.equal(this.$('#application-layout').length, 1, 'The application layout was rendered'); + assert.strictEqual(this.$('#engine-layout').length, 0, 'The engine layout was not rendered'); + assert.equal(this.$('#application-link.active').length, 1, 'The application link is active'); + assert.equal(this.$('#engine-link:not(.active)').length, 1, 'The engine link is not active'); + + assert.equal(this.$('h3.about').length, 1, 'The application about page is rendered'); + assert.equal(this.$('#self-link.active').length, 1, 'The application about link is active'); + assert.equal( + this.$('#home-link:not(.active)').length, + 1, + 'The application home link is not active' + ); + + await this.click('#engine-link'); + + assert.equal(this.$('#application-layout').length, 1, 'The application layout was rendered'); + assert.equal(this.$('#engine-layout').length, 1, 'The engine layout was rendered'); + assert.equal(this.$('#application-link.active').length, 1, 'The application link is active'); + assert.equal(this.$('#engine-link.active').length, 1, 'The engine link is active'); + assert.equal( + this.$('#engine-application-link.active').length, + 1, + 'The engine application link is active' + ); + + assert.equal(this.$('h3.engine-home').length, 1, 'The engine index page is rendered'); + assert.equal(this.$('#engine-self-link.active').length, 1, 'The engine index link is active'); + assert.equal( + this.$('#engine-about-link:not(.active)').length, + 1, + 'The engine about link is not active' + ); + + await this.click('#engine-about-link'); + + assert.equal(this.$('#application-layout').length, 1, 'The application layout was rendered'); + assert.equal(this.$('#engine-layout').length, 1, 'The engine layout was rendered'); + assert.equal(this.$('#application-link.active').length, 1, 'The application link is active'); + assert.equal(this.$('#engine-link.active').length, 1, 'The engine link is active'); + assert.equal( + this.$('#engine-application-link.active').length, + 1, + 'The engine application link is active' + ); + + assert.equal(this.$('h3.engine-about').length, 1, 'The engine about page is rendered'); + assert.equal(this.$('#engine-self-link.active').length, 1, 'The engine about link is active'); + assert.equal( + this.$('#engine-home-link:not(.active)').length, + 1, + 'The engine home link is not active' + ); + + await this.click('#engine-application-link'); + + assert.equal(this.$('#application-layout').length, 1, 'The application layout was rendered'); + assert.equal(this.$('#engine-layout').length, 1, 'The engine layout was rendered'); + assert.equal(this.$('#application-link.active').length, 1, 'The application link is active'); + assert.equal(this.$('#engine-link.active').length, 1, 'The engine link is active'); + assert.equal( + this.$('#engine-application-link.active').length, + 1, + 'The engine application link is active' + ); + + assert.equal(this.$('h3.engine-home').length, 1, 'The engine index page is rendered'); + assert.equal(this.$('#engine-self-link.active').length, 1, 'The engine index link is active'); + assert.equal( + this.$('#engine-about-link:not(.active)').length, + 1, + 'The engine about link is not active' + ); + + await this.click('#application-link'); + + assert.equal(this.$('#application-layout').length, 1, 'The application layout was rendered'); + assert.strictEqual(this.$('#engine-layout').length, 0, 'The engine layout was not rendered'); + assert.equal(this.$('#application-link.active').length, 1, 'The application link is active'); + assert.equal(this.$('#engine-link:not(.active)').length, 1, 'The engine link is not active'); + + assert.equal(this.$('h3.home').length, 1, 'The application index page is rendered'); + assert.equal(this.$('#self-link.active').length, 1, 'The application index link is active'); + assert.equal( + this.$('#about-link:not(.active)').length, + 1, + 'The application about link is not active' + ); + } } ); diff --git a/packages/@ember/-internals/routing/lib/services/routing.ts b/packages/@ember/-internals/routing/lib/services/routing.ts index caf18d3a944..8321daadbe4 100644 --- a/packages/@ember/-internals/routing/lib/services/routing.ts +++ b/packages/@ember/-internals/routing/lib/services/routing.ts @@ -6,6 +6,7 @@ import { readOnly } from '@ember/object/computed'; import { assign } from '@ember/polyfills'; import Service from '@ember/service'; import EmberRouter, { QueryParam } from '../system/router'; +import RouterState from '../system/router_state'; /** The Routing service is used by LinkComponent, and provides facilities for @@ -57,11 +58,10 @@ export default class RoutingService extends Service { isActiveForRoute( contexts: {}[], - queryParams: {}, + queryParams: QueryParam | undefined, routeName: string, - routerState: any, - isCurrentWhenSpecified: any - ) { + routerState: RouterState + ): boolean { let handlers = this.router._routerMicrolib.recognizer.handlersFor(routeName); let leafName = handlers[handlers.length - 1].handler; let maximumContexts = numberOfContextsAcceptedByHandler(routeName, handlers); @@ -80,7 +80,7 @@ export default class RoutingService extends Service { routeName = leafName; } - return routerState.isActiveIntent(routeName, contexts, queryParams, !isCurrentWhenSpecified); + return routerState.isActiveIntent(routeName, contexts, queryParams); } } diff --git a/packages/@ember/-internals/routing/lib/system/router_state.ts b/packages/@ember/-internals/routing/lib/system/router_state.ts index 8a4c6305498..886c8bd6ad9 100644 --- a/packages/@ember/-internals/routing/lib/system/router_state.ts +++ b/packages/@ember/-internals/routing/lib/system/router_state.ts @@ -18,18 +18,13 @@ export default class RouterState { this.routerJsState = routerJsState; } - isActiveIntent( - routeName: string, - models: {}[], - queryParams: QueryParam, - queryParamsMustMatch?: boolean - ) { + isActiveIntent(routeName: string, models: {}[], queryParams?: QueryParam): boolean { let state = this.routerJsState; if (!this.router.isActiveIntent(routeName, models, undefined, state)) { return false; } - if (queryParamsMustMatch && Object.keys(queryParams).length > 0) { + if (queryParams !== undefined && Object.keys(queryParams).length > 0) { let visibleQueryParams = assign({}, queryParams); this.emberRouter._prepareQueryParams(routeName, models, visibleQueryParams); diff --git a/packages/@ember/engine/index.d.ts b/packages/@ember/engine/index.d.ts index 3df5eac5b39..518a05dbebe 100644 --- a/packages/@ember/engine/index.d.ts +++ b/packages/@ember/engine/index.d.ts @@ -1,5 +1,7 @@ -import { Owner, LookupOptions, Factory, EngineInstanceOptions } from '@ember/-internals/owner'; -import EngineInstance from './instance'; +export { default as EngineInstance } from './instance'; +export function getEngineParent(instance: EngineInstance): EngineInstance | undefined; + +import { EngineInstanceOptions, Factory } from '@ember/-internals/owner'; export default class Engine { constructor(...args: any[]);