diff --git a/libs/components/pages/src/index.ts b/libs/components/pages/src/index.ts
index 96b13a8143..07d9a56525 100644
--- a/libs/components/pages/src/index.ts
+++ b/libs/components/pages/src/index.ts
@@ -5,6 +5,7 @@ export {
SkyActionHubNeedsAttentionClickHandlerArgs,
} from './lib/modules/action-hub/types/action-hub-needs-attention-click-handler';
export { SkyLinkListModule } from './lib/modules/link-list/link-list.module';
+export { SkyModalLinkListModule } from './lib/modules/modal-link-list/modal-link-list.module';
export { SkyNeedsAttentionModule } from './lib/modules/needs-attention/needs-attention.module';
export { SkyActionHubNeedsAttentionInput } from './lib/modules/action-hub/types/action-hub-needs-attention-input';
export { SkyPageLink } from './lib/modules/action-hub/types/page-link';
diff --git a/libs/components/pages/src/lib/modules/action-hub/action-hub.component.html b/libs/components/pages/src/lib/modules/action-hub/action-hub.component.html
index 9d864b7b27..271b06865e 100644
--- a/libs/components/pages/src/lib/modules/action-hub/action-hub.component.html
+++ b/libs/components/pages/src/lib/modules/action-hub/action-hub.component.html
@@ -26,7 +26,7 @@
diff --git a/libs/components/pages/src/lib/modules/modal-link-list/modal-link-list.component.html b/libs/components/pages/src/lib/modules/modal-link-list/modal-link-list.component.html
index 2b5ff769fa..f58dcc6561 100644
--- a/libs/components/pages/src/lib/modules/modal-link-list/modal-link-list.component.html
+++ b/libs/components/pages/src/lib/modules/modal-link-list/modal-link-list.component.html
@@ -1,11 +1,11 @@
-
-@if (links === 'loading') {
+
+@if (links() === 'loading') {
} @else {
- @if (linksArray.length > 0) {
+ @if (linksArray().length > 0) {
- @for (link of linksArray; track link) {
+ @for (link of linksArray(); track link) {
-
@if (link | linkAs: 'skyHref') {
@@ -50,6 +50,6 @@
- {{ title }}
+ {{ headingText() }}
diff --git a/libs/components/pages/src/lib/modules/modal-link-list/modal-link-list.component.spec.ts b/libs/components/pages/src/lib/modules/modal-link-list/modal-link-list.component.spec.ts
index 912161a0c0..605d7f801b 100644
--- a/libs/components/pages/src/lib/modules/modal-link-list/modal-link-list.component.spec.ts
+++ b/libs/components/pages/src/lib/modules/modal-link-list/modal-link-list.component.spec.ts
@@ -1,5 +1,6 @@
import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { SkyAppTestUtility } from '@skyux-sdk/testing';
import { SkyLogService } from '@skyux/core';
import { SkyModalService } from '@skyux/modals';
@@ -43,10 +44,18 @@ describe('SkyModalLinkListComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
- component.openModal({
- label: 'Link 1',
- modal: { component: MockStandaloneComponent, config: {} },
- });
+ fixture.componentRef.setInput('links', [
+ {
+ label: 'Link 1',
+ modal: { component: MockStandaloneComponent, config: {} },
+ },
+ ]);
+ fixture.detectChanges();
+ const link = Array.from(
+ fixture.nativeElement.querySelectorAll('button.sky-link-list-item'),
+ );
+ expect(link.length).toBe(1);
+ SkyAppTestUtility.fireDomEvent(link[0], 'click');
expect(openModalSpy).toHaveBeenCalledWith(MockStandaloneComponent, {});
});
@@ -54,17 +63,27 @@ describe('SkyModalLinkListComponent', () => {
const logger = TestBed.inject(SkyLogService);
spyOn(logger, 'deprecated');
expect(component).toBeTruthy();
- component.openModal({
- label: 'Link 1',
- modal: { component: MockComponent, config: {} },
- });
+ fixture.componentRef.setInput('links', [
+ {
+ label: 'Link 1',
+ modal: { component: MockComponent, config: {} },
+ },
+ ]);
+ fixture.detectChanges();
+ const link = Array.from(
+ fixture.nativeElement.querySelectorAll('button.sky-link-list-item'),
+ );
+ expect(link.length).toBe(1);
+ SkyAppTestUtility.fireDomEvent(link[0], 'click');
expect(logger.deprecated).toHaveBeenCalled();
expect(openModalSpy).toHaveBeenCalledWith(MockComponent, {});
});
it('should handle empty input', () => {
expect(component).toBeTruthy();
- component.links = undefined;
- expect(component.links).toBeUndefined();
+ fixture.componentRef.setInput('links', undefined);
+ fixture.detectChanges();
+ expect(component.links()).toBeUndefined();
+ expect(fixture.nativeElement.querySelector('ul.sky-link-list')).toBeFalsy();
});
});
diff --git a/libs/components/pages/src/lib/modules/modal-link-list/modal-link-list.component.ts b/libs/components/pages/src/lib/modules/modal-link-list/modal-link-list.component.ts
index 2010fcb6c7..c7f869bdaa 100644
--- a/libs/components/pages/src/lib/modules/modal-link-list/modal-link-list.component.ts
+++ b/libs/components/pages/src/lib/modules/modal-link-list/modal-link-list.component.ts
@@ -1,37 +1,48 @@
-import { Component, Input, inject, isStandalone } from '@angular/core';
+import {
+ Component,
+ computed,
+ inject,
+ input,
+ isStandalone,
+} from '@angular/core';
import { SkyLogService } from '@skyux/core';
import { SkyModalLegacyService, SkyModalService } from '@skyux/modals';
import { SkyPageModalLink } from '../action-hub/types/page-modal-link';
import { SkyPageModalLinksInput } from '../action-hub/types/page-modal-links-input';
+/**
+ * A component that displays a list of links such as within a `` component.
+ */
@Component({
selector: 'sky-modal-link-list',
templateUrl: './modal-link-list.component.html',
styleUrls: ['./modal-link-list.component.scss'],
})
export class SkyModalLinkListComponent {
- @Input()
- public set links(value: SkyPageModalLinksInput | undefined) {
- this.#_links = value;
- this.linksArray = Array.isArray(value) ? value : [];
- }
-
- public get links(): SkyPageModalLinksInput | undefined {
- return this.#_links;
- }
+ /**
+ * Option to pass links as an array of `SkyPageModalLink` objects or `'loading'` to display a loading indicator.
+ */
+ public readonly links = input();
- @Input()
- public title: string | undefined;
+ /**
+ * The text to display as the list's heading.
+ */
+ public readonly headingText = input();
- public linksArray: SkyPageModalLink[] = [];
-
- #_links: SkyPageModalLinksInput | undefined;
+ protected readonly linksArray = computed(() => {
+ const links = this.links();
+ if (Array.isArray(links)) {
+ return links;
+ } else {
+ return [];
+ }
+ });
readonly #logger = inject(SkyLogService, { optional: true });
readonly #modalSvc = inject(SkyModalService);
- public openModal(link: SkyPageModalLink): void {
+ protected openModal(link: SkyPageModalLink): void {
const modal = link.modal;
if (modal) {
diff --git a/libs/components/pages/src/lib/modules/page/page.module.ts b/libs/components/pages/src/lib/modules/page/page.module.ts
index 4c57d7dc17..36af50b3b3 100644
--- a/libs/components/pages/src/lib/modules/page/page.module.ts
+++ b/libs/components/pages/src/lib/modules/page/page.module.ts
@@ -3,6 +3,8 @@ import { NgModule } from '@angular/core';
import { SkyLinkListRecentlyAccessedComponent } from '../link-list-recently-accessed/link-list-recently-accessed.component';
import { SkyLinkListItemComponent } from '../link-list/link-list-item.component';
import { SkyLinkListComponent } from '../link-list/link-list.component';
+import { SkyLinkListModule } from '../link-list/link-list.module';
+import { SkyModalLinkListModule } from '../modal-link-list/modal-link-list.module';
import { SkyPageHeaderModule } from '../page-header/page-header.module';
import { SkyPageContentComponent } from './page-content.component';
@@ -12,15 +14,19 @@ import { SkyPageComponent } from './page.component';
@NgModule({
declarations: [SkyPageComponent, SkyPageContentComponent],
imports: [
+ SkyLinkListModule,
SkyPageLinksComponent,
SkyLinkListComponent,
SkyLinkListItemComponent,
SkyLinkListRecentlyAccessedComponent,
+ SkyModalLinkListModule,
],
exports: [
SkyLinkListComponent,
SkyLinkListItemComponent,
SkyLinkListRecentlyAccessedComponent,
+ SkyLinkListModule,
+ SkyModalLinkListModule,
SkyPageComponent,
SkyPageHeaderModule,
SkyPageContentComponent,
diff --git a/libs/components/router/src/lib/modules/href/fixtures/href-resolver-fixture.service.ts b/libs/components/router/src/lib/modules/href/fixtures/href-resolver-fixture.service.ts
index f4aa435560..c6b23e52dc 100644
--- a/libs/components/router/src/lib/modules/href/fixtures/href-resolver-fixture.service.ts
+++ b/libs/components/router/src/lib/modules/href/fixtures/href-resolver-fixture.service.ts
@@ -39,6 +39,8 @@ export class HrefResolverFixtureService implements SkyHrefResolver {
});
} else if (url.startsWith('error://')) {
throw new Error(`Error while resolving ${url}`);
+ } else if (url.startsWith('reject://')) {
+ return Promise.reject(`Unable to resolve ${url}`);
} else {
return Promise.resolve({
url,
diff --git a/libs/components/router/src/lib/modules/href/href.directive.spec.ts b/libs/components/router/src/lib/modules/href/href.directive.spec.ts
index db5fa4eb82..a2544d6a90 100644
--- a/libs/components/router/src/lib/modules/href/href.directive.spec.ts
+++ b/libs/components/router/src/lib/modules/href/href.directive.spec.ts
@@ -290,6 +290,18 @@ describe('SkyHref Directive', () => {
tick();
expect(element?.getAttribute('hidden')).toBeNull();
+
+ fixture.componentInstance.dynamicLink = 'reject://simple-app/example/page';
+ fixture.detectChanges();
+ tick();
+
+ expect(element?.getAttribute('hidden')).toBe('hidden');
+
+ fixture.componentInstance.dynamicLink = '1bb-nav://simple-app/fixed';
+ fixture.detectChanges();
+ tick();
+
+ expect(element?.getAttribute('hidden')).toBeNull();
}));
it('should handle the else parameter', fakeAsync(() => {
diff --git a/libs/components/router/src/lib/modules/href/href.directive.ts b/libs/components/router/src/lib/modules/href/href.directive.ts
index 013467385d..d3a69bf08e 100644
--- a/libs/components/router/src/lib/modules/href/href.directive.ts
+++ b/libs/components/router/src/lib/modules/href/href.directive.ts
@@ -1,18 +1,22 @@
import {
ApplicationRef,
ChangeDetectorRef,
+ DestroyRef,
Directive,
ElementRef,
EventEmitter,
HostListener,
Input,
- Optional,
Output,
Renderer2,
+ inject,
} from '@angular/core';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Router, UrlTree } from '@angular/router';
import { SkyAppConfig, SkyAppRuntimeConfigParamsProvider } from '@skyux/config';
+import { Subscription, catchError, finalize, from, of } from 'rxjs';
+
import { SkyHrefResolverService } from './href-resolver.service';
import { SkyHref } from './types/href';
import { SkyHrefChange } from './types/href-change';
@@ -41,7 +45,7 @@ export class SkyHrefDirective {
this.#_skyHref = skyHref ? skyHref.join('/') : '';
}
- void this.#checkRouteAccess();
+ this.#checkRouteAccess();
}
public get skyHref(): string {
@@ -92,34 +96,18 @@ export class SkyHrefDirective {
#_skyHref = '';
#_skyHrefElse: 'hide' | 'unlink' | undefined = 'hide';
- #router: Router;
- #renderer: Renderer2;
- #element: ElementRef;
- #skyAppConfig: SkyAppConfig | undefined;
- #paramsProvider: SkyAppRuntimeConfigParamsProvider | undefined;
- #hrefResolver: SkyHrefResolverService | undefined;
- #applicationRef: ApplicationRef | undefined;
- #changeDetectorRef: ChangeDetectorRef | undefined;
-
- constructor(
- router: Router,
- renderer: Renderer2,
- element: ElementRef,
- @Optional() skyAppConfig?: SkyAppConfig,
- @Optional() paramsProvider?: SkyAppRuntimeConfigParamsProvider,
- @Optional() hrefResolver?: SkyHrefResolverService,
- @Optional() applicationRef?: ApplicationRef,
- @Optional() changeDetectorRef?: ChangeDetectorRef,
- ) {
- this.#router = router;
- this.#renderer = renderer;
- this.#element = element;
- this.#skyAppConfig = skyAppConfig;
- this.#paramsProvider = paramsProvider;
- this.#hrefResolver = hrefResolver;
- this.#applicationRef = applicationRef;
- this.#changeDetectorRef = changeDetectorRef;
- }
+ readonly #router = inject(Router);
+ readonly #renderer = inject(Renderer2);
+ readonly #element = inject(ElementRef);
+ readonly #skyAppConfig = inject(SkyAppConfig, { optional: true });
+ readonly #paramsProvider = inject(SkyAppRuntimeConfigParamsProvider, {
+ optional: true,
+ });
+ readonly #hrefResolver = inject(SkyHrefResolverService, { optional: true });
+ readonly #applicationRef = inject(ApplicationRef);
+ readonly #changeDetectorRef = inject(ChangeDetectorRef);
+ readonly #destroyRef = inject(DestroyRef);
+ #checkingSubscription: Subscription | undefined;
@HostListener('click', [
'$event.button',
@@ -179,29 +167,37 @@ export class SkyHrefDirective {
this.skyHrefChange.emit({ userHasAccess: !change.hidden });
}
- async #checkRouteAccess(): Promise {
+ #checkRouteAccess(): void {
this.#route = {
url: this.skyHref,
userHasAccess: false,
};
- /* istanbul ignore else */
if (this.#hrefResolver && this.skyHref) {
this.#applyChanges(this.#getChanges());
+ this.#checkingSubscription?.unsubscribe();
try {
- const route = await this.#hrefResolver.resolveHref({
- url: this.skyHref,
- });
-
- this.#route = { ...route };
- this.#applyChanges(this.#getChanges());
-
- /* istanbul ignore else */
- if (this.#changeDetectorRef && this.#applicationRef) {
- this.#changeDetectorRef.markForCheck();
- this.#applicationRef.tick();
- }
+ this.#checkingSubscription = from(
+ this.#hrefResolver.resolveHref({ url: this.skyHref }),
+ )
+ .pipe(
+ catchError(() =>
+ of({
+ url: this.skyHref,
+ userHasAccess: false,
+ } as SkyHref),
+ ),
+ takeUntilDestroyed(this.#destroyRef),
+ finalize(() => {
+ this.#applyChanges(this.#getChanges());
+ }),
+ )
+ .subscribe((route) => {
+ this.#route = { ...route };
+ this.#changeDetectorRef.markForCheck();
+ this.#applicationRef.tick();
+ });
} catch {
- this.#applyChanges(this.#getChanges());
+ // Unable to resolve.
}
} else {
// no resolver or skyHref is falsy