Skip to content

Latest commit

 

History

History
576 lines (422 loc) · 15 KB

CHANGELOG.md

File metadata and controls

576 lines (422 loc) · 15 KB

Router Component Store changelog

15.0.0-rc.1 (2024-09-17)

Refactors

  • Replace StrictRouteParams with simple type (#325)
  • Replace StrictRouteData with simple type (#326)

15.0.0-rc.0 (2024-09-03)

Features

  • LocalRouterStore matches ActivatedRoute more closely (#309)
    • Use ActivatedRoute to serialize the router state for the local router store implementation (LocalRouterStore)
    • LocalRouterStore.currentRoute$ matches ActivatedRoute.snapshot
  • Remove optional type parameter from RouterStore#selectRouteData (#316)
  • Replace MinimalRouteData with StrictRouteData (#319)
  • Change RouterStore#routeData$ and MinimalActivatedRouteSnapshot#data types from Data to StrictRouteData (#319)
  • Use strict and immutable route parameters (#319, #321)
  • Use strict and immutable query parameters (#320)

BREAKING CHANGES

LocalRouterStore.currentRoute$ matches ActivatedRoute.snapshot

This change in implementation will make the local router store more closely match ActivatedRoute while the global router store matches NgRx Router Store selectors. Through complex route configurations, the router store implementations are exercised to identify edge case differences between them and any breaking changes introduced to the local router store.

BEFORE:

// URL: /parent/child/grandchild

@Component({
  /* (...) */
  providers: [provideLocalRouterStore()],
})
export class ChildComponent implements OnInit {
  #route = inject(ActivatedRoute);
  #routerStore = inject(RouterStore);

  ngOnInit() {
    const currentRouteSnapshot = this.#route.snapshot;
    console.log(currentRouteSnapshot.routeConfig.path);
    // -> "child"
    console.log(currentRouteSnapshot.url[0].path);
    // -> "child"

    firstValueFrom(this.#routerStore.currentRoute$).then((currentRoute) => {
      console.log(currentRoute.routeConfig.path);
      // -> "grandchild"
      console.log(currentRoute.url[0].path);
      // -> "grandchild"
    });
  }
}

AFTER:

// URL: /parent/child/grandchild

@Component({
  /* (...) */
  providers: [provideLocalRouterStore()],
})
export class ChildComponent implements OnInit {
  #route = inject(ActivatedRoute);
  #routerStore = inject(RouterStore);

  ngOnInit() {
    const currentRouteSnapshot = this.#route.snapshot;
    console.log(currentRouteSnapshot.routeConfig.path);
    // -> "child"
    console.log(currentRouteSnapshot.url[0].path);
    // -> "child"

    firstValueFrom(this.#routerStore.currentRoute$).then((currentRoute) => {
      console.log(currentRoute.routeConfig.path);
      // -> "child"
      console.log(currentRoute.url[0].path);
      // -> "child"
    });
  }
}

The type parameter is removed from RouterStore#selectRouteData for stricter typing and to enforce coercion

BEFORE:

// heroes.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';
@Component({
  // (...)
})
export class HeroesComponent {
  #routerStore = inject(RouterStore);
  limit$ = this.#routerStore.selectRouteData<number>('limit');
}

AFTER:

// heroes.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';
@Component({
  // (...)
})
export class HeroesComponent {
  #routerStore = inject(RouterStore);
  limit$ = this.#routerStore.selectRouteData('limit').pipe(x => Number(x));

The RouterStore#routeData$ selector emits StrictRouteData instead of Data

BEFORE:

// heroes.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';
@Component({
  // (...)
})
export class HeroesComponent {
  #routerStore = inject(RouterStore);
  limit$: Observable<number> = this.#routerStore.routeData$.pipe(
    map((routeData) => routeData['limit'])
  );
}

AFTER:

// heroes.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';
@Component({
  // (...)
})
export class HeroesComponent {
  #routerStore = inject(RouterStore);
  limit$: Observable<number> = this.#routerStore.routeData$.pipe(
    map(routeData => routeData['limit']),
    map(x => Number(x))
  );

RouterStore#routeParams$ and MinimalActivatedRouteSnapshot#params use StrictRouteData instead of Params. Members are read-only and of type string | undefined instead of any

TypeScript will fail to compile application code that has assumed a route type parameter type other than string | undefined.

BEFORE:

// heroes.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';

@Component({
  // (...)
})
export class DashboardComponent {
  #routerStore = inject(RouterStore);

  limit$: Observable<number> = this.#routerStore.routeParams$.pipe(
    map((params) => params['limit'])
  );
}

AFTER:

// heroes.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';

@Component({
  // (...)
})
export class DashboardComponent {
  #routerStore = inject(RouterStore);

  limit$: Observable<number> = this.#routerStore.routeParams$.pipe(
    map((params) => Number(params['limit'] ?? 10))
  );
}

StrictRouteData members are now read-only

TypeScript will fail to compile application code that mutates route data data structures.

BEFORE:

// heroes.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';

@Component({
  // (...)
})
export class DashboardComponent {
  #routerStore = inject(RouterStore);

  limit$: Observable<number> = this.#routerStore.routeData$.pipe(
    map((data) => {
      data['limit'] = Number(data['limit']);

      return data;
    }),
    map((data) => data['limit'])
  );
}

AFTER:

// heroes.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';

@Component({
  // (...)
})
export class DashboardComponent {
  #routerStore = inject(RouterStore);

  limit$: Observable<number> = this.#routerStore.routeData$.pipe(
    map((data) => Number(data['limit']))
  );
}

