Skip to content

Commit 8482098

Browse files
authored
Merge pull request #19223 from emberjs/engine-link-to
[BUGFIX] `<LinkTo>` should link within the engine when used inside one
2 parents 6840201 + 56af48b commit 8482098

File tree

7 files changed

+477
-36
lines changed

7 files changed

+477
-36
lines changed

packages/@ember/-internals/glimmer/lib/components/link-to.ts

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
*/
44

55
import { alias, computed } from '@ember/-internals/metal';
6+
import { getOwner } from '@ember/-internals/owner';
7+
import RouterState from '@ember/-internals/routing/lib/system/router_state';
68
import { isSimpleClick } from '@ember/-internals/views';
79
import { assert, warn } from '@ember/debug';
10+
import { EngineInstance, getEngineParent } from '@ember/engine';
811
import { flaggedInstrument } from '@ember/instrumentation';
912
import { inject as injectService } from '@ember/service';
1013
import { DEBUG } from '@glimmer/env';
@@ -487,6 +490,13 @@ const LinkComponent = EmberComponent.extend({
487490
init() {
488491
this._super(...arguments);
489492

493+
assert(
494+
'You attempted to use the <LinkTo> component within a routeless engine, this is not supported. ' +
495+
'If you are using the ember-engines addon, use the <LinkToExternal> component instead. ' +
496+
'See https://ember-engines.com/docs/links for more info.',
497+
!this._isEngine || this._engineMountPoint !== undefined
498+
);
499+
490500
// Map desired event name to invoke function
491501
let { eventName } = this;
492502
this.on(eventName, this, this._invoke);
@@ -497,9 +507,17 @@ const LinkComponent = EmberComponent.extend({
497507
_currentRouterState: alias('_routing.currentState'),
498508
_targetRouterState: alias('_routing.targetState'),
499509

510+
_isEngine: computed(function (this: any) {
511+
return getEngineParent(getOwner(this) as EngineInstance) !== undefined;
512+
}),
513+
514+
_engineMountPoint: computed(function (this: any) {
515+
return (getOwner(this) as EngineInstance).mountPoint;
516+
}),
517+
500518
_route: computed('route', '_currentRouterState', function computeLinkToComponentRoute(this: any) {
501519
let { route } = this;
502-
return route === UNDEFINED ? this._currentRoute : route;
520+
return this._namespaceRoute(route === UNDEFINED ? this._currentRoute : route);
503521
}),
504522

505523
_models: computed('model', 'models', function computeLinkToComponentModels(this: any) {
@@ -608,7 +626,7 @@ const LinkComponent = EmberComponent.extend({
608626
}
609627
),
610628

611-
_isActive(routerState: any) {
629+
_isActive(routerState: RouterState): boolean {
612630
if (this.loading) {
613631
return false;
614632
}
@@ -619,25 +637,17 @@ const LinkComponent = EmberComponent.extend({
619637
return currentWhen;
620638
}
621639

622-
let isCurrentWhenSpecified = Boolean(currentWhen);
640+
let { _models: models, _routing: routing } = this;
623641

624-
if (isCurrentWhenSpecified) {
625-
currentWhen = currentWhen.split(' ');
642+
if (typeof currentWhen === 'string') {
643+
return currentWhen
644+
.split(' ')
645+
.some((route) =>
646+
routing.isActiveForRoute(models, undefined, this._namespaceRoute(route), routerState)
647+
);
626648
} else {
627-
currentWhen = [this._route];
628-
}
629-
630-
let { _models: models, _query: query, _routing: routing } = this;
631-
632-
for (let i = 0; i < currentWhen.length; i++) {
633-
if (
634-
routing.isActiveForRoute(models, query, currentWhen[i], routerState, isCurrentWhenSpecified)
635-
) {
636-
return true;
637-
}
649+
return routing.isActiveForRoute(models, this._query, this._route, routerState);
638650
}
639-
640-
return false;
641651
},
642652

643653
transitioningIn: computed(
@@ -664,6 +674,18 @@ const LinkComponent = EmberComponent.extend({
664674
}
665675
),
666676

677+
_namespaceRoute(route: string): string {
678+
let { _engineMountPoint: mountPoint } = this;
679+
680+
if (mountPoint === undefined) {
681+
return route;
682+
} else if (route === 'application') {
683+
return mountPoint;
684+
} else {
685+
return `${mountPoint}.${route}`;
686+
}
687+
},
688+
667689
/**
668690
Event handler that invokes the link, activating the associated route.
669691

packages/@ember/-internals/glimmer/tests/integration/application/engine-test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -875,7 +875,7 @@ moduleFor(
875875

876876
["@test query params don't have stickiness by default between model"](assert) {
877877
assert.expect(1);
878-
let tmpl = '{{#link-to "blog.category" 1337}}Category 1337{{/link-to}}';
878+
let tmpl = '{{#link-to "category" 1337}}Category 1337{{/link-to}}';
879879
this.setupAppAndRoutableEngine();
880880
this.additionalEngineRegistrations(function () {
881881
this.register('template:category', compile(tmpl));
@@ -895,7 +895,7 @@ moduleFor(
895895
) {
896896
assert.expect(2);
897897
let tmpl =
898-
'{{#link-to "blog.author" 1337 class="author-1337"}}Author 1337{{/link-to}}{{#link-to "blog.author" 1 class="author-1"}}Author 1{{/link-to}}';
898+
'{{#link-to "author" 1337 class="author-1337"}}Author 1337{{/link-to}}{{#link-to "author" 1 class="author-1"}}Author 1{{/link-to}}';
899899
this.setupAppAndRoutableEngine();
900900
this.additionalEngineRegistrations(function () {
901901
this.register('template:author', compile(tmpl));

packages/@ember/-internals/glimmer/tests/integration/components/link-to/routing-angle-test.js

Lines changed: 212 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1-
import { moduleFor, ApplicationTestCase, runLoopSettled, runTask } from 'internal-test-helpers';
1+
import {
2+
ApplicationTestCase,
3+
ModuleBasedTestResolver,
4+
moduleFor,
5+
runLoopSettled,
6+
runTask,
7+
} from 'internal-test-helpers';
28
import Controller, { inject as injectController } from '@ember/controller';
39
import { A as emberA, RSVP } from '@ember/-internals/runtime';
410
import { alias } from '@ember/-internals/metal';
511
import { subscribe, reset } from '@ember/instrumentation';
612
import { Route, NoneLocation } from '@ember/-internals/routing';
713
import { EMBER_IMPROVED_INSTRUMENTATION } from '@ember/canary-features';
14+
import Engine from '@ember/engine';
815
import { DEBUG } from '@glimmer/env';
16+
import { compile } from '../../../utils/helpers';
917

1018
// IE includes the host name
1119
function normalizeUrl(url) {
@@ -348,6 +356,209 @@ moduleFor(
348356
);
349357
});
350358
}
359+
360+
async ['@test Using <LinkTo> inside a non-routable engine errors'](assert) {
361+
this.add(
362+
'engine:not-routable',
363+
class NotRoutableEngine extends Engine {
364+
Resolver = ModuleBasedTestResolver;
365+
366+
init() {
367+
super.init(...arguments);
368+
this.register(
369+
'template:application',
370+
compile(`<LinkTo @route='about'>About</LinkTo>`, {
371+
moduleName: 'non-routable/templates/application.hbs',
372+
})
373+
);
374+
}
375+
}
376+
);
377+
378+
this.addTemplate('index', `{{mount 'not-routable'}}`);
379+
380+
await assert.rejectsAssertion(
381+
this.visit('/'),
382+
'You attempted to use the <LinkTo> component within a routeless engine, this is not supported. ' +
383+
'If you are using the ember-engines addon, use the <LinkToExternal> component instead. ' +
384+
'See https://ember-engines.com/docs/links for more info.'
385+
);
386+
}
387+
388+
async ['@test Using <LinkTo> inside a routable engine link within the engine'](assert) {
389+
this.add(
390+
'engine:routable',
391+
class RoutableEngine extends Engine {
392+
Resolver = ModuleBasedTestResolver;
393+
394+
init() {
395+
super.init(...arguments);
396+
this.register(
397+
'template:application',
398+
compile(
399+
`
400+
<h2 id='engine-layout'>Routable Engine</h2>
401+
{{outlet}}
402+
<LinkTo @route='application' id='engine-application-link'>Engine Appliction</LinkTo>
403+
`,
404+
{
405+
moduleName: 'routable/templates/application.hbs',
406+
}
407+
)
408+
);
409+
this.register(
410+
'template:index',
411+
compile(
412+
`
413+
<h3 class='engine-home'>Engine Home</h3>
414+
<LinkTo @route='about' id='engine-about-link'>Engine About</LinkTo>
415+
<LinkTo @route='index' id='engine-self-link'>Engine Self</LinkTo>
416+
`,
417+
{
418+
moduleName: 'routable/templates/index.hbs',
419+
}
420+
)
421+
);
422+
this.register(
423+
'template:about',
424+
compile(
425+
`
426+
<h3 class='engine-about'>Engine About</h3>
427+
<LinkTo @route='index' id='engine-home-link'>Engine Home</LinkTo>
428+
<LinkTo @route='about' id='engine-self-link'>Engine Self</LinkTo>
429+
`,
430+
{
431+
moduleName: 'routable/templates/about.hbs',
432+
}
433+
)
434+
);
435+
}
436+
}
437+
);
438+
439+
this.router.map(function () {
440+
this.mount('routable');
441+
});
442+
443+
this.add('route-map:routable', function () {
444+
this.route('about');
445+
});
446+
447+
this.addTemplate(
448+
'application',
449+
`
450+
<h1 id='application-layout'>Application</h1>
451+
{{outlet}}
452+
<LinkTo @route='application' id='application-link'>Appliction</LinkTo>
453+
<LinkTo @route='routable' id='engine-link'>Engine</LinkTo>
454+
`
455+
);
456+
457+
await this.visit('/');
458+
459+
assert.equal(this.$('#application-layout').length, 1, 'The application layout was rendered');
460+
assert.strictEqual(this.$('#engine-layout').length, 0, 'The engine layout was not rendered');
461+
assert.equal(this.$('#application-link.active').length, 1, 'The application link is active');
462+
assert.equal(this.$('#engine-link:not(.active)').length, 1, 'The engine link is not active');
463+
464+
assert.equal(this.$('h3.home').length, 1, 'The application index page is rendered');
465+
assert.equal(this.$('#self-link.active').length, 1, 'The application index link is active');
466+
assert.equal(
467+
this.$('#about-link:not(.active)').length,
468+
1,
469+
'The application about link is not active'
470+
);
471+
472+
await this.click('#about-link');
473+
474+
assert.equal(this.$('#application-layout').length, 1, 'The application layout was rendered');
475+
assert.strictEqual(this.$('#engine-layout').length, 0, 'The engine layout was not rendered');
476+
assert.equal(this.$('#application-link.active').length, 1, 'The application link is active');
477+
assert.equal(this.$('#engine-link:not(.active)').length, 1, 'The engine link is not active');
478+
479+
assert.equal(this.$('h3.about').length, 1, 'The application about page is rendered');
480+
assert.equal(this.$('#self-link.active').length, 1, 'The application about link is active');
481+
assert.equal(
482+
this.$('#home-link:not(.active)').length,
483+
1,
484+
'The application home link is not active'
485+
);
486+
487+
await this.click('#engine-link');
488+
489+
assert.equal(this.$('#application-layout').length, 1, 'The application layout was rendered');
490+
assert.equal(this.$('#engine-layout').length, 1, 'The engine layout was rendered');
491+
assert.equal(this.$('#application-link.active').length, 1, 'The application link is active');
492+
assert.equal(this.$('#engine-link.active').length, 1, 'The engine link is active');
493+
assert.equal(
494+
this.$('#engine-application-link.active').length,
495+
1,
496+
'The engine application link is active'
497+
);
498+
499+
assert.equal(this.$('h3.engine-home').length, 1, 'The engine index page is rendered');
500+
assert.equal(this.$('#engine-self-link.active').length, 1, 'The engine index link is active');
501+
assert.equal(
502+
this.$('#engine-about-link:not(.active)').length,
503+
1,
504+
'The engine about link is not active'
505+
);
506+
507+
await this.click('#engine-about-link');
508+
509+
assert.equal(this.$('#application-layout').length, 1, 'The application layout was rendered');
510+
assert.equal(this.$('#engine-layout').length, 1, 'The engine layout was rendered');
511+
assert.equal(this.$('#application-link.active').length, 1, 'The application link is active');
512+
assert.equal(this.$('#engine-link.active').length, 1, 'The engine link is active');
513+
assert.equal(
514+
this.$('#engine-application-link.active').length,
515+
1,
516+
'The engine application link is active'
517+
);
518+
519+
assert.equal(this.$('h3.engine-about').length, 1, 'The engine about page is rendered');
520+
assert.equal(this.$('#engine-self-link.active').length, 1, 'The engine about link is active');
521+
assert.equal(
522+
this.$('#engine-home-link:not(.active)').length,
523+
1,
524+
'The engine home link is not active'
525+
);
526+
527+
await this.click('#engine-application-link');
528+
529+
assert.equal(this.$('#application-layout').length, 1, 'The application layout was rendered');
530+
assert.equal(this.$('#engine-layout').length, 1, 'The engine layout was rendered');
531+
assert.equal(this.$('#application-link.active').length, 1, 'The application link is active');
532+
assert.equal(this.$('#engine-link.active').length, 1, 'The engine link is active');
533+
assert.equal(
534+
this.$('#engine-application-link.active').length,
535+
1,
536+
'The engine application link is active'
537+
);
538+
539+
assert.equal(this.$('h3.engine-home').length, 1, 'The engine index page is rendered');
540+
assert.equal(this.$('#engine-self-link.active').length, 1, 'The engine index link is active');
541+
assert.equal(
542+
this.$('#engine-about-link:not(.active)').length,
543+
1,
544+
'The engine about link is not active'
545+
);
546+
547+
await this.click('#application-link');
548+
549+
assert.equal(this.$('#application-layout').length, 1, 'The application layout was rendered');
550+
assert.strictEqual(this.$('#engine-layout').length, 0, 'The engine layout was not rendered');
551+
assert.equal(this.$('#application-link.active').length, 1, 'The application link is active');
552+
assert.equal(this.$('#engine-link:not(.active)').length, 1, 'The engine link is not active');
553+
554+
assert.equal(this.$('h3.home').length, 1, 'The application index page is rendered');
555+
assert.equal(this.$('#self-link.active').length, 1, 'The application index link is active');
556+
assert.equal(
557+
this.$('#about-link:not(.active)').length,
558+
1,
559+
'The application about link is not active'
560+
);
561+
}
351562
}
352563
);
353564

0 commit comments

Comments
 (0)