Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 18 additions & 17 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,37 +48,38 @@
"license": "MIT",
"distDir": "dist",
"peerDependencies": {
"@angular/common": "^21.0.0",
"@angular/core": "^21.0.0",
"@angular/common": "^21.0.0 || ^22.0.0",
"@angular/core": "^21.0.0 || ^22.0.0",
"@uirouter/core": "^6.1.2",
"@uirouter/rx": "^1.0.0"
},
"devDependencies": {
"@analogjs/vite-plugin-angular": "^2.2.0",
"@analogjs/vitest-angular": "^2.2.0",
"@angular-devkit/architect": "^0.2100.0",
"@angular-devkit/build-angular": "^21.0.4",
"@angular/common": "^21.0.3",
"@angular/compiler": "^21.0.3",
"@angular/compiler-cli": "^21.0.3",
"@angular/core": "^21.0.3",
"@angular/platform-browser": "^21.0.3",
"@angular/platform-browser-dynamic": "^21.0.3",
"@analogjs/vite-plugin-angular": "^2.6.2",
"@analogjs/vitest-angular": "^2.6.2",
"@angular/build": "^22.0.1",
"@angular-devkit/architect": "^0.2200.1",
"@angular-devkit/build-angular": "^22.0.1",
"@angular/common": "^22.0.1",
"@angular/compiler": "^22.0.1",
"@angular/compiler-cli": "^22.0.1",
"@angular/core": "^22.0.1",
"@angular/platform-browser": "^22.0.1",
"@angular/platform-browser-dynamic": "^22.0.1",
"@eslint/js": "^9.28.0",
"@types/node": "^24.10.1",
"@uirouter/core": "^6.1.2",
"@uirouter/publish-scripts": "2.7.0",
"@uirouter/rx": "^1.0.0",
"eslint": "^9.28.0",
"husky": "^9.0.0",
"jsdom": "^27.4.0",
"ng-packagr": "^21.0.0",
"ng-packagr": "^22.0.0",
"prettier": "^3.4.0",
"pretty-quick": "^4.0.0",
"rxjs": "~7.8.2",
"tslib": "^2.8.1",
"@eslint/js": "^9.28.0",
"eslint": "^9.28.0",
"typescript-eslint": "^8.33.0",
"typescript": "~5.9.3",
"typescript": "~6.0.3",
"typescript-eslint": "^8.62.0",
"vitest": "^4.0.8",
"zone.js": "~0.16.0"
},
Expand Down
41 changes: 14 additions & 27 deletions src/directives/uiSref.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
RawParams,
UIRouter,
extend,
Obj,
Expand All @@ -16,6 +17,8 @@ import {
ElementRef,
Renderer2,
OnChanges,
OnDestroy,
OnInit,
SimpleChanges,
HostListener,
} from '@angular/core';
Expand All @@ -40,7 +43,7 @@ export class AnchorUISref {
return this._el.nativeElement.target === '_blank';
}

