Skip to content

Commit

Permalink
feat(router): Add option to skip the first view transition (#51825)
Browse files Browse the repository at this point in the history
This commit adds an option to the view transition feature to skip the first transition.
This option is not available in RouterModule.forRoot.

resolves #51815

PR Close #51825
  • Loading branch information
atscott authored and dylhunn committed Sep 22, 2023
1 parent 3c6258c commit 86e9146
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 53 deletions.
2 changes: 1 addition & 1 deletion goldens/public-api/router/index.md
Expand Up @@ -1116,7 +1116,7 @@ export function withPreloading(preloadingStrategy: Type<PreloadingStrategy>): Pr
export function withRouterConfig(options: RouterConfigOptions): RouterConfigurationFeature;

// @public
export function withViewTransitions(): ViewTransitionsFeature;
export function withViewTransitions(options?: ViewTransitionsFeatureOptions): ViewTransitionsFeature;

// (No @packageDocumentation comment for this package)

Expand Down
13 changes: 10 additions & 3 deletions packages/router/src/provide_router.ts
Expand Up @@ -22,7 +22,7 @@ import {ROUTER_SCROLLER, RouterScroller} from './router_scroller';
import {ActivatedRoute} from './router_state';
import {UrlSerializer} from './url_tree';
import {afterNextNavigation} from './utils/navigations';
import {CREATE_VIEW_TRANSITION, createViewTransition} from './utils/view_transition';
import {CREATE_VIEW_TRANSITION, createViewTransition, VIEW_TRANSITION_OPTIONS, ViewTransitionsFeatureOptions} from './utils/view_transition';


/**
Expand Down Expand Up @@ -728,8 +728,15 @@ export function withComponentInputBinding(): ComponentInputBindingFeature {
* @see https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API
* @experimental
*/
export function withViewTransitions(): ViewTransitionsFeature {
const providers = [{provide: CREATE_VIEW_TRANSITION, useValue: createViewTransition}];
export function withViewTransitions(options?: ViewTransitionsFeatureOptions):
ViewTransitionsFeature {
const providers = [
{provide: CREATE_VIEW_TRANSITION, useValue: createViewTransition},
{
provide: VIEW_TRANSITION_OPTIONS,
useValue: {skipNextTransition: !!options?.skipInitialTransition}
},
];
return routerFeature(RouterFeatureKind.ViewTransitionsFeature, providers);
}

Expand Down
23 changes: 21 additions & 2 deletions packages/router/src/utils/view_transition.ts
Expand Up @@ -13,6 +13,23 @@ import {afterNextRender, InjectionToken, Injector, NgZone} from '@angular/core';

export const CREATE_VIEW_TRANSITION =
new InjectionToken<typeof createViewTransition>(ngDevMode ? 'view transition helper' : '');
export const VIEW_TRANSITION_OPTIONS =
new InjectionToken<{skipNextTransition: boolean}>(ngDevMode ? 'view transition options' : '');

/**
* Options to configure the View Transitions integration in the Router.
*
* @experimental
* @publicApi
* @see withViewTransitions
*/
export interface ViewTransitionsFeatureOptions {
/**
* Skips the very first call to `startViewTransition`. This can be useful for disabling the
* animation during the application's initial loading phase.
*/
skipInitialTransition?: boolean;
}

/**
* A helper function for using browser view transitions. This function skips the call to
Expand All @@ -21,10 +38,12 @@ export const CREATE_VIEW_TRANSITION =
* @returns A Promise that resolves when the view transition callback begins.
*/
export function createViewTransition(injector: Injector): Promise<void> {
const transitionOptions = injector.get(VIEW_TRANSITION_OPTIONS);
const document = injector.get(DOCUMENT);
// Create promises outside the Angular zone to avoid causing extra change detections
return injector.get(NgZone).runOutsideAngular(() => {
const document = injector.get(DOCUMENT);
if (!document.startViewTransition) {
if (!document.startViewTransition || transitionOptions.skipNextTransition) {
transitionOptions.skipNextTransition = false;
return Promise.resolve();
}

Expand Down
47 changes: 0 additions & 47 deletions packages/router/test/bootstrap.spec.ts
Expand Up @@ -533,53 +533,6 @@ describe('bootstrap', () => {
expect(window.removeEventListener).toHaveBeenCalledWith('popstate', jasmine.any(Function));
expect(window.removeEventListener).toHaveBeenCalledWith('hashchange', jasmine.any(Function));
});

it('should have the correct event order when using view transitions', async () => {
@Component({
selector: 'component-a',
template: `a`,
standalone: true,
})
class ComponentA {
}
@Component({
selector: 'component-b',
template: `b`,
standalone: true,
})
class ComponentB {
}
@NgModule({
imports: [
BrowserModule, ComponentA, ComponentB,
RouterModule.forRoot(
[
{path: '', pathMatch: 'full', redirectTo: '/a'},
{path: 'a', component: ComponentA},
{path: 'b', component: ComponentB},
],
{
enableViewTransitions: true,
})
],
declarations: [RootCmp],
bootstrap: [RootCmp],
providers: [...testProviders],
})
class TestModule {
}


const res = await platformBrowserDynamic([]).bootstrapModule(TestModule);
const router = res.injector.get(Router);
const eventLog = [] as Event[];
router.events.subscribe(e => {
eventLog.push(e);
});

await router.navigateByUrl('/b');
expect(eventLog[eventLog.length - 1]).toBeInstanceOf(NavigationEnd);
});
}

it('can schedule a navigation from the NavigationEnd event #37460', (done) => {
Expand Down
82 changes: 82 additions & 0 deletions packages/router/test/view_transitions.spec.ts
@@ -0,0 +1,82 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {DOCUMENT} from '@angular/common';
import {Component, destroyPlatform} from '@angular/core';
import {bootstrapApplication} from '@angular/platform-browser';
import {withBody} from '@angular/private/testing';
import {Event, NavigationEnd, provideRouter, Router, withDisabledInitialNavigation, withViewTransitions} from '@angular/router';


describe('view transitions', () => {
if (isNode) {
it('are not available in node environment', () => {});
return;
}

beforeEach(destroyPlatform);
afterEach(destroyPlatform);
it('should create injector where ambient providers shadow explicit providers',
withBody('<test-app></test-app>', async () => {
@Component({
selector: 'test-app',
standalone: true,
template: ``,
})
class App {
}

const appRef = await bootstrapApplication(App, {
providers: [provideRouter(
[{path: '**', component: App}],
withDisabledInitialNavigation(),
withViewTransitions({skipInitialTransition: true}),
)]
});

const doc = appRef.injector.get(DOCUMENT);
if (!doc.startViewTransition) {
return;
}

const viewTransitionSpy = spyOn(doc, 'startViewTransition').and.callThrough();
await appRef.injector.get(Router).navigateByUrl('/a');
expect(viewTransitionSpy).not.toHaveBeenCalled();
await appRef.injector.get(Router).navigateByUrl('/b');
expect(viewTransitionSpy).toHaveBeenCalled();
}));

it('should have the correct event order when using view transitions',
withBody('<app></app>', async () => {
@Component({
selector: 'component-b',
template: `b`,
standalone: true,
})
class ComponentB {
}


@Component({standalone: true, template: '', selector: 'app'})
class App {
}


const res = await bootstrapApplication(App, {
providers: [provideRouter([{path: 'b', component: ComponentB}], withViewTransitions())]
});
const router = res.injector.get(Router);
const eventLog = [] as Event[];
router.events.subscribe(e => {
eventLog.push(e);
});

await router.navigateByUrl('/b');
expect(eventLog[eventLog.length - 1]).toBeInstanceOf(NavigationEnd);
}));
});

0 comments on commit 86e9146

Please sign in to comment.