Skip to content

Commit b210723

Browse files
author
Anatoly Ostrovsky
committed
Migrate router to transitions API
1 parent e2a459a commit b210723

13 files changed

Lines changed: 672 additions & 75 deletions

File tree

src/directive/controller/controller.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ describe("ngController", () => {
139139
);
140140
window.angular
141141
.module("myModule", [])
142-
.controller("Count", function CountController($scope) {
142+
.controller("Count", function CountController() {
143143
count += 1;
144144
});
145145

src/directive/include/include.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function ngIncludeDirective(
3838
controller: () => {
3939
/* empty */
4040
},
41-
compile(_element: Element, attr: Attributes & Record<string, string>) {
41+
compile(_element: Element, attr: Attributes) {
4242
const srcExp = attr.ngInclude || attr.src;
4343

4444
const onloadExp = attr.onload || "";

src/router/directives/view-directive.spec.js

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ describe("ngView", () => {
1717
$injector,
1818
$state,
1919
$anchorScroll,
20+
animateSpies,
2021
errorLog = [];
2122

2223
const aState = {
@@ -172,6 +173,13 @@ describe("ngView", () => {
172173
$state = _$state_;
173174
$anchorScroll = _$anchorScroll_;
174175
});
176+
177+
const $animate = $injector.get("$animate");
178+
179+
animateSpies = {
180+
enter: spyOn($animate, "enter").and.callThrough(),
181+
leave: spyOn($animate, "leave").and.callThrough(),
182+
};
175183
});
176184

177185
describe("linking ng-directive", () => {
@@ -437,6 +445,159 @@ describe("ngView", () => {
437445
expect(log).toContain("animLeave;");
438446
});
439447

448+
it("should activate routed views inside document.startViewTransition", async () => {
449+
await wait(350);
450+
elem.innerHTML = "<div><ng-view></ng-view></div>";
451+
$compile(elem)(scope);
452+
453+
let committedInsideCallback = false;
454+
455+
spyOn(document, "startViewTransition").and.callFake((callback) => {
456+
expect(elem.querySelector("ng-view").textContent).toBe("");
457+
458+
callback();
459+
460+
expect(elem.querySelector("ng-view").textContent).toBe(aState.template);
461+
committedInsideCallback = true;
462+
463+
return {
464+
updateCallbackDone: Promise.resolve(),
465+
finished: Promise.resolve(),
466+
};
467+
});
468+
469+
await $state.transitionTo("a");
470+
await wait(20);
471+
472+
expect(document.startViewTransition).toHaveBeenCalledTimes(1);
473+
expect(committedInsideCallback).toBeTrue();
474+
});
475+
476+
it("should not start a view transition for an ignored same-state transition", async () => {
477+
await wait(350);
478+
elem.innerHTML = "<div><ng-view></ng-view></div>";
479+
$compile(elem)(scope);
480+
481+
spyOn(document, "startViewTransition").and.callFake((callback) => {
482+
callback();
483+
484+
return {
485+
updateCallbackDone: Promise.resolve(),
486+
finished: Promise.resolve(),
487+
};
488+
});
489+
490+
await $state.transitionTo("a");
491+
await wait(20);
492+
493+
document.startViewTransition.calls.reset();
494+
495+
await $state.transitionTo("a");
496+
await wait(20);
497+
498+
expect(document.startViewTransition).not.toHaveBeenCalled();
499+
expect(elem.querySelector("ng-view").textContent).toBe(aState.template);
500+
});
501+
502+
it("should not use $animate enter or leave for routed view replacement", async () => {
503+
elem.innerHTML = "<div><ng-view></ng-view></div>";
504+
$compile(elem)(scope);
505+
506+
animateSpies.enter.calls.reset();
507+
animateSpies.leave.calls.reset();
508+
509+
await $state.transitionTo("a");
510+
await wait(20);
511+
await $state.transitionTo("b");
512+
await wait(20);
513+
514+
expect(animateSpies.enter).not.toHaveBeenCalled();
515+
expect(animateSpies.leave).not.toHaveBeenCalled();
516+
});
517+
518+
it("should not start a view transition for detached routed views", async () => {
519+
await wait(350);
520+
const detachedScope = scope.$new();
521+
const detachedElement = createElementFromHTML(
522+
"<div><ng-view></ng-view></div>",
523+
);
524+
525+
$compile(detachedElement)(detachedScope);
526+
527+
spyOn(document, "startViewTransition").and.callThrough();
528+
529+
await $state.transitionTo("a");
530+
await wait(20);
531+
532+
expect(document.startViewTransition).not.toHaveBeenCalled();
533+
expect(detachedElement.querySelector("ng-view").textContent).toBe(
534+
aState.template,
535+
);
536+
537+
detachedScope.$destroy();
538+
dealoc(detachedElement);
539+
});
540+
541+
it("should not wait for the visual view transition to finish before resolving the route transition", async () => {
542+
await wait(350);
543+
elem.innerHTML = "<div><ng-view></ng-view></div>";
544+
$compile(elem)(scope);
545+
546+
let finishVisualTransition;
547+
const finished = new Promise((resolve) => {
548+
finishVisualTransition = resolve;
549+
});
550+
551+
spyOn(document, "startViewTransition").and.callFake((callback) => {
552+
callback();
553+
554+
return {
555+
updateCallbackDone: Promise.resolve(),
556+
finished,
557+
};
558+
});
559+
560+
await $state.transitionTo("a");
561+
562+
expect(elem.querySelector("ng-view").textContent).toBe(aState.template);
563+
expect(document.startViewTransition).toHaveBeenCalledTimes(1);
564+
565+
finishVisualTransition();
566+
await wait(20);
567+
});
568+
569+
it("should commit overlapping routed view changes without waiting for the active view transition", async () => {
570+
await wait(350);
571+
elem.innerHTML = "<div><ng-view></ng-view></div>";
572+
$compile(elem)(scope);
573+
574+
let finishFirstTransition;
575+
const firstTransitionFinished = new Promise((resolve) => {
576+
finishFirstTransition = resolve;
577+
});
578+
579+
spyOn(document, "startViewTransition").and.callFake((callback) => {
580+
callback();
581+
582+
return {
583+
updateCallbackDone: Promise.resolve(),
584+
finished: firstTransitionFinished,
585+
};
586+
});
587+
588+
await $state.transitionTo("a");
589+
await wait(20);
590+
expect(elem.querySelector("ng-view").textContent).toBe(aState.template);
591+
592+
await $state.transitionTo("b");
593+
await wait(20);
594+
expect(elem.querySelector("ng-view").textContent).toBe(bState.template);
595+
expect(document.startViewTransition).toHaveBeenCalledTimes(1);
596+
597+
finishFirstTransition();
598+
await wait(20);
599+
});
600+
440601
it("should do ngClass animations", async () => {
441602
scope.classOn = false;
442603
elem.innerHTML =

src/router/directives/view-directive.ts

Lines changed: 15 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,6 @@ import {
1717
isInstanceOf,
1818
isString,
1919
} from "../../shared/utils.ts";
20-
import {
21-
createLazyAnimate,
22-
getAnimateForNode,
23-
} from "../../animations/lazy-animate.ts";
2420
import { ResolveContext } from "../resolve/resolve-context.ts";
2521
import { ViewConfig } from "../state/views.ts";
2622
import {
@@ -41,11 +37,6 @@ type PromiseResolvers<T> = {
4137
reject: (reason?: any) => void;
4238
};
4339

44-
type Renderer = {
45-
enter: (element: HTMLElement, target: HTMLElement, cb: () => void) => void;
46-
leave: (element: HTMLElement, cb: () => void) => void;
47-
};
48-
4940
function getFirstElementFromClone(
5041
clone: Node | Node[] | NodeList | DocumentFragment | null | undefined,
5142
): HTMLElement | null {
@@ -211,46 +202,18 @@ const controllerLastParamsChangedTransition = new WeakMap<
211202
* ```
212203
*/
213204

214-
ViewDirective.$inject = [_view, _state, _injector, _anchorScroll, _interpolate];
205+
ViewDirective.$inject = [_view, _state, _anchorScroll, _interpolate];
215206

216207
/**
217208
* Renders and updates the currently active view configuration.
218209
*/
219210
export function ViewDirective(
220211
$view: ViewService,
221212
$state: ng.StateService,
222-
$injector: ng.InjectorService,
223213
$anchorScroll: ng.AnchorScrollService,
224214
$interpolate: ng.InterpolateService,
225215
): ng.Directive {
226216
void $state;
227-
const getAnimate = createLazyAnimate($injector);
228-
229-
function getRenderer(): Renderer {
230-
return {
231-
enter(element: HTMLElement, target: HTMLElement, cb: () => void): void {
232-
const animate = getAnimateForNode(getAnimate, element);
233-
234-
if (animate) {
235-
animate.enter(element, null, target).done(cb);
236-
} else {
237-
target.after(element);
238-
cb();
239-
}
240-
},
241-
242-
leave(element: HTMLElement, cb: () => void): void {
243-
const animate = getAnimateForNode(getAnimate, element);
244-
245-
if (animate) {
246-
animate.leave(element).done(cb);
247-
} else {
248-
removeElement(element);
249-
cb();
250-
}
251-
},
252-
};
253-
}
254217

255218
const rootData: ActiveNgViewRootData = {
256219
$cfg: { viewDecl: { $context: $view._rootViewContext() as ViewContext } },
@@ -272,7 +235,6 @@ export function ViewDirective(
272235
) {
273236
const onloadExp = attrs.onload || "",
274237
autoScrollExp = attrs.autoscroll,
275-
renderer = getRenderer(),
276238
inherited =
277239
(getInheritedData($element, "$ngView") as
278240
| ActiveNgViewRootData
@@ -284,8 +246,6 @@ export function ViewDirective(
284246
) as ng.InterpolationFunction
285247
)(scope) || "$default";
286248

287-
let previousEl: HTMLElement | null = null;
288-
289249
let currentEl: HTMLElement | null = null;
290250

291251
let currentScope: ng.Scope | null = null;
@@ -299,6 +259,7 @@ export function ViewDirective(
299259

300260
const activeNgView: ActiveNgView = {
301261
id: directive.count++, // Global sequential ID for ng-view tags added to DOM
262+
element: $element,
302263
name, // ng-view name, retained internally for nested view matching
303264
fqn: parentFqn ? `${parentFqn}.${name}` : name, // fully qualified name, describes location in DOM
304265
config: null, // The active ViewConfig loaded for this ng-view
@@ -357,12 +318,8 @@ export function ViewDirective(
357318
scope.$on("$destroy", function () {
358319
unregister();
359320
});
360-
function cleanupLastView(): void {
361-
if (previousEl) {
362-
removeElement(previousEl);
363-
previousEl = null;
364-
}
365321

322+
function cleanupLastView(): void {
366323
if (currentScope) {
367324
currentScope.$destroy();
368325
currentScope = null;
@@ -373,11 +330,8 @@ export function ViewDirective(
373330
| NgViewAnimData
374331
| undefined;
375332

376-
renderer.leave(currentEl, function () {
377-
_viewData?.$$animLeave.resolve();
378-
previousEl = null;
379-
});
380-
previousEl = currentEl;
333+
removeElement(currentEl);
334+
_viewData?.$$animLeave.resolve();
381335
currentEl = null;
382336
}
383337
}
@@ -420,23 +374,20 @@ export function ViewDirective(
420374
setCacheData(node, "$ngViewAnim", $ngViewAnim);
421375
setCacheData(node, "$ngView", $ngViewData);
422376
});
423-
renderer.enter(elementClone, $element, () => {
424-
animEnter.resolve(undefined);
425-
426-
if (currentScope)
427-
currentScope.$emit("$viewContentAnimationEnded");
428-
429-
if (
430-
(isDefined(autoScrollExp) && !autoScrollExp) ||
431-
(autoScrollExp && scope.$eval(autoScrollExp))
432-
) {
433-
$anchorScroll(elementClone);
434-
}
435-
});
377+
$element.after(elementClone);
378+
animEnter.resolve(undefined);
436379
cleanupLastView();
380+
381+
if (
382+
(isDefined(autoScrollExp) && !autoScrollExp) ||
383+
(autoScrollExp && scope.$eval(autoScrollExp))
384+
) {
385+
$anchorScroll(elementClone);
386+
}
437387
}) as unknown as HTMLElement;
438388

439389
currentScope = newScope;
390+
currentScope.$emit("$viewContentAnimationEnded");
440391
/**
441392
* Fired once the view is **loaded**, *after* the DOM is rendered.
442393
*

0 commit comments

Comments
 (0)