diff --git a/apps/responsive-app-e2e/src/integration/app.spec.ts b/apps/responsive-app-e2e/src/integration/app.spec.ts index 642b3d5..1d8efe7 100644 --- a/apps/responsive-app-e2e/src/integration/app.spec.ts +++ b/apps/responsive-app-e2e/src/integration/app.spec.ts @@ -39,7 +39,7 @@ describe('responsive-app', () => { }); it('should show links and info on desktop', () => { - getNavigationLinks().should('have.length', 5); + getNavigationLinks().should('have.length', 6); cy.get('h2').contains('Select user profile!'); }); @@ -51,7 +51,7 @@ describe('responsive-app', () => { it('should show links on desktop profile', () => { cy.visit('/3'); - getNavigationLinks().should('have.length', 5); + getNavigationLinks().should('have.length', 6); }); it('should not show back button on desktop profile', () => { diff --git a/apps/responsive-app/src/app/app.component.html b/apps/responsive-app/src/app/app.component.html index 483439c..5a24143 100644 --- a/apps/responsive-app/src/app/app.component.html +++ b/apps/responsive-app/src/app/app.component.html @@ -48,6 +48,14 @@ linkActive="mat-list-single-selected-option" >Profile 5 + Profile 6 (Async) diff --git a/apps/responsive-app/src/app/app.module.ts b/apps/responsive-app/src/app/app.module.ts index dda93be..16c29c4 100644 --- a/apps/responsive-app/src/app/app.module.ts +++ b/apps/responsive-app/src/app/app.module.ts @@ -14,6 +14,7 @@ import { MediaDirective } from './use-media/use-media.directive'; import { SelectUserComponent } from './select-user/select-user.component'; import { UserProfileComponent } from './user-profile/user-profile.component'; import { UserListComponent } from './user-list/user-list.component'; +import { ValueToAsyncValuePipe } from './value-to-async-value.pipe'; @NgModule({ declarations: [ @@ -22,6 +23,7 @@ import { UserListComponent } from './user-list/user-list.component'; SelectUserComponent, UserProfileComponent, UserListComponent, + ValueToAsyncValuePipe, ], imports: [ BrowserModule, diff --git a/apps/responsive-app/src/app/value-to-async-value.pipe.ts b/apps/responsive-app/src/app/value-to-async-value.pipe.ts new file mode 100644 index 0000000..566f5ba --- /dev/null +++ b/apps/responsive-app/src/app/value-to-async-value.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { delay } from 'rxjs/operators'; + +@Pipe({ + name: 'valueToAsyncValue', +}) +export class ValueToAsyncValuePipe implements PipeTransform { + transform(value: string): Observable { + return of(value).pipe(delay(10)); + } +} diff --git a/libs/router/src/lib/link-active.directive.ts b/libs/router/src/lib/link-active.directive.ts index c3eb183..7a9c0bb 100644 --- a/libs/router/src/lib/link-active.directive.ts +++ b/libs/router/src/lib/link-active.directive.ts @@ -15,6 +15,7 @@ import { LinkTo } from './link-to.directive'; import { Router } from './router.service'; import { combineLatest, of, Subject, Subscription } from 'rxjs'; import { map, mapTo, startWith, takeUntil } from 'rxjs/operators'; +import { filterNullable } from './operators/filter-nullable.operator'; export interface LinkActiveOptions { exact: boolean; @@ -39,8 +40,8 @@ export class LinkActive implements AfterContentInit, OnDestroy, OnChanges { @ContentChildren(LinkTo, { descendants: true }) public links: QueryList< LinkTo >; - @Input('linkActive') activeClass = 'active'; - @Input() activeOptions: LinkActiveOptions; + @Input('linkActive') activeClass: string | null = 'active'; + @Input() activeOptions?: LinkActiveOptions | null; private _activeOptions: LinkActiveOptions = { exact: true }; private _destroy$ = new Subject(); private _linksSub!: Subscription; @@ -83,16 +84,20 @@ export class LinkActive implements AfterContentInit, OnDestroy, OnChanges { .map((link) => link.hrefUpdated.pipe( startWith(link.linkHref), - mapTo(link.linkHref) + mapTo(link.linkHref), + filterNullable() ) ) : []; + const link$ = this.link ? this.link.hrefUpdated.pipe( startWith(this.link.linkHref), - mapTo(this.link.linkHref) + mapTo(this.link.linkHref), + filterNullable() ) : of(''); + const router$ = this.router.url$.pipe( map((path) => this.router.getExternalUrl(path || '/')) ); @@ -109,14 +114,10 @@ export class LinkActive implements AfterContentInit, OnDestroy, OnChanges { checkActive(linkHrefs: string[], path: string) { const active = linkHrefs.reduce((isActive, current) => { const [href] = current.split('?'); - if (this._activeOptions.exact) { - isActive = isActive ? isActive : href === path; - } else { - isActive = isActive ? isActive : path.startsWith(href); + return isActive ? isActive : href === path; } - - return isActive; + return isActive ? isActive : path.startsWith(href); }, false); this.updateClasses(active); diff --git a/libs/router/src/lib/link-to.directive.ts b/libs/router/src/lib/link-to.directive.ts index ec6e512..1e6128d 100644 --- a/libs/router/src/lib/link-to.directive.ts +++ b/libs/router/src/lib/link-to.directive.ts @@ -23,19 +23,22 @@ const DEFAULT_TARGET = '_self'; @Directive({ selector: 'a[linkTo]' }) export class LinkTo { @Input() target = DEFAULT_TARGET; - @HostBinding('href') linkHref: string; + @HostBinding('href') linkHref?: string | null; - @Input() set linkTo(href: string) { + @Input() set linkTo(href: string | null | undefined) { + if (href === null || href === undefined) { + return; + } this._href = href; this._updateHref(); } - @Input() set queryParams(params: Params) { + @Input() set queryParams(params: Params | null | undefined) { this._query = params; this._updateHref(); } - @Input() set fragment(hash: string) { + @Input() set fragment(hash: string | null | undefined) { this._hash = hash; this._updateHref(); } @@ -54,6 +57,9 @@ export class LinkTo { */ @HostListener('click', ['$event']) onClick(event: any) { + if (!this._href) { + return; + } if (!this._comboClick(event) && this.target === DEFAULT_TARGET) { this.router.go(this._href, this._query, this._hash); diff --git a/libs/router/src/lib/operators/filter-nullable.operator.ts b/libs/router/src/lib/operators/filter-nullable.operator.ts new file mode 100644 index 0000000..b4417cd --- /dev/null +++ b/libs/router/src/lib/operators/filter-nullable.operator.ts @@ -0,0 +1,9 @@ +import { Observable } from 'rxjs'; +import { filter } from 'rxjs/operators'; + +export function isNotNullable(it: T): it is NonNullable { + return it !== null && it !== undefined; +} + +export const filterNullable = () => (source: Observable) => + source.pipe(filter(isNotNullable));