RouterStore#queryParams$ and MinimalActivatedRouteSnapshot#queryParams use StrictRouteParams instead of Params. Members are read-only and of type string | undefined instead of any

TypeScript will fail to compile application code that has assumed a query parameter type other than string | undefined.

BEFORE:

// heroes.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';

@Component({
  // (...)
})
export class DashboardComponent {
  #routerStore = inject(RouterStore);

  limit$: Observable<number> = this.#routerStore.queryParams$.pipe(
    map((params) => params['limit'])
  );
}

AFTER:

// heroes.component.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';

@Component({
  // (...)
})
export class DashboardComponent {
  #routerStore = inject(RouterStore);

  limit$: Observable<number> = this.#routerStore.queryParams$.pipe(
    map((params) => Number(params['limit'] ?? 10))
  );
}

Compatibility

To avoid compatibility issues, we now require the same RxJS peer dependency as NgRx ComponentStore, namely at least RxJS version 7.5 (#311).

  • Require Angular 15.0
  • Require @ngrx/component-store 15.0
  • Require RxJS 7.5
  • Require TypeScript 4.8

0.3.2 (2023-01-03)

Performance optimizations

  • Ignore non-essential router events when serializing the router state. Only NavigationStart, RoutesRecognized, NavigationEnd, NavigationCancel, and NavigationError events are essential.

0.3.1 (2023-01-03)

Features

  • Add factory for selecting router events of specific types: RouterStore#selectRouterEvents

0.3.0 (2022-12-19)

Features

  • Add factory for selecting specific route data: RouterStore#selectRouteData
  • Add route title to MinimalActivatedRouteSnapshot#title
  • Add route title selector: RouterStore#title$
  • Add type MinimalRouteData for serializable route data

BREAKING CHANGES

Remove symbol keys from Route data

To keep route data serializable, we have removed support for the Angular Router's Data type's symbol index in MinimalActivatedRouteSnapshot#data. In particular, this is done to remove the Symbol(RouteTitle) entry added by the Angular Router for internal use. Use our MinimalRouteData type instead of Data from @angular/router for route data.

Provider factories return provider arrays

The provideGlobalRouterStore and provideLocalRouterStore functions now return an array of providers (Provider[]) instead of a single provider (Provider). No changes required in your providers metadata, for example the following usage remains the same.

@Component({
  // (...)
  providers: [provideLocalRouterStore()],
})
// (...)

Compatibility

To support the stricter route title type introduced by the Angular Router, we now require at least the following peer dependencies.

  • Require Angular 15.0
  • Require @ngrx/component-store 15.0
  • Require RxJS 7.4
  • Require TypeScript 4.8

We have dropped TypeScript constructor parameter properties for ECMAScript compatibility, namely the useDefineForClassFields TypeScript compiler option is true (the default when targeting ES2022 or higher).

We have dropped TypeScript constructor parameter decorators for ECMAScript decorators compatibility.

0.2.0 (2022-10-24)

Features

  • Remove type parameter from selectQueryParam
  • Specify observable type returned from selectQueryParam
  • Remove type parameter from selectRouteParam
  • Specify observable type returned from selectRouteParam

Bug fixes

  • Fixes #272 by building the package using Angular 14. Standalone components and applications are now fully supported, including in component tests.

BREAKING CHANGES

Compatibility

To fully support standalone Angular applications and components, we now require at least the following peer dependencies.

  • Require Angular 14.0
  • Require @ngrx/component-store 14.0
  • Require RxJS 7.4
  • Require TypeScript 4.6

Stricter signature for selectQueryParam

Signature before:

selectQueryParam<TValue>(param: string): Observable<TValue>;

Signature after:

selectQueryParam(param: string): Observable<string | undefined>;
Migration

Loose types now yield compilation errors. Remove the type parameter to use the actual emitted type of string | undefined and optionally use operators to change or narrow the type.

Before:

// Actual emitted values are of type `string | undefined` regardless of what we specify
const filter$ = routerStore.selectQueryParam<string | null>('filter');

After:

// Emitted values are implicitly of type `string | undefined` and are only changeable through operators
const filter$ = routerStore
  .selectQueryParam('filter')
  .pipe(map((filter) => filter ?? null));

Stricter signature for selectRouteParam

Signature before:

selectRouteParam<TValue>(param: string): Observable<TValue>;

Signature after:

selectRouteParam(param: string): Observable<string | undefined>;
Migration

Loose types now yield compilation errors. Remove the type parameter to use the actual emitted type of string | undefined and optionally use operators to change or narrow the type.

Before:

// Actual emitted values are of type `string | undefined` regardless of what we specify
const id$ = routerStore.selectRouteParam<number>('id');

After:

// Emitted values are implicitly of type `string | undefined` and are only changeable through operators
const id$ = routerStore.selectRouteParam('id').pipe(
  map(id => id === undefined ? undefined : Number(id),
);

0.1.1 (2022-10-21)

Features

  • Add RouterStore
  • Remove LocalRouterStore
  • Add provideLocalRouterStore
  • Remove GlobalRouterStore
  • Add provideGlobalRouterStore

Bug fixes

  • Fix #272: Class constructor ComponentStore cannot be invoked without 'new'

BREAKING CHANGES

Require RxJS 7.2

We now require at least RxJS version 7.2 to import operators from the primary entry point of the rxjs package.

LocalRouterStore is removed

LocalRouterStore is replaced by RouterStore and provideLocalRouterStore.

Migration

Use provideLocalRouterStore() as component-level provider and inject RouterStore instead of LocalRouterStore.

Before:

// hero-detail.component.ts
// (...)
import { LocalRouterStore } from '@ngworker/router-component-store';

@Component({
  // (...)
  providers: [LocalRouterStore],
})
export class HeroDetailComponent {
  heroId$: Observable<string> = this.routerStore.selectQueryParam('id');

  constructor(private routerStore: LocalRouterStore) {}
}

After:

// hero-detail.component.ts
// (...)
import {
  provideLocalRouterStore,
  RouterStore,
} from '@ngworker/router-component-store';

@Component({
  // (...)
  providers: [provideLocalRouterStore()],
})
export class HeroDetailComponent {
  heroId$: Observable<string> = this.routerStore.selectQueryParam('id');

  constructor(private routerStore: RouterStore) {}
}

GlobalRouterStore is removed

GlobalRouterStore is replaced by RouterStore and provideGlobalRouterStore.

Migration

Add provideGlobalRouterStore() to your root environment injector and inject RouterStore instead of GlobalRouterStore.

Before:

// hero.service.ts
// (...)
import { GlobalRouterStore } from '@ngworker/router-component-store';

@Injectable({
  providedIn: 'root',
})
export class HeroService {
  activeHeroId$: Observable<string> = this.routerStore.selectQueryParam('id');

  constructor(private routerStore: GlobalRouterStore) {}
}

After:

// app.module.ts
// (...)
import { provideGlobalRouterStore } from '@ngworker/router-component-store';

@NgModule({
  // (...)
  providers: [provideGlobalRouterStore()],
})
export class AppModule {}
// hero.service.ts
// (...)
import { RouterStore } from '@ngworker/router-component-store';

@Injectable({
  providedIn: 'root',
})
export class HeroService {
  activeHeroId$: Observable<string> = this.routerStore.selectQueryParam('id');

  constructor(private routerStore: RouterStore) {}
}

0.0.2 (2021-11-20)

Refactorings

  • Encapsulate members inherited from ComponentStore

0.0.1 (2021-11-20)

Features

  • add GlobalRouterStore
  • add LocalRouterStore