update(href: string) {
update(href?: string | null) {
if (!isNullOrUndefined(href)) {
this._renderer.setProperty(this._el.nativeElement, 'href', href);
} else {
Expand Down Expand Up @@ -95,15 +98,15 @@ export class AnchorUISref {
exportAs: 'uiSref',
standalone: true,
})
export class UISref implements OnChanges {
export class UISref implements OnInit, OnDestroy {
/**
* `@Input('uiSref')` The name of the state to link to
*
* ```html
* <a uiSref="hoome">Home</a>
* ```
*/
@Input('uiSref') state: StateOrName;
@Input('uiSref') state?: StateOrName | null;

/**
* `@Input('uiParams')` The parameter values to use (as key/values)
Expand All @@ -112,7 +115,7 @@ export class UISref implements OnChanges {
* <a uiSref="book" [uiParams]="{ bookId: book.id }">Book {{ book.name }}</a>
* ```
*/
@Input('uiParams') params: any;
@Input('uiParams') params?: RawParams | null;

/**
* `@Input('uiOptions')` The transition options
Expand All @@ -121,7 +124,7 @@ export class UISref implements OnChanges {
* <a uiSref="books" [uiOptions]="{ reload: true }">Book {{ book.name }}</a>
* ```
*/
@Input('uiOptions') options: TransitionOptions;
@Input('uiOptions') options?: TransitionOptions | null;

/**
* An observable (ReplaySubject) of the state this UISref is targeting.
Expand All @@ -132,7 +135,7 @@ export class UISref implements OnChanges {
/** @internal */ private _emit = false;
/** @internal */ private _statesSub: Subscription;
/** @internal */ private _router: UIRouter;
/** @internal */ private _anchorUISref: AnchorUISref;
/** @internal */ private _anchorUISref?: AnchorUISref;
/** @internal */ private _parent: ParentUIViewInject;

constructor(
Expand All @@ -141,26 +144,10 @@ export class UISref implements OnChanges {
@Inject(UIView.PARENT_INJECT) parent: ParentUIViewInject
) {
this._router = _router;
this._anchorUISref = _anchorUISref;
this._anchorUISref = _anchorUISref ?? undefined;
this._parent = parent;

this._statesSub = _router.globals.states$.subscribe(() => this.update());
}

/** @internal */
set uiSref(val: StateOrName) {
this.state = val;
this.update();
}
/** @internal */
set uiParams(val: Obj) {
this.params = val;
this.update();
}
/** @internal */
set uiOptions(val: TransitionOptions) {
this.options = val;
this.update();
this._statesSub = _router.globals.states$?.subscribe(() => this.update()) ?? new Subscription();
}

ngOnInit() {
Expand All @@ -181,15 +168,15 @@ export class UISref implements OnChanges {
private update() {
const $state = this._router.stateService;
if (this._emit) {
const newTarget = $state.target(this.state, this.params, this.getOptions());
const newTarget = $state.target((this.state ?? null) as StateOrName, this.params ?? undefined, this.getOptions());
this.targetState$.next(newTarget);
}

if (this._anchorUISref) {
if (!this.state) {
this._anchorUISref.update(null);
} else {
const href = $state.href(this.state, this.params, this.getOptions()) || '';
const href = $state.href(this.state, this.params ?? undefined, this.getOptions()) || '';
this._anchorUISref.update(href);
}
}
Expand All @@ -215,7 +202,7 @@ export class UISref implements OnChanges {
return;
}

this._router.stateService.go(this.state, this.params, this.getOptions());
this._router.stateService.go(this.state, this.params ?? undefined, this.getOptions());
return false;
}
}
49 changes: 32 additions & 17 deletions src/directives/uiSrefStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ import {
Param,
PathUtils,
identity,
uniqR,
} from '@uirouter/core';

import { Subscription, Observable, BehaviorSubject, of, from, combineLatest, concat } from 'rxjs';
import { switchMap, map } from 'rxjs/operators';

function appendUnique<T>(acc: T[], token: T): T[] {
return acc.indexOf(token) === -1 ? acc.concat(token) : acc;
}

/** @internal */
interface TransEvt {
evt: string;
Expand Down Expand Up @@ -68,7 +71,8 @@ const pathMatches = (target: TargetState): Predicate<PathNode[]> => {
.reduce(unnestR, [])
.filter((param: Param) => Object.prototype.hasOwnProperty.call(targetParamVals, param.id));

return (path: PathNode[]) => {
return (path?: PathNode[]) => {
if (!path) return false;
const tailNode = tail(path);
if (!tailNode || tailNode.state !== state) return false;
const paramValues = PathUtils.paramValues(path);
Expand All @@ -84,7 +88,7 @@ const pathMatches = (target: TargetState): Predicate<PathNode[]> => {
* @internal
*/
function spreadToSubPaths(basePath: PathNode[], appendPath: PathNode[]): PathNode[][] {
return appendPath.map((node) => basePath.concat(PathUtils.subPath(appendPath, (n) => n.state === node.state)));
return appendPath.map((node) => basePath.concat(PathUtils.subPath(appendPath, (n) => n?.state === node.state)));
}

/**
Expand Down Expand Up @@ -189,26 +193,31 @@ export class UISrefStatus {
@Output('uiSrefStatus') uiSrefStatus = new EventEmitter<SrefStatus>(false);
/** Monitor all child components for UISref(s) */
@ContentChildren(UISref, { descendants: true })
private _srefs: QueryList<UISref>;
private _srefs!: QueryList<UISref>;

/** The current status */
status: SrefStatus;

/** @internal */ private _subscription: Subscription;
/** @internal */ private _srefChangesSub: Subscription;
/** @internal */ private _srefs$: BehaviorSubject<UISref[]>;
/** @internal */ private _subscription?: Subscription;
/** @internal */ private _srefChangesSub?: Subscription;
/** @internal */ private _srefs$?: BehaviorSubject<UISref[]>;
/** @internal */ private _globals: UIRouterGlobals;
/** @internal */ private _hostUiSref: UISref;
constructor(@Host() @Self() @Optional() _hostUiSref: UISref, _globals: UIRouterGlobals) {
/** @internal */ private _hostUiSref?: UISref;
constructor(@Host() @Self() @Optional() _hostUiSref: UISref | null, _globals: UIRouterGlobals) {
this._globals = _globals;
this._hostUiSref = _hostUiSref;
this._hostUiSref = _hostUiSref ?? undefined;
this.status = Object.assign({}, inactiveStatus);
}

ngAfterContentInit() {
const start$ = this._globals.start$;
if (!start$) {
return;
}

// Map each transition start event to a stream of:
// start -> (success|error)
const transEvents$: Observable<TransEvt> = this._globals.start$.pipe(
const transEvents$: Observable<TransEvt> = start$.pipe(
switchMap((trans: Transition) => {
const event = (evt: string) => ({ evt, trans }) as TransEvt;

Expand All @@ -223,17 +232,21 @@ export class UISrefStatus {
})
);

const withHostSref = (childrenSrefs: UISref[]) =>
childrenSrefs.concat(this._hostUiSref).filter(identity).reduce(uniqR, []);
const withHostSref = (childrenSrefs: UISref[]): UISref[] => {
const merged = this._hostUiSref ? childrenSrefs.concat(this._hostUiSref) : childrenSrefs.slice();
return merged.filter(identity).reduce<UISref[]>((acc, sref) => appendUnique(acc, sref), []);
};

// Watch the @ContentChildren UISref[] components and get their target states
this._srefs$ = new BehaviorSubject(withHostSref(this._srefs.toArray()));
this._srefChangesSub = this._srefs.changes.subscribe((srefs: QueryList<UISref>) =>
this._srefs$.next(withHostSref(srefs.toArray()))
this._srefs$?.next(withHostSref(srefs.toArray()))
);

const targetStates$: Observable<TargetState[]> = this._srefs$.pipe(
switchMap((srefs: UISref[]) => combineLatest<TargetState[]>(srefs.map((sref) => sref.targetState$)))
switchMap((srefs: UISref[]) =>
srefs.length ? combineLatest<TargetState[]>(srefs.map((sref) => sref.targetState$)) : of([] as TargetState[])
)
);

// Calculate the status of each UISref based on the transition event.
Expand All @@ -244,7 +257,7 @@ export class UISrefStatus {
return targetStates$.pipe(
map((targets: TargetState[]) => {
const statuses: SrefStatus[] = targets.map((target) => getSrefStatus(evt, target));
return statuses.reduce(mergeSrefStatus);
return statuses.reduce(mergeSrefStatus, { ...inactiveStatus, targetStates: [] });
})
);
})
Expand All @@ -256,7 +269,9 @@ export class UISrefStatus {
if (this._subscription) this._subscription.unsubscribe();
if (this._srefChangesSub) this._srefChangesSub.unsubscribe();
if (this._srefs$) this._srefs$.unsubscribe();
this._subscription = this._srefChangesSub = this._srefs$ = undefined;
this._subscription = undefined;
this._srefChangesSub = undefined;
this._srefs$ = undefined;
}

private _setStatus(status: SrefStatus) {
Expand Down
Loading