diff --git a/apps/codelab/src/app/codelabs/angular/menu-route.service.spec.ts b/apps/codelab/src/app/codelabs/angular/menu-route.service.spec.ts new file mode 100644 index 000000000..50d2c7dce --- /dev/null +++ b/apps/codelab/src/app/codelabs/angular/menu-route.service.spec.ts @@ -0,0 +1,55 @@ +import { TestBed } from '@angular/core/testing'; + +import { MenuRouteService } from './menu-route.service'; +import { ActivatedRoute } from '@angular/router'; +import { MENU_ROUTES, MenuRoutes } from '../../common'; + +describe('MenuRouteService', () => { + const menuRoutes: MenuRoutes = [ + { + path: 'previouslesson', + prod: true + }, + { + path: 'currentlesson', + prod: true + }, + { + path: 'nextlesson', + prod: true + } + ]; + + const activatedRouteStub = { + snapshot: { + pathFromRoot: [ + { + routeConfig: menuRoutes[1] + } + ] + } + } as ActivatedRoute; + + beforeEach(() => + TestBed.configureTestingModule({ + providers: [{ provide: MENU_ROUTES, useValue: menuRoutes }] + }) + ); + + it('should be created', () => { + const service: MenuRouteService = TestBed.inject(MenuRouteService); + expect(service).toBeTruthy(); + }); + + it('getPreviousLink should return previouslesson', () => { + const service: MenuRouteService = TestBed.inject(MenuRouteService); + const previousLink = service.getPreviousLink(activatedRouteStub); + expect(previousLink).toEqual('../../' + menuRoutes[0].path); + }); + + it('getNextLink should return nextlesson', () => { + const service: MenuRouteService = TestBed.inject(MenuRouteService); + const nextLink = service.getNextLink(activatedRouteStub); + expect(nextLink).toEqual('../../' + menuRoutes[2].path); + }); +}); diff --git a/apps/codelab/src/app/codelabs/angular/menu-route.service.ts b/apps/codelab/src/app/codelabs/angular/menu-route.service.ts new file mode 100644 index 000000000..bd3b87cd5 --- /dev/null +++ b/apps/codelab/src/app/codelabs/angular/menu-route.service.ts @@ -0,0 +1,59 @@ +import { Injectable, Inject } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { MENU_ROUTES, MenuRoute } from '../../common'; + +@Injectable({ + providedIn: 'root' +}) +export class MenuRouteService { + constructor(@Inject(MENU_ROUTES) private readonly menuRoutes) {} + + getPreviousLink(activeRoute: ActivatedRoute): string { + const index = this.getCurrentIndex(activeRoute); + if (index > 0) { + return this.getMenuRoutePathByIndex(index - 1); + } + return ''; + } + + getNextLink(activeRoute: ActivatedRoute): string { + const index = this.getCurrentIndex(activeRoute); + if (index < this.menuRoutes.length - 1) { + return this.getMenuRoutePathByIndex(index + 1); + } + return ''; + } + + private getCurrentIndex(activeRoute: ActivatedRoute): number { + // TODO: figure out a way to inject the ActivatedRoute instead of parameter + // This method gets the index of the current menuRoute. Ideally we should be able + // to inject in the ActivatedRoute in the constructor. However we noticed that + // probably because this is a service, activatedRoute has the value when the + // service is constructed and not the current activated route. We are using a + // workaround now which expects the calling method to pass the current activated + // route. Fix this to use DI. + const config = activeRoute.snapshot.pathFromRoot + .map(a => a.routeConfig) + .find(r => r && (r as MenuRoute).prod); + if (config == null) { + return -1; + } + return this.menuRoutes.findIndex(c => c.path === config.path); + } + + private getMenuRouteByIndex(index: number) { + return this.menuRoutes[index]; + } + + private getMenuRoutePathByIndex(index: number): string { + const indexRoute = this.getMenuRouteByIndex(index); + if (indexRoute != null) { + let path = indexRoute.path; + if (path) { + path = '../../' + path; + } + return path; + } + return ''; + } +} diff --git a/apps/codelab/src/app/common.ts b/apps/codelab/src/app/common.ts index fe2d8a183..17f69241b 100644 --- a/apps/codelab/src/app/common.ts +++ b/apps/codelab/src/app/common.ts @@ -3,7 +3,7 @@ import { Route } from '@angular/router'; export type MenuRoutes = MenuRoute[]; -interface MenuRoute extends Route { +export interface MenuRoute extends Route { name?: string; description?: string; page?: string; diff --git a/apps/codelab/src/app/components/slides/closing-slide/codelab-closing-slide.component.spec.ts b/apps/codelab/src/app/components/slides/closing-slide/codelab-closing-slide.component.spec.ts index 9cbbd0035..c01e7e3d2 100644 --- a/apps/codelab/src/app/components/slides/closing-slide/codelab-closing-slide.component.spec.ts +++ b/apps/codelab/src/app/components/slides/closing-slide/codelab-closing-slide.component.spec.ts @@ -1,13 +1,50 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - import { CodelabClosingSlideComponent } from './codelab-closing-slide.component'; +import { ActivatedRoute } from '@angular/router'; +import { SlidesDeckComponent } from '@codelab/slides/src/lib/deck/deck.component'; +import { MenuRoutes } from '../../../common'; +import { MenuRouteService } from '../../../codelabs/angular/menu-route.service'; describe('CodelabClosingSlideComponent', () => { let component: CodelabClosingSlideComponent; let fixture: ComponentFixture; + const menuRouteService = { + getNextLink: function() { + return 'nextlesson'; + } + }; + + const slidesDeckComponentStub = { + setNext: jasmine.createSpy('setNext') + }; + + const menuRoutes: MenuRoutes = [ + { + path: 'previouslesson', + prod: true + }, + { + path: 'currentlesson', + prod: true + }, + { + path: 'nextlesson', + prod: true + } + ]; + + const activatedRouteStub = { + snapshot: { pathFromRoot: [{ routeConfig: menuRoutes[1] }] } + }; + beforeEach(async(() => { TestBed.configureTestingModule({ + providers: [ + { provide: ActivatedRoute, useValue: activatedRouteStub }, + { provide: MenuRouteService, useValue: menuRouteService }, + { provide: SlidesDeckComponent, useValue: slidesDeckComponentStub } + ], declarations: [CodelabClosingSlideComponent] }).compileComponents(); })); diff --git a/apps/codelab/src/app/components/slides/closing-slide/codelab-closing-slide.component.ts b/apps/codelab/src/app/components/slides/closing-slide/codelab-closing-slide.component.ts index 064a1c8b8..9df71f74d 100644 --- a/apps/codelab/src/app/components/slides/closing-slide/codelab-closing-slide.component.ts +++ b/apps/codelab/src/app/components/slides/closing-slide/codelab-closing-slide.component.ts @@ -1,16 +1,26 @@ import { Component, Input, OnInit } from '@angular/core'; +import { SlidesDeckComponent } from '@codelab/slides/src/lib/deck/deck.component'; +import { MenuRouteService } from '../../../codelabs/angular/menu-route.service'; +import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'codelab-closing-slide', templateUrl: './codelab-closing-slide.component.html', styleUrls: ['./codelab-closing-slide.component.css'] }) -export class CodelabClosingSlideComponent implements OnInit { +export class CodelabClosingSlideComponent { @Input() header: String; @Input() body: String; @Input() footer: String; - constructor() {} - - ngOnInit() {} + constructor( + private readonly activeRoute: ActivatedRoute, + private readonly menuRouteService: MenuRouteService, + private readonly presentation: SlidesDeckComponent + ) { + if (this.presentation != null) { + const nextLink = this.menuRouteService.getNextLink(this.activeRoute); + this.presentation.setNext(nextLink); + } + } } diff --git a/apps/codelab/src/app/components/slides/title-slide/title-slide.component.spec.ts b/apps/codelab/src/app/components/slides/title-slide/title-slide.component.spec.ts index 26f3c42f2..43145b025 100644 --- a/apps/codelab/src/app/components/slides/title-slide/title-slide.component.spec.ts +++ b/apps/codelab/src/app/components/slides/title-slide/title-slide.component.spec.ts @@ -1,15 +1,66 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; import { By } from '@angular/platform-browser'; import { TitleSlideComponent } from './title-slide.component'; import { CodelabRippleAnimationComponent } from './ripple-animation/codelab-ripple-animation.component'; +import { SlidesDeckComponent } from '@codelab/slides/src/lib/deck/deck.component'; +import { MenuRouteService } from '../../../codelabs/angular/menu-route.service'; +import { MenuRoutes } from '../../../common'; +import { ActivatedRoute } from '@angular/router'; describe('TitleSlideComponent', () => { let component: TitleSlideComponent; let fixture: ComponentFixture; + const menuRouteService = { + getPreviousLink: function() { + return 'previouslesson'; + } + }; + + const slidesDeckComponentStub = { + previousLink: '', + nextLink: '', + setPrevious: jasmine.createSpy('setPrevious') + }; + + const menuRoutes: MenuRoutes = [ + { + path: 'previouslesson', + prod: true + }, + { + path: 'currentlesson', + prod: true + }, + { + path: 'nextlesson', + prod: true + } + ]; + + const activatedRouteStub = { + snapshot: { + pathFromRoot: [ + { + routeConfig: menuRoutes[1] + } + ] + } + }; + beforeEach(async(() => { TestBed.configureTestingModule({ + providers: [ + { provide: ActivatedRoute, useValue: activatedRouteStub }, + { provide: MenuRouteService, useValue: menuRouteService }, + { + provide: SlidesDeckComponent, + useFactory: () => slidesDeckComponentStub + } + ], + imports: [RouterTestingModule], declarations: [CodelabRippleAnimationComponent, TitleSlideComponent] }).compileComponents(); })); @@ -24,6 +75,10 @@ describe('TitleSlideComponent', () => { expect(component).toBeTruthy(); }); + it('should call setPrevious', () => { + expect(slidesDeckComponentStub.setPrevious).toHaveBeenCalled(); + }); + it('should render a title', () => { component.title = 'awesome title'; fixture.detectChanges(); diff --git a/apps/codelab/src/app/components/slides/title-slide/title-slide.component.ts b/apps/codelab/src/app/components/slides/title-slide/title-slide.component.ts index aa76cc2b4..fc06ca9a5 100644 --- a/apps/codelab/src/app/components/slides/title-slide/title-slide.component.ts +++ b/apps/codelab/src/app/components/slides/title-slide/title-slide.component.ts @@ -1,4 +1,7 @@ import { Component, Input } from '@angular/core'; +import { SlidesDeckComponent } from '@codelab/slides/src/lib/deck/deck.component'; +import { MenuRouteService } from '../../../codelabs/angular/menu-route.service'; +import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'codelab-title-slide', @@ -9,4 +12,17 @@ export class TitleSlideComponent { @Input() title: string; @Input() description: string; @Input() prereqs: string; + + constructor( + private readonly activeRoute: ActivatedRoute, + private readonly menuRouteService: MenuRouteService, + private readonly presentation: SlidesDeckComponent + ) { + if (this.presentation != null) { + const previousLink = this.menuRouteService.getPreviousLink( + this.activeRoute + ); + this.presentation.setPrevious(previousLink); + } + } } diff --git a/libs/slides/src/lib/arrows/slides-arrows.component.ts b/libs/slides/src/lib/arrows/slides-arrows.component.ts index 9d974670b..df07efce3 100644 --- a/libs/slides/src/lib/arrows/slides-arrows.component.ts +++ b/libs/slides/src/lib/arrows/slides-arrows.component.ts @@ -1,4 +1,4 @@ -import { Component, HostBinding } from '@angular/core'; +import { Component, HostBinding, Input } from '@angular/core'; import { SlidesDeckComponent } from '../deck/deck.component'; /** diff --git a/libs/slides/src/lib/deck/deck.component.ts b/libs/slides/src/lib/deck/deck.component.ts index 7c4c469a6..68b1118f6 100644 --- a/libs/slides/src/lib/deck/deck.component.ts +++ b/libs/slides/src/lib/deck/deck.component.ts @@ -11,7 +11,7 @@ import { TemplateRef } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; @Component({ selector: 'slide-deck', @@ -27,13 +27,16 @@ export class SlidesDeckComponent { @Output() slideAdded = new EventEmitter<{ index: number; id: string }>(); @HostBinding('class.has-milestone') hasMilestone = false; private milestone = ''; + private previousLink: string; + private nextLink: string; constructor( private readonly cdr: ChangeDetectorRef, - @Optional() private readonly route: ActivatedRoute + private readonly router: Router, + @Optional() private readonly activeRoute: ActivatedRoute ) { - if (route) { - this.milestone = route.snapshot.queryParams.milestone; + if (activeRoute) { + this.milestone = activeRoute.snapshot.queryParams.milestone; this.hasMilestone = !!this.milestone; } } @@ -51,18 +54,36 @@ export class SlidesDeckComponent { } nextSlide() { - this.goToSlide(this.activeSlideIndex + 1); + if (this.activeSlideIndex + 1 < this.slides.length) { + this.goToSlide(this.activeSlideIndex + 1); + } else if (this.nextLink) { + this.router.navigate([this.nextLink], { relativeTo: this.activeRoute }); + } } previousSlide() { - this.goToSlide(this.activeSlideIndex - 1); + if (this.activeSlideIndex > 0) { + this.goToSlide(this.activeSlideIndex - 1); + } else if (this.previousLink) { + this.router.navigate([this.previousLink], { + relativeTo: this.activeRoute + }); + } } canGoNext(): boolean { - return this.activeSlideIndex + 1 < this.slides.length; + return this.activeSlideIndex + 1 < this.slides.length || !!this.nextLink; } canGoPrevious(): boolean { - return this.activeSlideIndex > 0; + return this.activeSlideIndex > 0 || !!this.previousLink; + } + + public setPrevious(previousLink) { + this.previousLink = previousLink; + } + + public setNext(nextLink) { + this.nextLink = nextLink; } }