From 88a7bfa5dd0a7ef6dc9d506e446e41055df652d3 Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Thu, 5 Dec 2024 09:28:59 +0100 Subject: [PATCH 01/46] release(state): 19.0.0 --- libs/state/CHANGELOG.md | 14 ++++++++++++++ libs/state/package.json | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/libs/state/CHANGELOG.md b/libs/state/CHANGELOG.md index 40c5c24fc..ba823edf5 100644 --- a/libs/state/CHANGELOG.md +++ b/libs/state/CHANGELOG.md @@ -2,6 +2,20 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +# [19.0.0](https://github.com/rx-angular/rx-angular/compare/state@18.1.0...state@19.0.0) (2024-12-05) + + +### Features + +* **state:** upgrade to ng-19 ([cd6941e](https://github.com/rx-angular/rx-angular/commit/cd6941e23558ebd7c0d463703f2810a8889e6c36)) + + +### BREAKING CHANGES + +* **state:** bump peerDependency to angular 19 + + + # [18.1.0](https://github.com/rx-angular/rx-angular/compare/state@18.0.0...state@18.1.0) (2024-10-03) diff --git a/libs/state/package.json b/libs/state/package.json index dcdd2bcf6..38fcf39ea 100644 --- a/libs/state/package.json +++ b/libs/state/package.json @@ -1,6 +1,6 @@ { "name": "@rx-angular/state", - "version": "18.1.0", + "version": "19.0.0", "description": "@rx-angular/state is a light-weight, flexible, strongly typed and tested tool dedicated to reduce the complexity of managing component state and side effects in angular", "publishConfig": { "access": "public" From 767e96b3cb2231e4057ec4eff02ab0c4022b3b8a Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Thu, 5 Dec 2024 11:59:24 +0100 Subject: [PATCH 02/46] release(isr): 19.0.0 --- libs/isr/CHANGELOG.md | 20 ++++++++++++++++++++ libs/isr/package.json | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/libs/isr/CHANGELOG.md b/libs/isr/CHANGELOG.md index 8261490e8..2e9667a21 100644 --- a/libs/isr/CHANGELOG.md +++ b/libs/isr/CHANGELOG.md @@ -2,6 +2,26 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +# [19.0.0](https://github.com/rx-angular/rx-angular/compare/isr@18.1.0...isr@19.0.0) (2024-12-05) + + +### Bug Fixes + +* **isr:** fix eslint issue ([08b814f](https://github.com/rx-angular/rx-angular/commit/08b814f323a22b94e2419e2c0146d1a88e9745ff)) + + +### Features + +* **isr:** add custom cache key generation logic ([821bd12](https://github.com/rx-angular/rx-angular/commit/821bd1202ef7ad7582a0013ad871f6e51a59a6e1)) +* **isr:** upgrade to ng-19 ([faa25ed](https://github.com/rx-angular/rx-angular/commit/faa25ed818f9d7b317dd1fdf17209a723042dc84)) + + +### BREAKING CHANGES + +* **isr:** bump peerDependency to angular 19 + + + # [18.1.0](https://github.com/rx-angular/rx-angular/compare/isr@18.0.3...isr@18.1.0) (2024-09-04) diff --git a/libs/isr/package.json b/libs/isr/package.json index 6c296e03d..56dfc82ce 100644 --- a/libs/isr/package.json +++ b/libs/isr/package.json @@ -2,7 +2,7 @@ "name": "@rx-angular/isr", "author": "Enea Jahollari", "description": "Incremental Static Regeneration for Angular", - "version": "18.1.0", + "version": "19.0.0", "peerDependencies": { "@angular/common": "^19.0.0", "@angular/core": "^19.0.0", From 339b2e3e69e2ed49d368f33c45fa0bdaac8820f4 Mon Sep 17 00:00:00 2001 From: Enea Jahollari Date: Tue, 10 Dec 2024 12:50:45 +0100 Subject: [PATCH 03/46] fix: replace toObservableMicrotask private API with proper solution --- libs/cdk/internals/core/src/index.ts | 1 + .../core/src/lib/toObservableMicrotask.ts | 47 +++++++++++++++++++ libs/state/src/lib/rx-state.service.ts | 13 +++-- libs/template/for/src/lib/for.directive.ts | 4 +- libs/template/if/src/lib/if.directive.ts | 4 +- libs/template/let/src/lib/let.directive.ts | 6 ++- 6 files changed, 64 insertions(+), 11 deletions(-) create mode 100644 libs/cdk/internals/core/src/lib/toObservableMicrotask.ts diff --git a/libs/cdk/internals/core/src/index.ts b/libs/cdk/internals/core/src/index.ts index c68d1236a..8085dade1 100644 --- a/libs/cdk/internals/core/src/index.ts +++ b/libs/cdk/internals/core/src/index.ts @@ -2,3 +2,4 @@ export { accumulateObservables } from './lib/accumulateObservables'; export { getZoneUnPatchedApi } from './lib/get-zone-unpatched-api'; export { ObservableAccumulation, ObservableMap } from './lib/model'; export { timeoutSwitchMapWith } from './lib/timeout'; +export { toObservableMicrotaskInternal } from './lib/toObservableMicrotask'; diff --git a/libs/cdk/internals/core/src/lib/toObservableMicrotask.ts b/libs/cdk/internals/core/src/lib/toObservableMicrotask.ts new file mode 100644 index 000000000..7d1490a38 --- /dev/null +++ b/libs/cdk/internals/core/src/lib/toObservableMicrotask.ts @@ -0,0 +1,47 @@ +import { + assertInInjectionContext, + DestroyRef, + effect, + inject, + Injector, + Signal, + untracked, +} from '@angular/core'; +import { toObservable, ToObservableOptions } from '@angular/core/rxjs-interop'; +import { Observable, ReplaySubject } from 'rxjs'; + +// Copied from angular/core/rxjs-interop/src/to_observable.ts -> because it's a private API +// https://github.com/angular/angular/blob/46f00f951842dd117653df6cca3bfd5ee5baa0f1/packages/core/rxjs-interop/src/to_observable.ts#L72 +export function toObservableMicrotaskInternal( + source: Signal, + options?: ToObservableOptions, +): Observable { + if (!options?.injector) { + assertInInjectionContext(toObservable); + } + + const injector = options?.injector ?? inject(Injector); + const subject = new ReplaySubject(1); + + const watcher = effect( + () => { + let value: T; + try { + value = source(); + } catch (err) { + untracked(() => subject.error(err)); + return; + } + untracked(() => subject.next(value)); + }, + // forceRoot will ensure that the effect will be scheduled as a microtask + { injector, manualCleanup: true, forceRoot: true }, + ); + + injector.get(DestroyRef).onDestroy(() => { + watcher.destroy(); + subject.complete(); + }); + + return subject.asObservable(); +} diff --git a/libs/state/src/lib/rx-state.service.ts b/libs/state/src/lib/rx-state.service.ts index 13eb21f62..c3ee7ede2 100644 --- a/libs/state/src/lib/rx-state.service.ts +++ b/libs/state/src/lib/rx-state.service.ts @@ -7,7 +7,8 @@ import { OnDestroy, Signal, } from '@angular/core'; -import { ɵtoObservableMicrotask, toSignal } from '@angular/core/rxjs-interop'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { toObservableMicrotaskInternal } from '@rx-angular/cdk/internals/core'; import { AccumulationFn, createAccumulationObservable, @@ -570,7 +571,9 @@ export class RxState if (isSignal(keyOrInputOrSlice$) && !projectOrSlices$ && !projectValueFn) { this.accumulator.nextSliceObservable( - ɵtoObservableMicrotask(keyOrInputOrSlice$, { injector: this.injector }), + toObservableMicrotaskInternal(keyOrInputOrSlice$, { + injector: this.injector, + }), ); return; } @@ -596,7 +599,7 @@ export class RxState !projectValueFn ) { const projectionStateFn = projectOrSlices$; - const slice$ = ɵtoObservableMicrotask(keyOrInputOrSlice$, { + const slice$ = toObservableMicrotaskInternal(keyOrInputOrSlice$, { injector: this.injector, }).pipe( map((v) => projectionStateFn(this.accumulator.state, v as Value)), @@ -622,7 +625,7 @@ export class RxState isSignal(projectOrSlices$) && !projectValueFn ) { - const slice$ = ɵtoObservableMicrotask(projectOrSlices$, { + const slice$ = toObservableMicrotaskInternal(projectOrSlices$, { injector: this.injector, }).pipe(map((value) => ({ ...{}, [keyOrInputOrSlice$]: value }))); this.accumulator.nextSliceObservable(slice$); @@ -653,7 +656,7 @@ export class RxState isSignal(projectOrSlices$) ) { const key: Key = keyOrInputOrSlice$; - const slice$ = ɵtoObservableMicrotask(projectOrSlices$, { + const slice$ = toObservableMicrotaskInternal(projectOrSlices$, { injector: this.injector, }).pipe( map((value) => ({ diff --git a/libs/template/for/src/lib/for.directive.ts b/libs/template/for/src/lib/for.directive.ts index 056d3b6e2..a778cb819 100644 --- a/libs/template/for/src/lib/for.directive.ts +++ b/libs/template/for/src/lib/for.directive.ts @@ -18,11 +18,11 @@ import { TrackByFunction, ViewContainerRef, } from '@angular/core'; -import { ɵtoObservableMicrotask } from '@angular/core/rxjs-interop'; import { coerceDistinctWith, coerceObservableWith, } from '@rx-angular/cdk/coercing'; +import { toObservableMicrotaskInternal } from '@rx-angular/cdk/internals/core'; import { RxStrategyNames, RxStrategyProvider, @@ -132,7 +132,7 @@ export class RxFor = NgIterable> this.staticValue = undefined; this.renderStatic = false; this.observables$.next( - ɵtoObservableMicrotask(potentialSignalOrObservable, { + toObservableMicrotaskInternal(potentialSignalOrObservable, { injector: this.injector, }), ); diff --git a/libs/template/if/src/lib/if.directive.ts b/libs/template/if/src/lib/if.directive.ts index 2872e8960..072f6cc2a 100644 --- a/libs/template/if/src/lib/if.directive.ts +++ b/libs/template/if/src/lib/if.directive.ts @@ -14,8 +14,8 @@ import { TemplateRef, ViewContainerRef, } from '@angular/core'; -import { ɵtoObservableMicrotask } from '@angular/core/rxjs-interop'; import { coerceAllFactory } from '@rx-angular/cdk/coercing'; +import { toObservableMicrotaskInternal } from '@rx-angular/cdk/internals/core'; import { createTemplateNotifier, RxNotificationKind, @@ -558,7 +558,7 @@ export class RxIf if (changes.rxIf) { if (isSignal(this.rxIf)) { this.templateNotifier.next( - ɵtoObservableMicrotask(this.rxIf, { injector: this.injector }), + toObservableMicrotaskInternal(this.rxIf, { injector: this.injector }), ); } else { this.templateNotifier.next(this.rxIf); diff --git a/libs/template/let/src/lib/let.directive.ts b/libs/template/let/src/lib/let.directive.ts index 6d8a6ca97..0a64eb6c6 100644 --- a/libs/template/let/src/lib/let.directive.ts +++ b/libs/template/let/src/lib/let.directive.ts @@ -16,8 +16,8 @@ import { TemplateRef, ViewContainerRef, } from '@angular/core'; -import { ɵtoObservableMicrotask } from '@angular/core/rxjs-interop'; import { coerceAllFactory } from '@rx-angular/cdk/coercing'; +import { toObservableMicrotaskInternal } from '@rx-angular/cdk/internals/core'; import { createTemplateNotifier, RxNotification, @@ -590,7 +590,9 @@ export class RxLet implements OnInit, OnDestroy, OnChanges { if (changes.rxLet) { if (isSignal(this.rxLet)) { this.observablesHandler.next( - ɵtoObservableMicrotask(this.rxLet, { injector: this.injector }), + toObservableMicrotaskInternal(this.rxLet, { + injector: this.injector, + }), ); } else { this.observablesHandler.next(this.rxLet); From ca4c7d0153a9419c76b585cd1e923fdbf9629655 Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Thu, 19 Dec 2024 10:19:26 +0100 Subject: [PATCH 04/46] feat(template): introduce rx-virtual-view --- .../features/template/template-shell.menu.ts | 6 + .../template/template-shell.module.ts | 15 +- .../virtual-view/virtual-content.component.ts | 10 + .../virtual-view/virtual-item.component.ts | 22 ++ .../virtual-placeholder.component.ts | 8 + .../virtual-view-demo.component.ts | 196 +++++++++++++++ .../virtual-view/virtual-view.menu.ts | 6 + .../virtual-view/virtual-view.routes.ts | 16 ++ libs/template/.eslintrc.json | 1 + libs/template/virtual-view/ng-package.json | 7 + libs/template/virtual-view/src/index.ts | 5 + libs/template/virtual-view/src/lib/model.ts | 20 ++ .../virtual-view/src/lib/resize-observer.ts | 37 +++ .../src/lib/virtual-view-cache.ts | 43 ++++ .../lib/virtual-view-observer.directive.ts | 87 +++++++ .../lib/virtual-view-placeholder.directive.ts | 10 + .../lib/virtual-view-template.directive.ts | 17 ++ .../src/lib/virtual-view.directive.ts | 229 ++++++++++++++++++ tsconfig.base.json | 3 + 19 files changed, 734 insertions(+), 4 deletions(-) create mode 100644 apps/demos/src/app/features/template/virtual-view/virtual-content.component.ts create mode 100644 apps/demos/src/app/features/template/virtual-view/virtual-item.component.ts create mode 100644 apps/demos/src/app/features/template/virtual-view/virtual-placeholder.component.ts create mode 100644 apps/demos/src/app/features/template/virtual-view/virtual-view-demo.component.ts create mode 100644 apps/demos/src/app/features/template/virtual-view/virtual-view.menu.ts create mode 100644 apps/demos/src/app/features/template/virtual-view/virtual-view.routes.ts create mode 100644 libs/template/virtual-view/ng-package.json create mode 100644 libs/template/virtual-view/src/index.ts create mode 100644 libs/template/virtual-view/src/lib/model.ts create mode 100644 libs/template/virtual-view/src/lib/resize-observer.ts create mode 100644 libs/template/virtual-view/src/lib/virtual-view-cache.ts create mode 100644 libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts create mode 100644 libs/template/virtual-view/src/lib/virtual-view-placeholder.directive.ts create mode 100644 libs/template/virtual-view/src/lib/virtual-view-template.directive.ts create mode 100644 libs/template/virtual-view/src/lib/virtual-view.directive.ts diff --git a/apps/demos/src/app/features/template/template-shell.menu.ts b/apps/demos/src/app/features/template/template-shell.menu.ts index dc5457194..227a629ca 100644 --- a/apps/demos/src/app/features/template/template-shell.menu.ts +++ b/apps/demos/src/app/features/template/template-shell.menu.ts @@ -7,6 +7,7 @@ import { RX_VIRTUAL_FOR_MENU_ITEMS } from './rx-virtual-for/rx-virtual-for.menu' import { MENU_ITEMS as VIEWPORT_PRIO_MENU_ITEMS } from './viewport-prio/viewport-prio.menu'; import { MENU_ITEMS as STRATEGY_MENU_ITEMS } from './strategies/concurrent-strategies.menu'; import { MENU_ITEMS as PIPES_MENU_ITEMS } from './pipes/pipes.menu'; +import { VIRTUAL_VIEW_MENU_ITEMS } from './virtual-view/virtual-view.menu'; export const TEMPLATE_MENU = [ { @@ -62,4 +63,9 @@ export const TEMPLATE_MENU = [ link: 'view-port-prio', children: VIEWPORT_PRIO_MENU_ITEMS, }, + { + label: 'Virtual View', + link: 'virtual-view', + children: VIRTUAL_VIEW_MENU_ITEMS, + }, ]; diff --git a/apps/demos/src/app/features/template/template-shell.module.ts b/apps/demos/src/app/features/template/template-shell.module.ts index 3c7a72709..7f209ad9c 100644 --- a/apps/demos/src/app/features/template/template-shell.module.ts +++ b/apps/demos/src/app/features/template/template-shell.module.ts @@ -31,7 +31,7 @@ const ROUTES: Routes = [ path: 'rx-virtual-for', loadChildren: () => import('./rx-virtual-for/rx-virtual-for.module').then( - (m) => m.RxVirtualForDemoModule + (m) => m.RxVirtualForDemoModule, ), }, { @@ -48,7 +48,7 @@ const ROUTES: Routes = [ path: 'rx-context', loadChildren: () => import('./rx-context/rx-context.routed.module').then( - (m) => m.RxContextRoutedModule + (m) => m.RxContextRoutedModule, ), }, { @@ -60,14 +60,21 @@ const ROUTES: Routes = [ path: 'view-port-prio', loadChildren: () => import('./viewport-prio/viewport-prio-demo.module').then( - (m) => m.ViewportPrioModule + (m) => m.ViewportPrioModule, + ), + }, + { + path: 'virtual-view', + loadChildren: () => + import('./virtual-view/virtual-view.routes').then( + (m) => m.VIRTUAL_VIEW_ROUTES, ), }, { path: 'render-callback', loadChildren: () => import('./render-callback/render-callback.module').then( - (m) => m.RenderCallbackModule + (m) => m.RenderCallbackModule, ), }, ]; diff --git a/apps/demos/src/app/features/template/virtual-view/virtual-content.component.ts b/apps/demos/src/app/features/template/virtual-view/virtual-content.component.ts new file mode 100644 index 000000000..e33d1323d --- /dev/null +++ b/apps/demos/src/app/features/template/virtual-view/virtual-content.component.ts @@ -0,0 +1,10 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +@Component({ + selector: 'virtual-content', + template: `{{ item().content }}`, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VirtualContent { + item = input<{ id: number; content: string }>(); +} diff --git a/apps/demos/src/app/features/template/virtual-view/virtual-item.component.ts b/apps/demos/src/app/features/template/virtual-view/virtual-item.component.ts new file mode 100644 index 000000000..0c2cddbeb --- /dev/null +++ b/apps/demos/src/app/features/template/virtual-view/virtual-item.component.ts @@ -0,0 +1,22 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { + RxVirtualViewPlaceholder, + RxVirtualViewTemplate, +} from '@rx-angular/template/virtual-view'; + +@Component({ + selector: 'virtual-item', + template: ` +
+ {{ item().content }} +
+
+ {{ item().content }} +
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [RxVirtualViewTemplate, RxVirtualViewPlaceholder], +}) +export class VirtualItem { + item = input<{ id: number; content: string }>(); +} diff --git a/apps/demos/src/app/features/template/virtual-view/virtual-placeholder.component.ts b/apps/demos/src/app/features/template/virtual-view/virtual-placeholder.component.ts new file mode 100644 index 000000000..56e36b695 --- /dev/null +++ b/apps/demos/src/app/features/template/virtual-view/virtual-placeholder.component.ts @@ -0,0 +1,8 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'virtual-placeholder', + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VirtualPlaceholder {} diff --git a/apps/demos/src/app/features/template/virtual-view/virtual-view-demo.component.ts b/apps/demos/src/app/features/template/virtual-view/virtual-view-demo.component.ts new file mode 100644 index 000000000..0f4952860 --- /dev/null +++ b/apps/demos/src/app/features/template/virtual-view/virtual-view-demo.component.ts @@ -0,0 +1,196 @@ +import { + ChangeDetectionStrategy, + Component, + viewChild, + ViewEncapsulation, +} from '@angular/core'; +import { + RxVirtualView, + RxVirtualViewObserver, + RxVirtualViewPlaceholder, + RxVirtualViewTemplate, +} from '@rx-angular/template/virtual-view'; +import { VirtualContent } from './virtual-content.component'; +import { VirtualItem } from './virtual-item.component'; +import { VirtualPlaceholder } from './virtual-placeholder.component'; + +@Component({ + selector: 'virtual-view-demo', + template: ` +
+
+ + +
+
+

Inline, no placeholder, keepLastKnownSize

+ @for (item of values; track item.id) { +
+
+ {{ item.content }} +
+
+ } +
+
+

Inline, with placeholder

+ @for (item of values; track item.id) { +
+
content before
+
+ {{ item.content }} +
+
content after
+
+ {{ item.content }} +
+
+ } +
+
+

Inline, startWithPlaceholderAsap

+ @for (item of values; track item.id) { +
+
content before
+
+ {{ item.content }} +
+
content after
+
+ {{ item.content }} +
+
+ } +
+
+

With Components

+ @for (item of values; track item.id) { +
+ + +
+ } +
+
+

On Component (embedded)

+ @for (item of values; track item.id) { + + } +
+
+

Category 3

+ @for (item of values; track item.id) { +
+
+ {{ item.content }} +
+
+ {{ item.content }} +
+
+ } +
+
+

Category 4

+ @for (item of values; track item.id) { +
+
+ {{ item.content }} +
+
+ {{ item.content }} +
+
+ } +
+
+ `, + styles: [ + ` + .container { + height: 100%; + max-height: 100%; + overflow-y: scroll; + } + .item-wrapper { + height: 500px; + width: 400px; + overflow: auto; + } + + .content.placeholder { + color: blue; + } + + .item { + display: block; + width: 250px; + /*overflow: hidden; + flex-shrink: 0;*/ + /*height: 50px;*/ + /*will-change: transform;*/ + border: 1px solid green; + padding: 10px 0; + box-shadow: 1px 1px 1px 1px rgba(0, 0, 0, 0.13); + } + `, + ], + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + RxVirtualViewObserver, + RxVirtualView, + RxVirtualViewTemplate, + RxVirtualViewPlaceholder, + VirtualPlaceholder, + VirtualContent, + VirtualItem, + ], +}) +export class VirtualViewDemoComponent { + observer = viewChild(RxVirtualViewObserver); + values = new Array<{ id: number; content: string }>(200) + .fill(null) + .map((v, id) => ({ + id, + content: randomContent(), + })); +} + +const randomContent = () => { + return new Array(Math.max(1, Math.floor(Math.random() * 25))) + .fill('') + .map(() => randomWord()) + .join(' '); +}; + +const randomWord = () => { + const words = [ + 'Apple', + 'Banana', + 'The', + 'Orange', + 'House', + 'Boat', + 'Lake', + 'Car', + 'And', + ]; + return words[Math.floor(Math.random() * words.length)]; +}; diff --git a/apps/demos/src/app/features/template/virtual-view/virtual-view.menu.ts b/apps/demos/src/app/features/template/virtual-view/virtual-view.menu.ts new file mode 100644 index 000000000..7e5191630 --- /dev/null +++ b/apps/demos/src/app/features/template/virtual-view/virtual-view.menu.ts @@ -0,0 +1,6 @@ +export const VIRTUAL_VIEW_MENU_ITEMS = [ + { + label: 'Basic Example', + link: 'basic-example', + }, +]; diff --git a/apps/demos/src/app/features/template/virtual-view/virtual-view.routes.ts b/apps/demos/src/app/features/template/virtual-view/virtual-view.routes.ts new file mode 100644 index 000000000..c0453c32f --- /dev/null +++ b/apps/demos/src/app/features/template/virtual-view/virtual-view.routes.ts @@ -0,0 +1,16 @@ +import { Routes } from '@angular/router'; + +export const VIRTUAL_VIEW_ROUTES: Routes = [ + { + path: '', + redirectTo: 'basic-example', + pathMatch: 'full', + }, + { + path: 'basic-example', + loadComponent: () => + import('./virtual-view-demo.component').then( + (m) => m.VirtualViewDemoComponent, + ), + }, +]; diff --git a/libs/template/.eslintrc.json b/libs/template/.eslintrc.json index f87791bbc..5ddf9ea84 100644 --- a/libs/template/.eslintrc.json +++ b/libs/template/.eslintrc.json @@ -11,6 +11,7 @@ "rules": { "@angular-eslint/directive-selector": "off", "@angular-eslint/directive-class-suffix": "off", + "@angular-eslint/component-class-suffix": "off", "@angular-eslint/component-selector": [ "error", { diff --git a/libs/template/virtual-view/ng-package.json b/libs/template/virtual-view/ng-package.json new file mode 100644 index 000000000..d224a9f14 --- /dev/null +++ b/libs/template/virtual-view/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "src/index.ts", + "flatModuleFile": "template-virtual-view" + } +} diff --git a/libs/template/virtual-view/src/index.ts b/libs/template/virtual-view/src/index.ts new file mode 100644 index 000000000..d7c3f5273 --- /dev/null +++ b/libs/template/virtual-view/src/index.ts @@ -0,0 +1,5 @@ +export { RxVirtualView } from './lib/virtual-view.directive'; +export { VirtualViewCache } from './lib/virtual-view-cache'; +export { RxVirtualViewObserver } from './lib/virtual-view-observer.directive'; +export { RxVirtualViewPlaceholder } from './lib/virtual-view-placeholder.directive'; +export { RxVirtualViewTemplate } from './lib/virtual-view-template.directive'; diff --git a/libs/template/virtual-view/src/lib/model.ts b/libs/template/virtual-view/src/lib/model.ts new file mode 100644 index 000000000..c91d6549d --- /dev/null +++ b/libs/template/virtual-view/src/lib/model.ts @@ -0,0 +1,20 @@ +import { TemplateRef, ViewContainerRef } from '@angular/core'; +import { Observable } from 'rxjs'; + +export interface _RxVirtualViewTemplate { + viewContainerRef: ViewContainerRef; + templateRef: TemplateRef; +} + +export interface _RxVirtualViewPlaceholder { + templateRef: TemplateRef; +} + +export abstract class _RxVirtualViewObserver { + abstract register(virtualView: HTMLElement): Observable; +} + +export abstract class _RxVirtualView { + abstract registerTemplate(template: _RxVirtualViewTemplate): void; + abstract registerPlaceholder(placeholder: _RxVirtualViewPlaceholder): void; +} diff --git a/libs/template/virtual-view/src/lib/resize-observer.ts b/libs/template/virtual-view/src/lib/resize-observer.ts new file mode 100644 index 000000000..7a48fcf60 --- /dev/null +++ b/libs/template/virtual-view/src/lib/resize-observer.ts @@ -0,0 +1,37 @@ +import { DestroyRef, inject, Injectable } from '@angular/core'; +import { Observable, ReplaySubject, Subject } from 'rxjs'; +import { distinctUntilChanged, tap } from 'rxjs/operators'; + +@Injectable() +export class RxaResizeObserver { + #destroyRef = inject(DestroyRef); + #resizeObserver = new ResizeObserver((entries) => { + entries.forEach((entry) => { + if (this.#elements.has(entry.target)) + this.#elements.get(entry.target).next(entry); + }); + }); + + /** @internal */ + #elements = new WeakMap>(); + + constructor() { + this.#destroyRef.onDestroy(() => this.#resizeObserver.disconnect()); + } + + observeElement( + element: Element, + options?: ResizeObserverOptions, + ): Observable { + const resizeEvent$ = new ReplaySubject(1); + this.#elements.set(element, resizeEvent$); + this.#resizeObserver.observe(element, options); + + return this.#elements.get(element).pipe( + distinctUntilChanged(), + tap({ + unsubscribe: () => this.#elements.delete(element), + }), + ); + } +} diff --git a/libs/template/virtual-view/src/lib/virtual-view-cache.ts b/libs/template/virtual-view/src/lib/virtual-view-cache.ts new file mode 100644 index 000000000..648ee135c --- /dev/null +++ b/libs/template/virtual-view/src/lib/virtual-view-cache.ts @@ -0,0 +1,43 @@ +import { Injectable, OnDestroy, ViewRef } from '@angular/core'; + +@Injectable() +export class VirtualViewCache implements OnDestroy { + #maxTemplates = 20; + #templateCache = new Map(); + #maxPlaceholders = 20; + #placeholderCache = new Map(); + + storePlaceholder(key: unknown, view: ViewRef) { + if (this.#placeholderCache.size >= this.#maxPlaceholders) { + this.#placeholderCache.delete( + this.#placeholderCache.entries().next().value[0], + ); + } + this.#placeholderCache.set(key, view); + } + + getPlaceholder(key: unknown) { + return this.#placeholderCache.get(key); + } + + storeTemplate(key: unknown, view: ViewRef) { + if (this.#templateCache.size >= this.#maxTemplates) { + this.#templateCache.delete(this.#templateCache.entries().next().value[0]); + } + this.#templateCache.set(key, view); + } + + getTemplate(key: unknown) { + return this.#templateCache.get(key); + } + + clear(key: unknown) { + this.#templateCache.delete(key); + this.#placeholderCache.delete(key); + } + + ngOnDestroy() { + this.#templateCache.clear(); + this.#placeholderCache.clear(); + } +} diff --git a/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts b/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts new file mode 100644 index 000000000..5aab392ff --- /dev/null +++ b/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts @@ -0,0 +1,87 @@ +import { + computed, + Directive, + ElementRef, + inject, + input, + OnInit, +} from '@angular/core'; +import { BehaviorSubject, combineLatest, ReplaySubject, Subject } from 'rxjs'; +import { distinctUntilChanged, map, startWith, tap } from 'rxjs/operators'; +import { _RxVirtualViewObserver } from './model'; +import { RxaResizeObserver } from './resize-observer'; +import { VirtualViewCache } from './virtual-view-cache'; + +@Directive({ + selector: '[rxVirtualViewObserver]', + standalone: true, + providers: [ + VirtualViewCache, + RxaResizeObserver, + { provide: _RxVirtualViewObserver, useExisting: RxVirtualViewObserver }, + ], +}) +export class RxVirtualViewObserver implements OnInit { + #elementRef = inject>(ElementRef); + + #observer: IntersectionObserver | null = null; + + root = input(); + rootMargin = input(''); + threshold = input(0); + + #rootElement = computed(() => { + const root = this.root(); + if (root) { + if (root instanceof ElementRef) { + return root.nativeElement; + } + return root; + } else if (root === null) { + return null; + } + return this.#elementRef.nativeElement; + }); + + #elements = new WeakMap>(); + + #forcedHidden$ = new BehaviorSubject(false); + + ngOnInit(): void { + this.#observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (this.#elements.has(entry.target)) + this.#elements.get(entry.target).next(entry.isIntersecting); + }); + }, + { + root: this.#rootElement(), + rootMargin: this.rootMargin(), + threshold: this.threshold(), + }, + ); + } + + hideAll(): void { + this.#forcedHidden$.next(true); + } + + showAllVisible(): void { + this.#forcedHidden$.next(false); + } + + register(virtualView: HTMLElement) { + const isVisible$ = new ReplaySubject(1); + this.#elements.set(virtualView, isVisible$); + this.#observer.observe(virtualView); + return combineLatest([isVisible$, this.#forcedHidden$]).pipe( + map(([isVisible, forcedHidden]) => (forcedHidden ? false : isVisible)), + startWith(false), + distinctUntilChanged(), + tap({ + unsubscribe: () => this.#elements.delete(virtualView), + }), + ); + } +} diff --git a/libs/template/virtual-view/src/lib/virtual-view-placeholder.directive.ts b/libs/template/virtual-view/src/lib/virtual-view-placeholder.directive.ts new file mode 100644 index 000000000..0cf892182 --- /dev/null +++ b/libs/template/virtual-view/src/lib/virtual-view-placeholder.directive.ts @@ -0,0 +1,10 @@ +import { Directive, inject, TemplateRef } from '@angular/core'; +import { _RxVirtualView, _RxVirtualViewPlaceholder } from './model'; + +@Directive({ selector: '[rxVirtualViewPlaceholder]', standalone: true }) +export class RxVirtualViewPlaceholder implements _RxVirtualViewPlaceholder { + #virtualView = inject(_RxVirtualView); + constructor(public templateRef: TemplateRef) { + this.#virtualView.registerPlaceholder(this); + } +} diff --git a/libs/template/virtual-view/src/lib/virtual-view-template.directive.ts b/libs/template/virtual-view/src/lib/virtual-view-template.directive.ts new file mode 100644 index 000000000..10086ebfe --- /dev/null +++ b/libs/template/virtual-view/src/lib/virtual-view-template.directive.ts @@ -0,0 +1,17 @@ +import { + Directive, + inject, + TemplateRef, + ViewContainerRef, +} from '@angular/core'; +import { _RxVirtualViewTemplate } from './model'; +import { RxVirtualView } from './virtual-view.directive'; + +@Directive({ selector: '[rxVirtualViewTemplate]', standalone: true }) +export class RxVirtualViewTemplate implements _RxVirtualViewTemplate { + #virtualView = inject(RxVirtualView); + viewContainerRef = inject(ViewContainerRef); + constructor(public templateRef: TemplateRef) { + this.#virtualView.registerTemplate(this); + } +} diff --git a/libs/template/virtual-view/src/lib/virtual-view.directive.ts b/libs/template/virtual-view/src/lib/virtual-view.directive.ts new file mode 100644 index 000000000..0a9af9f55 --- /dev/null +++ b/libs/template/virtual-view/src/lib/virtual-view.directive.ts @@ -0,0 +1,229 @@ +import { + AfterContentInit, + booleanAttribute, + computed, + DestroyRef, + Directive, + ElementRef, + EmbeddedViewRef, + inject, + input, + signal, +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { RxStrategyProvider } from '@rx-angular/cdk/render-strategies'; +import { connectable, NEVER, Observable, ReplaySubject } from 'rxjs'; +import { distinctUntilChanged, switchMap, tap } from 'rxjs/operators'; +import { + _RxVirtualView, + _RxVirtualViewObserver, + _RxVirtualViewPlaceholder, + _RxVirtualViewTemplate, +} from './model'; +import { RxaResizeObserver } from './resize-observer'; +import { VirtualViewCache } from './virtual-view-cache'; + +declare const ngDevMode: boolean; + +type BooleanInput = string | boolean | null | undefined; + +@Directive({ + selector: '[rxVirtualView]', + host: { + '[style.--rx-vw-h]': 'height()', + '[style.--rx-vw-w]': 'width()', + '[style.min-height]': 'minHeight()', + '[style.min-width]': 'minWidth()', + '[style.contain]': 'containment()', + '[style.contain-intrinsic-width]': 'intrinsicWidth()', + '[style.contain-intrinsic-height]': 'intrinsicHeight()', + '[style.content-visibility]': 'useContentVisibility() ? "auto" : null', + }, + providers: [{ provide: _RxVirtualView, useExisting: RxVirtualView }], +}) +export class RxVirtualView implements AfterContentInit, _RxVirtualView { + template: _RxVirtualViewTemplate; + placeholder?: _RxVirtualViewPlaceholder; + + #observer = inject(_RxVirtualViewObserver); + #elementRef = inject>(ElementRef); + #strategyProvider = inject(RxStrategyProvider); + #viewCache = inject(VirtualViewCache); + #resizeObserver = inject(RxaResizeObserver); + #destroyRef = inject(DestroyRef); + + cacheEnabled = input(true, { + transform: booleanAttribute, + }); + + startWithPlaceholderAsap = input(false, { + transform: booleanAttribute, + }); + + keepLastKnownSize = input(false, { + transform: booleanAttribute, + }); + + useContentVisibility = input(false, { + transform: booleanAttribute, + }); + + useContainment = input(true, { + transform: booleanAttribute, + }); + + #placeholderVisible = signal(false); + #templateVisible = false; + #visible$ = connectable( + this.#observer.register(this.#elementRef.nativeElement), + { + connector: () => new ReplaySubject(1), + }, + ); + + size = signal({ width: 0, height: 0 }); + width = computed(() => + this.size().width ? `${this.size().width}px` : 'auto', + ); + height = computed(() => + this.size().height ? `${this.size().height}px` : 'auto', + ); + containment = computed(() => { + if (!this.useContainment()) { + return null; + } + return this.useContentVisibility() && this.#placeholderVisible() + ? 'size layout paint' + : 'content'; + }); + intrinsicWidth = computed(() => { + if (!this.useContentVisibility()) { + return null; + } + return this.width() === 'auto' ? 'auto' : `auto ${this.width()}`; + }); + intrinsicHeight = computed(() => { + if (!this.useContentVisibility()) { + return null; + } + return this.height() === 'auto' ? 'auto' : `auto ${this.height()}`; + }); + + minHeight = computed(() => { + return this.keepLastKnownSize() && this.#placeholderVisible() + ? this.height() + : null; + }); + minWidth = computed(() => { + return this.keepLastKnownSize() && this.#placeholderVisible() + ? this.width() + : null; + }); + + constructor() { + const visibleSub = this.#visible$.connect(); + this.#destroyRef.onDestroy(() => visibleSub.unsubscribe()); + } + + ngAfterContentInit() { + if (ngDevMode && !this.template) { + throw new Error( + 'RxVirtualView expects you to provide a RxVirtualViewTemplate', + ); + } + if (this.startWithPlaceholderAsap()) { + this.renderPlaceholder(); + } + this.#visible$ + .pipe( + distinctUntilChanged(), + switchMap((visible) => { + if (visible) { + return this.#templateVisible + ? NEVER + : this.showTemplate$().pipe( + switchMap((view) => { + const resize$ = this.observeElementSize$(); + view.detectChanges(); + return resize$; + }), + tap(({ borderBoxSize }) => { + this.size.set({ + width: borderBoxSize[0].inlineSize, + height: borderBoxSize[0].blockSize, + }); + }), + ); + } + return this.#placeholderVisible() ? NEVER : this.showPlaceholder$(); + }), + takeUntilDestroyed(this.#destroyRef), + tap({ + unsubscribe: () => { + this.#viewCache.clear(this); + }, + }), + ) + .subscribe(); + } + + registerTemplate(template: _RxVirtualViewTemplate) { + this.template = template; + } + + registerPlaceholder(placeholder: _RxVirtualViewPlaceholder) { + this.placeholder = placeholder; + } + + private showTemplate$(): Observable> { + return this.#strategyProvider.schedule( + () => { + this.#templateVisible = true; + this.#placeholderVisible.set(false); + const placeHolder = this.template.viewContainerRef.detach(); + if (this.cacheEnabled() && placeHolder) { + this.#viewCache.storePlaceholder(this, placeHolder); + } else if (!this.cacheEnabled() && placeHolder) { + placeHolder.destroy(); + } + const tmpl = + (this.#viewCache.getTemplate(this) as EmbeddedViewRef) ?? + this.template.templateRef.createEmbeddedView({}); + this.template.viewContainerRef.insert(tmpl); + placeHolder?.detectChanges(); + + return tmpl; + }, + { scope: this }, + ); + } + + private showPlaceholder$() { + return this.#strategyProvider.schedule(() => this.renderPlaceholder(), { + scope: this, + }); + } + + private renderPlaceholder() { + this.#placeholderVisible.set(true); + this.#templateVisible = false; + const template = this.template.viewContainerRef.detach(); + if (this.cacheEnabled() && template) { + this.#viewCache.storeTemplate(this, template); + } else if (!this.cacheEnabled() && template) { + template.destroy(); + } + template?.detectChanges(); + if (this.placeholder) { + const placeholderRef = + this.#viewCache.getPlaceholder(this) ?? + this.placeholder.templateRef.createEmbeddedView({}); + this.template.viewContainerRef.insert(placeholderRef); + placeholderRef.detectChanges(); + } + } + + private observeElementSize$() { + return this.#resizeObserver.observeElement(this.#elementRef.nativeElement); + } +} diff --git a/tsconfig.base.json b/tsconfig.base.json index ae7fad2cf..358ea747b 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -59,6 +59,9 @@ "@rx-angular/template/let": ["libs/template/let/src/index.ts"], "@rx-angular/template/push": ["libs/template/push/src/index.ts"], "@rx-angular/template/unpatch": ["libs/template/unpatch/src/index.ts"], + "@rx-angular/template/virtual-view": [ + "libs/template/virtual-view/src/index.ts" + ], "@test-helpers/rx-angular": ["libs/test-helpers/src/index.ts"] } }, From a8460cf1c08852c7c619c7ba56b3b1f546b6e14a Mon Sep 17 00:00:00 2001 From: Enea Jahollari Date: Thu, 19 Dec 2024 11:21:50 +0100 Subject: [PATCH 05/46] feat(template): add jsdocs and token-based configuration --- libs/template/virtual-view/src/lib/model.ts | 12 +++ .../src/lib/virtual-view-cache.ts | 54 +++++++++- .../src/lib/virtual-view.config.ts | 62 ++++++++++++ .../src/lib/virtual-view.directive.ts | 98 +++++++++++-------- 4 files changed, 181 insertions(+), 45 deletions(-) create mode 100644 libs/template/virtual-view/src/lib/virtual-view.config.ts diff --git a/libs/template/virtual-view/src/lib/model.ts b/libs/template/virtual-view/src/lib/model.ts index c91d6549d..39180a3cf 100644 --- a/libs/template/virtual-view/src/lib/model.ts +++ b/libs/template/virtual-view/src/lib/model.ts @@ -1,19 +1,31 @@ import { TemplateRef, ViewContainerRef } from '@angular/core'; import { Observable } from 'rxjs'; +/** + * @internal + */ export interface _RxVirtualViewTemplate { viewContainerRef: ViewContainerRef; templateRef: TemplateRef; } +/** + * @internal + */ export interface _RxVirtualViewPlaceholder { templateRef: TemplateRef; } +/** + * @internal + */ export abstract class _RxVirtualViewObserver { abstract register(virtualView: HTMLElement): Observable; } +/** + * @internal + */ export abstract class _RxVirtualView { abstract registerTemplate(template: _RxVirtualViewTemplate): void; abstract registerPlaceholder(placeholder: _RxVirtualViewPlaceholder): void; diff --git a/libs/template/virtual-view/src/lib/virtual-view-cache.ts b/libs/template/virtual-view/src/lib/virtual-view-cache.ts index 648ee135c..da554e186 100644 --- a/libs/template/virtual-view/src/lib/virtual-view-cache.ts +++ b/libs/template/virtual-view/src/lib/virtual-view-cache.ts @@ -1,12 +1,33 @@ -import { Injectable, OnDestroy, ViewRef } from '@angular/core'; +import { inject, Injectable, OnDestroy, ViewRef } from '@angular/core'; +import { VIRTUAL_VIEW_CONFIG_TOKEN } from './virtual-view.config'; +/** + * A service that caches templates and placeholders to optimize view rendering. + * It makes sure that all cached resources are cleared when the service is destroyed. + */ @Injectable() export class VirtualViewCache implements OnDestroy { - #maxTemplates = 20; + private #config = inject(VIRTUAL_VIEW_CONFIG_TOKEN); + + // Maximum number of templates that can be stored in the cache. + #maxTemplates = this.#config.maxTemplates; + + // Cache for storing template views, identified by a unique key, which is the directive instance. #templateCache = new Map(); - #maxPlaceholders = 20; + + // Maximum number of placeholders that can be stored in the cache. + #maxPlaceholders = this.#config.maxPlaceholders; + + // Cache for storing placeholder views, identified by a unique key. #placeholderCache = new Map(); + /** + * Stores a placeholder view in the cache. When the cache reaches its limit, + * the oldest entry is removed. + * + * @param key - The key used to identify the placeholder in the cache. + * @param view - The ViewRef of the placeholder to cache. + */ storePlaceholder(key: unknown, view: ViewRef) { if (this.#placeholderCache.size >= this.#maxPlaceholders) { this.#placeholderCache.delete( @@ -16,10 +37,23 @@ export class VirtualViewCache implements OnDestroy { this.#placeholderCache.set(key, view); } + /** + * Retrieves a cached placeholder view using the specified key. + * + * @param key - The key of the placeholder to retrieve. + * @returns The ViewRef of the cached placeholder, or undefined if not found. + */ getPlaceholder(key: unknown) { return this.#placeholderCache.get(key); } + /** + * Stores a template view in the cache. When the cache reaches its limit, + * the oldest entry is removed. + * + * @param key - The key used to identify the template in the cache. + * @param view - The ViewRef of the template to cache. + */ storeTemplate(key: unknown, view: ViewRef) { if (this.#templateCache.size >= this.#maxTemplates) { this.#templateCache.delete(this.#templateCache.entries().next().value[0]); @@ -27,15 +61,29 @@ export class VirtualViewCache implements OnDestroy { this.#templateCache.set(key, view); } + /** + * Retrieves a cached template view using the specified key. + * + * @param key - The key of the template to retrieve. + * @returns The ViewRef of the cached template, or undefined if not found. + */ getTemplate(key: unknown) { return this.#templateCache.get(key); } + /** + * Clears both template and placeholder caches for a given key. + * + * @param key - The key of the template and placeholder to remove. + */ clear(key: unknown) { this.#templateCache.delete(key); this.#placeholderCache.delete(key); } + /** + * Clears all cached resources when the service is destroyed. + */ ngOnDestroy() { this.#templateCache.clear(); this.#placeholderCache.clear(); diff --git a/libs/template/virtual-view/src/lib/virtual-view.config.ts b/libs/template/virtual-view/src/lib/virtual-view.config.ts new file mode 100644 index 000000000..4207bf3ff --- /dev/null +++ b/libs/template/virtual-view/src/lib/virtual-view.config.ts @@ -0,0 +1,62 @@ +import { InjectionToken, Provider } from '@angular/core'; + +export const VIRTUAL_VIEW_CONFIG_TOKEN = + new InjectionToken('VIRTUAL_VIEW_CONFIG_TOKEN', { + providedIn: 'root', + factory: () => VIRTUAL_VIEW_CONFIG_DEFAULT, + }); + +export interface RxVirtualViewConfig { + /** + * The maximum number of templates that can be stored in the cache. + * Defaults to 20. + */ + maxTemplates?: number; + + /** + * The maximum number of placeholders that can be stored in the cache. + * Defaults to 20. + */ + maxPlaceholders?: number; +} + +export const VIRTUAL_VIEW_CONFIG_DEFAULT: RxVirtualViewConfig = { + maxTemplates: 20, + maxPlaceholders: 20, +}; + +/** + * Provides a configuration object for the `VirtualView` service. + * + * Can be used to customize the behavior of the `VirtualView` service. + * + * Default configuration: + * - maxTemplates: 20 + * - maxPlaceholders: 20 + * + * Example usage: + * + * ```ts + * import { provideVirtualViewConfig } from '@rx-angular/template/virtual-view'; + * + * const appConfig: ApplicationConfig = { + * providers: [ + * provideVirtualViewConfig({ + * maxTemplates: 50, + * maxPlaceholders: 50, + * }), + * ], + * }; + * ``` + * + * @param config - The configuration object. + * @returns An object that can be provided to the `VirtualView` service. + */ +export function provideVirtualViewConfig( + config: RxVirtualViewConfig, +): Provider { + return { + provide: VIRTUAL_VIEW_CONFIG_TOKEN, + useValue: config, + } satisfies Provider; +} diff --git a/libs/template/virtual-view/src/lib/virtual-view.directive.ts b/libs/template/virtual-view/src/lib/virtual-view.directive.ts index 0a9af9f55..8b67b2d35 100644 --- a/libs/template/virtual-view/src/lib/virtual-view.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view.directive.ts @@ -25,8 +25,6 @@ import { VirtualViewCache } from './virtual-view-cache'; declare const ngDevMode: boolean; -type BooleanInput = string | boolean | null | undefined; - @Directive({ selector: '[rxVirtualView]', host: { @@ -42,53 +40,50 @@ type BooleanInput = string | boolean | null | undefined; providers: [{ provide: _RxVirtualView, useExisting: RxVirtualView }], }) export class RxVirtualView implements AfterContentInit, _RxVirtualView { - template: _RxVirtualViewTemplate; - placeholder?: _RxVirtualViewPlaceholder; + readonly #observer = inject(_RxVirtualViewObserver); + readonly #elementRef = inject>(ElementRef); + readonly #strategyProvider = inject(RxStrategyProvider); + readonly #viewCache = inject(VirtualViewCache); + readonly #resizeObserver = inject(RxaResizeObserver); + readonly #destroyRef = inject(DestroyRef); - #observer = inject(_RxVirtualViewObserver); - #elementRef = inject>(ElementRef); - #strategyProvider = inject(RxStrategyProvider); - #viewCache = inject(VirtualViewCache); - #resizeObserver = inject(RxaResizeObserver); - #destroyRef = inject(DestroyRef); + private template: _RxVirtualViewTemplate; + private placeholder?: _RxVirtualViewPlaceholder; - cacheEnabled = input(true, { - transform: booleanAttribute, - }); + readonly cacheEnabled = input(true, { transform: booleanAttribute }); - startWithPlaceholderAsap = input(false, { + readonly startWithPlaceholderAsap = input(false, { transform: booleanAttribute, }); - keepLastKnownSize = input(false, { - transform: booleanAttribute, - }); + readonly keepLastKnownSize = input(false, { transform: booleanAttribute }); - useContentVisibility = input(false, { - transform: booleanAttribute, - }); + readonly useContentVisibility = input(false, { transform: booleanAttribute }); - useContainment = input(true, { - transform: booleanAttribute, - }); + readonly useContainment = input(true, { transform: booleanAttribute }); + + readonly #placeholderVisible = signal(false); - #placeholderVisible = signal(false); - #templateVisible = false; - #visible$ = connectable( + #templateIsShown = false; + + readonly #visible$ = connectable( this.#observer.register(this.#elementRef.nativeElement), { connector: () => new ReplaySubject(1), }, ); - size = signal({ width: 0, height: 0 }); - width = computed(() => + readonly size = signal({ width: 0, height: 0 }); + + readonly width = computed(() => this.size().width ? `${this.size().width}px` : 'auto', ); - height = computed(() => + + readonly height = computed(() => this.size().height ? `${this.size().height}px` : 'auto', ); - containment = computed(() => { + + readonly containment = computed(() => { if (!this.useContainment()) { return null; } @@ -96,25 +91,26 @@ export class RxVirtualView implements AfterContentInit, _RxVirtualView { ? 'size layout paint' : 'content'; }); - intrinsicWidth = computed(() => { + + readonly intrinsicWidth = computed(() => { if (!this.useContentVisibility()) { return null; } return this.width() === 'auto' ? 'auto' : `auto ${this.width()}`; }); - intrinsicHeight = computed(() => { + readonly intrinsicHeight = computed(() => { if (!this.useContentVisibility()) { return null; } return this.height() === 'auto' ? 'auto' : `auto ${this.height()}`; }); - minHeight = computed(() => { + readonly minHeight = computed(() => { return this.keepLastKnownSize() && this.#placeholderVisible() ? this.height() : null; }); - minWidth = computed(() => { + readonly minWidth = computed(() => { return this.keepLastKnownSize() && this.#placeholderVisible() ? this.width() : null; @@ -139,7 +135,7 @@ export class RxVirtualView implements AfterContentInit, _RxVirtualView { distinctUntilChanged(), switchMap((visible) => { if (visible) { - return this.#templateVisible + return this.#templateIsShown ? NEVER : this.showTemplate$().pipe( switchMap((view) => { @@ -178,7 +174,7 @@ export class RxVirtualView implements AfterContentInit, _RxVirtualView { private showTemplate$(): Observable> { return this.#strategyProvider.schedule( () => { - this.#templateVisible = true; + this.#templateIsShown = true; this.#placeholderVisible.set(false); const placeHolder = this.template.viewContainerRef.detach(); if (this.cacheEnabled() && placeHolder) { @@ -204,20 +200,38 @@ export class RxVirtualView implements AfterContentInit, _RxVirtualView { }); } + /** + * Renders a placeholder within the view container, and hides the template. + * + * If we already have a template and cache enabled, we store the template in + * the cache, so we can reuse it later. + * + * When we want to render the placeholder, we try to get it from the cache, + * and if it is not available, we create a new one. + * + * Then insert the placeholder into the view container and trigger a CD. + */ private renderPlaceholder() { this.#placeholderVisible.set(true); - this.#templateVisible = false; + this.#templateIsShown = false; + const template = this.template.viewContainerRef.detach(); - if (this.cacheEnabled() && template) { - this.#viewCache.storeTemplate(this, template); - } else if (!this.cacheEnabled() && template) { - template.destroy(); + + if (template) { + if (this.cacheEnabled()) { + this.#viewCache.storeTemplate(this, template); + } else { + template.destroy(); + } + + template?.detectChanges(); } - template?.detectChanges(); + if (this.placeholder) { const placeholderRef = this.#viewCache.getPlaceholder(this) ?? this.placeholder.templateRef.createEmbeddedView({}); + this.template.viewContainerRef.insert(placeholderRef); placeholderRef.detectChanges(); } From 1ba1998ecd4568a369ec63329a26959a37102418 Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Thu, 19 Dec 2024 21:26:22 +0100 Subject: [PATCH 06/46] refactor(template): improve error handling --- .../src/lib/virtual-view-cache.ts | 2 +- .../src/lib/virtual-view.directive.ts | 30 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/libs/template/virtual-view/src/lib/virtual-view-cache.ts b/libs/template/virtual-view/src/lib/virtual-view-cache.ts index da554e186..a8b864a31 100644 --- a/libs/template/virtual-view/src/lib/virtual-view-cache.ts +++ b/libs/template/virtual-view/src/lib/virtual-view-cache.ts @@ -7,7 +7,7 @@ import { VIRTUAL_VIEW_CONFIG_TOKEN } from './virtual-view.config'; */ @Injectable() export class VirtualViewCache implements OnDestroy { - private #config = inject(VIRTUAL_VIEW_CONFIG_TOKEN); + #config = inject(VIRTUAL_VIEW_CONFIG_TOKEN); // Maximum number of templates that can be stored in the cache. #maxTemplates = this.#config.maxTemplates; diff --git a/libs/template/virtual-view/src/lib/virtual-view.directive.ts b/libs/template/virtual-view/src/lib/virtual-view.directive.ts index 8b67b2d35..8af26a0be 100644 --- a/libs/template/virtual-view/src/lib/virtual-view.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view.directive.ts @@ -12,7 +12,7 @@ import { } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { RxStrategyProvider } from '@rx-angular/cdk/render-strategies'; -import { connectable, NEVER, Observable, ReplaySubject } from 'rxjs'; +import { NEVER, Observable, ReplaySubject } from 'rxjs'; import { distinctUntilChanged, switchMap, tap } from 'rxjs/operators'; import { _RxVirtualView, @@ -23,8 +23,6 @@ import { import { RxaResizeObserver } from './resize-observer'; import { VirtualViewCache } from './virtual-view-cache'; -declare const ngDevMode: boolean; - @Directive({ selector: '[rxVirtualView]', host: { @@ -40,11 +38,11 @@ declare const ngDevMode: boolean; providers: [{ provide: _RxVirtualView, useExisting: RxVirtualView }], }) export class RxVirtualView implements AfterContentInit, _RxVirtualView { - readonly #observer = inject(_RxVirtualViewObserver); + readonly #observer = inject(_RxVirtualViewObserver, { optional: true }); readonly #elementRef = inject>(ElementRef); readonly #strategyProvider = inject(RxStrategyProvider); - readonly #viewCache = inject(VirtualViewCache); - readonly #resizeObserver = inject(RxaResizeObserver); + readonly #viewCache = inject(VirtualViewCache, { optional: true }); + readonly #resizeObserver = inject(RxaResizeObserver, { optional: true }); readonly #destroyRef = inject(DestroyRef); private template: _RxVirtualViewTemplate; @@ -66,12 +64,7 @@ export class RxVirtualView implements AfterContentInit, _RxVirtualView { #templateIsShown = false; - readonly #visible$ = connectable( - this.#observer.register(this.#elementRef.nativeElement), - { - connector: () => new ReplaySubject(1), - }, - ); + readonly #visible$ = new ReplaySubject(1); readonly size = signal({ width: 0, height: 0 }); @@ -117,12 +110,19 @@ export class RxVirtualView implements AfterContentInit, _RxVirtualView { }); constructor() { - const visibleSub = this.#visible$.connect(); - this.#destroyRef.onDestroy(() => visibleSub.unsubscribe()); + if (!this.#observer) { + throw new Error( + 'RxVirtualView expects you to provide a RxVirtualViewObserver', + ); + } + this.#observer + .register(this.#elementRef.nativeElement) + .pipe(takeUntilDestroyed()) + .subscribe((visible) => this.#visible$.next(visible)); } ngAfterContentInit() { - if (ngDevMode && !this.template) { + if (!this.template) { throw new Error( 'RxVirtualView expects you to provide a RxVirtualViewTemplate', ); From 8accd30f36236ee1032b46ae69ceec73e1f2d279 Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Thu, 19 Dec 2024 21:39:22 +0100 Subject: [PATCH 07/46] fix(template): properly handle subscriptions & cleanup --- .../lib/virtual-view-observer.directive.ts | 12 +++++- .../src/lib/virtual-view.directive.ts | 37 ++++++++++++------- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts b/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts index 5aab392ff..f24ef1415 100644 --- a/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts @@ -4,6 +4,7 @@ import { ElementRef, inject, input, + OnDestroy, OnInit, } from '@angular/core'; import { BehaviorSubject, combineLatest, ReplaySubject, Subject } from 'rxjs'; @@ -21,7 +22,7 @@ import { VirtualViewCache } from './virtual-view-cache'; { provide: _RxVirtualViewObserver, useExisting: RxVirtualViewObserver }, ], }) -export class RxVirtualViewObserver implements OnInit { +export class RxVirtualViewObserver implements OnInit, OnDestroy { #elementRef = inject>(ElementRef); #observer: IntersectionObserver | null = null; @@ -43,7 +44,7 @@ export class RxVirtualViewObserver implements OnInit { return this.#elementRef.nativeElement; }); - #elements = new WeakMap>(); + #elements = new Map>(); #forcedHidden$ = new BehaviorSubject(false); @@ -63,6 +64,13 @@ export class RxVirtualViewObserver implements OnInit { ); } + ngOnDestroy() { + this.#elements.clear(); + this.#observer?.disconnect(); + this.#observer = null; + this.#elementRef = null; + } + hideAll(): void { this.#forcedHidden$.next(true); } diff --git a/libs/template/virtual-view/src/lib/virtual-view.directive.ts b/libs/template/virtual-view/src/lib/virtual-view.directive.ts index 8af26a0be..b2d257c6c 100644 --- a/libs/template/virtual-view/src/lib/virtual-view.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view.directive.ts @@ -8,6 +8,7 @@ import { EmbeddedViewRef, inject, input, + OnDestroy, signal, } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -37,7 +38,9 @@ import { VirtualViewCache } from './virtual-view-cache'; }, providers: [{ provide: _RxVirtualView, useExisting: RxVirtualView }], }) -export class RxVirtualView implements AfterContentInit, _RxVirtualView { +export class RxVirtualView + implements AfterContentInit, _RxVirtualView, OnDestroy +{ readonly #observer = inject(_RxVirtualViewObserver, { optional: true }); readonly #elementRef = inject>(ElementRef); readonly #strategyProvider = inject(RxStrategyProvider); @@ -45,8 +48,8 @@ export class RxVirtualView implements AfterContentInit, _RxVirtualView { readonly #resizeObserver = inject(RxaResizeObserver, { optional: true }); readonly #destroyRef = inject(DestroyRef); - private template: _RxVirtualViewTemplate; - private placeholder?: _RxVirtualViewPlaceholder; + #template: _RxVirtualViewTemplate; + #placeholder?: _RxVirtualViewPlaceholder; readonly cacheEnabled = input(true, { transform: booleanAttribute }); @@ -122,7 +125,7 @@ export class RxVirtualView implements AfterContentInit, _RxVirtualView { } ngAfterContentInit() { - if (!this.template) { + if (!this.#template) { throw new Error( 'RxVirtualView expects you to provide a RxVirtualViewTemplate', ); @@ -153,22 +156,28 @@ export class RxVirtualView implements AfterContentInit, _RxVirtualView { } return this.#placeholderVisible() ? NEVER : this.showPlaceholder$(); }), - takeUntilDestroyed(this.#destroyRef), tap({ unsubscribe: () => { this.#viewCache.clear(this); }, }), + takeUntilDestroyed(this.#destroyRef), ) .subscribe(); } + ngOnDestroy() { + // WE DON'T NEED THAT... but enea insists! + this.#template = null; + this.#placeholder = null; + } + registerTemplate(template: _RxVirtualViewTemplate) { - this.template = template; + this.#template = template; } registerPlaceholder(placeholder: _RxVirtualViewPlaceholder) { - this.placeholder = placeholder; + this.#placeholder = placeholder; } private showTemplate$(): Observable> { @@ -176,7 +185,7 @@ export class RxVirtualView implements AfterContentInit, _RxVirtualView { () => { this.#templateIsShown = true; this.#placeholderVisible.set(false); - const placeHolder = this.template.viewContainerRef.detach(); + const placeHolder = this.#template.viewContainerRef.detach(); if (this.cacheEnabled() && placeHolder) { this.#viewCache.storePlaceholder(this, placeHolder); } else if (!this.cacheEnabled() && placeHolder) { @@ -184,8 +193,8 @@ export class RxVirtualView implements AfterContentInit, _RxVirtualView { } const tmpl = (this.#viewCache.getTemplate(this) as EmbeddedViewRef) ?? - this.template.templateRef.createEmbeddedView({}); - this.template.viewContainerRef.insert(tmpl); + this.#template.templateRef.createEmbeddedView({}); + this.#template.viewContainerRef.insert(tmpl); placeHolder?.detectChanges(); return tmpl; @@ -215,7 +224,7 @@ export class RxVirtualView implements AfterContentInit, _RxVirtualView { this.#placeholderVisible.set(true); this.#templateIsShown = false; - const template = this.template.viewContainerRef.detach(); + const template = this.#template.viewContainerRef.detach(); if (template) { if (this.cacheEnabled()) { @@ -227,12 +236,12 @@ export class RxVirtualView implements AfterContentInit, _RxVirtualView { template?.detectChanges(); } - if (this.placeholder) { + if (this.#placeholder) { const placeholderRef = this.#viewCache.getPlaceholder(this) ?? - this.placeholder.templateRef.createEmbeddedView({}); + this.#placeholder.templateRef.createEmbeddedView({}); - this.template.viewContainerRef.insert(placeholderRef); + this.#template.viewContainerRef.insert(placeholderRef); placeholderRef.detectChanges(); } } From caf5cf4df559d954d442f347b83e7173881b4396 Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Thu, 19 Dec 2024 22:08:56 +0100 Subject: [PATCH 08/46] feat(template): make virtual-view use config defaults --- .../src/lib/virtual-view.config.ts | 49 ++++++++++++++----- .../src/lib/virtual-view.directive.ts | 39 ++++++++++++--- 2 files changed, 68 insertions(+), 20 deletions(-) diff --git a/libs/template/virtual-view/src/lib/virtual-view.config.ts b/libs/template/virtual-view/src/lib/virtual-view.config.ts index 4207bf3ff..2c59f9bf4 100644 --- a/libs/template/virtual-view/src/lib/virtual-view.config.ts +++ b/libs/template/virtual-view/src/lib/virtual-view.config.ts @@ -1,4 +1,5 @@ import { InjectionToken, Provider } from '@angular/core'; +import { RxStrategyNames } from '@rx-angular/cdk/render-strategies'; export const VIRTUAL_VIEW_CONFIG_TOKEN = new InjectionToken('VIRTUAL_VIEW_CONFIG_TOKEN', { @@ -7,22 +8,40 @@ export const VIRTUAL_VIEW_CONFIG_TOKEN = }); export interface RxVirtualViewConfig { - /** - * The maximum number of templates that can be stored in the cache. - * Defaults to 20. - */ - maxTemplates?: number; + keepLastKnownSize?: boolean; + useContentVisibility?: boolean; + useContainment?: boolean; + placeholderStrategy?: RxStrategyNames; + templateStrategy?: RxStrategyNames; + cacheEnabled?: boolean; + startWithPlaceholderAsap?: boolean; + cache?: { + /** + * The maximum number of templates that can be stored in the cache. + * Defaults to 20. + */ + maxTemplates?: number; - /** - * The maximum number of placeholders that can be stored in the cache. - * Defaults to 20. - */ - maxPlaceholders?: number; + /** + * The maximum number of placeholders that can be stored in the cache. + * Defaults to 20. + */ + maxPlaceholders?: number; + }; } export const VIRTUAL_VIEW_CONFIG_DEFAULT: RxVirtualViewConfig = { - maxTemplates: 20, - maxPlaceholders: 20, + keepLastKnownSize: false, + useContentVisibility: false, + useContainment: true, + placeholderStrategy: 'low', + templateStrategy: 'normal', + startWithPlaceholderAsap: false, + cacheEnabled: true, + cache: { + maxTemplates: 20, + maxPlaceholders: 20, + }, }; /** @@ -57,6 +76,10 @@ export function provideVirtualViewConfig( ): Provider { return { provide: VIRTUAL_VIEW_CONFIG_TOKEN, - useValue: config, + useValue: { + ...VIRTUAL_VIEW_CONFIG_DEFAULT, + ...config, + cache: { ...VIRTUAL_VIEW_CONFIG_DEFAULT.cache, ...(config?.cache ?? {}) }, + }, } satisfies Provider; } diff --git a/libs/template/virtual-view/src/lib/virtual-view.directive.ts b/libs/template/virtual-view/src/lib/virtual-view.directive.ts index b2d257c6c..868883362 100644 --- a/libs/template/virtual-view/src/lib/virtual-view.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view.directive.ts @@ -12,7 +12,10 @@ import { signal, } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { RxStrategyProvider } from '@rx-angular/cdk/render-strategies'; +import { + RxStrategyNames, + RxStrategyProvider, +} from '@rx-angular/cdk/render-strategies'; import { NEVER, Observable, ReplaySubject } from 'rxjs'; import { distinctUntilChanged, switchMap, tap } from 'rxjs/operators'; import { @@ -22,6 +25,7 @@ import { _RxVirtualViewTemplate, } from './model'; import { RxaResizeObserver } from './resize-observer'; +import { VIRTUAL_VIEW_CONFIG_TOKEN } from './virtual-view.config'; import { VirtualViewCache } from './virtual-view-cache'; @Directive({ @@ -47,21 +51,41 @@ export class RxVirtualView readonly #viewCache = inject(VirtualViewCache, { optional: true }); readonly #resizeObserver = inject(RxaResizeObserver, { optional: true }); readonly #destroyRef = inject(DestroyRef); + readonly #config = inject(VIRTUAL_VIEW_CONFIG_TOKEN); #template: _RxVirtualViewTemplate; #placeholder?: _RxVirtualViewPlaceholder; - readonly cacheEnabled = input(true, { transform: booleanAttribute }); + readonly cacheEnabled = input(this.#config.cacheEnabled, { + transform: booleanAttribute, + }); + + readonly startWithPlaceholderAsap = input( + this.#config.startWithPlaceholderAsap, + { + transform: booleanAttribute, + }, + ); + + readonly keepLastKnownSize = input(this.#config.keepLastKnownSize, { + transform: booleanAttribute, + }); - readonly startWithPlaceholderAsap = input(false, { + readonly useContentVisibility = input(this.#config.useContentVisibility, { transform: booleanAttribute, }); - readonly keepLastKnownSize = input(false, { transform: booleanAttribute }); + readonly useContainment = input(this.#config.useContainment, { + transform: booleanAttribute, + }); - readonly useContentVisibility = input(false, { transform: booleanAttribute }); + readonly placeholderStrategy = input>( + this.#config.placeholderStrategy, + ); - readonly useContainment = input(true, { transform: booleanAttribute }); + readonly templateStrategy = input>( + this.#config.templateStrategy, + ); readonly #placeholderVisible = signal(false); @@ -199,13 +223,14 @@ export class RxVirtualView return tmpl; }, - { scope: this }, + { scope: this, strategy: this.templateStrategy() }, ); } private showPlaceholder$() { return this.#strategyProvider.schedule(() => this.renderPlaceholder(), { scope: this, + strategy: this.placeholderStrategy(), }); } From 441a2e7865c85013f17f2f40ef982cf7a030bce0 Mon Sep 17 00:00:00 2001 From: Enea Jahollari Date: Thu, 19 Dec 2024 22:20:48 +0100 Subject: [PATCH 09/46] feat(template): add more docs for directives --- .../lib/virtual-view-observer.directive.ts | 58 +++++++++++++++ .../src/lib/virtual-view.directive.ts | 70 +++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts b/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts index f24ef1415..b8c64a73a 100644 --- a/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts @@ -13,6 +13,23 @@ import { _RxVirtualViewObserver } from './model'; import { RxaResizeObserver } from './resize-observer'; import { VirtualViewCache } from './virtual-view-cache'; +/** + * The RxVirtualViewObserver directive observes the virtual view and emits a boolean value indicating whether the virtual view is visible. + * This is the container for the RxVirtualView directives. + * + * This is a mandatory directive for the RxVirtualView directives to work. + * + * @example + * ```html + *
+ *
+ *
Virtual View 1
+ *
Loading...
+ *
+ *
+ * ``` + * + */ @Directive({ selector: '[rxVirtualViewObserver]', standalone: true, @@ -27,8 +44,27 @@ export class RxVirtualViewObserver implements OnInit, OnDestroy { #observer: IntersectionObserver | null = null; + /** + * The root element to observe. + * + * If not provided, the root element is the element that the directive is attached to. + */ root = input(); + + /** + * The root margin to observe. + * + * This is useful when you want to observe the virtual view in a specific area of the root element. + */ rootMargin = input(''); + + /** + * The threshold to observe. + * + * If you want to observe the virtual view when it is partially visible, you can set the threshold to a number between 0 and 1. + * + * For example, if you set the threshold to 0.5, the virtual view will be observed when it is half visible. + */ threshold = input(0); #rootElement = computed(() => { @@ -71,18 +107,40 @@ export class RxVirtualViewObserver implements OnInit, OnDestroy { this.#elementRef = null; } + /** + * Hide all the virtual views. + * + * This is useful when you want to hide all the virtual views when the user cannot see them. + * + * For example, when the user opens a modal, you can hide all the virtual views to improve performance. + * + * **IMPORTANT:** + * + * Don't forget to call `showAllVisible()` when you want to show the virtual views again. + */ hideAll(): void { this.#forcedHidden$.next(true); } + /** + * Show all the virtual views that are currently visible. + * + * This needs to be called if `hideAll()` was called before. + */ showAllVisible(): void { this.#forcedHidden$.next(false); } register(virtualView: HTMLElement) { const isVisible$ = new ReplaySubject(1); + + // Store the view and the visibility state in the map. + // This allows us to retrieve the visibility state later. this.#elements.set(virtualView, isVisible$); + + // Start observing the virtual view immediately. this.#observer.observe(virtualView); + return combineLatest([isVisible$, this.#forcedHidden$]).pipe( map(([isVisible, forcedHidden]) => (forcedHidden ? false : isVisible)), startWith(false), diff --git a/libs/template/virtual-view/src/lib/virtual-view.directive.ts b/libs/template/virtual-view/src/lib/virtual-view.directive.ts index 868883362..5f0bdc28d 100644 --- a/libs/template/virtual-view/src/lib/virtual-view.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view.directive.ts @@ -28,6 +28,29 @@ import { RxaResizeObserver } from './resize-observer'; import { VIRTUAL_VIEW_CONFIG_TOKEN } from './virtual-view.config'; import { VirtualViewCache } from './virtual-view-cache'; +/** + * The RxVirtualView directive is a directive that allows you to create virtual views. + * + * It can be used on an element/component to create a virtual view. + * + * It works by using 3 directives: + * - `rxVirtualViewTemplate`: The template to render when the virtual view is visible. + * - `rxVirtualViewPlaceholder`: The placeholder to render when the virtual view is not visible. + * - `rxVirtualViewObserver`: The directive that observes the virtual view and emits a boolean value indicating whether the virtual view is visible. + * + * The `rxVirtualViewObserver` directive is mandatory for the `rxVirtualView` directive to work. + * And it needs to be a sibling of the `rxVirtualView` directive. + * + * @example + * ```html + *
+ *
+ *
Virtual View 1
+ *
Loading...
+ *
+ *
+ * ``` + */ @Directive({ selector: '[rxVirtualView]', host: { @@ -56,10 +79,23 @@ export class RxVirtualView #template: _RxVirtualViewTemplate; #placeholder?: _RxVirtualViewPlaceholder; + /** + * Useful when we want to cache the templates and placeholders to optimize view rendering. + * + * Enabled by default. + */ readonly cacheEnabled = input(this.#config.cacheEnabled, { transform: booleanAttribute, }); + /** + * Whether to start with the placeholder asap or not. + * + * If `true`, the placeholder will be rendered immediately, without waiting for the template to be visible. + * This is useful when you want to render the placeholder immediately, but you don't want to wait for the template to be visible. + * + * This is to counter concurrent rendering, and to avoid flickering. + */ readonly startWithPlaceholderAsap = input( this.#config.startWithPlaceholderAsap, { @@ -67,22 +103,44 @@ export class RxVirtualView }, ); + /** + * This will keep the last known size of the host element while the template is visible. + */ readonly keepLastKnownSize = input(this.#config.keepLastKnownSize, { transform: booleanAttribute, }); + /** + * Whether to use content visibility or not. + * + * It will add the `content-visibility` CSS class to the host element, together with + * `contain-intrinsic-width` and `contain-intrinsic-height` CSS properties. + */ readonly useContentVisibility = input(this.#config.useContentVisibility, { transform: booleanAttribute, }); + /** + * Whether to use containment or not. + * + * It will add `contain` css property with: + * - `size layout paint`: if `useContentVisibility` is `true` && placeholder is visible + * - `content`: if `useContentVisibility` is `false` || template is visible + */ readonly useContainment = input(this.#config.useContainment, { transform: booleanAttribute, }); + /** + * The strategy to use for rendering the placeholder. + */ readonly placeholderStrategy = input>( this.#config.placeholderStrategy, ); + /** + * The strategy to use for rendering the template. + */ readonly templateStrategy = input>( this.#config.templateStrategy, ); @@ -204,6 +262,10 @@ export class RxVirtualView this.#placeholder = placeholder; } + /** + * Shows the template using the configured rendering strategy (by default: normal). + * @private + */ private showTemplate$(): Observable> { return this.#strategyProvider.schedule( () => { @@ -227,6 +289,10 @@ export class RxVirtualView ); } + /** + * Shows the placeholder using the configured rendering strategy (by default: low). + * @private + */ private showPlaceholder$() { return this.#strategyProvider.schedule(() => this.renderPlaceholder(), { scope: this, @@ -271,6 +337,10 @@ export class RxVirtualView } } + /** + * Observes the element size and emits the size as an observable. This is used to calculate the containment. + * @private + */ private observeElementSize$() { return this.#resizeObserver.observeElement(this.#elementRef.nativeElement); } From 4bc010fd058f38b7b95156c9c3decb390bcfbc35 Mon Sep 17 00:00:00 2001 From: Enea Jahollari Date: Thu, 19 Dec 2024 22:25:45 +0100 Subject: [PATCH 10/46] feat(template): add developer preview jsdoc --- .../virtual-view/src/lib/resize-observer.ts | 5 +++++ .../src/lib/virtual-view-cache.ts | 2 ++ .../lib/virtual-view-observer.directive.ts | 1 + .../lib/virtual-view-placeholder.directive.ts | 19 +++++++++++++++++++ .../lib/virtual-view-template.directive.ts | 19 +++++++++++++++++++ .../src/lib/virtual-view.config.ts | 2 ++ .../src/lib/virtual-view.directive.ts | 2 ++ 7 files changed, 50 insertions(+) diff --git a/libs/template/virtual-view/src/lib/resize-observer.ts b/libs/template/virtual-view/src/lib/resize-observer.ts index 7a48fcf60..8c5b85c5f 100644 --- a/libs/template/virtual-view/src/lib/resize-observer.ts +++ b/libs/template/virtual-view/src/lib/resize-observer.ts @@ -2,6 +2,11 @@ import { DestroyRef, inject, Injectable } from '@angular/core'; import { Observable, ReplaySubject, Subject } from 'rxjs'; import { distinctUntilChanged, tap } from 'rxjs/operators'; +/** + * A service that observes the resize of the elements. + * + * @developerPreview + */ @Injectable() export class RxaResizeObserver { #destroyRef = inject(DestroyRef); diff --git a/libs/template/virtual-view/src/lib/virtual-view-cache.ts b/libs/template/virtual-view/src/lib/virtual-view-cache.ts index a8b864a31..6692aff83 100644 --- a/libs/template/virtual-view/src/lib/virtual-view-cache.ts +++ b/libs/template/virtual-view/src/lib/virtual-view-cache.ts @@ -4,6 +4,8 @@ import { VIRTUAL_VIEW_CONFIG_TOKEN } from './virtual-view.config'; /** * A service that caches templates and placeholders to optimize view rendering. * It makes sure that all cached resources are cleared when the service is destroyed. + * + * @developerPreview */ @Injectable() export class VirtualViewCache implements OnDestroy { diff --git a/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts b/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts index b8c64a73a..d5c8050d9 100644 --- a/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts @@ -29,6 +29,7 @@ import { VirtualViewCache } from './virtual-view-cache'; * * ``` * + * @developerPreview */ @Directive({ selector: '[rxVirtualViewObserver]', diff --git a/libs/template/virtual-view/src/lib/virtual-view-placeholder.directive.ts b/libs/template/virtual-view/src/lib/virtual-view-placeholder.directive.ts index 0cf892182..744f7283b 100644 --- a/libs/template/virtual-view/src/lib/virtual-view-placeholder.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view-placeholder.directive.ts @@ -1,6 +1,25 @@ import { Directive, inject, TemplateRef } from '@angular/core'; import { _RxVirtualView, _RxVirtualViewPlaceholder } from './model'; +/** + * The RxVirtualViewPlaceholder directive is a directive that allows you to create a placeholder for the virtual view. + * + * It can be used on an element/component to create a placeholder for the virtual view. + * + * It needs to be a sibling of the `rxVirtualView` directive. + * + * @example + * ```html + *
+ *
+ *
Virtual View 1
+ *
Loading...
+ *
+ *
+ * ``` + * + * @developerPreview + */ @Directive({ selector: '[rxVirtualViewPlaceholder]', standalone: true }) export class RxVirtualViewPlaceholder implements _RxVirtualViewPlaceholder { #virtualView = inject(_RxVirtualView); diff --git a/libs/template/virtual-view/src/lib/virtual-view-template.directive.ts b/libs/template/virtual-view/src/lib/virtual-view-template.directive.ts index 10086ebfe..e1b6f11cb 100644 --- a/libs/template/virtual-view/src/lib/virtual-view-template.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view-template.directive.ts @@ -7,6 +7,25 @@ import { import { _RxVirtualViewTemplate } from './model'; import { RxVirtualView } from './virtual-view.directive'; +/** + * The RxVirtualViewTemplate directive is a directive that allows you to create a template for the virtual view. + * + * It can be used on an element/component to create a template for the virtual view. + * + * It needs to be a sibling of the `rxVirtualView` directive. + * + * @example + * ```html + *
+ *
+ *
Virtual View 1
+ *
Loading...
+ *
+ *
+ * ``` + * + * @developerPreview + */ @Directive({ selector: '[rxVirtualViewTemplate]', standalone: true }) export class RxVirtualViewTemplate implements _RxVirtualViewTemplate { #virtualView = inject(RxVirtualView); diff --git a/libs/template/virtual-view/src/lib/virtual-view.config.ts b/libs/template/virtual-view/src/lib/virtual-view.config.ts index 2c59f9bf4..2adfd6e64 100644 --- a/libs/template/virtual-view/src/lib/virtual-view.config.ts +++ b/libs/template/virtual-view/src/lib/virtual-view.config.ts @@ -68,6 +68,8 @@ export const VIRTUAL_VIEW_CONFIG_DEFAULT: RxVirtualViewConfig = { * }; * ``` * + * @developerPreview + * * @param config - The configuration object. * @returns An object that can be provided to the `VirtualView` service. */ diff --git a/libs/template/virtual-view/src/lib/virtual-view.directive.ts b/libs/template/virtual-view/src/lib/virtual-view.directive.ts index 5f0bdc28d..b7abe9835 100644 --- a/libs/template/virtual-view/src/lib/virtual-view.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view.directive.ts @@ -50,6 +50,8 @@ import { VirtualViewCache } from './virtual-view-cache'; * * * ``` + * + * @developerPreview */ @Directive({ selector: '[rxVirtualView]', From 9b7841c8c29b538288f05adefb9545c1fe3b5c0a Mon Sep 17 00:00:00 2001 From: Enea Jahollari Date: Thu, 19 Dec 2024 22:33:35 +0100 Subject: [PATCH 11/46] feat(template): add sample docs --- .../template/api/virtual-view-directive.mdx | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 apps/docs/docs/template/api/virtual-view-directive.mdx diff --git a/apps/docs/docs/template/api/virtual-view-directive.mdx b/apps/docs/docs/template/api/virtual-view-directive.mdx new file mode 100644 index 000000000..b44d8859d --- /dev/null +++ b/apps/docs/docs/template/api/virtual-view-directive.mdx @@ -0,0 +1,75 @@ +--- +sidebar_label: 'RxVirtualView' +sidebar_position: 7 +title: 'RxVirtualView' +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Motivation + +### Motivation + +When dealing with large lists or data sets, rendering a large number of DOM elements can significantly impact performance, leading to slow initial load times and sluggish interactions. This is where the RxVirtualView directive comes in. It provides a way to optimize rendering by only displaying the elements that are currently visible in the viewport. This technique, known as virtual scrolling or windowing, can drastically improve the performance of your Angular applications, especially when handling extensive lists. + +### Features + +- Performance Optimization: Only renders visible elements, improving initial load time and overall performance. +- Template and Placeholder: Allows for defining a template for visible items and a placeholder for out-of-view items. +- Observability: Observes the virtual view to determine which items are visible. +- Configurability: Provides options for customizing behavior, such as rendering strategies and caching. + +### Usage + +The RxVirtualView directive is designed to work seamlessly with other related directives: + +- `rxVirtualViewTemplate`: Defines the template for the virtual view elements. +- `rxVirtualViewPlaceholder`: Defines the placeholder for elements that are not in the view. +- `rxVirtualViewObserver`: The container for rxVirtualView directives, responsible for observing the visibility of elements. + +### Basic Usage + +```angular2html +
+
+
Virtual View 1
+
Loading...
+
+
+``` + +This setup will: + +1. Use rxVirtualViewObserver to monitor the visibility of the rxVirtualView element. +2. Render the content of rxVirtualViewTemplate when the element is visible. +3. Show the rxVirtualViewPlaceholder when the element is not visible. + +### Advanced Usage + +You can further customize the behavior of RxVirtualView using these optional inputs: + +- `cacheEnabled`: Whether to cache the templates and placeholders. +- `startWithPlaceholderAsap`: Whether to render the placeholder immediately. +- `keepLastKnownSize`: Whether to keep the last known size of the element. +- `useContentVisibility`: Whether to use the content-visibility CSS property. +- `useContainment`: Whether to use the contain CSS property. +- `placeholderStrategy`: The rendering strategy for the placeholder. +- `templateStrategy`: The rendering strategy for the template. + +### Examples + +Optimizing a long list + +```angular2html +
    + @for (item of items; track item.id) { +
  • +
    {{ item.name }}
    +
    Loading...
    +
  • + } +
+``` + +This example demonstrates how to use RxVirtualView to optimize a long list by only rendering the visible list items. From 8202a13588d817005249cf7605e4f57162637c48 Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Fri, 20 Dec 2024 10:03:43 +0100 Subject: [PATCH 12/46] docs(template): improve virtual view docs --- .../template/api/virtual-view-directive.mdx | 75 +++++++++++++------ 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/apps/docs/docs/template/api/virtual-view-directive.mdx b/apps/docs/docs/template/api/virtual-view-directive.mdx index b44d8859d..c6c82060c 100644 --- a/apps/docs/docs/template/api/virtual-view-directive.mdx +++ b/apps/docs/docs/template/api/virtual-view-directive.mdx @@ -9,32 +9,49 @@ import TabItem from '@theme/TabItem'; ## Motivation -### Motivation +A large number of DOM elements can significantly impact performance, leading to slow initial load times and sluggish interactions. -When dealing with large lists or data sets, rendering a large number of DOM elements can significantly impact performance, leading to slow initial load times and sluggish interactions. This is where the RxVirtualView directive comes in. It provides a way to optimize rendering by only displaying the elements that are currently visible in the viewport. This technique, known as virtual scrolling or windowing, can drastically improve the performance of your Angular applications, especially when handling extensive lists. +When dealing with large lists or data sets there is a technique, known as virtual scrolling or windowing. +It drastically improves the performance of your Angular applications. -### Features +However, if you are not working with plain lists & highly dynamic components, the concept of virtual scrolling isn't applicable. +This applies to: + +- masonry like layouts +- dynamic grids +- landing pages with widgets + +This is where the RxVirtualView directive comes in. It provides a simple way to only display the elements that are currently visible to +the user. + +## Features - Performance Optimization: Only renders visible elements, improving initial load time and overall performance. - Template and Placeholder: Allows for defining a template for visible items and a placeholder for out-of-view items. -- Observability: Observes the virtual view to determine which items are visible. - Configurability: Provides options for customizing behavior, such as rendering strategies and caching. -### Usage +## Usage -The RxVirtualView directive is designed to work seamlessly with other related directives: +RxVirtualView is designed to work in combination with related directives: -- `rxVirtualViewTemplate`: Defines the template for the virtual view elements. -- `rxVirtualViewPlaceholder`: Defines the placeholder for elements that are not in the view. -- `rxVirtualViewObserver`: The container for rxVirtualView directives, responsible for observing the visibility of elements. +- `rxVirtualView`: Defines the DOM node being observed for visibility. +- `rxVirtualViewTemplate`: Defines the template shown when the observed node is visible. +- `rxVirtualViewPlaceholder`: Defines the placeholder shown when the observed node isn't visible. +- `rxVirtualViewObserver`: Defines the node being used for the `IntersectionObserver`. Provides cache & other services. ### Basic Usage -```angular2html -
-
+```html + +
+
+
Virtual View 1
-
Loading...
+ +
+ Placeholder +
+
``` @@ -57,19 +74,29 @@ You can further customize the behavior of RxVirtualView using these optional inp - `placeholderStrategy`: The rendering strategy for the placeholder. - `templateStrategy`: The rendering strategy for the template. -### Examples +## Examples + +### Optimizing Lists + +This example demonstrates how to use RxVirtualView to optimize a long list by only rendering the visible list items. +We are only rendering the `item` component when it's visible to the user. Otherwise, it gets replaced by an empty div. + +:::tip Define placeholder dimensions -Optimizing a long list +The placeholder is what makes or breaks your experience with `RxVirtualView`. In best case it's just +an empty container which has just the same dimensions as its template it should replace. -```angular2html -
    +This will make sure you don't run into stuttery scrolling behavior and layout shifts. + +::: + +```html +
    @for (item of items; track item.id) { -
  • -
    {{ item.name }}
    -
    Loading...
    -
  • +
    + +
    +
    } -
+
``` - -This example demonstrates how to use RxVirtualView to optimize a long list by only rendering the visible list items. From 8cfebad1041a7b25ff58a608474ea6939771b183 Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Fri, 20 Dec 2024 10:41:19 +0100 Subject: [PATCH 13/46] docs(template): improve virtual view docs --- .../template/api/virtual-view-directive.mdx | 21 +++++++++++------- .../rx-virtual-view/rx-virtual-view.jpg | Bin 0 -> 33908 bytes 2 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 apps/docs/static/img/template/rx-virtual-view/rx-virtual-view.jpg diff --git a/apps/docs/docs/template/api/virtual-view-directive.mdx b/apps/docs/docs/template/api/virtual-view-directive.mdx index c6c82060c..c37e0237d 100644 --- a/apps/docs/docs/template/api/virtual-view-directive.mdx +++ b/apps/docs/docs/template/api/virtual-view-directive.mdx @@ -7,28 +7,33 @@ title: 'RxVirtualView' import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +:::info Developer preview + +This feature is under developer preview. It won't follow semver. + +::: + ## Motivation A large number of DOM elements can significantly impact performance, leading to slow initial load times and sluggish interactions. +Especially mobile users have a very limited viewport available. Most of the pages contents are hidden below +the fold. So why render them at all? + When dealing with large lists or data sets there is a technique, known as virtual scrolling or windowing. It drastically improves the performance of your Angular applications. -However, if you are not working with plain lists & highly dynamic components, the concept of virtual scrolling isn't applicable. -This applies to: +However, if you are not working with plain lists, or highly dynamic components, the concept of virtual scrolling isn't applicable. +This is true for: -- masonry like layouts +- masonry layouts - dynamic grids - landing pages with widgets This is where the RxVirtualView directive comes in. It provides a simple way to only display the elements that are currently visible to the user. -## Features - -- Performance Optimization: Only renders visible elements, improving initial load time and overall performance. -- Template and Placeholder: Allows for defining a template for visible items and a placeholder for out-of-view items. -- Configurability: Provides options for customizing behavior, such as rendering strategies and caching. +![rx-virtual-view](../../../static/img/template/rx-virtual-view/rx-virtual-view.jpg) ## Usage diff --git a/apps/docs/static/img/template/rx-virtual-view/rx-virtual-view.jpg b/apps/docs/static/img/template/rx-virtual-view/rx-virtual-view.jpg new file mode 100644 index 0000000000000000000000000000000000000000..af0296d5f2c0023577562f7451b1a27dde7ee762 GIT binary patch literal 33908 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 JqOSL)e*?x^@aO;l literal 0 HcmV?d00001 From 061b4fa453eff5748fe58aebde1f9876f3575a8b Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Fri, 20 Dec 2024 10:49:31 +0100 Subject: [PATCH 14/46] fix(template): rxjs 6 compat --- .../virtual-view/src/lib/virtual-view.directive.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/libs/template/virtual-view/src/lib/virtual-view.directive.ts b/libs/template/virtual-view/src/lib/virtual-view.directive.ts index b7abe9835..f5858f3b3 100644 --- a/libs/template/virtual-view/src/lib/virtual-view.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view.directive.ts @@ -17,7 +17,7 @@ import { RxStrategyProvider, } from '@rx-angular/cdk/render-strategies'; import { NEVER, Observable, ReplaySubject } from 'rxjs'; -import { distinctUntilChanged, switchMap, tap } from 'rxjs/operators'; +import { distinctUntilChanged, finalize, switchMap, tap } from 'rxjs/operators'; import { _RxVirtualView, _RxVirtualViewObserver, @@ -240,10 +240,8 @@ export class RxVirtualView } return this.#placeholderVisible() ? NEVER : this.showPlaceholder$(); }), - tap({ - unsubscribe: () => { - this.#viewCache.clear(this); - }, + finalize(() => { + this.#viewCache.clear(this); }), takeUntilDestroyed(this.#destroyRef), ) From dcd4f1a1604485b7c908e4d5d5a93e1faa0a7eaa Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Fri, 20 Dec 2024 10:50:15 +0100 Subject: [PATCH 15/46] refactor(template): improve config typing --- .../src/lib/virtual-view-cache.ts | 4 ++-- .../src/lib/virtual-view.config.ts | 24 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/libs/template/virtual-view/src/lib/virtual-view-cache.ts b/libs/template/virtual-view/src/lib/virtual-view-cache.ts index 6692aff83..8b67b80a1 100644 --- a/libs/template/virtual-view/src/lib/virtual-view-cache.ts +++ b/libs/template/virtual-view/src/lib/virtual-view-cache.ts @@ -12,13 +12,13 @@ export class VirtualViewCache implements OnDestroy { #config = inject(VIRTUAL_VIEW_CONFIG_TOKEN); // Maximum number of templates that can be stored in the cache. - #maxTemplates = this.#config.maxTemplates; + #maxTemplates = this.#config.cache.maxTemplates; // Cache for storing template views, identified by a unique key, which is the directive instance. #templateCache = new Map(); // Maximum number of placeholders that can be stored in the cache. - #maxPlaceholders = this.#config.maxPlaceholders; + #maxPlaceholders = this.#config.cache.maxPlaceholders; // Cache for storing placeholder views, identified by a unique key. #placeholderCache = new Map(); diff --git a/libs/template/virtual-view/src/lib/virtual-view.config.ts b/libs/template/virtual-view/src/lib/virtual-view.config.ts index 2adfd6e64..2d8536704 100644 --- a/libs/template/virtual-view/src/lib/virtual-view.config.ts +++ b/libs/template/virtual-view/src/lib/virtual-view.config.ts @@ -8,25 +8,25 @@ export const VIRTUAL_VIEW_CONFIG_TOKEN = }); export interface RxVirtualViewConfig { - keepLastKnownSize?: boolean; - useContentVisibility?: boolean; - useContainment?: boolean; - placeholderStrategy?: RxStrategyNames; - templateStrategy?: RxStrategyNames; - cacheEnabled?: boolean; - startWithPlaceholderAsap?: boolean; - cache?: { + keepLastKnownSize: boolean; + useContentVisibility: boolean; + useContainment: boolean; + placeholderStrategy: RxStrategyNames; + templateStrategy: RxStrategyNames; + cacheEnabled: boolean; + startWithPlaceholderAsap: boolean; + cache: { /** * The maximum number of templates that can be stored in the cache. * Defaults to 20. */ - maxTemplates?: number; + maxTemplates: number; /** * The maximum number of placeholders that can be stored in the cache. * Defaults to 20. */ - maxPlaceholders?: number; + maxPlaceholders: number; }; } @@ -74,7 +74,9 @@ export const VIRTUAL_VIEW_CONFIG_DEFAULT: RxVirtualViewConfig = { * @returns An object that can be provided to the `VirtualView` service. */ export function provideVirtualViewConfig( - config: RxVirtualViewConfig, + config: Partial< + RxVirtualViewConfig & { cache: Partial } + >, ): Provider { return { provide: VIRTUAL_VIEW_CONFIG_TOKEN, From ce61bd9fd01e8856471b6f4147a648741f87ab14 Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Fri, 20 Dec 2024 10:59:20 +0100 Subject: [PATCH 16/46] fix(template): destroy cached views --- .../src/lib/virtual-view-cache.ts | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/libs/template/virtual-view/src/lib/virtual-view-cache.ts b/libs/template/virtual-view/src/lib/virtual-view-cache.ts index 8b67b80a1..7e3191245 100644 --- a/libs/template/virtual-view/src/lib/virtual-view-cache.ts +++ b/libs/template/virtual-view/src/lib/virtual-view-cache.ts @@ -31,10 +31,11 @@ export class VirtualViewCache implements OnDestroy { * @param view - The ViewRef of the placeholder to cache. */ storePlaceholder(key: unknown, view: ViewRef) { + if (this.#maxPlaceholders <= 0) { + return; + } if (this.#placeholderCache.size >= this.#maxPlaceholders) { - this.#placeholderCache.delete( - this.#placeholderCache.entries().next().value[0], - ); + this.#removeOldestEntry(this.#placeholderCache); } this.#placeholderCache.set(key, view); } @@ -57,8 +58,11 @@ export class VirtualViewCache implements OnDestroy { * @param view - The ViewRef of the template to cache. */ storeTemplate(key: unknown, view: ViewRef) { + if (this.#maxTemplates <= 0) { + return; + } if (this.#templateCache.size >= this.#maxTemplates) { - this.#templateCache.delete(this.#templateCache.entries().next().value[0]); + this.#removeOldestEntry(this.#templateCache); } this.#templateCache.set(key, view); } @@ -79,7 +83,9 @@ export class VirtualViewCache implements OnDestroy { * @param key - The key of the template and placeholder to remove. */ clear(key: unknown) { + this.#templateCache.get(key)?.destroy(); this.#templateCache.delete(key); + this.#placeholderCache.get(key)?.destroy(); this.#placeholderCache.delete(key); } @@ -87,7 +93,16 @@ export class VirtualViewCache implements OnDestroy { * Clears all cached resources when the service is destroyed. */ ngOnDestroy() { + this.#templateCache.forEach((view) => view.destroy()); + this.#placeholderCache.forEach((view) => view.destroy()); this.#templateCache.clear(); this.#placeholderCache.clear(); } + + #removeOldestEntry(cache: Map) { + const oldestValue = cache.entries().next().value?.[0]; + if (oldestValue !== undefined) { + cache.delete(oldestValue); + } + } } From b42304a1059558de16f939576ce96a011572657b Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Fri, 20 Dec 2024 21:04:03 +0100 Subject: [PATCH 17/46] refactor(template): improve virtual view config typings --- libs/template/virtual-view/src/lib/virtual-view.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/template/virtual-view/src/lib/virtual-view.config.ts b/libs/template/virtual-view/src/lib/virtual-view.config.ts index 2d8536704..bfcbdc25a 100644 --- a/libs/template/virtual-view/src/lib/virtual-view.config.ts +++ b/libs/template/virtual-view/src/lib/virtual-view.config.ts @@ -75,7 +75,7 @@ export const VIRTUAL_VIEW_CONFIG_DEFAULT: RxVirtualViewConfig = { */ export function provideVirtualViewConfig( config: Partial< - RxVirtualViewConfig & { cache: Partial } + RxVirtualViewConfig & { cache?: Partial } >, ): Provider { return { From 4c496b447056f97ff02cbffbf472274671d8cbdb Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Fri, 20 Dec 2024 21:04:17 +0100 Subject: [PATCH 18/46] perf(template): fix memory leaks in view cache --- .../virtual-view/src/lib/virtual-view-cache.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/libs/template/virtual-view/src/lib/virtual-view-cache.ts b/libs/template/virtual-view/src/lib/virtual-view-cache.ts index 7e3191245..c337d196a 100644 --- a/libs/template/virtual-view/src/lib/virtual-view-cache.ts +++ b/libs/template/virtual-view/src/lib/virtual-view-cache.ts @@ -32,6 +32,7 @@ export class VirtualViewCache implements OnDestroy { */ storePlaceholder(key: unknown, view: ViewRef) { if (this.#maxPlaceholders <= 0) { + view.destroy(); return; } if (this.#placeholderCache.size >= this.#maxPlaceholders) { @@ -47,7 +48,9 @@ export class VirtualViewCache implements OnDestroy { * @returns The ViewRef of the cached placeholder, or undefined if not found. */ getPlaceholder(key: unknown) { - return this.#placeholderCache.get(key); + const view = this.#placeholderCache.get(key); + this.#placeholderCache.delete(key); + return view; } /** @@ -59,6 +62,7 @@ export class VirtualViewCache implements OnDestroy { */ storeTemplate(key: unknown, view: ViewRef) { if (this.#maxTemplates <= 0) { + view.destroy(); return; } if (this.#templateCache.size >= this.#maxTemplates) { @@ -74,7 +78,9 @@ export class VirtualViewCache implements OnDestroy { * @returns The ViewRef of the cached template, or undefined if not found. */ getTemplate(key: unknown) { - return this.#templateCache.get(key); + const view = this.#templateCache.get(key); + this.#templateCache.delete(key); + return view; } /** @@ -100,9 +106,11 @@ export class VirtualViewCache implements OnDestroy { } #removeOldestEntry(cache: Map) { - const oldestValue = cache.entries().next().value?.[0]; + const oldestValue = cache.entries().next().value; if (oldestValue !== undefined) { - cache.delete(oldestValue); + const [key, view] = oldestValue; + view?.destroy(); + cache.delete(key); } } } From 2b6a5a641b61a2b4d3007b0311acab18ed84ab86 Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Sat, 21 Dec 2024 20:49:13 +0100 Subject: [PATCH 19/46] docs(template): improve virtual view docs --- .../template/api/virtual-view-directive.mdx | 133 ++++++++++++++---- 1 file changed, 105 insertions(+), 28 deletions(-) diff --git a/apps/docs/docs/template/api/virtual-view-directive.mdx b/apps/docs/docs/template/api/virtual-view-directive.mdx index c37e0237d..12996ef67 100644 --- a/apps/docs/docs/template/api/virtual-view-directive.mdx +++ b/apps/docs/docs/template/api/virtual-view-directive.mdx @@ -35,25 +35,26 @@ the user. ![rx-virtual-view](../../../static/img/template/rx-virtual-view/rx-virtual-view.jpg) -## Usage +## Basic Usage RxVirtualView is designed to work in combination with related directives: +- `rxVirtualViewObserver`: Defines the node being used for the `IntersectionObserver`. Provides cache & other services. - `rxVirtualView`: Defines the DOM node being observed for visibility. - `rxVirtualViewTemplate`: Defines the template shown when the observed node is visible. -- `rxVirtualViewPlaceholder`: Defines the placeholder shown when the observed node isn't visible. -- `rxVirtualViewObserver`: Defines the node being used for the `IntersectionObserver`. Provides cache & other services. +- `rxVirtualViewPlaceholder`: (Optional) Defines the placeholder shown when the observed node isn't visible. -### Basic Usage +### Show a widget when it's visible, otherwise show a placeholder ```html
-
- -
Virtual View 1
- -
+ +
+ + + +
Placeholder
@@ -67,25 +68,6 @@ This setup will: 2. Render the content of rxVirtualViewTemplate when the element is visible. 3. Show the rxVirtualViewPlaceholder when the element is not visible. -### Advanced Usage - -You can further customize the behavior of RxVirtualView using these optional inputs: - -- `cacheEnabled`: Whether to cache the templates and placeholders. -- `startWithPlaceholderAsap`: Whether to render the placeholder immediately. -- `keepLastKnownSize`: Whether to keep the last known size of the element. -- `useContentVisibility`: Whether to use the content-visibility CSS property. -- `useContainment`: Whether to use the contain CSS property. -- `placeholderStrategy`: The rendering strategy for the placeholder. -- `templateStrategy`: The rendering strategy for the template. - -## Examples - -### Optimizing Lists - -This example demonstrates how to use RxVirtualView to optimize a long list by only rendering the visible list items. -We are only rendering the `item` component when it's visible to the user. Otherwise, it gets replaced by an empty div. - :::tip Define placeholder dimensions The placeholder is what makes or breaks your experience with `RxVirtualView`. In best case it's just @@ -95,6 +77,11 @@ This will make sure you don't run into stuttery scrolling behavior and layout sh ::: +### Optimize lists with @for + +This example demonstrates how to use RxVirtualView to optimize lists by only rendering the visible list items. +We are only rendering the `item` component when it's visible to the user. Otherwise, it gets replaced by an empty div. + ```html
@for (item of items; track item.id) { @@ -105,3 +92,93 @@ This will make sure you don't run into stuttery scrolling behavior and layout sh }
``` + +## Configuration & Inputs + +### RxVirtualViewObserver Inputs + +| Input | Type | description | +| ------------ | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `root` | ` ElementRef \ HTMLElement \ null` | The element where the IntersectionObserver is applied to. `null` referes to the browser viewport. See more [here](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#root) | +| `rootMargin` | `string` | Margin around the root. See more [here](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#rootMargin) | +| `threshold` | `number \ number[]` | Indicate at what percentage of the target's visibility the observer's callback should be executed. See more [here](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#threshold) | + +### RxVirtualView Inputs + +| Input | Type | description | +| -------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --- | ------------------- | +| `cacheEnabled` | `boolean` | Useful when we want to cache the templates and placeholders to optimize view rendering. | +| `startWithPlaceholderAsap` | `boolean` | Whether to start with the placeholder asap or not. If `true`, the placeholder will be rendered immediately, without waiting for the template to be visible. This is useful when you want to render the placeholder immediately, but you don't want to wait for the template to be visible. This is to counter concurrent rendering, and to avoid flickering. | +| `keepLastKnownSize` | `boolean` | This will keep the last known size of the host element while the template is visible. It sets 'minHeight' to the host node | +| `useContentVisibility` | `boolean` | It will add the `content-visibility` CSS class to the host element, together with `contain-intrinsic-width` and `contain-intrinsic-height` CSS properties. | +| `useContainment` | `boolean` | It will add `contain` css property with:
- `size layout paint`: if `useContentVisibility` is `true` && placeholder is visible
- `content`: if `useContentVisibility` is `false` | | template is visible | +| `placeholderStrategy` | `boolean` | The strategy to use for rendering the placeholder.
Defaults to: `low`
[Read more about strategies](../../cdk/render-strategies/strategies/concurrent-strategies) | +| `templateStrategy` | `boolean` | The strategy to use for rendering the template.
Defaults to: `normal`
[Read more about strategies](../../cdk/render-strategies/strategies/concurrent-strategies) | + +### RxVirtualViewConfig + +Defines an interface representing all configuration that can be adjusted on provider level. + +```typescript +interface RxVirtualViewConfig { + keepLastKnownSize: boolean; + useContentVisibility: boolean; + useContainment: boolean; + placeholderStrategy: RxStrategyNames; + templateStrategy: RxStrategyNames; + cacheEnabled: boolean; + startWithPlaceholderAsap: boolean; + cache: { + /** + * The maximum number of templates that can be stored in the cache. + * Defaults to 20. + */ + maxTemplates: number; + + /** + * The maximum number of placeholders that can be stored in the cache. + * Defaults to 20. + */ + maxPlaceholders: number; + }; +} +``` + +### Customize the config + +When you want to customize the default configuration on provider level, you can use the `provideVirtualViewConfig` function. + +```typescript +import { ApplicationConfig } from '@angular/core'; +import { provideVirtualViewConfig } from '@rx-angular/template/virtual-view'; + +const appConfig: ApplicationConfig = { + providers: [ + provideVirtualViewConfig({ + /* your custom configuration */ + }), + ], +}; +``` + +### Default configuration + +This is the default configuration which will be used when no other config was provided. + +```typescript + +{ + keepLastKnownSize: false, + useContentVisibility: false, + useContainment: true, + placeholderStrategy: 'low', + templateStrategy: 'normal', + startWithPlaceholderAsap: false, + cacheEnabled: true, + cache: { + maxTemplates: 20, + maxPlaceholders: 20, + }, +}; + +``` From a871a18f39dcae2bf4f67bfba74ab2ae3e829b28 Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Sat, 21 Dec 2024 20:51:20 +0100 Subject: [PATCH 20/46] refactor(template): remove rxjs 7 only apis --- libs/template/virtual-view/src/lib/resize-observer.ts | 6 ++---- .../virtual-view/src/lib/virtual-view-observer.directive.ts | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/libs/template/virtual-view/src/lib/resize-observer.ts b/libs/template/virtual-view/src/lib/resize-observer.ts index 8c5b85c5f..e90c7b5d2 100644 --- a/libs/template/virtual-view/src/lib/resize-observer.ts +++ b/libs/template/virtual-view/src/lib/resize-observer.ts @@ -1,6 +1,6 @@ import { DestroyRef, inject, Injectable } from '@angular/core'; import { Observable, ReplaySubject, Subject } from 'rxjs'; -import { distinctUntilChanged, tap } from 'rxjs/operators'; +import { distinctUntilChanged, finalize } from 'rxjs/operators'; /** * A service that observes the resize of the elements. @@ -34,9 +34,7 @@ export class RxaResizeObserver { return this.#elements.get(element).pipe( distinctUntilChanged(), - tap({ - unsubscribe: () => this.#elements.delete(element), - }), + finalize(() => this.#elements.delete(element)), ); } } diff --git a/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts b/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts index d5c8050d9..78d936f8f 100644 --- a/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts @@ -8,7 +8,7 @@ import { OnInit, } from '@angular/core'; import { BehaviorSubject, combineLatest, ReplaySubject, Subject } from 'rxjs'; -import { distinctUntilChanged, map, startWith, tap } from 'rxjs/operators'; +import { distinctUntilChanged, finalize, map, startWith } from 'rxjs/operators'; import { _RxVirtualViewObserver } from './model'; import { RxaResizeObserver } from './resize-observer'; import { VirtualViewCache } from './virtual-view-cache'; @@ -146,9 +146,7 @@ export class RxVirtualViewObserver implements OnInit, OnDestroy { map(([isVisible, forcedHidden]) => (forcedHidden ? false : isVisible)), startWith(false), distinctUntilChanged(), - tap({ - unsubscribe: () => this.#elements.delete(virtualView), - }), + finalize(() => this.#elements.delete(virtualView)), ); } } From c15f3fdde25a39ea9ed94e51fcea099f4db09169 Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Sun, 22 Dec 2024 15:09:03 +0100 Subject: [PATCH 21/46] fix(template): fix memory leaks in observers --- libs/template/virtual-view/src/lib/resize-observer.ts | 5 ++++- .../src/lib/virtual-view-observer.directive.ts | 8 +++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/libs/template/virtual-view/src/lib/resize-observer.ts b/libs/template/virtual-view/src/lib/resize-observer.ts index e90c7b5d2..997f79155 100644 --- a/libs/template/virtual-view/src/lib/resize-observer.ts +++ b/libs/template/virtual-view/src/lib/resize-observer.ts @@ -34,7 +34,10 @@ export class RxaResizeObserver { return this.#elements.get(element).pipe( distinctUntilChanged(), - finalize(() => this.#elements.delete(element)), + finalize(() => { + this.#resizeObserver.unobserve(element); + this.#elements.delete(element); + }), ); } } diff --git a/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts b/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts index 78d936f8f..ccba27fe2 100644 --- a/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts @@ -8,7 +8,7 @@ import { OnInit, } from '@angular/core'; import { BehaviorSubject, combineLatest, ReplaySubject, Subject } from 'rxjs'; -import { distinctUntilChanged, finalize, map, startWith } from 'rxjs/operators'; +import { distinctUntilChanged, finalize, map } from 'rxjs/operators'; import { _RxVirtualViewObserver } from './model'; import { RxaResizeObserver } from './resize-observer'; import { VirtualViewCache } from './virtual-view-cache'; @@ -144,9 +144,11 @@ export class RxVirtualViewObserver implements OnInit, OnDestroy { return combineLatest([isVisible$, this.#forcedHidden$]).pipe( map(([isVisible, forcedHidden]) => (forcedHidden ? false : isVisible)), - startWith(false), distinctUntilChanged(), - finalize(() => this.#elements.delete(virtualView)), + finalize(() => { + this.#observer.unobserve(virtualView); + this.#elements.delete(virtualView); + }), ); } } From b8c895a72f2e685cccc96dbeac85756bd95fb00f Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Sun, 22 Dec 2024 15:09:33 +0100 Subject: [PATCH 22/46] refactor(template): lazy subscribe to observer --- .../virtual-view/src/lib/virtual-view.directive.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/template/virtual-view/src/lib/virtual-view.directive.ts b/libs/template/virtual-view/src/lib/virtual-view.directive.ts index f5858f3b3..5583b7a88 100644 --- a/libs/template/virtual-view/src/lib/virtual-view.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view.directive.ts @@ -202,10 +202,6 @@ export class RxVirtualView 'RxVirtualView expects you to provide a RxVirtualViewObserver', ); } - this.#observer - .register(this.#elementRef.nativeElement) - .pipe(takeUntilDestroyed()) - .subscribe((visible) => this.#visible$.next(visible)); } ngAfterContentInit() { @@ -217,6 +213,10 @@ export class RxVirtualView if (this.startWithPlaceholderAsap()) { this.renderPlaceholder(); } + this.#observer + .register(this.#elementRef.nativeElement) + .pipe(takeUntilDestroyed(this.#destroyRef)) + .subscribe((visible) => this.#visible$.next(visible)); this.#visible$ .pipe( distinctUntilChanged(), From b69f039a286a49db2a1c5d59e89dd47c47435f22 Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Sun, 22 Dec 2024 15:09:50 +0100 Subject: [PATCH 23/46] test(template): implement basic tests for virtual view --- .../lib/tests/virtual-view.directive.spec.ts | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 libs/template/virtual-view/src/lib/tests/virtual-view.directive.spec.ts diff --git a/libs/template/virtual-view/src/lib/tests/virtual-view.directive.spec.ts b/libs/template/virtual-view/src/lib/tests/virtual-view.directive.spec.ts new file mode 100644 index 000000000..ee6e9425a --- /dev/null +++ b/libs/template/virtual-view/src/lib/tests/virtual-view.directive.spec.ts @@ -0,0 +1,138 @@ +import { Component, input } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { RX_RENDER_STRATEGIES_CONFIG } from '@rx-angular/cdk/render-strategies'; +import { tap } from 'rxjs'; +import { provideVirtualViewConfig } from '../virtual-view.config'; +import { RxVirtualView } from '../virtual-view.directive'; +import { RxVirtualViewObserver } from '../virtual-view-observer.directive'; +import { RxVirtualViewPlaceholder } from '../virtual-view-placeholder.directive'; +import { RxVirtualViewTemplate } from '../virtual-view-template.directive'; + +@Component({ + template: ` +
+
+
ze-template
+ @if (withPlaceholder()) { +
+ ze-placeholder +
+ } +
+
+ `, + standalone: true, + imports: [ + RxVirtualViewObserver, + RxVirtualView, + RxVirtualViewPlaceholder, + RxVirtualViewTemplate, + ], +}) +class VirtualViewTestComponent { + withPlaceholder = input(true); +} + +class IntersectionObserverMock { + static cb: (entries: IntersectionObserverEntry[]) => void; + + constructor( + public cb: (entries: IntersectionObserverEntry[]) => void, + init?: IntersectionObserverInit, + ) { + IntersectionObserverMock.cb = cb; + } + + observe(element: Element) {} + + unobserve(element: Element) {} + + disconnect() {} +} + +class ResizeObserverMock { + static cb: (entries: ResizeObserverEntry[]) => void; + + constructor(public cb: (entries: ResizeObserverEntry[]) => void) { + ResizeObserverMock.cb = cb; + } + + observe(element: Element, options?: ResizeObserverOptions) {} + + unobserve(element: Element) {} + + disconnect() {} +} + +describe('RxVirtualView', () => { + let origIntersectionObserver; + let origResizeObserver; + let fixture: ComponentFixture; + beforeEach(() => { + origIntersectionObserver = window.IntersectionObserver; + origResizeObserver = window.ResizeObserver; + window.IntersectionObserver = IntersectionObserverMock as any; + window.ResizeObserver = ResizeObserverMock as any; + + TestBed.configureTestingModule({ + imports: [VirtualViewTestComponent], + providers: [ + { + provide: RX_RENDER_STRATEGIES_CONFIG, + useValue: { + primaryStrategy: 'sync', + customStrategies: { + sync: { + name: 'sync', + work: (cdRef) => { + cdRef.detectChanges(); + }, + behavior: + ({ work }) => + (o$) => + o$.pipe(tap(() => work())), + }, + }, + }, + }, + provideVirtualViewConfig({ + placeholderStrategy: 'sync', + templateStrategy: 'sync', + }), + ], + }); + fixture = TestBed.createComponent(VirtualViewTestComponent); + }); + + afterEach(() => { + window.IntersectionObserver = origIntersectionObserver; + window.ResizeObserver = origResizeObserver; + }); + + it('should display template when visible', () => { + fixture.detectChanges(); + const view = fixture.debugElement.query(By.css('.widget')).nativeElement; + IntersectionObserverMock.cb([ + { isIntersecting: true, target: view } as any, + ]); + expect(view.textContent.trim()).toEqual('ze-template'); + }); + it('should display nothing when not visible and no placeholder', () => { + fixture.componentRef.setInput('withPlaceholder', false); + fixture.detectChanges(); + const view = fixture.debugElement.query(By.css('.widget')).nativeElement; + IntersectionObserverMock.cb([ + { isIntersecting: false, target: view } as any, + ]); + expect(view.textContent.trim()).toEqual(''); + }); + it('should display placeholder when not visible', () => { + fixture.detectChanges(); + const view = fixture.debugElement.query(By.css('.widget')).nativeElement; + IntersectionObserverMock.cb([ + { isIntersecting: false, target: view } as any, + ]); + expect(view.textContent.trim()).toEqual('ze-placeholder'); + }); +}); From 7890a1639acf2f40c9a2ba1baee54e5e7c7c5ab2 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Mon, 30 Sep 2024 12:34:02 +0200 Subject: [PATCH 24/46] docs(state): update manage entites using ngrx/entity guide with functional approach --- .../manage-entities-using-ngrx-entity.md | 126 ---------- .../manage-entities-using-ngrx-entity.mdx | 222 ++++++++++++++++++ 2 files changed, 222 insertions(+), 126 deletions(-) delete mode 100644 apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.md create mode 100644 apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx diff --git a/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.md b/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.md deleted file mode 100644 index 66b413452..000000000 --- a/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -sidebar_position: 2 -title: Manage entities using @ngrx/entity -# Renamed from libs/state/docs/snippets/manage-collections-with-ngrx-entity.md ---- - -_Author: [@Phhansen](https://github.com/Phhansen)_ - -# Manage entities using `@ngrx/entity` - -When working with collections or arrays in our state, we tend to write many repeated code when we want to add, update or delete items from these collections. - -In NgRx, they have created a helper library called [@ngrx/entity adapter](https://ngrx.io/guide/entity/adapter). The adapter provides a simple API to manipulate and query these collections, hiding much repetitive code needed. - -Let's say we have a collection of type `Item` as part of our component state; - -```typescript -interface Item { - id: string; - name: string; -} - -interface ComponentState { - items: Item[]; - loading: boolean; -} -``` - -Now, if we want to add one item to our array _(in an immutable way)_, we replace the `items` array in the state with a new reference. - -```typescript -@Component({ - selector: 'my-component', -}) -export class MyComponent extends RxState { - readonly addItem$ = new Subject(); - - constructor() { - super(); - - this.connect(this.addItem$, (oldState, itemName) => { - const newItem = { - id: uuid(), // unique hash generation fn() - name: itemName, - }; - - return { - ...oldState, - items: [...oldState.items, newItem], - }; - }); - } -} -``` - -If we want to update one item, we have to query the `items` array first to get a hold of the item and then construct a new array again. - -What about deleting an item? You get the picture. **It´s a lot of code**, and it will grow even more if we have several types of collections in our state. - -## Using `@ngrx/entity` - -Now let us see how our code will look when using `@ngrx/entity`. - -```typescript -interface Item { - id: string; - name: string; -} - -interface ComponentState extends EntityState { - loading: boolean; -} - -const adapter: EntityAdapter = createEntityAdapter({ - selectId: (item: Item) => item.id, -}); -``` - -The entity adapter needs a `selectId` function which is used to query items by `id` within the collection. - -Now let's see how the component has changed: - -```typescript -@Component({ - selector: 'my-component', -}) -export class MyComponent extends RxState { - readonly addItem$ = new Subject(); - - constructor() { - super(); - - this.connect(this.addItem$, (oldState, itemName) => - adapter.addOne({ id: uuid(), name: itemName() }, oldState) - ); - } -} -``` - -The `addOne()` function is just one of many functions that help us manipulate the collection. - -Delete an item? `removeOne(item.id, oldState)`. - -Check out the [full list of adapter collection methods](https://ngrx.io/guide/entity/adapter#adapter-collection-methods) - -## Selecting state with `@ngrx/entity` - -The entity adapter comes with a small set of default selectors we can use right out of the box. - -```typescript -import { select } from '@ngrx/store'; - -const { selectIds, selectEntities, selectAll, selectTotal } = - adapter.getSelectors(); - -@Component({ - selector: 'my-component', -}) -export class MyComponent extends RxState { - readonly items$ = this.select(select(selectAll)); - - constructor() { - super(); - } -} -``` diff --git a/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx b/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx new file mode 100644 index 000000000..766d59d7e --- /dev/null +++ b/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx @@ -0,0 +1,222 @@ +--- +sidebar_position: 2 +title: Manage entities using @ngrx/entity +# Renamed from libs/state/docs/snippets/manage-collections-with-ngrx-entity.md +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +_Author: [@Phhansen](https://github.com/Phhansen)_ + +# Manage entities using `@ngrx/entity` + +When working with collections or arrays in our state, we tend to write many repeated code when we want to add, update or delete items from these collections. + +In NgRx, they have created a helper library called [@ngrx/entity adapter](https://ngrx.io/guide/entity/adapter). The adapter provides a simple API to manipulate and query these collections, hiding much repetitive code needed. + +Let's say we have a collection of type `Item` as part of our component state; + +```typescript +interface Item { + id: string; + name: string; +} + +interface ComponentState { + items: Item[]; + loading: boolean; +} +``` + +Now, if we want to add one item to our array _(in an immutable way)_, we replace the `items` array in the state with a new reference. + + + + + + ```typescript + + @Component({ + selector: 'my-component', + +}) +export class MyComponent extends RxState { +readonly addItem$ = new Subject(); + + constructor() { + super(); + + this.connect(this.addItem$, (oldState, itemName) => { + const newItem = { + id: uuid(), // unique hash generation fn() + name: itemName, + +}; + + return { + ...oldState, + items: [...oldState.items, newItem], + +}; +}); +} +} + + ``` + + + + + ```typescript + @Component({ + selector: 'my-component', + +}) +export class MyComponent { +readonly addItem$ = new Subject(); + + readonly #state = rxState(({connect}) => { + connect(this.addItem$, (oldState, itemName) => { + const newItem = { + id: uuid(), // unique hash generation fn() + name: itemName, + +}; + + return { + ...oldState, + items: [...oldState.items, newItem], + +}; +}) +}) + +} +``` + + + + + +If we want to update one item, we have to query the `items` array first to get a hold of the item and then construct a new array again. + +What about deleting an item? You get the picture. **It´s a lot of code**, and it will grow even more if we have several types of collections in our state. + +## Using `@ngrx/entity` + +Now let us see how our code will look when using `@ngrx/entity`. + +```typescript +interface Item { + string; + string; +} + +interface ComponentState extends EntityState { + loading: boolean; +} + +const adapter: EntityAdapter = createEntityAdapter({ + selectId: (item: Item) => item.id, +}); +``` + +The entity adapter needs a `selectId` function which is used to query items by `id` within the collection. + +Now let's see how the component has changed: + + + + + + ```typescript + @Component({ + selector: 'my-component', + }) + export class MyComponent extends RxState { + readonly addItem$ = new Subject(); + + constructor() { + super(); + + this.connect(this.addItem$, (oldState, itemName) => + adapter.addOne({ id: uuid(), name: itemName() }, oldState) + ); + } + } + ``` + + + + + ```typescript + @Component({ + selector: 'my-component', + }) + export class MyComponent { + readonly addItem$ = new Subject(); + readonly #state = rxState(({connect}) => { + connect(this.addItem$, (oldState, itemName) => + adapter.addOne({ id: uuid(), name: itemName() }, oldState) + ) + } + } + ``` + + + + + +The `addOne()` function is just one of many functions that help us manipulate the collection. + +Delete an item? `removeOne(item.id, oldState)`. + +Check out the [full list of adapter collection methods](https://ngrx.io/guide/entity/adapter#adapter-collection-methods) + +## Selecting state with `@ngrx/entity` + +The entity adapter comes with a small set of default selectors we can use right out of the box. + + + + + + + ```typescript + import { select } from '@ngrx/store'; + + const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors(); + + @Component({ + selector: 'my-component', + }) + export class MyComponent extends RxState { + readonly items$ = this.select(select(selectAll)); + + constructor() { + super(); + } + } + ``` + + + + + import { select } from '@ngrx/store'; + + const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors(); + + ```typescript + @Component({ + selector: 'my-component', + }) + export class MyComponent { + readonly #state = rxState() + readonly items$ = this.#state.select(select(selectAll)); + } + ``` + + + + From 7b269cb88a6ac1e0159c58d14eb1999b0633f26f Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Mon, 30 Sep 2024 12:38:33 +0200 Subject: [PATCH 25/46] docs(state): update reuse ngrx selectors guide with functional approach --- ...esuse-ngrx-selectors-to-compose-state.mdx} | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) rename apps/docs/docs/state/integrations/{resuse-ngrx-selectors-to-compose-state.md => resuse-ngrx-selectors-to-compose-state.mdx} (63%) diff --git a/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.md b/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.mdx similarity index 63% rename from apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.md rename to apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.mdx index c05f3f237..b75b5d111 100644 --- a/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.md +++ b/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.mdx @@ -5,6 +5,9 @@ title: Reusing ngrx selectors to compose state # Renamed from libs/state/docs/snippets/composing-state-using-ngrx-selectors.md --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + _Author: [@Phhansen](https://github.com/Phhansen)_ # Reusing ngrx selectors to compose state @@ -53,3 +56,42 @@ export class ItemListComponent extends RxState { } } ``` + + + + + + ```typescript + import { select } from '@ngrx/store'; + + @Component() + export class ItemListComponent extends RxState { + readonly visibleItems$ = this.state.select(select(selectVisibleItems)); + + constructor() { + super(); + } + } + + ``` + + + + + ```typescript + import { select } from '@ngrx/store'; + + @Component() + export class ItemListComponent{ + readonly #state = rxState() + readonly visibleItems$ = this.#state.select(select(selectVisibleItems)); + + constructor() { + super(); + } + } + ``` + + + + From eeabe62805d3f6e53b22f5d50496a6bacb70e619 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Mon, 30 Sep 2024 12:52:56 +0200 Subject: [PATCH 26/46] docs(state): update "use rxState as global state" guide with functional approach --- .../manage-entities-using-ngrx-entity.mdx | 6 +- ...resuse-ngrx-selectors-to-compose-state.mdx | 18 +- .../recipes/use-rxstate-as-global-state.md | 217 --------- .../recipes/use-rxstate-as-global-state.mdx | 422 ++++++++++++++++++ 4 files changed, 433 insertions(+), 230 deletions(-) delete mode 100644 apps/docs/docs/state/recipes/use-rxstate-as-global-state.md create mode 100644 apps/docs/docs/state/recipes/use-rxstate-as-global-state.mdx diff --git a/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx b/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx index 766d59d7e..4963de743 100644 --- a/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx +++ b/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx @@ -93,7 +93,8 @@ readonly addItem$ = new Subject(); }) } -``` + +```` @@ -120,7 +121,7 @@ interface ComponentState extends EntityState { const adapter: EntityAdapter = createEntityAdapter({ selectId: (item: Item) => item.id, }); -``` +```` The entity adapter needs a `selectId` function which is used to query items by `id` within the collection. @@ -178,7 +179,6 @@ Check out the [full list of adapter collection methods](https://ngrx.io/guide/en The entity adapter comes with a small set of default selectors we can use right out of the box. - diff --git a/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.mdx b/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.mdx index b75b5d111..c7ae55e2f 100644 --- a/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.mdx +++ b/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.mdx @@ -35,11 +35,7 @@ const selectItems = (state: ComponentState) => state.items; const selectVisibleIds = (state: ComponentState) => state.visibleIds; -const selectVisibleItems = createSelector( - selectVisibleIds, - selectItems, - (visibleIds, items) => visibleIds.map((id) => items[id]) -); +const selectVisibleItems = createSelector(selectVisibleIds, selectItems, (visibleIds, items) => visibleIds.map((id) => items[id])); ``` Using this in our component will look like this: @@ -70,8 +66,9 @@ export class ItemListComponent extends RxState { constructor() { super(); - } - } + +} +} ``` @@ -88,9 +85,10 @@ export class ItemListComponent extends RxState { constructor() { super(); - } - } - ``` + +} +} +``` diff --git a/apps/docs/docs/state/recipes/use-rxstate-as-global-state.md b/apps/docs/docs/state/recipes/use-rxstate-as-global-state.md deleted file mode 100644 index 13316c32a..000000000 --- a/apps/docs/docs/state/recipes/use-rxstate-as-global-state.md +++ /dev/null @@ -1,217 +0,0 @@ ---- -sidebar_position: 3 -sidebar_label: Use RxState as Global State -title: How to manage global state -# Renamed from libs/state/docs/snippets/manage-global-state.md ---- - -_Author: [@Phhansen](https://github.com/Phhansen)_ - -# How to manage global state - -For an explainer on how to easily identify what should be global state and what should local state, please read: [Difference between Global and Local state](./determine-state-type.md). - -## Example - -As with the global/local state snippet, we'll be doing the same example to-do app with 2 views. - -### To do - -- Renders a list of `tasks` that must be completed and a `counter` that shows how many tasks left to do. -- The list can be expanded or collapsed and has property `isExpanded`. -- Gets tasks array from endpoint _tasks/get_ and filters out tasks that already answered. - -```typescript -interface TodosState { - tasks: Task[]; - isExpanded: boolean; -} - -@Component({ - selector: 'todos', - templateUrl: './todo.component.html', -}) -export class TodoComponent extends RxState { - readonly tasks$ = this.select('tasks'); - readonly counter$ = this.select( - map((state) => state.tasks), - map((tasks) => tasks.length) - ); - readonly isExpanded$ = this.select('isExpanded'); - - constructor(private tasksService: TasksService) { - super(); - - /* Filter out tasks that are done */ - this.connect( - 'tasks', - this.tasksService - .fetchTasks() - .pipe(filter((tasks) => tasks.filter((task) => !task.done))) - ); - } -} -``` - -### Setup - -- Renders a list of all existing `tasks` and a `counter` that shows the total amount of tasks. -- The list can be expanded or collapsed and has property `isExpanded`. -- Gets tasks as array from endpoint _tasks/get_. - -```typescript -interface AllTodosState { - tasks: Task[]; - isExpanded: boolean; -} - -@Component({ - selector: 'all-tasks', - templateUrl: './all-tasks.component.html', -}) -export class AllTasksComponent extends RxState { - readonly tasks$ = this.select('tasks'); - readonly counter$ = this.select( - map((state) => state.tasks), - map((tasks) => tasks.length) - ); - readonly isExpanded$ = this.select('isExpanded'); - - constructor(private tasksService: TasksService) { - super(); - - /* Fetch tasks from backend */ - this.connect('tasks', this.tasksService.fetchTasks()); - } -} -``` - -### What is global and what is local? - -Looking at the above examples, let us see what is **local** and what is **global**! - -- `counter` property is a part of **local** state of each view. The counter value is specific for each view. -- `isExpanded` property is also part of **local** state. Both lists can be expanded/collapsed but this status isn't shared between them and they don't care about this status of each other. -- `tasks` array is a part of our app **global** state. This array needed for each view and received from the same endpoint. We don't need to load it twice. It is time to introduce a global layer to our application and move tasks array and retrieving logic there. - -### Moving the `tasks` array to our **global** state - -We can handle **global** state in different ways, but for this snippet we´re going to use an `injectionToken`. - -```typescript -import { InjectionToken } from '@angular/core'; -import { RxState } from '@rx-angular/state'; - -export interface Task { - id: number; - label: string; - done: boolean; -} - -export interface GlobalState { - tasks: Task[]; -} - -export const GLOBAL_RX_STATE = new InjectionToken>( - 'GLOBAL_RX_STATE' -); -``` - -We then _provide_ the `injectionToken` in our `app.module.ts`. - -```typescript -import { GLOBAL_RX_STATE, GlobalState } from "./rx-state"; -... - -@NgModule({ - imports: [...], - declarations: [...], - providers: [{ - provide: GLOBAL_RX_STATE, useFactory: () => new RxState() - }], - bootstrap: [...] -}) -export class AppModule {} -``` - -We can then load the `tasks` in the `AppComponent` via our `tasksService.fetchTasks()` and just have our `TodoComponent` and `AllTasksComponent` connect to the global state. - -```typescript -import { GLOBAL_RX_STATE } from './rx-state'; - -@Component({ - selector: 'my-app', - templateUrl: './app.component.html', - styleUrls: [ './app.component.css' ] -}) -export class AppComponent { -constructor(@Inject(GLOBAL_RX_STATE) private state, private tasksService: TasksService) { - /* Fetch tasks from backend */ - this.state.connect("tasks", this.tasksService.fetchTasks()); -} -``` - -And our updated `TodoComponent` - -```typescript -interface TodosState { - tasks: Task[]; - isExpanded: boolean; -} - -@Component({ - selector: 'todos', - templateUrl: './todo.component.html', -}) -export class TodoComponent extends RxState { - readonly tasks$ = this.select('tasks'); - readonly counter$ = this.select( - map((state) => state.tasks), - map((tasks) => tasks.length) - ); - readonly isExpanded$ = this.select('isExpanded'); - - constructor( - @Inject(GLOBAL_RX_STATE) private globalState: RxState - ) { - super(); - - /* Connect to global state and filter out already completed tasks */ - this.connect( - 'tasks', - this.globalState - .select('tasks') - .pipe(map((tasks) => tasks.filter((task) => !task.done))) - ); - } -} -``` - -Here we `connect` to the global state instance and filter out the already completed tasks. - -Our `AllTasksComponent` is slightly different in that it doesn't actually need to filter anything, and thus it only needs to manage the **local** `isExpanded` value, and just have the `tasks` and `counter` values come directly from the **global** state. - -```typescript -interface AllTodosState { - isExpanded: boolean; -} - -@Component({ - selector: 'all-tasks', - templateUrl: './all-tasks.component.html', -}) -export class AllTasksComponent extends RxState { - readonly tasks$ = this.globalState.select('tasks'); - readonly counter$ = this.globalState.select( - map((state) => state.tasks), - map((tasks) => tasks.length) - ); - readonly isExpanded$ = this.select('isExpanded'); - - constructor( - @Inject(GLOBAL_RX_STATE) private globalState: RxState - ) { - super(); - } -} -``` diff --git a/apps/docs/docs/state/recipes/use-rxstate-as-global-state.mdx b/apps/docs/docs/state/recipes/use-rxstate-as-global-state.mdx new file mode 100644 index 000000000..5bffcb256 --- /dev/null +++ b/apps/docs/docs/state/recipes/use-rxstate-as-global-state.mdx @@ -0,0 +1,422 @@ +--- +sidebar_position: 3 +sidebar_label: Use RxState as Global State +title: How to manage global state +# Renamed from libs/state/docs/snippets/manage-global-state.md +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +_Author: [@Phhansen](https://github.com/Phhansen)_ + +# How to manage global state + +For an explainer on how to easily identify what should be global state and what should local state, please read: [Difference between Global and Local state](./determine-state-type.md). + +## Example + +As with the global/local state snippet, we'll be doing the same example to-do app with 2 views. + +### To do + +- Renders a list of `tasks` that must be completed and a `counter` that shows how many tasks left to do. +- The list can be expanded or collapsed and has property `isExpanded`. +- Gets tasks array from endpoint _tasks/get_ and filters out tasks that already answered. + + + + + + ```typescript + interface TodosState { + tasks: Task[]; + isExpanded: boolean; + + } + +@Component({ +selector: 'todos', +templateUrl: './todo.component.html', +}) +export class TodoComponent extends RxState { +readonly tasks$ = this.select('tasks'); +readonly counter$ = this.select( +map((state) => state.tasks), +map((tasks) => tasks.length) +); +readonly isExpanded$ = this.select('isExpanded'); + +constructor(private tasksService: TasksService) { +super(); + + /* Filter out tasks that are done */ + this.connect( + 'tasks', + this.tasksService + .fetchTasks() + .pipe(filter((tasks) => tasks.filter((task) => !task.done))) + ); + +} +} +``` + + + + + ```typescript + +interface TodosState { +tasks: Task[]; +isExpanded: boolean; +} + +@Component({ +selector: 'todos', +templateUrl: './todo.component.html', +}) +export class TodoComponent { +readonly #state = rxState() +readonly tasks$ = this.#state.select('tasks'); +readonly counter$ = this.#state.select( +map((state) => state.tasks), +map((tasks) => tasks.length) +); +readonly isExpanded$ = this.#state.select('isExpanded'); + +constructor(private tasksService: TasksService) { + + /* Filter out tasks that are done */ + this.#state.connect( + 'tasks', + this.tasksService + .fetchTasks() + .pipe(filter((tasks) => tasks.filter((task) => !task.done))) + ); + +} +} +``` + + + + + +### Setup + +- Renders a list of all existing `tasks` and a `counter` that shows the total amount of tasks. +- The list can be expanded or collapsed and has property `isExpanded`. +- Gets tasks as array from endpoint _tasks/get_. + + + + + + ```typescript + interface AllTodosState { + tasks: Task[]; + isExpanded: boolean; + } + + @Component({ + selector: 'all-tasks', + templateUrl: './all-tasks.component.html', + }) + export class AllTasksComponent extends RxState { + readonly tasks$ = this.select('tasks'); + readonly counter$ = this.select( + map((state) => state.tasks), + map((tasks) => tasks.length) + ); + readonly isExpanded$ = this.select('isExpanded'); + + constructor(private tasksService: TasksService) { + super(); + + /* Fetch tasks from backend */ + this.connect('tasks', this.tasksService.fetchTasks()); + } + } + ``` + + + + + ```typescript + + interface AllTodosState { + tasks: Task[]; + isExpanded: boolean; + } + +@Component({ +selector: 'all-tasks', +templateUrl: './all-tasks.component.html', +}) +export class AllTasksComponent { +readonly #state = rxState() +readonly tasks$ = this.#state.select('tasks'); +readonly counter$ = this.#state.select( +map((state) => state.tasks), +map((tasks) => tasks.length) +); +readonly isExpanded$ = this.#state.select('isExpanded'); + +constructor(private tasksService: TasksService) { +super(); + + /* Fetch tasks from backend */ + this.#state.connect('tasks', this.tasksService.fetchTasks()); + +} +} + +```` + + + + + + +### What is global and what is local? + +Looking at the above examples, let us see what is **local** and what is **global**! + +- `counter` property is a part of **local** state of each view. The counter value is specific for each view. +- `isExpanded` property is also part of **local** state. Both lists can be expanded/collapsed but this status isn't shared between them and they don't care about this status of each other. +- `tasks` array is a part of our app **global** state. This array needed for each view and received from the same endpoint. We don't need to load it twice. It is time to introduce a global layer to our application and move tasks array and retrieving logic there. + +### Moving the `tasks` array to our **global** state + +We can handle **global** state in different ways, but for this snippet we´re going to use an `injectionToken`. + +```typescript +import { InjectionToken } from '@angular/core'; +import { RxState } from '@rx-angular/state'; + +export interface Task { + id: number; + label: string; + done: boolean; +} + +export interface GlobalState { + tasks: Task[]; +} + +export const GLOBAL_RX_STATE = new InjectionToken>( + 'GLOBAL_RX_STATE' +); +```` + +We then _provide_ the `injectionToken` in our `app.module.ts`. + + + + + + ```typescript + import { GLOBAL_RX_STATE, GlobalState } from "./rx-state"; + ... + + @NgModule({ + imports: [...], + declarations: [...], + providers: [{ + provide: GLOBAL_RX_STATE, useFactory: () => new RxState() + }], + bootstrap: [...] + }) + export class AppModule {} + ``` + + + + + ```typescript + import { GLOBAL_RX_STATE, GlobalState } from "./rx-state"; + ... + + @NgModule({ + imports: [...], + declarations: [...], + providers: [{ + provide: GLOBAL_RX_STATE, useFactory: () => rxState() + }], + bootstrap: [...] + }) + export class AppModule {} + ``` + + + + + +We can then load the `tasks` in the `AppComponent` via our `tasksService.fetchTasks()` and just have our `TodoComponent` and `AllTasksComponent` connect to the global state. + +```typescript +import { GLOBAL_RX_STATE } from './rx-state'; + +@Component({ + selector: 'my-app', + templateUrl: './app.component.html', + styleUrls: [ './app.component.css' ] +}) +export class AppComponent { +constructor(@Inject(GLOBAL_RX_STATE) private state, private tasksService: TasksService) { + /* Fetch tasks from backend */ + this.state.connect("tasks", this.tasksService.fetchTasks()); +} +``` + +And our updated `TodoComponent` + + + + + + ```typescript + interface TodosState { + +tasks: Task[]; +isExpanded: boolean; +} + +@Component({ +selector: 'todos', +templateUrl: './todo.component.html', +}) +export class TodoComponent extends RxState { +readonly tasks$ = this.select('tasks'); +readonly counter$ = this.select( +map((state) => state.tasks), +map((tasks) => tasks.length) +); +readonly isExpanded$ = this.select('isExpanded'); + +constructor( +@Inject(GLOBAL_RX_STATE) private globalState: RxState +) { +super(); + + /* Connect to global state and filter out already completed tasks */ + this.connect( + 'tasks', + this.globalState + .select('tasks') + .pipe(map((tasks) => tasks.filter((task) => !task.done))) + ); + +} +} +``` + + + + + ```typescript + interface TodosState { + tasks: Task[]; + isExpanded: boolean; + } + +@Component({ +selector: 'todos', +templateUrl: './todo.component.html', +}) +export class TodoComponent { +readonly #state = rxState() +readonly tasks$ = this.#state.select('tasks'); +readonly counter$ = this.#state.select( +map((state) => state.tasks), +map((tasks) => tasks.length) +); +readonly isExpanded$ = this.#state.select('isExpanded'); + +constructor( +@Inject(GLOBAL_RX_STATE) private globalState: RxState +) { +super(); + + /* Connect to global state and filter out already completed tasks */ + this.#state.connect( + 'tasks', + this.globalState + .select('tasks') + .pipe(map((tasks) => tasks.filter((task) => !task.done))) + ); + +} +} +``` + + + + + +Here we `connect` to the global state instance and filter out the already completed tasks. + +Our `AllTasksComponent` is slightly different in that it doesn't actually need to filter anything, and thus it only needs to manage the **local** `isExpanded` value, and just have the `tasks` and `counter` values come directly from the **global** state. + + + + + + ```typescript + interface AllTodosState { + isExpanded: boolean; + } + + @Component({ + selector: 'all-tasks', + templateUrl: './all-tasks.component.html', + }) + export class AllTasksComponent extends RxState { + readonly tasks$ = this.globalState.select('tasks'); + readonly counter$ = this.globalState.select( + map((state) => state.tasks), + map((tasks) => tasks.length) + ); + readonly isExpanded$ = this.select('isExpanded'); + + constructor( + @Inject(GLOBAL_RX_STATE) private globalState: RxState + ) { + super(); + } + } + ``` + + + + + ```typescript + interface AllTodosState { + isExpanded: boolean; + } + + @Component({ + selector: 'all-tasks', + templateUrl: './all-tasks.component.html', + }) + export class AllTasksComponent { + readonly #state = rxState(); + readonly tasks$ = this.globalState.select('tasks'); + readonly counter$ = this.globalState.select( + map((state) => state.tasks), + map((tasks) => tasks.length) + ); + readonly isExpanded$ = this.#state.select('isExpanded'); + + constructor( + @Inject(GLOBAL_RX_STATE) private globalState: RxState + ) { + super(); + } + } + ``` + + + + From 2ab6ce3978daf1536b8b9ae272b37395231d9e6a Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Mon, 30 Sep 2024 13:10:14 +0200 Subject: [PATCH 27/46] docs(state): update "work with hostbindings" guide with functional approach --- ...resuse-ngrx-selectors-to-compose-state.mdx | 2 + .../recipes/use-rxstate-as-global-state.mdx | 10 +- .../state/recipes/work-with-hostbindings.md | 156 ---------- .../state/recipes/work-with-hostbindings.mdx | 271 ++++++++++++++++++ 4 files changed, 279 insertions(+), 160 deletions(-) delete mode 100644 apps/docs/docs/state/recipes/work-with-hostbindings.md create mode 100644 apps/docs/docs/state/recipes/work-with-hostbindings.mdx diff --git a/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.mdx b/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.mdx index c7ae55e2f..f9aaed6ae 100644 --- a/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.mdx +++ b/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.mdx @@ -88,8 +88,10 @@ export class ItemListComponent extends RxState { } } + ``` +``` diff --git a/apps/docs/docs/state/recipes/use-rxstate-as-global-state.mdx b/apps/docs/docs/state/recipes/use-rxstate-as-global-state.mdx index 5bffcb256..5e8039949 100644 --- a/apps/docs/docs/state/recipes/use-rxstate-as-global-state.mdx +++ b/apps/docs/docs/state/recipes/use-rxstate-as-global-state.mdx @@ -60,7 +60,8 @@ super(); } } -``` + +```` @@ -97,7 +98,7 @@ constructor(private tasksService: TasksService) { } } -``` +```` @@ -310,7 +311,8 @@ super(); } } -``` + +```` @@ -349,7 +351,7 @@ super(); } } -``` +```` diff --git a/apps/docs/docs/state/recipes/work-with-hostbindings.md b/apps/docs/docs/state/recipes/work-with-hostbindings.md deleted file mode 100644 index 7f85f2585..000000000 --- a/apps/docs/docs/state/recipes/work-with-hostbindings.md +++ /dev/null @@ -1,156 +0,0 @@ ---- -sidebar_position: 2 -sidebar_label: Work with HostBindings -title: HostBindings -# Renamed from libs/state/docs/snippets/hostbindings.md ---- - -# HostBindings - -Some examples how to reactively handle basic [`HostBindings`](https://angular.io/api/core/HostBinding) with `@rx-angular/state` `RxState`. - -Sadly `HostBindings` are not able to bind to `Observable` sources out of the box. So we have to come up with custom solutions -in order to have fully reactive components. - -In the following examples we will use the `rxLet` directive or the `push` pipe as replacements for angular's `async` pipe. -`rxLet` and `push` belong to the not yet released `@rx-angular/template` package. - -Furthermore we want to express that we will come up with a more convenient solution facing this problem. This can be seen as WIP and -should not be the long term solution to handle `HostBindings` in a fully reactive way. - -Imagine you have the following state which you want to bind to properties of your host element. - -```typescript -interface ComponentState { - visible: boolean; - top: number; - maxHeight: number; -} -``` - -## Be aware of changeDetection - HostBindings are not reactive - -In this setup we assign our `HostBindings` to the `get()` method of our state. - -As stated in the title, we have to be aware changeDetection. On every changeDetection cycle, angular will re-evaluate -all `HostBindings`. If our component doesn't get flagged as dirty, our `HostBindings` won't get updated. So we have to make -sure that state changes that are related to the `HostBindings` value are actually triggering a re-render. - -```typescript -@Component({ - providers: [RxState], -}) -export class RxComponent { - // Modifying the class - @HostBinding('[class.is-hidden]') get isHidden() { - return !this.state.get().visible; - } - // Modifying styles - @HostBinding('[style.marginTop]') get marginTop() { - return `${this.state.get().top}px`; - } - // Modifying styles - @HostBinding('[style.maxHeight]') get maxHeight() { - return `${this.state.get().maxHeight}px`; - } - - constructor(private state: RxState) {} -} -``` - -With this setup in place we have two options to get things done. - -### Call ChangeDetection manually - -Since rendering is a side-effect, we could utilize the `hold` method and register -a function which handles change detection for us. - -```typescript -@Component({ - providers: [RxState], -}) -export class RxComponent { - // Modifying the class - @HostBinding('[class.is-hidden]') get isHidden() { - return !this.state.get().visible; - } - // Modifying styles - @HostBinding('[style.marginTop]') get marginTop() { - return `${this.state.get().top}px`; - } - // Modifying styles - @HostBinding('[style.maxHeight]') get maxHeight() { - return `${this.state.get().maxHeight}px`; - } - - constructor( - private state: RxState, - private cdRef: ChangeDetectorRef - ) { - state.hold(state.select(), () => this.cdRef.markForCheck()); - } -} -``` - -By calling `ChangeDetectorRef#markForCheck` after every state change, we flag our component dirty when needed and let angular's -`ChangeDetection` do it's magic for us. - -### Let the template handle changeDetection - -If you happen to need your variables not only for your `HostBindings` but as well in the view, we could easily let -our viewHelpers take care of detecting changes. Just make sure all of your variables needed for the `HostBindings` are bound -to the view correctly. - -Inside the component: - -```typescript - readonly viewState$ = this.state.select(); -``` - -Inside the template: - -```html - - I am a visible span - -``` - -In this scenario, the `rxLet` directive will flag your component as dirty every time a new state arrives. By assigning the -whole state object as to your `viewModel$`, any change will result in a re-rendering, thus updating your `HostBindings`. - -## Render on your own - -With this setup you can opt-out of the `ChangeDetection` of angular and manage `HostBindings` completely on your own. -This approach even works when calling `ChangeDetectorRef#detach` for your component. -We will utilize the `ElementRef` itself for this purpose and manipulate the DOM on our own. - -Feel free to use angular's `Renderer2` if you want an abstraction layer, should work the exact same way. - -```typescript -@Component({ - providers: [RxState], -}) -export class RxComponent { - constructor( - private state: RxState, - private elementRef: ElementRef, - private cdRef: ChangeDetectorRef - ) { - // optional: cdRef.detach(); - this.state.hold(this.state.select(), ({ visible, top, maxHeight }) => { - const { nativeElement } = elementRef; - nativeElement.style.marginTop = `${top ? top : 0}px`; - nativeElement.style.maxHeight = `${maxHeight ? maxHeight : 100}px`; - // by using this, we could assign more classes - const classList: { [cls: string]: boolean } = { - 'is-hidden': !visible, - }; - Object.keys(classList).forEach((cls) => { - classList[cls] - ? nativeElement.classList.add(cls) - : nativeElement.classList.remove(cls); - }); - }); - } -} -``` diff --git a/apps/docs/docs/state/recipes/work-with-hostbindings.mdx b/apps/docs/docs/state/recipes/work-with-hostbindings.mdx new file mode 100644 index 000000000..7b0630de2 --- /dev/null +++ b/apps/docs/docs/state/recipes/work-with-hostbindings.mdx @@ -0,0 +1,271 @@ +--- +sidebar_position: 2 +sidebar_label: Work with HostBindings +title: HostBindings +# Renamed from libs/state/docs/snippets/hostbindings.md +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# HostBindings + +Some examples how to reactively handle basic [`HostBindings`](https://angular.io/api/core/HostBinding) with `@rx-angular/state` `RxState`. + +Sadly `HostBindings` are not able to bind to `Observable` sources out of the box. So we have to come up with custom solutions +in order to have fully reactive components. + +In the following examples we will use the `rxLet` directive or the `push` pipe as replacements for angular's `async` pipe. +`rxLet` and `push` belong to the not yet released `@rx-angular/template` package. + +Furthermore we want to express that we will come up with a more convenient solution facing this problem. This can be seen as WIP and +should not be the long term solution to handle `HostBindings` in a fully reactive way. + +Imagine you have the following state which you want to bind to properties of your host element. + +```typescript +interface ComponentState { + visible: boolean; + top: number; + maxHeight: number; +} +``` + +## Be aware of changeDetection - HostBindings are not reactive + +In this setup we assign our `HostBindings` to the `get()` method of our state. + +As stated in the title, we have to be aware changeDetection. On every changeDetection cycle, angular will re-evaluate +all `HostBindings`. If our component doesn't get flagged as dirty, our `HostBindings` won't get updated. So we have to make +sure that state changes that are related to the `HostBindings` value are actually triggering a re-render. + + + + + + ```typescript + @Component({ + providers: [RxState], + }) + export class RxComponent { + // Modifying the class + @HostBinding('[class.is-hidden]') get isHidden() { + return !this.state.get().visible; + } + // Modifying styles + @HostBinding('[style.marginTop]') get marginTop() { + return `${this.state.get().top}px`; + } + // Modifying styles + @HostBinding('[style.maxHeight]') get maxHeight() { + return `${this.state.get().maxHeight}px`; + } + + constructor(private state: RxState) {} + } + ``` + + + + + import { select } from '@ngrx/store'; + + const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors(); + + ```typescript + @Component({...}) + export class RxComponent { + readonly #state = rxState() + // Modifying the class + @HostBinding('[class.is-hidden]') get isHidden() { + return !this.#state.get().visible; + } + // Modifying styles + @HostBinding('[style.marginTop]') get marginTop() { + return `${this.#state.get().top}px`; + } + // Modifying styles + @HostBinding('[style.maxHeight]') get maxHeight() { + return `${this.#state.get().maxHeight}px`; + } + + ``` + + + +With this setup in place we have two options to get things done. + +### Call ChangeDetection manually + +Since rendering is a side-effect, we could utilize the `hold` method and register +a function which handles change detection for us. + + + + + + ```typescript + @Component({ + providers: [RxState], + }) + export class RxComponent { + // Modifying the class + @HostBinding('[class.is-hidden]') get isHidden() { + return !this.state.get().visible; + } + // Modifying styles + @HostBinding('[style.marginTop]') get marginTop() { + return `${this.state.get().top}px`; + } + // Modifying styles + @HostBinding('[style.maxHeight]') get maxHeight() { + return `${this.state.get().maxHeight}px`; + } + + constructor( + private state: RxState, + private cdRef: ChangeDetectorRef + ) { + state.hold(state.select(), () => this.cdRef.markForCheck()); + } + } + ``` + + + + + import { select } from '@ngrx/store'; + + const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors(); + + ```typescript + @Component({...}) + export class RxComponent { + readonly #state = rxState() + readonly #effects = rxEffects(); + // Modifying the class + @HostBinding('[class.is-hidden]') get isHidden() { + return !this.#state.get().visible; + } + // Modifying styles + @HostBinding('[style.marginTop]') get marginTop() { + return `${this.#state.get().top}px`; + } + // Modifying styles + @HostBinding('[style.maxHeight]') get maxHeight() { + return `${this.#state.get().maxHeight}px`; + } + + constructor( + private state: RxState, + private cdRef: ChangeDetectorRef + ) { + this.#effects.register(state.select(), () => this.cdRef.markForCheck()); + } + } + ``` + + + +By calling `ChangeDetectorRef#markForCheck` after every state change, we flag our component dirty when needed and let angular's +`ChangeDetection` do it's magic for us. + +### Let the template handle changeDetection + +If you happen to need your variables not only for your `HostBindings` but as well in the view, we could easily let +our viewHelpers take care of detecting changes. Just make sure all of your variables needed for the `HostBindings` are bound +to the view correctly. + +Inside the component: + +```typescript + readonly viewState$ = this.state.select(); +``` + +Inside the template: + +```html + + I am a visible span + +``` + +In this scenario, the `rxLet` directive will flag your component as dirty every time a new state arrives. By assigning the +whole state object as to your `viewModel$`, any change will result in a re-rendering, thus updating your `HostBindings`. + +## Render on your own + +With this setup you can opt-out of the `ChangeDetection` of angular and manage `HostBindings` completely on your own. +This approach even works when calling `ChangeDetectorRef#detach` for your component. +We will utilize the `ElementRef` itself for this purpose and manipulate the DOM on our own. + +Feel free to use angular's `Renderer2` if you want an abstraction layer, should work the exact same way. + + + + + + ```typescript + @Component({ + providers: [RxState], + }) + export class RxComponent { + constructor( + private state: RxState, + private elementRef: ElementRef, + private cdRef: ChangeDetectorRef + ) { + // optional: cdRef.detach(); + this.state.hold(this.state.select(), ({ visible, top, maxHeight }) => { + const { nativeElement } = elementRef; + nativeElement.style.marginTop = `${top ? top : 0}px`; + nativeElement.style.maxHeight = `${maxHeight ? maxHeight : 100}px`; + // by using this, we could assign more classes + const classList: { [cls: string]: boolean } = { + 'is-hidden': !visible, + }; + Object.keys(classList).forEach((cls) => { + classList[cls] + ? nativeElement.classList.add(cls) + : nativeElement.classList.remove(cls); + }); + }); + } + } + ``` + + + + + import { select } from '@ngrx/store'; + + const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors(); + + ```typescript + @Component({...}) + export class RxComponent { + readonly #state: RxState + constructor( + private elementRef: ElementRef, + private cdRef: ChangeDetectorRef + ) { + // optional: cdRef.detach(); + this.state.hold(this.#state.select(), ({ visible, top, maxHeight }) => { + const { nativeElement } = elementRef; + nativeElement.style.marginTop = `${top ? top : 0}px`; + nativeElement.style.maxHeight = `${maxHeight ? maxHeight : 100}px`; + // by using this, we could assign more classes + const classList: { [cls: string]: boolean } = { + 'is-hidden': !visible, + }; + Object.keys(classList).forEach((cls) => { + classList[cls] + ? nativeElement.classList.add(cls) + : nativeElement.classList.remove(cls); + }); + }); + } + } + ``` + + From efb1e1641449832e751f95f7977b6bf17d1c346d Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Mon, 30 Sep 2024 13:15:52 +0200 Subject: [PATCH 28/46] docs(state): update "load data on route change" guide with functional approach --- .../manage-entities-using-ngrx-entity.mdx | 3 +- .../recipes/load-data-on-route-change.md | 81 ------------ .../recipes/load-data-on-route-change.mdx | 123 ++++++++++++++++++ 3 files changed, 124 insertions(+), 83 deletions(-) delete mode 100644 apps/docs/docs/state/recipes/load-data-on-route-change.md create mode 100644 apps/docs/docs/state/recipes/load-data-on-route-change.mdx diff --git a/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx b/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx index 4963de743..91f49b881 100644 --- a/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx +++ b/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx @@ -203,11 +203,10 @@ The entity adapter comes with a small set of default selectors we can use right + ```typescript import { select } from '@ngrx/store'; - const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors(); - ```typescript @Component({ selector: 'my-component', }) diff --git a/apps/docs/docs/state/recipes/load-data-on-route-change.md b/apps/docs/docs/state/recipes/load-data-on-route-change.md deleted file mode 100644 index 3e4d974a9..000000000 --- a/apps/docs/docs/state/recipes/load-data-on-route-change.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -sidebar_position: 2 -title: Load data on route change -# Renamed from libs/state/docs/snippets/loading-state-and-data-fetching.md ---- - -# Load data on route change - -On every URL change fetch users from the back end and deal with loading flags - -## Imperative - -```typescript -@Component({ - selector: 'my-comp', - template: ` - -
{{ users$ | async }}
`, - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class MyComponent { - readonly subscription: Subscription; - readonly user$ = new BehaviorSubject(null); - readonly isLoading$ = new BehaviorSubject(false); - - constructor(private router: Router, - private userService: UserService) { - - const fetchUserOnUrlChangeEffect$ = this.router.params.pipe( - tap(() => this.isLoading$.next(true)), - switchMap(p => this.userService.getUser(p.user).pipe( - map(res => ({user: res.user})), - tap((user) => this.user$.next(user)), - )) - tap(() => this.isLoading$.next(false)), - ); - - this.subscription = fetchUserOnUrlChangeEffect$ - .subscribe(); - - } - - onDestroy() { - this.subscription.unsubscribe(); - } -} -``` - -## Reactive - -```typescript -@Component({ - selector: 'my-comp', - template: ` - -
{{ user$ | push }}
- `, - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class MyComponent { - readonly user$ = this.state.select('user'); - readonly isLoading$ = this.state.select('isLoading'); - - constructor( - private router: Router, - private userService: UserService, - private state: RxState<{ user: string; isLoading: boolean }> - ) { - const fetchUserOnUrlChange$ = this.router.params.pipe( - switchMap((p) => - this.userService.getUser(p.user).pipe( - map((res) => ({ user: res.user })), - startWith({ isLoading: true }), - endWith({ isLoading: false }) - ) - ) - ); - this.state.connect(fetchUserOnUrlChange$); - } -} -``` diff --git a/apps/docs/docs/state/recipes/load-data-on-route-change.mdx b/apps/docs/docs/state/recipes/load-data-on-route-change.mdx new file mode 100644 index 000000000..4efcb093e --- /dev/null +++ b/apps/docs/docs/state/recipes/load-data-on-route-change.mdx @@ -0,0 +1,123 @@ +--- +sidebar_position: 2 +title: Load data on route change +# Renamed from libs/state/docs/snippets/loading-state-and-data-fetching.md +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Load data on route change + +On every URL change fetch users from the back end and deal with loading flags + +## Imperative + +```typescript +@Component({ + selector: 'my-comp', + template: ` + +
{{ users$ | async }}
`, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class MyComponent { + readonly subscription: Subscription; + readonly user$ = new BehaviorSubject(null); + readonly isLoading$ = new BehaviorSubject(false); + + constructor(private router: Router, + private userService: UserService) { + + const fetchUserOnUrlChangeEffect$ = this.router.params.pipe( + tap(() => this.isLoading$.next(true)), + switchMap(p => this.userService.getUser(p.user).pipe( + map(res => ({user: res.user})), + tap((user) => this.user$.next(user)), + )) + tap(() => this.isLoading$.next(false)), + ); + + this.subscription = fetchUserOnUrlChangeEffect$ + .subscribe(); + + } + + onDestroy() { + this.subscription.unsubscribe(); + } +} +``` + +## Reactive + + + + ```typescript + @Component({ + selector: 'my-comp', + template: ` + +
{{ user$ | push }}
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, + }) + export class MyComponent { + readonly user$ = this.state.select('user'); + readonly isLoading$ = this.state.select('isLoading'); + + constructor( + private router: Router, + private userService: UserService, + private state: RxState<{ user: string; isLoading: boolean }> + ) { + const fetchUserOnUrlChange$ = this.router.params.pipe( + switchMap((p) => + this.userService.getUser(p.user).pipe( + map((res) => ({ user: res.user })), + startWith({ isLoading: true }), + endWith({ isLoading: false }) + ) + ) + ); + this.state.connect(fetchUserOnUrlChange$); + } + } + ``` + +
+ + ```typescript + @Component({ + selector: 'my-comp', + template: ` + +
{{ user$ | push }}
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, + }) + export class MyComponent { + readonly #state = rxState<{ user: string; isLoading: boolean }>() + readonly user$ = this.state.select('user'); + readonly isLoading$ = this.state.select('isLoading'); + + constructor( + private router: Router, + private userService: UserService, + ) { + const fetchUserOnUrlChange$ = this.router.params.pipe( + switchMap((p) => + this.userService.getUser(p.user).pipe( + map((res) => ({ user: res.user })), + startWith({ isLoading: true }), + endWith({ isLoading: false }) + ) + ) + ); + this.#state.connect(fetchUserOnUrlChange$); + } + } + ``` + +
+
From 2d6f2af663827cdd318db3eb21164011eded434f Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Mon, 30 Sep 2024 13:19:47 +0200 Subject: [PATCH 29/46] docs(state): update "run partial updates" guide with functional approach --- .../docs/state/recipes/run-partial-updates.md | 40 ----------- .../state/recipes/run-partial-updates.mdx | 67 +++++++++++++++++++ 2 files changed, 67 insertions(+), 40 deletions(-) delete mode 100644 apps/docs/docs/state/recipes/run-partial-updates.md create mode 100644 apps/docs/docs/state/recipes/run-partial-updates.mdx diff --git a/apps/docs/docs/state/recipes/run-partial-updates.md b/apps/docs/docs/state/recipes/run-partial-updates.md deleted file mode 100644 index 879d885a5..000000000 --- a/apps/docs/docs/state/recipes/run-partial-updates.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -sidebar_position: 1 -sidebar_label: Run partial updates -title: How can I run partial updates? -# Renamed from libs/state/docs/snippets/how-can-i-run-partial-state-updates.md ---- - -# How can I run partial updates? - -`RxState` has partial updates built in. Every change sent to the state over `set` or `connect` is treated as partial update. -An instance of `RxState` typed with `T` accepts `Partial` in the `set` and `connect` method. - -The partial update can happen directly by providing a `Partial` or over a reduce function `(oldState, change) => newState`. - -```typescript -import { RxState } from `rx-angular/state`; -interface ComponentState { - title: string; - list: string[]; - loading: boolean; -} - -class AnyComponent extends RxState { - updateTitle() { - this.set({ title: 'Hello!' }); - } - - resetList() { - this.connect(this.globalState$.list$({ list: [], loading: false })); - } -} -``` - -Internally the state update looks like this: - -```typescript -newState$.pipe( - scan((oldState, newPartialState) => ({ ...oldState, ...newPartialState })) -); -``` diff --git a/apps/docs/docs/state/recipes/run-partial-updates.mdx b/apps/docs/docs/state/recipes/run-partial-updates.mdx new file mode 100644 index 000000000..d6536a23e --- /dev/null +++ b/apps/docs/docs/state/recipes/run-partial-updates.mdx @@ -0,0 +1,67 @@ +--- +sidebar_position: 1 +sidebar_label: Run partial updates +title: How can I run partial updates? +# Renamed from libs/state/docs/snippets/how-can-i-run-partial-state-updates.md +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# How can I run partial updates? + +`RxState` has partial updates built in. Every change sent to the state over `set` or `connect` is treated as partial update. +An instance of `RxState` typed with `T` accepts `Partial` in the `set` and `connect` method. + +The partial update can happen directly by providing a `Partial` or over a reduce function `(oldState, change) => newState`. + + + + ```typescript + import { RxState } from `rx-angular/state`; + interface ComponentState { + title: string; + list: string[]; + loading: boolean; + } + + class AnyComponent extends RxState { + updateTitle() { + this.set({ title: 'Hello!' }); + } + + resetList() { + this.connect(this.globalState$.list$({ list: [], loading: false })); + } + } + ``` + + + ```typescript + import { rxState } from `rx-angular/state`; + interface ComponentState { + title: string; + list: string[]; + loading: boolean; + } + + class AnyComponent { + readonly #state = rxState() + updateTitle() { + this.#state.set({ title: 'Hello!' }); + } + + resetList() { + this.#state.connect(this.globalState$.list$({ list: [], loading: false })); + } + } + ``` + + + + +Internally the state update looks like this: + +```typescript +newState$.pipe(scan((oldState, newPartialState) => ({ ...oldState, ...newPartialState }))); +``` From 6f8cbda2925f6d7755eb9bcbc272db06a1c464f0 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Thu, 3 Oct 2024 06:58:46 +0200 Subject: [PATCH 30/46] docs(state): update "mangae viewmodels" guide with functional approach --- ...nage-viewmodel.md => manage-viewmodel.mdx} | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) rename apps/docs/docs/state/recipes/{manage-viewmodel.md => manage-viewmodel.mdx} (68%) diff --git a/apps/docs/docs/state/recipes/manage-viewmodel.md b/apps/docs/docs/state/recipes/manage-viewmodel.mdx similarity index 68% rename from apps/docs/docs/state/recipes/manage-viewmodel.md rename to apps/docs/docs/state/recipes/manage-viewmodel.mdx index 4e2adda67..b8bda1eab 100644 --- a/apps/docs/docs/state/recipes/manage-viewmodel.md +++ b/apps/docs/docs/state/recipes/manage-viewmodel.mdx @@ -5,6 +5,9 @@ title: Selecting the ViewModel # Renamed from libs/state/docs/snippets/selecting-the-viewmodel.md --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + # Selecting the ViewModel Here are some useful strategies to properly handle `ViewModels` with `@rx-angular/state`. In this examples we will use standalone [`selectSlice`](../api/rxjs-operators/select-slice.md) operator. @@ -58,6 +61,8 @@ It returns an Observable that emits a distinct subset of the received object. Utilizing it inside of the `RxState#select` method enables you to pluck a _distinct_ `ViewModel` directly out of your state. + + ```typescript @Component() export class ViewModelComponent extends RxState { @@ -77,6 +82,29 @@ export class ViewModelComponent extends RxState { } } ``` + + +```typescript +@Component() +export class ViewModelComponent { + readonly #state = rxState(); + readonly viewModel$: Observable = this.#state.select( + selectSlice(['title', 'list', 'created', 'visibleItemIds']), + map(({ title, list, created, visibleItemIds }) => ({ + title, + created, + total: list.length, + visibleItems: list.filter((item) => + visibleItemIds.some((itemId) => itemId === item.id) + ), + })) + ); +} +``` + + + + ## Multiple Observables and selectSlice: @@ -100,6 +128,8 @@ This way you may achieve more control over what to render when, e.g. lazy render ``` + + ```typescript interface ComponentViewModel { main$: Observable<{ title: string; created: Date }>; @@ -125,3 +155,30 @@ export class ViewModelComponent extends RxState { } } ``` + + +```typescript +interface ComponentViewModel { + main$: Observable<{ title: string; created: Date }>; + list$: Observable<{ total: number; visibleItems: Item[] }>; +} + +@Component() +export class ViewModelComponent { + readonly #state = rxState(); + readonly viewModel: ComponentViewModel = { + main$: this.#state.select(selectSlice(['title', 'created'])), + list$: this.#state.select( + selectSlice(['list', 'visibleItemIds']), + map(({ list, visibleItemIds }) => ({ + total: list.length, + visibleItems: list.filter((item) => + visibleItemIds.some((itemId) => itemId === item.id) + ), + })) + ), + }; +} +``` + + From 59ea35366ddfd846da5c3b147441d3f148fe0296 Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sat, 5 Oct 2024 06:46:13 +0200 Subject: [PATCH 31/46] docs(state): update "migrating to rxstate" guide with functional approach --- ...to-rxstate.md => migrating-to-rxstate.mdx} | 91 +++++++++++-------- 1 file changed, 51 insertions(+), 40 deletions(-) rename apps/docs/docs/state/tutorials/{migrating-to-rxstate.md => migrating-to-rxstate.mdx} (87%) diff --git a/apps/docs/docs/state/tutorials/migrating-to-rxstate.md b/apps/docs/docs/state/tutorials/migrating-to-rxstate.mdx similarity index 87% rename from apps/docs/docs/state/tutorials/migrating-to-rxstate.md rename to apps/docs/docs/state/tutorials/migrating-to-rxstate.mdx index 9a801f354..87ca57279 100644 --- a/apps/docs/docs/state/tutorials/migrating-to-rxstate.md +++ b/apps/docs/docs/state/tutorials/migrating-to-rxstate.mdx @@ -3,6 +3,9 @@ title: Migrating to RxState # Renamed from libs/state/docs/snippets/behavior-subject-vs-rx-state.md --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + # Migrating to RxState Let's take a look at a simple checklist app, see how it can be implemented in an imperative way, and after that, we will iterate over it and add some reactiveness. We skip any additional logic such as routing, error handling etc., in these examples. @@ -113,7 +116,7 @@ export class State { select(path: K): Observable { return this.data$.pipe( - map((state) => state[path]) + map((state) => state[path]), // some additional logic ); } @@ -229,11 +232,7 @@ Now we need a place from which we can **trigger** this event. `@Input id: string Also, we need to write a logic for getting our checklist from API and storing a response: ```ts -initHandler$ = this.init$.pipe( - switchMap((id) => - this.api.get(id).pipe(tap((checklist) => this.state.patch(checklist))) - ) -); +initHandler$ = this.init$.pipe(switchMap((id) => this.api.get(id).pipe(tap((checklist) => this.state.patch(checklist))))); ``` So far, so good. Inside `switchMap`, we are getting value passed to `init$` and switching to our API call. We @@ -261,13 +260,7 @@ Answering logic ```ts answerHandler$ = this.answer$.pipe( withLatestFrom(this.tasks$), - switchMap(([id, tasks]) => - this.api - .answerTask(id) - .pipe( - tap(() => this.state.patch({ tasks: tasks.filter((t) => t.id !== id) })) - ) - ) + switchMap(([id, tasks]) => this.api.answerTask(id).pipe(tap(() => this.state.patch({ tasks: tasks.filter((t) => t.id !== id) })))), ); ``` @@ -308,31 +301,17 @@ export class ChecklistComponent implements OnDestroy { init$ = new Subject(); answer$ = new Subject(); - initHandler$ = this.init$.pipe( - switchMap((id) => - this.api.get(id).pipe(tap((checklist) => this.state.patch(checklist))) - ) - ); + initHandler$ = this.init$.pipe(switchMap((id) => this.api.get(id).pipe(tap((checklist) => this.state.patch(checklist))))); answerHandler$ = this.answer$.pipe( withLatestFrom(this.tasks$), - switchMap(([id, tasks]) => - this.api - .answerTask(id) - .pipe( - tap(() => - this.state.patch({ tasks: tasks.filter((t) => t.id !== id) }) - ) - ) - ) + switchMap(([id, tasks]) => this.api.answerTask(id).pipe(tap(() => this.state.patch({ tasks: tasks.filter((t) => t.id !== id) })))), ); private destroy$ = new Subject(); constructor(private api: TodoApiService) { - merge(this.initHandler$, this.answerHandler$) - .pipe(takeUntil(this.destroy$)) - .subscribe(); + merge(this.initHandler$, this.answerHandler$).pipe(takeUntil(this.destroy$)).subscribe(); } ngOnDestroy(): void { @@ -410,9 +389,7 @@ Now we need to update our `answerHandler$` so it will return an id of task that (api returns only status code). And connect it to our `tasks` property. ```ts -answerHandler$ = this.answer$.pipe( - switchMap((id) => this.api.answerTask(id).pipe(map(() => id))) -); +answerHandler$ = this.answer$.pipe(switchMap((id) => this.api.answerTask(id).pipe(map(() => id)))); ``` ```ts @@ -430,6 +407,8 @@ our source. More on possible `connect` variants [here](../api/rx-state.md#connec **Full component code** + + ```ts export class ChecklistComponent { @Input() set id(id: string) { @@ -446,18 +425,50 @@ export class ChecklistComponent { // HANDLERS initHandler$ = this.init$.pipe(switchMap((id) => this.api.get(id))); - answerHandler$ = this.answer$.pipe( - switchMap((id) => this.api.answerTask(id).pipe(map(() => id))) - ); + answerHandler$ = this.answer$.pipe(switchMap((id) => this.api.answerTask(id).pipe(map(() => id)))); - constructor(private api: TodoApiService, private state: RxState) { + constructor( + private api: TodoApiService, + private state: RxState, + ) { this.state.connect(this.initHandler$); - this.state.connect('tasks', this.answerHandler$, (state, id) => - state.tasks.filter((t) => t.id !== id) - ); + this.state.connect('tasks', this.answerHandler$, (state, id) => state.tasks.filter((t) => t.id !== id)); } } ``` + + +```ts +export class ChecklistComponent { + readonly #state = rxState(); + @Input() set id(id: string) { + this.init$.next(id); + } + + // READS + name$ = this.#state.select('name'); + tasks$ = this.#state.select('tasks'); + + // EVENTS + init$ = new Subject(); + answer$ = new Subject(); + + // HANDLERS + initHandler$ = this.init$.pipe(switchMap((id) => this.api.get(id))); + answerHandler$ = this.answer$.pipe(switchMap((id) => this.api.answerTask(id).pipe(map(() => id)))); + + constructor( + private api: TodoApiService, + ) { + this.#state.connect(this.initHandler$); + this.#state.connect('tasks', this.answerHandler$, (state, id) => state.tasks.filter((t) => t.id !== id)); + } +} +``` + + + + **Summary:** From 7aa8d4823bdb0319b938c1d300db602f1dd6b12f Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Sat, 5 Oct 2024 07:22:56 +0200 Subject: [PATCH 32/46] docs(state): correct formatting of mdx docs --- .../manage-entities-using-ngrx-entity.mdx | 236 ++++----- ...resuse-ngrx-selectors-to-compose-state.mdx | 51 +- .../recipes/load-data-on-route-change.mdx | 122 +++-- .../state/recipes/run-partial-updates.mdx | 79 ++- .../recipes/use-rxstate-as-global-state.mdx | 475 ++++++++---------- .../state/recipes/work-with-hostbindings.mdx | 342 ++++++------- 6 files changed, 594 insertions(+), 711 deletions(-) diff --git a/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx b/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx index 91f49b881..04d2b97c9 100644 --- a/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx +++ b/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx @@ -32,74 +32,61 @@ interface ComponentState { Now, if we want to add one item to our array _(in an immutable way)_, we replace the `items` array in the state with a new reference. - - - - ```typescript - - @Component({ - selector: 'my-component', - + +```typescript +@Component({ + selector: 'my-component', }) export class MyComponent extends RxState { -readonly addItem$ = new Subject(); + readonly addItem$ = new Subject(); - constructor() { + constructor() { super(); this.connect(this.addItem$, (oldState, itemName) => { - const newItem = { - id: uuid(), // unique hash generation fn() - name: itemName, - -}; - - return { - ...oldState, - items: [...oldState.items, newItem], - -}; -}); -} + const newItem = { + id: uuid(), // unique hash generation fn() + name: itemName, + }; + + return { + ...oldState, + items: [...oldState.items, newItem], + }; + }); + } } - - ``` - - - - - ```typescript - @Component({ - selector: 'my-component', - +``` + + +```typescript +@Component({ + selector: 'my-component', }) export class MyComponent { -readonly addItem$ = new Subject(); - - readonly #state = rxState(({connect}) => { - connect(this.addItem$, (oldState, itemName) => { - const newItem = { - id: uuid(), // unique hash generation fn() - name: itemName, - -}; - - return { - ...oldState, - items: [...oldState.items, newItem], - -}; -}) -}) - + readonly #state = rxState(); + readonly addItem$ = new Subject(); + + constructor() { + + this.#state.connect(this.addItem$, (oldState, itemName) => { + const newItem = { + id: uuid(), // unique hash generation fn() + name: itemName, + }; + + return { + ...oldState, + items: [...oldState.items, newItem], + }; + }); + } } - -```` - - - +``` + + If we want to update one item, we have to query the `items` array first to get a hold of the item and then construct a new array again. What about deleting an item? You get the picture. **It´s a lot of code**, and it will grow even more if we have several types of collections in our state. @@ -127,47 +114,44 @@ The entity adapter needs a `selectId` function which is used to query items by ` Now let's see how the component has changed: - - - + + +```typescript +@Component({ + selector: 'my-component', +}) +export class MyComponent extends RxState { + readonly addItem$ = new Subject(); - ```typescript - @Component({ - selector: 'my-component', - }) - export class MyComponent extends RxState { - readonly addItem$ = new Subject(); + constructor() { + super(); - constructor() { - super(); + this.connect(this.addItem$, (oldState, itemName) => + adapter.addOne({ id: uuid(), name: itemName() }, oldState) + ); + } +} +``` + + +```typescript +@Component({ + selector: 'my-component', +}) +export class MyComponent { + readonly #state = rxState(); + readonly addItem$ = new Subject(); - this.connect(this.addItem$, (oldState, itemName) => + constructor() { + this.#state.connect(this.addItem$, (oldState, itemName) => adapter.addOne({ id: uuid(), name: itemName() }, oldState) - ); - } - } - ``` - - - - - ```typescript - @Component({ - selector: 'my-component', - }) - export class MyComponent { - readonly addItem$ = new Subject(); - readonly #state = rxState(({connect}) => { - connect(this.addItem$, (oldState, itemName) => - adapter.addOne({ id: uuid(), name: itemName() }, oldState) - ) - } - } - ``` - - - - + ); + } +} +``` + + + The `addOne()` function is just one of many functions that help us manipulate the collection. @@ -179,43 +163,41 @@ Check out the [full list of adapter collection methods](https://ngrx.io/guide/en The entity adapter comes with a small set of default selectors we can use right out of the box. - - - - - ```typescript - import { select } from '@ngrx/store'; - - const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors(); - - @Component({ - selector: 'my-component', - }) - export class MyComponent extends RxState { - readonly items$ = this.select(select(selectAll)); + + +```typescript +import { select } from '@ngrx/store'; - constructor() { - super(); - } - } - ``` +const { selectIds, selectEntities, selectAll, selectTotal } = + adapter.getSelectors(); - +@Component({ + selector: 'my-component', +}) +export class MyComponent extends RxState { + readonly items$ = this.select(select(selectAll)); - - ```typescript - import { select } from '@ngrx/store'; - const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors(); + constructor() { + super(); + } +} +``` + + +```typescript +import { select } from '@ngrx/store'; - @Component({ - selector: 'my-component', - }) - export class MyComponent { - readonly #state = rxState() - readonly items$ = this.#state.select(select(selectAll)); - } - ``` +const { selectIds, selectEntities, selectAll, selectTotal } = + adapter.getSelectors(); - +@Component({ + selector: 'my-component', +}) +export class MyComponent { + readonly #state = rxState(); + readonly items$ = this.#state.select(select(selectAll)); +} +``` + + - diff --git a/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.mdx b/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.mdx index f9aaed6ae..5ae1a8e33 100644 --- a/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.mdx +++ b/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.mdx @@ -40,6 +40,8 @@ const selectVisibleItems = createSelector(selectVisibleIds, selectItems, (visibl Using this in our component will look like this: + + ```typescript import { select } from '@ngrx/store'; @@ -52,46 +54,17 @@ export class ItemListComponent extends RxState { } } ``` + + +```typescript +import { select } from '@ngrx/store'; - - - - - ```typescript - import { select } from '@ngrx/store'; - - @Component() - export class ItemListComponent extends RxState { - readonly visibleItems$ = this.state.select(select(selectVisibleItems)); - - constructor() { - super(); - -} -} - - ``` - - - - - ```typescript - import { select } from '@ngrx/store'; - - @Component() - export class ItemListComponent{ - readonly #state = rxState() - readonly visibleItems$ = this.#state.select(select(selectVisibleItems)); - - constructor() { - super(); - -} +@Component() +export class ItemListComponent { + readonly #state = rxState(); + readonly visibleItems$ = this.#state.select(select(selectVisibleItems)); } - ``` - - - + -``` + diff --git a/apps/docs/docs/state/recipes/load-data-on-route-change.mdx b/apps/docs/docs/state/recipes/load-data-on-route-change.mdx index 4efcb093e..cc5a6833b 100644 --- a/apps/docs/docs/state/recipes/load-data-on-route-change.mdx +++ b/apps/docs/docs/state/recipes/load-data-on-route-change.mdx @@ -52,72 +52,70 @@ export class MyComponent { ## Reactive - - ```typescript - @Component({ - selector: 'my-comp', - template: ` - -
{{ user$ | push }}
- `, - changeDetection: ChangeDetectionStrategy.OnPush, - }) - export class MyComponent { - readonly user$ = this.state.select('user'); - readonly isLoading$ = this.state.select('isLoading'); - - constructor( - private router: Router, - private userService: UserService, - private state: RxState<{ user: string; isLoading: boolean }> - ) { - const fetchUserOnUrlChange$ = this.router.params.pipe( - switchMap((p) => - this.userService.getUser(p.user).pipe( + +```typescript +@Component({ + selector: 'my-comp', + template: ` + +
{{ user$ | push }}
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MyComponent { + readonly user$ = this.state.select('user'); + readonly isLoading$ = this.state.select('isLoading'); + + constructor( + private router: Router, + private userService: UserService, + private state: RxState<{ user: string; isLoading: boolean }> + ) { + const fetchUserOnUrlChange$ = this.router.params.pipe( + switchMap((p) => + this.userService.getUser(p.user).pipe( map((res) => ({ user: res.user })), startWith({ isLoading: true }), endWith({ isLoading: false }) - ) - ) - ); - this.state.connect(fetchUserOnUrlChange$); - } - } - ``` - -
- - ```typescript - @Component({ - selector: 'my-comp', - template: ` - -
{{ user$ | push }}
- `, - changeDetection: ChangeDetectionStrategy.OnPush, - }) - export class MyComponent { - readonly #state = rxState<{ user: string; isLoading: boolean }>() - readonly user$ = this.state.select('user'); - readonly isLoading$ = this.state.select('isLoading'); - - constructor( - private router: Router, - private userService: UserService, - ) { - const fetchUserOnUrlChange$ = this.router.params.pipe( - switchMap((p) => - this.userService.getUser(p.user).pipe( + ) + ) + ); + this.state.connect(fetchUserOnUrlChange$); + } +} +``` +
+ +```typescript +@Component({ + selector: 'my-comp', + template: ` + +
{{ user$ | push }}
+ `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MyComponent { + readonly #state = rxState<{ user: string; isLoading: boolean }>() + readonly user$ = this.#state.select('user'); + readonly isLoading$ = this.#state.select('isLoading'); + + constructor( + private router: Router, + private userService: UserService + ) { + const fetchUserOnUrlChange$ = this.router.params.pipe( + switchMap((p) => + this.userService.getUser(p.user).pipe( map((res) => ({ user: res.user })), startWith({ isLoading: true }), endWith({ isLoading: false }) - ) - ) - ); - this.#state.connect(fetchUserOnUrlChange$); - } - } - ``` - -
+ ) + ) + ); + this.#state.connect(fetchUserOnUrlChange$); + } +} +``` +
diff --git a/apps/docs/docs/state/recipes/run-partial-updates.mdx b/apps/docs/docs/state/recipes/run-partial-updates.mdx index d6536a23e..cd0b89646 100644 --- a/apps/docs/docs/state/recipes/run-partial-updates.mdx +++ b/apps/docs/docs/state/recipes/run-partial-updates.mdx @@ -15,50 +15,49 @@ An instance of `RxState` typed with `T` accepts `Partial` in the `set` and `c The partial update can happen directly by providing a `Partial` or over a reduce function `(oldState, change) => newState`. - - - ```typescript - import { RxState } from `rx-angular/state`; - interface ComponentState { - title: string; - list: string[]; - loading: boolean; - } - - class AnyComponent extends RxState { - updateTitle() { - this.set({ title: 'Hello!' }); - } + + +```typescript +import { RxState } from `rx-angular/state`; +interface ComponentState { + title: string; + list: string[]; + loading: boolean; +} - resetList() { - this.connect(this.globalState$.list$({ list: [], loading: false })); - } - } - ``` - - - ```typescript - import { rxState } from `rx-angular/state`; - interface ComponentState { - title: string; - list: string[]; - loading: boolean; - } +class AnyComponent extends RxState { + updateTitle() { + this.set({ title: 'Hello!' }); + } - class AnyComponent { - readonly #state = rxState() - updateTitle() { - this.#state.set({ title: 'Hello!' }); - } + resetList() { + this.connect(this.globalState$.list$({ list: [], loading: false })); + } +} +``` + + +```typescript +import { rxState } from `rx-angular/state`; +interface ComponentState { + title: string; + list: string[]; + loading: boolean; +} - resetList() { - this.#state.connect(this.globalState$.list$({ list: [], loading: false })); - } - } - ``` - +class AnyComponent { + readonly #state = rxState(); + updateTitle() { + this.set({ title: 'Hello!' }); + } - + resetList() { + this.#state.connect(this.globalState$.list$({ list: [], loading: false })); + } +} +``` + + Internally the state update looks like this: diff --git a/apps/docs/docs/state/recipes/use-rxstate-as-global-state.mdx b/apps/docs/docs/state/recipes/use-rxstate-as-global-state.mdx index 5e8039949..5797a654f 100644 --- a/apps/docs/docs/state/recipes/use-rxstate-as-global-state.mdx +++ b/apps/docs/docs/state/recipes/use-rxstate-as-global-state.mdx @@ -24,31 +24,28 @@ As with the global/local state snippet, we'll be doing the same example to-do ap - The list can be expanded or collapsed and has property `isExpanded`. - Gets tasks array from endpoint _tasks/get_ and filters out tasks that already answered. - - - - - ```typescript - interface TodosState { - tasks: Task[]; - isExpanded: boolean; - - } + + +```typescript +interface TodosState { + tasks: Task[]; + isExpanded: boolean; +} @Component({ -selector: 'todos', -templateUrl: './todo.component.html', + selector: 'todos', + templateUrl: './todo.component.html', }) export class TodoComponent extends RxState { -readonly tasks$ = this.select('tasks'); -readonly counter$ = this.select( -map((state) => state.tasks), -map((tasks) => tasks.length) -); -readonly isExpanded$ = this.select('isExpanded'); + readonly tasks$ = this.select('tasks'); + readonly counter$ = this.select( + map((state) => state.tasks), + map((tasks) => tasks.length) + ); + readonly isExpanded$ = this.select('isExpanded'); -constructor(private tasksService: TasksService) { -super(); + constructor(private tasksService: TasksService) { + super(); /* Filter out tasks that are done */ this.connect( @@ -57,37 +54,31 @@ super(); .fetchTasks() .pipe(filter((tasks) => tasks.filter((task) => !task.done))) ); - -} + } } - -```` - - - - - ```typescript - +``` + + +```typescript interface TodosState { -tasks: Task[]; -isExpanded: boolean; + tasks: Task[]; + isExpanded: boolean; } @Component({ -selector: 'todos', -templateUrl: './todo.component.html', + selector: 'todos', + templateUrl: './todo.component.html', }) export class TodoComponent { -readonly #state = rxState() -readonly tasks$ = this.#state.select('tasks'); -readonly counter$ = this.#state.select( -map((state) => state.tasks), -map((tasks) => tasks.length) -); -readonly isExpanded$ = this.#state.select('isExpanded'); - -constructor(private tasksService: TasksService) { - + readonly #state = rxState(); + readonly tasks$ = this.#state.select('tasks'); + readonly counter$ = this.#state.select( + map((state) => state.tasks), + map((tasks) => tasks.length) + ); + readonly isExpanded$ = this.#state.select('isExpanded'); + + constructor(private tasksService: TasksService) { /* Filter out tasks that are done */ this.#state.connect( 'tasks', @@ -95,14 +86,12 @@ constructor(private tasksService: TasksService) { .fetchTasks() .pipe(filter((tasks) => tasks.filter((task) => !task.done))) ); - -} + } } -```` - - +``` + + - ### Setup @@ -110,74 +99,64 @@ constructor(private tasksService: TasksService) { - The list can be expanded or collapsed and has property `isExpanded`. - Gets tasks as array from endpoint _tasks/get_. - - - - - ```typescript - interface AllTodosState { - tasks: Task[]; - isExpanded: boolean; - } - - @Component({ - selector: 'all-tasks', - templateUrl: './all-tasks.component.html', - }) - export class AllTasksComponent extends RxState { - readonly tasks$ = this.select('tasks'); - readonly counter$ = this.select( - map((state) => state.tasks), - map((tasks) => tasks.length) - ); - readonly isExpanded$ = this.select('isExpanded'); - - constructor(private tasksService: TasksService) { - super(); - - /* Fetch tasks from backend */ - this.connect('tasks', this.tasksService.fetchTasks()); - } - } - ``` + + +```typescript +interface AllTodosState { + tasks: Task[]; + isExpanded: boolean; +} - +@Component({ + selector: 'all-tasks', + templateUrl: './all-tasks.component.html', +}) +export class AllTasksComponent extends RxState { + readonly tasks$ = this.select('tasks'); + readonly counter$ = this.select( + map((state) => state.tasks), + map((tasks) => tasks.length) + ); + readonly isExpanded$ = this.select('isExpanded'); - - ```typescript + constructor(private tasksService: TasksService) { + super(); - interface AllTodosState { + /* Fetch tasks from backend */ + this.connect('tasks', this.tasksService.fetchTasks()); + } +} +``` + + +```typescript +interface AllTodosState { tasks: Task[]; isExpanded: boolean; - } +} @Component({ -selector: 'all-tasks', -templateUrl: './all-tasks.component.html', + selector: 'all-tasks', + templateUrl: './all-tasks.component.html', }) export class AllTasksComponent { -readonly #state = rxState() -readonly tasks$ = this.#state.select('tasks'); -readonly counter$ = this.#state.select( -map((state) => state.tasks), -map((tasks) => tasks.length) -); -readonly isExpanded$ = this.#state.select('isExpanded'); - -constructor(private tasksService: TasksService) { -super(); - + readonly #state = rxState(); + readonly tasks$ = this.#state.select('tasks'); + readonly counter$ = this.#state.select( + map((state) => state.tasks), + map((tasks) => tasks.length) + ); + readonly isExpanded$ = this.#state.select('isExpanded'); + + constructor(private tasksService: TasksService) { /* Fetch tasks from backend */ this.#state.connect('tasks', this.tasksService.fetchTasks()); - -} + } } +``` + + -```` - - - - ### What is global and what is local? @@ -213,46 +192,41 @@ export const GLOBAL_RX_STATE = new InjectionToken>( We then _provide_ the `injectionToken` in our `app.module.ts`. - - - - - ```typescript - import { GLOBAL_RX_STATE, GlobalState } from "./rx-state"; - ... - - @NgModule({ - imports: [...], - declarations: [...], - providers: [{ - provide: GLOBAL_RX_STATE, useFactory: () => new RxState() - }], - bootstrap: [...] - }) - export class AppModule {} - ``` - - - - - ```typescript - import { GLOBAL_RX_STATE, GlobalState } from "./rx-state"; - ... - - @NgModule({ - imports: [...], - declarations: [...], - providers: [{ - provide: GLOBAL_RX_STATE, useFactory: () => rxState() - }], - bootstrap: [...] - }) - export class AppModule {} - ``` - - + + +```typescript +import { GLOBAL_RX_STATE, GlobalState } from "./rx-state"; +... + +@NgModule({ + imports: [...], + declarations: [...], + providers: [{ + provide: GLOBAL_RX_STATE, useFactory: () => new RxState() + }], + bootstrap: [...] +}) +export class AppModule {} +``` + + +```typescript +import { GLOBAL_RX_STATE, GlobalState } from "./rx-state"; +... + +@NgModule({ + imports: [...], + declarations: [...], + providers: [{ + provide: GLOBAL_RX_STATE, useFactory: () => rxState() + }], + bootstrap: [...] +}) +export class AppModule {} +``` + + - We can then load the `tasks` in the `AppComponent` via our `tasksService.fetchTasks()` and just have our `TodoComponent` and `AllTasksComponent` connect to the global state. @@ -273,33 +247,30 @@ constructor(@Inject(GLOBAL_RX_STATE) private state, private tasksService: TasksS And our updated `TodoComponent` - - - - - ```typescript - interface TodosState { - -tasks: Task[]; -isExpanded: boolean; + + +```typescript +interface TodosState { + tasks: Task[]; + isExpanded: boolean; } @Component({ -selector: 'todos', -templateUrl: './todo.component.html', + selector: 'todos', + templateUrl: './todo.component.html', }) export class TodoComponent extends RxState { -readonly tasks$ = this.select('tasks'); -readonly counter$ = this.select( -map((state) => state.tasks), -map((tasks) => tasks.length) -); -readonly isExpanded$ = this.select('isExpanded'); - -constructor( -@Inject(GLOBAL_RX_STATE) private globalState: RxState -) { -super(); + readonly tasks$ = this.select('tasks'); + readonly counter$ = this.select( + map((state) => state.tasks), + map((tasks) => tasks.length) + ); + readonly isExpanded$ = this.select('isExpanded'); + + constructor( + @Inject(GLOBAL_RX_STATE) private globalState: RxState + ) { + super(); /* Connect to global state and filter out already completed tasks */ this.connect( @@ -308,39 +279,33 @@ super(); .select('tasks') .pipe(map((tasks) => tasks.filter((task) => !task.done))) ); - + } } +``` + + +```typescript +interface TodosState { + tasks: Task[]; + isExpanded: boolean; } -```` - - - - - ```typescript - interface TodosState { - tasks: Task[]; - isExpanded: boolean; - } - @Component({ -selector: 'todos', -templateUrl: './todo.component.html', + selector: 'todos', + templateUrl: './todo.component.html', }) export class TodoComponent { -readonly #state = rxState() -readonly tasks$ = this.#state.select('tasks'); -readonly counter$ = this.#state.select( -map((state) => state.tasks), -map((tasks) => tasks.length) -); -readonly isExpanded$ = this.#state.select('isExpanded'); - -constructor( -@Inject(GLOBAL_RX_STATE) private globalState: RxState -) { -super(); - + readonly #state = rxState(); + readonly tasks$ = this.#state.select('tasks'); + readonly counter$ = this.#state.select( + map((state) => state.tasks), + map((tasks) => tasks.length) + ); + readonly isExpanded$ = this.#state.select('isExpanded'); + + constructor( + @Inject(GLOBAL_RX_STATE) private globalState: RxState + ) { /* Connect to global state and filter out already completed tasks */ this.#state.connect( 'tasks', @@ -348,77 +313,67 @@ super(); .select('tasks') .pipe(map((tasks) => tasks.filter((task) => !task.done))) ); - -} + } } -```` - - +``` + + - Here we `connect` to the global state instance and filter out the already completed tasks. Our `AllTasksComponent` is slightly different in that it doesn't actually need to filter anything, and thus it only needs to manage the **local** `isExpanded` value, and just have the `tasks` and `counter` values come directly from the **global** state. - - - - - ```typescript - interface AllTodosState { - isExpanded: boolean; - } - - @Component({ - selector: 'all-tasks', - templateUrl: './all-tasks.component.html', - }) - export class AllTasksComponent extends RxState { - readonly tasks$ = this.globalState.select('tasks'); - readonly counter$ = this.globalState.select( - map((state) => state.tasks), - map((tasks) => tasks.length) - ); - readonly isExpanded$ = this.select('isExpanded'); - - constructor( - @Inject(GLOBAL_RX_STATE) private globalState: RxState - ) { - super(); - } - } - ``` - - - - - ```typescript - interface AllTodosState { - isExpanded: boolean; - } - - @Component({ - selector: 'all-tasks', - templateUrl: './all-tasks.component.html', - }) - export class AllTasksComponent { - readonly #state = rxState(); - readonly tasks$ = this.globalState.select('tasks'); - readonly counter$ = this.globalState.select( - map((state) => state.tasks), - map((tasks) => tasks.length) - ); - readonly isExpanded$ = this.#state.select('isExpanded'); - - constructor( - @Inject(GLOBAL_RX_STATE) private globalState: RxState - ) { - super(); - } - } - ``` - - - - + + +```typescript +interface AllTodosState { + isExpanded: boolean; +} + +@Component({ + selector: 'all-tasks', + templateUrl: './all-tasks.component.html', +}) +export class AllTasksComponent extends RxState { + readonly tasks$ = this.globalState.select('tasks'); + readonly counter$ = this.globalState.select( + map((state) => state.tasks), + map((tasks) => tasks.length) + ); + readonly isExpanded$ = this.select('isExpanded'); + + constructor( + @Inject(GLOBAL_RX_STATE) private globalState: RxState + ) { + super(); + } +} +``` + + +```typescript +interface AllTodosState { + isExpanded: boolean; +} + +@Component({ + selector: 'all-tasks', + templateUrl: './all-tasks.component.html', +}) +export class AllTasksComponent { + readonly #state = rxState(); + readonly tasks$ = this.globalState.select('tasks'); + readonly counter$ = this.globalState.select( + map((state) => state.tasks), + map((tasks) => tasks.length) + ); + readonly isExpanded$ = this.#state.select('isExpanded'); + + constructor( + @Inject(GLOBAL_RX_STATE) private globalState: RxState + ) {} +} +``` + + diff --git a/apps/docs/docs/state/recipes/work-with-hostbindings.mdx b/apps/docs/docs/state/recipes/work-with-hostbindings.mdx index 7b0630de2..c7d00838c 100644 --- a/apps/docs/docs/state/recipes/work-with-hostbindings.mdx +++ b/apps/docs/docs/state/recipes/work-with-hostbindings.mdx @@ -39,59 +39,51 @@ As stated in the title, we have to be aware changeDetection. On every changeDete all `HostBindings`. If our component doesn't get flagged as dirty, our `HostBindings` won't get updated. So we have to make sure that state changes that are related to the `HostBindings` value are actually triggering a re-render. - - - - - ```typescript - @Component({ - providers: [RxState], - }) - export class RxComponent { - // Modifying the class - @HostBinding('[class.is-hidden]') get isHidden() { - return !this.state.get().visible; - } - // Modifying styles - @HostBinding('[style.marginTop]') get marginTop() { - return `${this.state.get().top}px`; - } - // Modifying styles - @HostBinding('[style.maxHeight]') get maxHeight() { - return `${this.state.get().maxHeight}px`; - } - - constructor(private state: RxState) {} - } - ``` - - - - - import { select } from '@ngrx/store'; - - const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors(); - - ```typescript - @Component({...}) - export class RxComponent { - readonly #state = rxState() - // Modifying the class - @HostBinding('[class.is-hidden]') get isHidden() { - return !this.#state.get().visible; - } - // Modifying styles - @HostBinding('[style.marginTop]') get marginTop() { - return `${this.#state.get().top}px`; - } - // Modifying styles - @HostBinding('[style.maxHeight]') get maxHeight() { - return `${this.#state.get().maxHeight}px`; - } - - ``` - - + + +```typescript +@Component({ + providers: [RxState], +}) +export class RxComponent { + // Modifying the class + @HostBinding('[class.is-hidden]') get isHidden() { + return !this.state.get().visible; + } + // Modifying styles + @HostBinding('[style.marginTop]') get marginTop() { + return `${this.state.get().top}px`; + } + // Modifying styles + @HostBinding('[style.maxHeight]') get maxHeight() { + return `${this.state.get().maxHeight}px`; + } + + constructor(private state: RxState) {} +} +``` + + +```typescript +@Component({...}) +export class RxComponent { + readonly #state = rxState() + // Modifying the class + @HostBinding('[class.is-hidden]') get isHidden() { + return !this.#state.get().visible; + } + // Modifying styles + @HostBinding('[style.marginTop]') get marginTop() { + return `${this.#state.get().top}px`; + } + // Modifying styles + @HostBinding('[style.maxHeight]') get maxHeight() { + return `${this.#state.get().maxHeight}px`; + } +} +``` + + With this setup in place we have two options to get things done. @@ -100,72 +92,63 @@ With this setup in place we have two options to get things done. Since rendering is a side-effect, we could utilize the `hold` method and register a function which handles change detection for us. - - - - - ```typescript - @Component({ - providers: [RxState], - }) - export class RxComponent { - // Modifying the class - @HostBinding('[class.is-hidden]') get isHidden() { - return !this.state.get().visible; - } - // Modifying styles - @HostBinding('[style.marginTop]') get marginTop() { - return `${this.state.get().top}px`; - } - // Modifying styles - @HostBinding('[style.maxHeight]') get maxHeight() { - return `${this.state.get().maxHeight}px`; - } - - constructor( - private state: RxState, - private cdRef: ChangeDetectorRef - ) { - state.hold(state.select(), () => this.cdRef.markForCheck()); - } - } - ``` - - - - - import { select } from '@ngrx/store'; - - const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors(); - - ```typescript - @Component({...}) - export class RxComponent { - readonly #state = rxState() - readonly #effects = rxEffects(); - // Modifying the class - @HostBinding('[class.is-hidden]') get isHidden() { - return !this.#state.get().visible; - } - // Modifying styles - @HostBinding('[style.marginTop]') get marginTop() { - return `${this.#state.get().top}px`; - } - // Modifying styles - @HostBinding('[style.maxHeight]') get maxHeight() { - return `${this.#state.get().maxHeight}px`; - } - - constructor( - private state: RxState, - private cdRef: ChangeDetectorRef - ) { - this.#effects.register(state.select(), () => this.cdRef.markForCheck()); - } - } - ``` - - + + +```typescript +@Component({ + providers: [RxState], +}) +export class RxComponent { + // Modifying the class + @HostBinding('[class.is-hidden]') get isHidden() { + return !this.state.get().visible; + } + // Modifying styles + @HostBinding('[style.marginTop]') get marginTop() { + return `${this.state.get().top}px`; + } + // Modifying styles + @HostBinding('[style.maxHeight]') get maxHeight() { + return `${this.state.get().maxHeight}px`; + } + + constructor( + private state: RxState, + private cdRef: ChangeDetectorRef + ) { + state.hold(state.select(), () => this.cdRef.markForCheck()); + } +} +``` + + +```typescript +@Component({...}) +export class RxComponent { + readonly #state = rxState() + readonly #effects = rxEffects(); + // Modifying the class + @HostBinding('[class.is-hidden]') get isHidden() { + return !this.#state.get().visible; + } + // Modifying styles + @HostBinding('[style.marginTop]') get marginTop() { + return `${this.#state.get().top}px`; + } + // Modifying styles + @HostBinding('[style.maxHeight]') get maxHeight() { + return `${this.#state.get().maxHeight}px`; + } + + constructor( + private cdRef: ChangeDetectorRef + ) { + this.#effects.register(this.#state.select(), () => this.cdRef.markForCheck()); + } +} +``` + + By calling `ChangeDetectorRef#markForCheck` after every state change, we flag our component dirty when needed and let angular's `ChangeDetection` do it's magic for us. @@ -201,71 +184,64 @@ We will utilize the `ElementRef` itself for this purpose and manipulate the DOM Feel free to use angular's `Renderer2` if you want an abstraction layer, should work the exact same way. - - - - - ```typescript - @Component({ - providers: [RxState], - }) - export class RxComponent { - constructor( - private state: RxState, - private elementRef: ElementRef, - private cdRef: ChangeDetectorRef - ) { - // optional: cdRef.detach(); - this.state.hold(this.state.select(), ({ visible, top, maxHeight }) => { - const { nativeElement } = elementRef; - nativeElement.style.marginTop = `${top ? top : 0}px`; - nativeElement.style.maxHeight = `${maxHeight ? maxHeight : 100}px`; - // by using this, we could assign more classes - const classList: { [cls: string]: boolean } = { + + +```typescript +@Component({ + providers: [RxState], +}) +export class RxComponent { + constructor( + private state: RxState, + private elementRef: ElementRef, + private cdRef: ChangeDetectorRef + ) { + // optional: cdRef.detach(); + this.state.hold(this.state.select(), ({ visible, top, maxHeight }) => { + const { nativeElement } = elementRef; + nativeElement.style.marginTop = `${top ? top : 0}px`; + nativeElement.style.maxHeight = `${maxHeight ? maxHeight : 100}px`; + // by using this, we could assign more classes + const classList: { [cls: string]: boolean } = { 'is-hidden': !visible, - }; - Object.keys(classList).forEach((cls) => { + }; + Object.keys(classList).forEach((cls) => { classList[cls] - ? nativeElement.classList.add(cls) - : nativeElement.classList.remove(cls); - }); - }); - } - } - ``` - - - - - import { select } from '@ngrx/store'; - - const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors(); - - ```typescript - @Component({...}) - export class RxComponent { - readonly #state: RxState - constructor( - private elementRef: ElementRef, - private cdRef: ChangeDetectorRef - ) { - // optional: cdRef.detach(); - this.state.hold(this.#state.select(), ({ visible, top, maxHeight }) => { - const { nativeElement } = elementRef; - nativeElement.style.marginTop = `${top ? top : 0}px`; - nativeElement.style.maxHeight = `${maxHeight ? maxHeight : 100}px`; - // by using this, we could assign more classes - const classList: { [cls: string]: boolean } = { + ? nativeElement.classList.add(cls) + : nativeElement.classList.remove(cls); + }); + }); + } +} +``` + + +```typescript +@Component({...}) +export class RxComponent { + readonly #state = rxState() + readonly #effects = rxEffects(); + constructor( + private elementRef: ElementRef, + private cdRef: ChangeDetectorRef + ) { + // optional: cdRef.detach(); + this.#effects.register(this.#state.select(), ({ visible, top, maxHeight }) => { + const { nativeElement } = elementRef; + nativeElement.style.marginTop = `${top ? top : 0}px`; + nativeElement.style.maxHeight = `${maxHeight ? maxHeight : 100}px`; + // by using this, we could assign more classes + const classList: { [cls: string]: boolean } = { 'is-hidden': !visible, - }; - Object.keys(classList).forEach((cls) => { + }; + Object.keys(classList).forEach((cls) => { classList[cls] - ? nativeElement.classList.add(cls) - : nativeElement.classList.remove(cls); - }); - }); - } - } - ``` - - + ? nativeElement.classList.add(cls) + : nativeElement.classList.remove(cls); + }); + }); + } +} +``` + + From 5f2c58b2a518c828463d55cbd062851ce9ab963c Mon Sep 17 00:00:00 2001 From: Michael Berger Date: Tue, 15 Oct 2024 06:39:43 +0200 Subject: [PATCH 33/46] chore(state): run format:write --- .../manage-entities-using-ngrx-entity.mdx | 49 +++--- ...resuse-ngrx-selectors-to-compose-state.mdx | 15 +- .../recipes/load-data-on-route-change.mdx | 38 ++--- .../docs/state/recipes/manage-viewmodel.mdx | 34 ++-- .../state/recipes/run-partial-updates.mdx | 18 ++- .../recipes/use-rxstate-as-global-state.mdx | 152 +++++++++--------- .../state/recipes/work-with-hostbindings.mdx | 26 +-- .../state/tutorials/migrating-to-rxstate.mdx | 36 ++--- 8 files changed, 195 insertions(+), 173 deletions(-) diff --git a/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx b/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx index 04d2b97c9..29cd8128f 100644 --- a/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx +++ b/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx @@ -40,8 +40,8 @@ Now, if we want to add one item to our array _(in an immutable way)_, we replace export class MyComponent extends RxState { readonly addItem$ = new Subject(); - constructor() { - super(); +constructor() { +super(); this.connect(this.addItem$, (oldState, itemName) => { const newItem = { @@ -54,9 +54,11 @@ export class MyComponent extends RxState { items: [...oldState.items, newItem], }; }); - } + } -``` +} + +````
```typescript @@ -82,11 +84,11 @@ export class MyComponent { }); } } -``` +```` + - If we want to update one item, we have to query the `items` array first to get a hold of the item and then construct a new array again. What about deleting an item? You get the picture. **It´s a lot of code**, and it will grow even more if we have several types of collections in our state. @@ -108,7 +110,7 @@ interface ComponentState extends EntityState { const adapter: EntityAdapter = createEntityAdapter({ selectId: (item: Item) => item.id, }); -```` +``` The entity adapter needs a `selectId` function which is used to query items by `id` within the collection. @@ -123,15 +125,17 @@ Now let's see how the component has changed: export class MyComponent extends RxState { readonly addItem$ = new Subject(); - constructor() { - super(); +constructor() { +super(); this.connect(this.addItem$, (oldState, itemName) => adapter.addOne({ id: uuid(), name: itemName() }, oldState) ); - } + } -``` +} + +```` ```typescript @@ -148,11 +152,11 @@ export class MyComponent { ); } } -``` +```` + - The `addOne()` function is just one of many functions that help us manipulate the collection. Delete an item? `removeOne(item.id, oldState)`. @@ -169,19 +173,20 @@ The entity adapter comes with a small set of default selectors we can use right import { select } from '@ngrx/store'; const { selectIds, selectEntities, selectAll, selectTotal } = - adapter.getSelectors(); +adapter.getSelectors(); @Component({ - selector: 'my-component', +selector: 'my-component', }) export class MyComponent extends RxState { - readonly items$ = this.select(select(selectAll)); +readonly items$ = this.select(select(selectAll)); - constructor() { - super(); - } +constructor() { +super(); } -``` +} + +```` ```typescript @@ -197,7 +202,7 @@ export class MyComponent { readonly #state = rxState(); readonly items$ = this.#state.select(select(selectAll)); } -``` +```` + - diff --git a/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.mdx b/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.mdx index 5ae1a8e33..7019c7b9b 100644 --- a/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.mdx +++ b/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.mdx @@ -47,13 +47,14 @@ import { select } from '@ngrx/store'; @Component() export class ItemListComponent extends RxState { - readonly visibleItems$ = this.state.select(select(selectVisibleItems)); +readonly visibleItems$ = this.state.select(select(selectVisibleItems)); - constructor() { - super(); - } +constructor() { +super(); } -``` +} + +```` ```typescript @@ -64,7 +65,7 @@ export class ItemListComponent { readonly #state = rxState(); readonly visibleItems$ = this.#state.select(select(selectVisibleItems)); } -``` +```` + - diff --git a/apps/docs/docs/state/recipes/load-data-on-route-change.mdx b/apps/docs/docs/state/recipes/load-data-on-route-change.mdx index cc5a6833b..5ad22a3d8 100644 --- a/apps/docs/docs/state/recipes/load-data-on-route-change.mdx +++ b/apps/docs/docs/state/recipes/load-data-on-route-change.mdx @@ -66,24 +66,25 @@ export class MyComponent { readonly user$ = this.state.select('user'); readonly isLoading$ = this.state.select('isLoading'); - constructor( - private router: Router, - private userService: UserService, - private state: RxState<{ user: string; isLoading: boolean }> - ) { - const fetchUserOnUrlChange$ = this.router.params.pipe( - switchMap((p) => - this.userService.getUser(p.user).pipe( - map((res) => ({ user: res.user })), - startWith({ isLoading: true }), - endWith({ isLoading: false }) - ) - ) - ); - this.state.connect(fetchUserOnUrlChange$); - } +constructor( +private router: Router, +private userService: UserService, +private state: RxState<{ user: string; isLoading: boolean }> +) { +const fetchUserOnUrlChange$ = this.router.params.pipe( +switchMap((p) => +this.userService.getUser(p.user).pipe( +map((res) => ({ user: res.user })), +startWith({ isLoading: true }), +endWith({ isLoading: false }) +) +) +); +this.state.connect(fetchUserOnUrlChange$); } -``` +} + +```` ```typescript @@ -116,6 +117,7 @@ export class MyComponent { this.#state.connect(fetchUserOnUrlChange$); } } -``` +```` + diff --git a/apps/docs/docs/state/recipes/manage-viewmodel.mdx b/apps/docs/docs/state/recipes/manage-viewmodel.mdx index b8bda1eab..a7b0c80a5 100644 --- a/apps/docs/docs/state/recipes/manage-viewmodel.mdx +++ b/apps/docs/docs/state/recipes/manage-viewmodel.mdx @@ -138,23 +138,24 @@ interface ComponentViewModel { @Component() export class ViewModelComponent extends RxState { - readonly viewModel: ComponentViewModel = { - main$: this.state.select(selectSlice(['title', 'created'])), +readonly viewModel: ComponentViewModel = { +main$: this.state.select(selectSlice(['title', 'created'])), list$: this.state.select( - selectSlice(['list', 'visibleItemIds']), - map(({ list, visibleItemIds }) => ({ - total: list.length, - visibleItems: list.filter((item) => - visibleItemIds.some((itemId) => itemId === item.id) - ), - })) - ), - }; - constructor() { - super(); - } +selectSlice(['list', 'visibleItemIds']), +map(({ list, visibleItemIds }) => ({ +total: list.length, +visibleItems: list.filter((item) => +visibleItemIds.some((itemId) => itemId === item.id) +), +})) +), +}; +constructor() { +super(); } -``` +} + +```` ```typescript @@ -179,6 +180,7 @@ export class ViewModelComponent { ), }; } -``` +```` + diff --git a/apps/docs/docs/state/recipes/run-partial-updates.mdx b/apps/docs/docs/state/recipes/run-partial-updates.mdx index cd0b89646..29f5eb9c6 100644 --- a/apps/docs/docs/state/recipes/run-partial-updates.mdx +++ b/apps/docs/docs/state/recipes/run-partial-updates.mdx @@ -26,15 +26,16 @@ interface ComponentState { } class AnyComponent extends RxState { - updateTitle() { - this.set({ title: 'Hello!' }); - } +updateTitle() { +this.set({ title: 'Hello!' }); +} - resetList() { - this.connect(this.globalState$.list$({ list: [], loading: false })); - } +resetList() { +this.connect(this.globalState$.list$({ list: [], loading: false })); } -``` +} + +```` ```typescript @@ -55,7 +56,8 @@ class AnyComponent { this.#state.connect(this.globalState$.list$({ list: [], loading: false })); } } -``` +```` + diff --git a/apps/docs/docs/state/recipes/use-rxstate-as-global-state.mdx b/apps/docs/docs/state/recipes/use-rxstate-as-global-state.mdx index 5797a654f..ed536ac3f 100644 --- a/apps/docs/docs/state/recipes/use-rxstate-as-global-state.mdx +++ b/apps/docs/docs/state/recipes/use-rxstate-as-global-state.mdx @@ -33,19 +33,19 @@ interface TodosState { } @Component({ - selector: 'todos', - templateUrl: './todo.component.html', +selector: 'todos', +templateUrl: './todo.component.html', }) export class TodoComponent extends RxState { - readonly tasks$ = this.select('tasks'); - readonly counter$ = this.select( - map((state) => state.tasks), - map((tasks) => tasks.length) - ); - readonly isExpanded$ = this.select('isExpanded'); +readonly tasks$ = this.select('tasks'); +readonly counter$ = this.select( +map((state) => state.tasks), +map((tasks) => tasks.length) +); +readonly isExpanded$ = this.select('isExpanded'); - constructor(private tasksService: TasksService) { - super(); +constructor(private tasksService: TasksService) { +super(); /* Filter out tasks that are done */ this.connect( @@ -54,9 +54,11 @@ export class TodoComponent extends RxState { .fetchTasks() .pipe(filter((tasks) => tasks.filter((task) => !task.done))) ); - } + } -``` +} + +```` ```typescript @@ -88,11 +90,11 @@ export class TodoComponent { ); } } -``` +```` + - ### Setup - Renders a list of all existing `tasks` and a `counter` that shows the total amount of tasks. @@ -108,25 +110,27 @@ interface AllTodosState { } @Component({ - selector: 'all-tasks', - templateUrl: './all-tasks.component.html', +selector: 'all-tasks', +templateUrl: './all-tasks.component.html', }) export class AllTasksComponent extends RxState { - readonly tasks$ = this.select('tasks'); - readonly counter$ = this.select( - map((state) => state.tasks), - map((tasks) => tasks.length) - ); - readonly isExpanded$ = this.select('isExpanded'); +readonly tasks$ = this.select('tasks'); +readonly counter$ = this.select( +map((state) => state.tasks), +map((tasks) => tasks.length) +); +readonly isExpanded$ = this.select('isExpanded'); - constructor(private tasksService: TasksService) { - super(); +constructor(private tasksService: TasksService) { +super(); /* Fetch tasks from backend */ this.connect('tasks', this.tasksService.fetchTasks()); - } + } -``` +} + +```` ```typescript @@ -153,12 +157,11 @@ export class AllTasksComponent { this.#state.connect('tasks', this.tasksService.fetchTasks()); } } -``` +```` + - - ### What is global and what is local? Looking at the above examples, let us see what is **local** and what is **global**! @@ -185,10 +188,8 @@ export interface GlobalState { tasks: Task[]; } -export const GLOBAL_RX_STATE = new InjectionToken>( - 'GLOBAL_RX_STATE' -); -```` +export const GLOBAL_RX_STATE = new InjectionToken>('GLOBAL_RX_STATE'); +``` We then _provide_ the `injectionToken` in our `app.module.ts`. @@ -199,15 +200,16 @@ import { GLOBAL_RX_STATE, GlobalState } from "./rx-state"; ... @NgModule({ - imports: [...], - declarations: [...], - providers: [{ - provide: GLOBAL_RX_STATE, useFactory: () => new RxState() - }], - bootstrap: [...] +imports: [...], +declarations: [...], +providers: [{ +provide: GLOBAL_RX_STATE, useFactory: () => new RxState() +}], +bootstrap: [...] }) export class AppModule {} -``` + +```` ```typescript @@ -223,11 +225,11 @@ import { GLOBAL_RX_STATE, GlobalState } from "./rx-state"; bootstrap: [...] }) export class AppModule {} -``` +```` + - We can then load the `tasks` in the `AppComponent` via our `tasksService.fetchTasks()` and just have our `TodoComponent` and `AllTasksComponent` connect to the global state. ```typescript @@ -256,21 +258,21 @@ interface TodosState { } @Component({ - selector: 'todos', - templateUrl: './todo.component.html', +selector: 'todos', +templateUrl: './todo.component.html', }) export class TodoComponent extends RxState { - readonly tasks$ = this.select('tasks'); - readonly counter$ = this.select( - map((state) => state.tasks), - map((tasks) => tasks.length) - ); - readonly isExpanded$ = this.select('isExpanded'); +readonly tasks$ = this.select('tasks'); +readonly counter$ = this.select( +map((state) => state.tasks), +map((tasks) => tasks.length) +); +readonly isExpanded$ = this.select('isExpanded'); - constructor( - @Inject(GLOBAL_RX_STATE) private globalState: RxState - ) { - super(); +constructor( +@Inject(GLOBAL_RX_STATE) private globalState: RxState +) { +super(); /* Connect to global state and filter out already completed tasks */ this.connect( @@ -279,9 +281,11 @@ export class TodoComponent extends RxState { .select('tasks') .pipe(map((tasks) => tasks.filter((task) => !task.done))) ); - } + } -``` +} + +```` ```typescript @@ -315,11 +319,11 @@ export class TodoComponent { ); } } -``` +```` + - Here we `connect` to the global state instance and filter out the already completed tasks. Our `AllTasksComponent` is slightly different in that it doesn't actually need to filter anything, and thus it only needs to manage the **local** `isExpanded` value, and just have the `tasks` and `counter` values come directly from the **global** state. @@ -332,24 +336,25 @@ interface AllTodosState { } @Component({ - selector: 'all-tasks', - templateUrl: './all-tasks.component.html', +selector: 'all-tasks', +templateUrl: './all-tasks.component.html', }) export class AllTasksComponent extends RxState { - readonly tasks$ = this.globalState.select('tasks'); - readonly counter$ = this.globalState.select( - map((state) => state.tasks), - map((tasks) => tasks.length) - ); - readonly isExpanded$ = this.select('isExpanded'); +readonly tasks$ = this.globalState.select('tasks'); +readonly counter$ = this.globalState.select( +map((state) => state.tasks), +map((tasks) => tasks.length) +); +readonly isExpanded$ = this.select('isExpanded'); - constructor( - @Inject(GLOBAL_RX_STATE) private globalState: RxState - ) { - super(); - } +constructor( +@Inject(GLOBAL_RX_STATE) private globalState: RxState +) { +super(); } -``` +} + +```` ```typescript @@ -374,6 +379,7 @@ export class AllTasksComponent { @Inject(GLOBAL_RX_STATE) private globalState: RxState ) {} } -``` +```` + diff --git a/apps/docs/docs/state/recipes/work-with-hostbindings.mdx b/apps/docs/docs/state/recipes/work-with-hostbindings.mdx index c7d00838c..d978cdb0c 100644 --- a/apps/docs/docs/state/recipes/work-with-hostbindings.mdx +++ b/apps/docs/docs/state/recipes/work-with-hostbindings.mdx @@ -59,9 +59,10 @@ export class RxComponent { return `${this.state.get().maxHeight}px`; } - constructor(private state: RxState) {} +constructor(private state: RxState) {} } -``` + +```` ```typescript @@ -81,7 +82,8 @@ export class RxComponent { return `${this.#state.get().maxHeight}px`; } } -``` +```` + @@ -112,14 +114,15 @@ export class RxComponent { return `${this.state.get().maxHeight}px`; } - constructor( - private state: RxState, - private cdRef: ChangeDetectorRef - ) { - state.hold(state.select(), () => this.cdRef.markForCheck()); - } +constructor( +private state: RxState, +private cdRef: ChangeDetectorRef +) { +state.hold(state.select(), () => this.cdRef.markForCheck()); } -``` +} + +```` ```typescript @@ -146,7 +149,8 @@ export class RxComponent { this.#effects.register(this.#state.select(), () => this.cdRef.markForCheck()); } } -``` +```` + diff --git a/apps/docs/docs/state/tutorials/migrating-to-rxstate.mdx b/apps/docs/docs/state/tutorials/migrating-to-rxstate.mdx index 87ca57279..b8e7c6802 100644 --- a/apps/docs/docs/state/tutorials/migrating-to-rxstate.mdx +++ b/apps/docs/docs/state/tutorials/migrating-to-rxstate.mdx @@ -415,27 +415,28 @@ export class ChecklistComponent { this.init$.next(id); } - // READS - name$ = this.state.select('name'); - tasks$ = this.state.select('tasks'); +// READS +name$ = this.state.select('name'); +tasks$ = this.state.select('tasks'); - // EVENTS - init$ = new Subject(); - answer$ = new Subject(); +// EVENTS +init$ = new Subject(); +answer$ = new Subject(); - // HANDLERS - initHandler$ = this.init$.pipe(switchMap((id) => this.api.get(id))); +// HANDLERS +initHandler$ = this.init$.pipe(switchMap((id) => this.api.get(id))); answerHandler$ = this.answer$.pipe(switchMap((id) => this.api.answerTask(id).pipe(map(() => id)))); - constructor( - private api: TodoApiService, - private state: RxState, - ) { - this.state.connect(this.initHandler$); +constructor( +private api: TodoApiService, +private state: RxState, +) { +this.state.connect(this.initHandler$); this.state.connect('tasks', this.answerHandler$, (state, id) => state.tasks.filter((t) => t.id !== id)); - } } -``` +} + +```` ```ts @@ -464,12 +465,11 @@ export class ChecklistComponent { this.#state.connect('tasks', this.answerHandler$, (state, id) => state.tasks.filter((t) => t.id !== id)); } } -``` +```` + - - **Summary:** - Both reading and writing are reactive. From b3cab5938cede5dbc21ac66bb1642cf491881aa9 Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Sun, 22 Dec 2024 23:59:37 +0100 Subject: [PATCH 34/46] chore: fix docs build --- .../manage-entities-using-ngrx-entity.mdx | 73 ++++--- ...resuse-ngrx-selectors-to-compose-state.mdx | 17 +- .../recipes/load-data-on-route-change.mdx | 51 ++--- .../docs/state/recipes/manage-viewmodel.mdx | 58 +++--- .../state/recipes/run-partial-updates.mdx | 10 +- .../recipes/use-rxstate-as-global-state.mdx | 189 +++++++++--------- .../state/recipes/work-with-hostbindings.mdx | 45 +++-- .../state/tutorials/migrating-to-rxstate.mdx | 41 ++-- 8 files changed, 262 insertions(+), 222 deletions(-) diff --git a/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx b/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx index 29cd8128f..9214f5924 100644 --- a/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx +++ b/apps/docs/docs/state/integrations/manage-entities-using-ngrx-entity.mdx @@ -32,16 +32,21 @@ interface ComponentState { Now, if we want to add one item to our array _(in an immutable way)_, we replace the `items` array in the state with a new reference. + + ```typescript +import { RxState } from '@rx-angular/state'; +import { Component } from '@angular/core'; + @Component({ selector: 'my-component', }) -export class MyComponent extends RxState { - readonly addItem$ = new Subject(); +export class MyComponent { + addItem$ = new Subject(); -constructor() { -super(); + constructor() { + super(); this.connect(this.addItem$, (oldState, itemName) => { const newItem = { @@ -54,13 +59,14 @@ super(); items: [...oldState.items, newItem], }; }); - -} + } } +``` -```` + + ```typescript @Component({ selector: 'my-component', @@ -70,7 +76,6 @@ export class MyComponent { readonly addItem$ = new Subject(); constructor() { - this.#state.connect(this.addItem$, (oldState, itemName) => { const newItem = { id: uuid(), // unique hash generation fn() @@ -84,9 +89,10 @@ export class MyComponent { }); } } -```` +``` + If we want to update one item, we have to query the `items` array first to get a hold of the item and then construct a new array again. @@ -117,7 +123,9 @@ The entity adapter needs a `selectId` function which is used to query items by ` Now let's see how the component has changed: + + ```typescript @Component({ selector: 'my-component', @@ -125,19 +133,18 @@ Now let's see how the component has changed: export class MyComponent extends RxState { readonly addItem$ = new Subject(); -constructor() { -super(); - - this.connect(this.addItem$, (oldState, itemName) => - adapter.addOne({ id: uuid(), name: itemName() }, oldState) - ); + constructor() { + super(); + this.connect(this.addItem$, (oldState, itemName) => adapter.addOne({ id: uuid(), name: itemName() }, oldState)); + } } -} +``` -```` + + ```typescript @Component({ selector: 'my-component', @@ -147,14 +154,13 @@ export class MyComponent { readonly addItem$ = new Subject(); constructor() { - this.#state.connect(this.addItem$, (oldState, itemName) => - adapter.addOne({ id: uuid(), name: itemName() }, oldState) - ); + this.#state.connect(this.addItem$, (oldState, itemName) => adapter.addOne({ id: uuid(), name: itemName() }, oldState)); } } -```` +``` + The `addOne()` function is just one of many functions that help us manipulate the collection. @@ -168,32 +174,34 @@ Check out the [full list of adapter collection methods](https://ngrx.io/guide/en The entity adapter comes with a small set of default selectors we can use right out of the box. + + ```typescript import { select } from '@ngrx/store'; -const { selectIds, selectEntities, selectAll, selectTotal } = -adapter.getSelectors(); +const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors(); @Component({ -selector: 'my-component', + selector: 'my-component', }) export class MyComponent extends RxState { -readonly items$ = this.select(select(selectAll)); + readonly items$ = this.select(select(selectAll)); -constructor() { -super(); -} + constructor() { + super(); + } } +``` -```` + + ```typescript import { select } from '@ngrx/store'; -const { selectIds, selectEntities, selectAll, selectTotal } = - adapter.getSelectors(); +const { selectIds, selectEntities, selectAll, selectTotal } = adapter.getSelectors(); @Component({ selector: 'my-component', @@ -202,7 +210,8 @@ export class MyComponent { readonly #state = rxState(); readonly items$ = this.#state.select(select(selectAll)); } -```` +``` + diff --git a/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.mdx b/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.mdx index 7019c7b9b..ea8ec0722 100644 --- a/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.mdx +++ b/apps/docs/docs/state/integrations/resuse-ngrx-selectors-to-compose-state.mdx @@ -41,22 +41,26 @@ const selectVisibleItems = createSelector(selectVisibleIds, selectItems, (visibl Using this in our component will look like this: + + ```typescript import { select } from '@ngrx/store'; @Component() export class ItemListComponent extends RxState { -readonly visibleItems$ = this.state.select(select(selectVisibleItems)); + readonly visibleItems$ = this.state.select(select(selectVisibleItems)); -constructor() { -super(); -} + constructor() { + super(); + } } +``` -```` + + ```typescript import { select } from '@ngrx/store'; @@ -65,7 +69,8 @@ export class ItemListComponent { readonly #state = rxState(); readonly visibleItems$ = this.#state.select(select(selectVisibleItems)); } -```` +``` + diff --git a/apps/docs/docs/state/recipes/load-data-on-route-change.mdx b/apps/docs/docs/state/recipes/load-data-on-route-change.mdx index 5ad22a3d8..2c08f6c57 100644 --- a/apps/docs/docs/state/recipes/load-data-on-route-change.mdx +++ b/apps/docs/docs/state/recipes/load-data-on-route-change.mdx @@ -52,7 +52,9 @@ export class MyComponent { ## Reactive + + ```typescript @Component({ selector: 'my-comp', @@ -66,27 +68,29 @@ export class MyComponent { readonly user$ = this.state.select('user'); readonly isLoading$ = this.state.select('isLoading'); -constructor( -private router: Router, -private userService: UserService, -private state: RxState<{ user: string; isLoading: boolean }> -) { -const fetchUserOnUrlChange$ = this.router.params.pipe( -switchMap((p) => -this.userService.getUser(p.user).pipe( -map((res) => ({ user: res.user })), -startWith({ isLoading: true }), -endWith({ isLoading: false }) -) -) -); -this.state.connect(fetchUserOnUrlChange$); -} + constructor( + private router: Router, + private userService: UserService, + private state: RxState<{ user: string; isLoading: boolean }>, + ) { + const fetchUserOnUrlChange$ = this.router.params.pipe( + switchMap((p) => + this.userService.getUser(p.user).pipe( + map((res) => ({ user: res.user })), + startWith({ isLoading: true }), + endWith({ isLoading: false }), + ), + ), + ); + this.state.connect(fetchUserOnUrlChange$); + } } +``` -```` + + ```typescript @Component({ selector: 'my-comp', @@ -97,27 +101,28 @@ this.state.connect(fetchUserOnUrlChange$); changeDetection: ChangeDetectionStrategy.OnPush, }) export class MyComponent { - readonly #state = rxState<{ user: string; isLoading: boolean }>() + readonly #state = rxState<{ user: string; isLoading: boolean }>(); readonly user$ = this.#state.select('user'); readonly isLoading$ = this.#state.select('isLoading'); constructor( private router: Router, - private userService: UserService + private userService: UserService, ) { const fetchUserOnUrlChange$ = this.router.params.pipe( switchMap((p) => this.userService.getUser(p.user).pipe( map((res) => ({ user: res.user })), startWith({ isLoading: true }), - endWith({ isLoading: false }) - ) - ) + endWith({ isLoading: false }), + ), + ), ); this.#state.connect(fetchUserOnUrlChange$); } } -```` +``` + diff --git a/apps/docs/docs/state/recipes/manage-viewmodel.mdx b/apps/docs/docs/state/recipes/manage-viewmodel.mdx index a7b0c80a5..6be2298cd 100644 --- a/apps/docs/docs/state/recipes/manage-viewmodel.mdx +++ b/apps/docs/docs/state/recipes/manage-viewmodel.mdx @@ -62,7 +62,9 @@ It returns an Observable that emits a distinct subset of the received object. Utilizing it inside of the `RxState#select` method enables you to pluck a _distinct_ `ViewModel` directly out of your state. + + ```typescript @Component() export class ViewModelComponent extends RxState { @@ -72,18 +74,19 @@ export class ViewModelComponent extends RxState { title, created, total: list.length, - visibleItems: list.filter((item) => - visibleItemIds.some((itemId) => itemId === item.id) - ), - })) + visibleItems: list.filter((item) => visibleItemIds.some((itemId) => itemId === item.id)), + })), ); constructor() { super(); } } ``` + + + ```typescript @Component() export class ViewModelComponent { @@ -94,10 +97,8 @@ export class ViewModelComponent { title, created, total: list.length, - visibleItems: list.filter((item) => - visibleItemIds.some((itemId) => itemId === item.id) - ), - })) + visibleItems: list.filter((item) => visibleItemIds.some((itemId) => itemId === item.id)), + })), ); } ``` @@ -129,7 +130,9 @@ This way you may achieve more control over what to render when, e.g. lazy render ``` + + ```typescript interface ComponentViewModel { main$: Observable<{ title: string; created: Date }>; @@ -138,26 +141,26 @@ interface ComponentViewModel { @Component() export class ViewModelComponent extends RxState { -readonly viewModel: ComponentViewModel = { -main$: this.state.select(selectSlice(['title', 'created'])), + readonly viewModel: ComponentViewModel = { + main$: this.state.select(selectSlice(['title', 'created'])), list$: this.state.select( -selectSlice(['list', 'visibleItemIds']), -map(({ list, visibleItemIds }) => ({ -total: list.length, -visibleItems: list.filter((item) => -visibleItemIds.some((itemId) => itemId === item.id) -), -})) -), -}; -constructor() { -super(); -} + selectSlice(['list', 'visibleItemIds']), + map(({ list, visibleItemIds }) => ({ + total: list.length, + visibleItems: list.filter((item) => visibleItemIds.some((itemId) => itemId === item.id)), + })), + ), + }; + constructor() { + super(); + } } +``` -```` + + ```typescript interface ComponentViewModel { main$: Observable<{ title: string; created: Date }>; @@ -173,14 +176,13 @@ export class ViewModelComponent { selectSlice(['list', 'visibleItemIds']), map(({ list, visibleItemIds }) => ({ total: list.length, - visibleItems: list.filter((item) => - visibleItemIds.some((itemId) => itemId === item.id) - ), - })) + visibleItems: list.filter((item) => visibleItemIds.some((itemId) => itemId === item.id)), + })), ), }; } -```` +``` + diff --git a/apps/docs/docs/state/recipes/run-partial-updates.mdx b/apps/docs/docs/state/recipes/run-partial-updates.mdx index 29f5eb9c6..82efe3e51 100644 --- a/apps/docs/docs/state/recipes/run-partial-updates.mdx +++ b/apps/docs/docs/state/recipes/run-partial-updates.mdx @@ -16,7 +16,9 @@ An instance of `RxState` typed with `T` accepts `Partial` in the `set` and `c The partial update can happen directly by providing a `Partial` or over a reduce function `(oldState, change) => newState`. + + ```typescript import { RxState } from `rx-angular/state`; interface ComponentState { @@ -35,9 +37,12 @@ this.connect(this.globalState$.list$({ list: [], loading: false })); } } -```` +``` + + + ```typescript import { rxState } from `rx-angular/state`; interface ComponentState { @@ -56,9 +61,10 @@ class AnyComponent { this.#state.connect(this.globalState$.list$({ list: [], loading: false })); } } -```` +``` + Internally the state update looks like this: diff --git a/apps/docs/docs/state/recipes/use-rxstate-as-global-state.mdx b/apps/docs/docs/state/recipes/use-rxstate-as-global-state.mdx index ed536ac3f..df03d5798 100644 --- a/apps/docs/docs/state/recipes/use-rxstate-as-global-state.mdx +++ b/apps/docs/docs/state/recipes/use-rxstate-as-global-state.mdx @@ -25,7 +25,9 @@ As with the global/local state snippet, we'll be doing the same example to-do ap - Gets tasks array from endpoint _tasks/get_ and filters out tasks that already answered. + + ```typescript interface TodosState { tasks: Task[]; @@ -33,34 +35,30 @@ interface TodosState { } @Component({ -selector: 'todos', -templateUrl: './todo.component.html', + selector: 'todos', + templateUrl: './todo.component.html', }) export class TodoComponent extends RxState { -readonly tasks$ = this.select('tasks'); -readonly counter$ = this.select( -map((state) => state.tasks), -map((tasks) => tasks.length) -); -readonly isExpanded$ = this.select('isExpanded'); + readonly tasks$ = this.select('tasks'); + readonly counter$ = this.select( + map((state) => state.tasks), + map((tasks) => tasks.length), + ); + readonly isExpanded$ = this.select('isExpanded'); -constructor(private tasksService: TasksService) { -super(); + constructor(private tasksService: TasksService) { + super(); /* Filter out tasks that are done */ - this.connect( - 'tasks', - this.tasksService - .fetchTasks() - .pipe(filter((tasks) => tasks.filter((task) => !task.done))) - ); - -} + this.connect('tasks', this.tasksService.fetchTasks().pipe(filter((tasks) => tasks.filter((task) => !task.done)))); + } } +``` -```` + + ```typescript interface TodosState { tasks: Task[]; @@ -76,23 +74,19 @@ export class TodoComponent { readonly tasks$ = this.#state.select('tasks'); readonly counter$ = this.#state.select( map((state) => state.tasks), - map((tasks) => tasks.length) + map((tasks) => tasks.length), ); readonly isExpanded$ = this.#state.select('isExpanded'); constructor(private tasksService: TasksService) { /* Filter out tasks that are done */ - this.#state.connect( - 'tasks', - this.tasksService - .fetchTasks() - .pipe(filter((tasks) => tasks.filter((task) => !task.done))) - ); + this.#state.connect('tasks', this.tasksService.fetchTasks().pipe(filter((tasks) => tasks.filter((task) => !task.done)))); } } -```` +``` + ### Setup @@ -102,7 +96,9 @@ export class TodoComponent { - Gets tasks as array from endpoint _tasks/get_. + + ```typescript interface AllTodosState { tasks: Task[]; @@ -110,29 +106,30 @@ interface AllTodosState { } @Component({ -selector: 'all-tasks', -templateUrl: './all-tasks.component.html', + selector: 'all-tasks', + templateUrl: './all-tasks.component.html', }) export class AllTasksComponent extends RxState { -readonly tasks$ = this.select('tasks'); -readonly counter$ = this.select( -map((state) => state.tasks), -map((tasks) => tasks.length) -); -readonly isExpanded$ = this.select('isExpanded'); + readonly tasks$ = this.select('tasks'); + readonly counter$ = this.select( + map((state) => state.tasks), + map((tasks) => tasks.length), + ); + readonly isExpanded$ = this.select('isExpanded'); -constructor(private tasksService: TasksService) { -super(); + constructor(private tasksService: TasksService) { + super(); /* Fetch tasks from backend */ this.connect('tasks', this.tasksService.fetchTasks()); - -} + } } +``` -```` + + ```typescript interface AllTodosState { tasks: Task[]; @@ -148,7 +145,7 @@ export class AllTasksComponent { readonly tasks$ = this.#state.select('tasks'); readonly counter$ = this.#state.select( map((state) => state.tasks), - map((tasks) => tasks.length) + map((tasks) => tasks.length), ); readonly isExpanded$ = this.#state.select('isExpanded'); @@ -157,9 +154,10 @@ export class AllTasksComponent { this.#state.connect('tasks', this.tasksService.fetchTasks()); } } -```` +``` + ### What is global and what is local? @@ -194,7 +192,9 @@ export const GLOBAL_RX_STATE = new InjectionToken>('GLOBAL_ We then _provide_ the `injectionToken` in our `app.module.ts`. + + ```typescript import { GLOBAL_RX_STATE, GlobalState } from "./rx-state"; ... @@ -209,9 +209,12 @@ bootstrap: [...] }) export class AppModule {} -```` +``` + + + ```typescript import { GLOBAL_RX_STATE, GlobalState } from "./rx-state"; ... @@ -225,9 +228,10 @@ import { GLOBAL_RX_STATE, GlobalState } from "./rx-state"; bootstrap: [...] }) export class AppModule {} -```` +``` + We can then load the `tasks` in the `AppComponent` via our `tasksService.fetchTasks()` and just have our `TodoComponent` and `AllTasksComponent` connect to the global state. @@ -250,7 +254,9 @@ constructor(@Inject(GLOBAL_RX_STATE) private state, private tasksService: TasksS And our updated `TodoComponent` + + ```typescript interface TodosState { tasks: Task[]; @@ -258,36 +264,30 @@ interface TodosState { } @Component({ -selector: 'todos', -templateUrl: './todo.component.html', + selector: 'todos', + templateUrl: './todo.component.html', }) export class TodoComponent extends RxState { -readonly tasks$ = this.select('tasks'); -readonly counter$ = this.select( -map((state) => state.tasks), -map((tasks) => tasks.length) -); -readonly isExpanded$ = this.select('isExpanded'); - -constructor( -@Inject(GLOBAL_RX_STATE) private globalState: RxState -) { -super(); + readonly tasks$ = this.select('tasks'); + readonly counter$ = this.select( + map((state) => state.tasks), + map((tasks) => tasks.length), + ); + readonly isExpanded$ = this.select('isExpanded'); - /* Connect to global state and filter out already completed tasks */ - this.connect( - 'tasks', - this.globalState - .select('tasks') - .pipe(map((tasks) => tasks.filter((task) => !task.done))) - ); + constructor(@Inject(GLOBAL_RX_STATE) private globalState: RxState) { + super(); + /* Connect to global state and filter out already completed tasks */ + this.connect('tasks', this.globalState.select('tasks').pipe(map((tasks) => tasks.filter((task) => !task.done)))); + } } -} +``` -```` + + ```typescript interface TodosState { tasks: Task[]; @@ -303,25 +303,19 @@ export class TodoComponent { readonly tasks$ = this.#state.select('tasks'); readonly counter$ = this.#state.select( map((state) => state.tasks), - map((tasks) => tasks.length) + map((tasks) => tasks.length), ); readonly isExpanded$ = this.#state.select('isExpanded'); - constructor( - @Inject(GLOBAL_RX_STATE) private globalState: RxState - ) { + constructor(@Inject(GLOBAL_RX_STATE) private globalState: RxState) { /* Connect to global state and filter out already completed tasks */ - this.#state.connect( - 'tasks', - this.globalState - .select('tasks') - .pipe(map((tasks) => tasks.filter((task) => !task.done))) - ); + this.#state.connect('tasks', this.globalState.select('tasks').pipe(map((tasks) => tasks.filter((task) => !task.done)))); } } -```` +``` + Here we `connect` to the global state instance and filter out the already completed tasks. @@ -329,34 +323,36 @@ Here we `connect` to the global state instance and filter out the already comple Our `AllTasksComponent` is slightly different in that it doesn't actually need to filter anything, and thus it only needs to manage the **local** `isExpanded` value, and just have the `tasks` and `counter` values come directly from the **global** state. + + ```typescript interface AllTodosState { isExpanded: boolean; } @Component({ -selector: 'all-tasks', -templateUrl: './all-tasks.component.html', + selector: 'all-tasks', + templateUrl: './all-tasks.component.html', }) export class AllTasksComponent extends RxState { -readonly tasks$ = this.globalState.select('tasks'); -readonly counter$ = this.globalState.select( -map((state) => state.tasks), -map((tasks) => tasks.length) -); -readonly isExpanded$ = this.select('isExpanded'); - -constructor( -@Inject(GLOBAL_RX_STATE) private globalState: RxState -) { -super(); -} + readonly tasks$ = this.globalState.select('tasks'); + readonly counter$ = this.globalState.select( + map((state) => state.tasks), + map((tasks) => tasks.length), + ); + readonly isExpanded$ = this.select('isExpanded'); + + constructor(@Inject(GLOBAL_RX_STATE) private globalState: RxState) { + super(); + } } +``` -```` + + ```typescript interface AllTodosState { isExpanded: boolean; @@ -371,15 +367,14 @@ export class AllTasksComponent { readonly tasks$ = this.globalState.select('tasks'); readonly counter$ = this.globalState.select( map((state) => state.tasks), - map((tasks) => tasks.length) + map((tasks) => tasks.length), ); readonly isExpanded$ = this.#state.select('isExpanded'); - constructor( - @Inject(GLOBAL_RX_STATE) private globalState: RxState - ) {} + constructor(@Inject(GLOBAL_RX_STATE) private globalState: RxState) {} } -```` +``` + diff --git a/apps/docs/docs/state/recipes/work-with-hostbindings.mdx b/apps/docs/docs/state/recipes/work-with-hostbindings.mdx index d978cdb0c..89505fb10 100644 --- a/apps/docs/docs/state/recipes/work-with-hostbindings.mdx +++ b/apps/docs/docs/state/recipes/work-with-hostbindings.mdx @@ -40,7 +40,9 @@ all `HostBindings`. If our component doesn't get flagged as dirty, our `HostBind sure that state changes that are related to the `HostBindings` value are actually triggering a re-render. + + ```typescript @Component({ providers: [RxState], @@ -59,12 +61,14 @@ export class RxComponent { return `${this.state.get().maxHeight}px`; } -constructor(private state: RxState) {} + constructor(private state: RxState) {} } +``` -```` + + ```typescript @Component({...}) export class RxComponent { @@ -82,9 +86,10 @@ export class RxComponent { return `${this.#state.get().maxHeight}px`; } } -```` +``` + With this setup in place we have two options to get things done. @@ -95,7 +100,9 @@ Since rendering is a side-effect, we could utilize the `hold` method and registe a function which handles change detection for us. + + ```typescript @Component({ providers: [RxState], @@ -114,17 +121,19 @@ export class RxComponent { return `${this.state.get().maxHeight}px`; } -constructor( -private state: RxState, -private cdRef: ChangeDetectorRef -) { -state.hold(state.select(), () => this.cdRef.markForCheck()); -} + constructor( + private state: RxState, + private cdRef: ChangeDetectorRef, + ) { + state.hold(state.select(), () => this.cdRef.markForCheck()); + } } +``` -```` + + ```typescript @Component({...}) export class RxComponent { @@ -149,9 +158,10 @@ export class RxComponent { this.#effects.register(this.#state.select(), () => this.cdRef.markForCheck()); } } -```` +``` + By calling `ChangeDetectorRef#markForCheck` after every state change, we flag our component dirty when needed and let angular's @@ -189,7 +199,9 @@ We will utilize the `ElementRef` itself for this purpose and manipulate the DOM Feel free to use angular's `Renderer2` if you want an abstraction layer, should work the exact same way. + + ```typescript @Component({ providers: [RxState], @@ -198,7 +210,7 @@ export class RxComponent { constructor( private state: RxState, private elementRef: ElementRef, - private cdRef: ChangeDetectorRef + private cdRef: ChangeDetectorRef, ) { // optional: cdRef.detach(); this.state.hold(this.state.select(), ({ visible, top, maxHeight }) => { @@ -210,16 +222,17 @@ export class RxComponent { 'is-hidden': !visible, }; Object.keys(classList).forEach((cls) => { - classList[cls] - ? nativeElement.classList.add(cls) - : nativeElement.classList.remove(cls); + classList[cls] ? nativeElement.classList.add(cls) : nativeElement.classList.remove(cls); }); }); } } ``` + + + ```typescript @Component({...}) export class RxComponent { @@ -247,5 +260,7 @@ export class RxComponent { } } ``` + + diff --git a/apps/docs/docs/state/tutorials/migrating-to-rxstate.mdx b/apps/docs/docs/state/tutorials/migrating-to-rxstate.mdx index b8e7c6802..b08355a55 100644 --- a/apps/docs/docs/state/tutorials/migrating-to-rxstate.mdx +++ b/apps/docs/docs/state/tutorials/migrating-to-rxstate.mdx @@ -408,37 +408,41 @@ our source. More on possible `connect` variants [here](../api/rx-state.md#connec **Full component code** + + ```ts export class ChecklistComponent { @Input() set id(id: string) { this.init$.next(id); } -// READS -name$ = this.state.select('name'); -tasks$ = this.state.select('tasks'); + // READS + name$ = this.state.select('name'); + tasks$ = this.state.select('tasks'); -// EVENTS -init$ = new Subject(); -answer$ = new Subject(); + // EVENTS + init$ = new Subject(); + answer$ = new Subject(); -// HANDLERS -initHandler$ = this.init$.pipe(switchMap((id) => this.api.get(id))); + // HANDLERS + initHandler$ = this.init$.pipe(switchMap((id) => this.api.get(id))); answerHandler$ = this.answer$.pipe(switchMap((id) => this.api.answerTask(id).pipe(map(() => id)))); -constructor( -private api: TodoApiService, -private state: RxState, -) { -this.state.connect(this.initHandler$); + constructor( + private api: TodoApiService, + private state: RxState, + ) { + this.state.connect(this.initHandler$); this.state.connect('tasks', this.answerHandler$, (state, id) => state.tasks.filter((t) => t.id !== id)); + } } -} +``` -```` + + ```ts export class ChecklistComponent { readonly #state = rxState(); @@ -458,16 +462,15 @@ export class ChecklistComponent { initHandler$ = this.init$.pipe(switchMap((id) => this.api.get(id))); answerHandler$ = this.answer$.pipe(switchMap((id) => this.api.answerTask(id).pipe(map(() => id)))); - constructor( - private api: TodoApiService, - ) { + constructor(private api: TodoApiService) { this.#state.connect(this.initHandler$); this.#state.connect('tasks', this.answerHandler$, (state, id) => state.tasks.filter((t) => t.id !== id)); } } -```` +``` + **Summary:** From c32a158bc716a9223e9f44b413c53b274f0c06f3 Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Mon, 23 Dec 2024 14:18:26 +0100 Subject: [PATCH 35/46] refactor(template): don't expose viewcache --- libs/template/virtual-view/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/template/virtual-view/src/index.ts b/libs/template/virtual-view/src/index.ts index d7c3f5273..a05cea76c 100644 --- a/libs/template/virtual-view/src/index.ts +++ b/libs/template/virtual-view/src/index.ts @@ -1,5 +1,4 @@ export { RxVirtualView } from './lib/virtual-view.directive'; -export { VirtualViewCache } from './lib/virtual-view-cache'; export { RxVirtualViewObserver } from './lib/virtual-view-observer.directive'; export { RxVirtualViewPlaceholder } from './lib/virtual-view-placeholder.directive'; export { RxVirtualViewTemplate } from './lib/virtual-view-template.directive'; From 376329f6ab570356220afc0eae4d57a6a3a2b575 Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Mon, 23 Dec 2024 14:19:33 +0100 Subject: [PATCH 36/46] refactor(template): finetune API --- libs/template/virtual-view/src/lib/model.ts | 8 ++- .../virtual-view/src/lib/resize-observer.ts | 7 ++- .../lib/virtual-view-observer.directive.ts | 17 ++++++- .../src/lib/virtual-view.directive.ts | 51 ++++++++++++------- 4 files changed, 60 insertions(+), 23 deletions(-) diff --git a/libs/template/virtual-view/src/lib/model.ts b/libs/template/virtual-view/src/lib/model.ts index 39180a3cf..2ae8f53fe 100644 --- a/libs/template/virtual-view/src/lib/model.ts +++ b/libs/template/virtual-view/src/lib/model.ts @@ -20,7 +20,13 @@ export interface _RxVirtualViewPlaceholder { * @internal */ export abstract class _RxVirtualViewObserver { - abstract register(virtualView: HTMLElement): Observable; + abstract observeElementVisibility( + virtualView: HTMLElement, + ): Observable; + abstract observeElementSize( + element: Element, + options?: ResizeObserverOptions, + ): Observable; } /** diff --git a/libs/template/virtual-view/src/lib/resize-observer.ts b/libs/template/virtual-view/src/lib/resize-observer.ts index 997f79155..62489fcfb 100644 --- a/libs/template/virtual-view/src/lib/resize-observer.ts +++ b/libs/template/virtual-view/src/lib/resize-observer.ts @@ -18,10 +18,13 @@ export class RxaResizeObserver { }); /** @internal */ - #elements = new WeakMap>(); + #elements = new Map>(); constructor() { - this.#destroyRef.onDestroy(() => this.#resizeObserver.disconnect()); + this.#destroyRef.onDestroy(() => { + this.#elements.clear(); + this.#resizeObserver.disconnect(); + }); } observeElement( diff --git a/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts b/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts index ccba27fe2..b01e49fdd 100644 --- a/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts @@ -7,7 +7,13 @@ import { OnDestroy, OnInit, } from '@angular/core'; -import { BehaviorSubject, combineLatest, ReplaySubject, Subject } from 'rxjs'; +import { + BehaviorSubject, + combineLatest, + Observable, + ReplaySubject, + Subject, +} from 'rxjs'; import { distinctUntilChanged, finalize, map } from 'rxjs/operators'; import { _RxVirtualViewObserver } from './model'; import { RxaResizeObserver } from './resize-observer'; @@ -45,6 +51,8 @@ export class RxVirtualViewObserver implements OnInit, OnDestroy { #observer: IntersectionObserver | null = null; + #resizeObserver = inject(RxaResizeObserver, { self: true }); + /** * The root element to observe. * @@ -151,4 +159,11 @@ export class RxVirtualViewObserver implements OnInit, OnDestroy { }), ); } + + observeElementSize( + element: Element, + options?: ResizeObserverOptions, + ): Observable { + return this.#resizeObserver.observeElement(element, options); + } } diff --git a/libs/template/virtual-view/src/lib/virtual-view.directive.ts b/libs/template/virtual-view/src/lib/virtual-view.directive.ts index 5583b7a88..bf9243c34 100644 --- a/libs/template/virtual-view/src/lib/virtual-view.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view.directive.ts @@ -17,14 +17,19 @@ import { RxStrategyProvider, } from '@rx-angular/cdk/render-strategies'; import { NEVER, Observable, ReplaySubject } from 'rxjs'; -import { distinctUntilChanged, finalize, switchMap, tap } from 'rxjs/operators'; +import { + distinctUntilChanged, + finalize, + map, + switchMap, + tap, +} from 'rxjs/operators'; import { _RxVirtualView, _RxVirtualViewObserver, _RxVirtualViewPlaceholder, _RxVirtualViewTemplate, } from './model'; -import { RxaResizeObserver } from './resize-observer'; import { VIRTUAL_VIEW_CONFIG_TOKEN } from './virtual-view.config'; import { VirtualViewCache } from './virtual-view-cache'; @@ -74,7 +79,6 @@ export class RxVirtualView readonly #elementRef = inject>(ElementRef); readonly #strategyProvider = inject(RxStrategyProvider); readonly #viewCache = inject(VirtualViewCache, { optional: true }); - readonly #resizeObserver = inject(RxaResizeObserver, { optional: true }); readonly #destroyRef = inject(DestroyRef); readonly #config = inject(VIRTUAL_VIEW_CONFIG_TOKEN); @@ -147,6 +151,19 @@ export class RxVirtualView this.#config.templateStrategy, ); + /** + * A function extracting width & height from a ResizeObserverEntry + */ + readonly extractSize = + input<(entry: ResizeObserverEntry) => { width: number; height: number }>( + defaultExtractSize, + ); + + /** + * ResizeObserverOptions + */ + readonly resizeObserverOptions = input(); + readonly #placeholderVisible = signal(false); #templateIsShown = false; @@ -214,7 +231,7 @@ export class RxVirtualView this.renderPlaceholder(); } this.#observer - .register(this.#elementRef.nativeElement) + .observeElementVisibility(this.#elementRef.nativeElement) .pipe(takeUntilDestroyed(this.#destroyRef)) .subscribe((visible) => this.#visible$.next(visible)); this.#visible$ @@ -226,16 +243,15 @@ export class RxVirtualView ? NEVER : this.showTemplate$().pipe( switchMap((view) => { - const resize$ = this.observeElementSize$(); + const resize$ = this.#observer.observeElementSize( + this.#elementRef.nativeElement, + this.resizeObserverOptions(), + ); view.detectChanges(); return resize$; }), - tap(({ borderBoxSize }) => { - this.size.set({ - width: borderBoxSize[0].inlineSize, - height: borderBoxSize[0].blockSize, - }); - }), + map(this.extractSize()), + tap(({ width, height }) => this.size.set({ width, height })), ); } return this.#placeholderVisible() ? NEVER : this.showPlaceholder$(); @@ -336,12 +352,9 @@ export class RxVirtualView placeholderRef.detectChanges(); } } - - /** - * Observes the element size and emits the size as an observable. This is used to calculate the containment. - * @private - */ - private observeElementSize$() { - return this.#resizeObserver.observeElement(this.#elementRef.nativeElement); - } } + +const defaultExtractSize = (entry: ResizeObserverEntry) => ({ + width: entry.borderBoxSize[0].inlineSize, + height: entry.borderBoxSize[0].blockSize, +}); From 7c21631ddde6b53d92734696c5c1bb20c68ba8f0 Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Mon, 23 Dec 2024 14:23:26 +0100 Subject: [PATCH 37/46] fix(template): fix API usage --- .../src/lib/virtual-view-observer.directive.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts b/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts index b01e49fdd..75a2968e8 100644 --- a/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts @@ -46,7 +46,10 @@ import { VirtualViewCache } from './virtual-view-cache'; { provide: _RxVirtualViewObserver, useExisting: RxVirtualViewObserver }, ], }) -export class RxVirtualViewObserver implements OnInit, OnDestroy { +export class RxVirtualViewObserver + extends _RxVirtualViewObserver + implements OnInit, OnDestroy +{ #elementRef = inject>(ElementRef); #observer: IntersectionObserver | null = null; @@ -140,7 +143,7 @@ export class RxVirtualViewObserver implements OnInit, OnDestroy { this.#forcedHidden$.next(false); } - register(virtualView: HTMLElement) { + observeElementVisibility(virtualView: HTMLElement) { const isVisible$ = new ReplaySubject(1); // Store the view and the visibility state in the map. From 6905cec5201d52e7e55890deab99f41093f73618 Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Mon, 23 Dec 2024 14:28:51 +0100 Subject: [PATCH 38/46] docs(template): improve virtual view docs --- apps/docs/docs/template/api/virtual-view-directive.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/docs/docs/template/api/virtual-view-directive.mdx b/apps/docs/docs/template/api/virtual-view-directive.mdx index 12996ef67..dd9d3f242 100644 --- a/apps/docs/docs/template/api/virtual-view-directive.mdx +++ b/apps/docs/docs/template/api/virtual-view-directive.mdx @@ -106,12 +106,12 @@ We are only rendering the `item` component when it's visible to the user. Otherw ### RxVirtualView Inputs | Input | Type | description | -| -------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --- | ------------------- | +| -------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `cacheEnabled` | `boolean` | Useful when we want to cache the templates and placeholders to optimize view rendering. | | `startWithPlaceholderAsap` | `boolean` | Whether to start with the placeholder asap or not. If `true`, the placeholder will be rendered immediately, without waiting for the template to be visible. This is useful when you want to render the placeholder immediately, but you don't want to wait for the template to be visible. This is to counter concurrent rendering, and to avoid flickering. | | `keepLastKnownSize` | `boolean` | This will keep the last known size of the host element while the template is visible. It sets 'minHeight' to the host node | | `useContentVisibility` | `boolean` | It will add the `content-visibility` CSS class to the host element, together with `contain-intrinsic-width` and `contain-intrinsic-height` CSS properties. | -| `useContainment` | `boolean` | It will add `contain` css property with:
- `size layout paint`: if `useContentVisibility` is `true` && placeholder is visible
- `content`: if `useContentVisibility` is `false` | | template is visible | +| `useContainment` | `boolean` | It will add `contain` css property with:
- `size layout paint`: if `useContentVisibility` is `true` && placeholder is visible
- `content`: if `useContentVisibility` is `false` or template is visible | | `placeholderStrategy` | `boolean` | The strategy to use for rendering the placeholder.
Defaults to: `low`
[Read more about strategies](../../cdk/render-strategies/strategies/concurrent-strategies) | | `templateStrategy` | `boolean` | The strategy to use for rendering the template.
Defaults to: `normal`
[Read more about strategies](../../cdk/render-strategies/strategies/concurrent-strategies) | @@ -146,7 +146,7 @@ interface RxVirtualViewConfig { ### Customize the config -When you want to customize the default configuration on provider level, you can use the `provideVirtualViewConfig` function. +When you want to customize the default configuration on any provider level (e.g. component, appConfig, route, ...), you can use the `provideVirtualViewConfig` function. ```typescript import { ApplicationConfig } from '@angular/core'; From 98a447e0bd427ac062e5a9a8f2f6fc0d148d8f45 Mon Sep 17 00:00:00 2001 From: Enea Jahollari Date: Mon, 23 Dec 2024 20:43:22 +0100 Subject: [PATCH 39/46] feat(template): slightly better types --- .../virtual-view/src/lib/resize-observer.ts | 4 +-- .../lib/virtual-view-observer.directive.ts | 7 ++--- .../src/lib/virtual-view.directive.ts | 31 ++++++++++--------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/libs/template/virtual-view/src/lib/resize-observer.ts b/libs/template/virtual-view/src/lib/resize-observer.ts index 62489fcfb..62492319d 100644 --- a/libs/template/virtual-view/src/lib/resize-observer.ts +++ b/libs/template/virtual-view/src/lib/resize-observer.ts @@ -13,7 +13,7 @@ export class RxaResizeObserver { #resizeObserver = new ResizeObserver((entries) => { entries.forEach((entry) => { if (this.#elements.has(entry.target)) - this.#elements.get(entry.target).next(entry); + this.#elements.get(entry.target)!.next(entry); }); }); @@ -35,7 +35,7 @@ export class RxaResizeObserver { this.#elements.set(element, resizeEvent$); this.#resizeObserver.observe(element, options); - return this.#elements.get(element).pipe( + return resizeEvent$.pipe( distinctUntilChanged(), finalize(() => { this.#resizeObserver.unobserve(element); diff --git a/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts b/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts index 75a2968e8..e3340d128 100644 --- a/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts @@ -101,7 +101,7 @@ export class RxVirtualViewObserver (entries) => { entries.forEach((entry) => { if (this.#elements.has(entry.target)) - this.#elements.get(entry.target).next(entry.isIntersecting); + this.#elements.get(entry.target)?.next(entry.isIntersecting); }); }, { @@ -116,7 +116,6 @@ export class RxVirtualViewObserver this.#elements.clear(); this.#observer?.disconnect(); this.#observer = null; - this.#elementRef = null; } /** @@ -151,13 +150,13 @@ export class RxVirtualViewObserver this.#elements.set(virtualView, isVisible$); // Start observing the virtual view immediately. - this.#observer.observe(virtualView); + this.#observer?.observe(virtualView); return combineLatest([isVisible$, this.#forcedHidden$]).pipe( map(([isVisible, forcedHidden]) => (forcedHidden ? false : isVisible)), distinctUntilChanged(), finalize(() => { - this.#observer.unobserve(virtualView); + this.#observer?.unobserve(virtualView); this.#elements.delete(virtualView); }), ); diff --git a/libs/template/virtual-view/src/lib/virtual-view.directive.ts b/libs/template/virtual-view/src/lib/virtual-view.directive.ts index bf9243c34..c33baa48b 100644 --- a/libs/template/virtual-view/src/lib/virtual-view.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view.directive.ts @@ -82,8 +82,8 @@ export class RxVirtualView readonly #destroyRef = inject(DestroyRef); readonly #config = inject(VIRTUAL_VIEW_CONFIG_TOKEN); - #template: _RxVirtualViewTemplate; - #placeholder?: _RxVirtualViewPlaceholder; + #template: _RxVirtualViewTemplate | null = null; + #placeholder: _RxVirtualViewPlaceholder | null = null; /** * Useful when we want to cache the templates and placeholders to optimize view rendering. @@ -230,10 +230,12 @@ export class RxVirtualView if (this.startWithPlaceholderAsap()) { this.renderPlaceholder(); } + this.#observer - .observeElementVisibility(this.#elementRef.nativeElement) + ?.observeElementVisibility(this.#elementRef.nativeElement) .pipe(takeUntilDestroyed(this.#destroyRef)) .subscribe((visible) => this.#visible$.next(visible)); + this.#visible$ .pipe( distinctUntilChanged(), @@ -243,7 +245,7 @@ export class RxVirtualView ? NEVER : this.showTemplate$().pipe( switchMap((view) => { - const resize$ = this.#observer.observeElementSize( + const resize$ = this.#observer!.observeElementSize( this.#elementRef.nativeElement, this.resizeObserverOptions(), ); @@ -257,7 +259,7 @@ export class RxVirtualView return this.#placeholderVisible() ? NEVER : this.showPlaceholder$(); }), finalize(() => { - this.#viewCache.clear(this); + this.#viewCache!.clear(this); }), takeUntilDestroyed(this.#destroyRef), ) @@ -265,7 +267,6 @@ export class RxVirtualView } ngOnDestroy() { - // WE DON'T NEED THAT... but enea insists! this.#template = null; this.#placeholder = null; } @@ -287,16 +288,16 @@ export class RxVirtualView () => { this.#templateIsShown = true; this.#placeholderVisible.set(false); - const placeHolder = this.#template.viewContainerRef.detach(); + const placeHolder = this.#template!.viewContainerRef.detach(); if (this.cacheEnabled() && placeHolder) { - this.#viewCache.storePlaceholder(this, placeHolder); + this.#viewCache!.storePlaceholder(this, placeHolder); } else if (!this.cacheEnabled() && placeHolder) { placeHolder.destroy(); } const tmpl = - (this.#viewCache.getTemplate(this) as EmbeddedViewRef) ?? - this.#template.templateRef.createEmbeddedView({}); - this.#template.viewContainerRef.insert(tmpl); + (this.#viewCache!.getTemplate(this) as EmbeddedViewRef) ?? + this.#template!.templateRef.createEmbeddedView({}); + this.#template!.viewContainerRef.insert(tmpl); placeHolder?.detectChanges(); return tmpl; @@ -331,11 +332,11 @@ export class RxVirtualView this.#placeholderVisible.set(true); this.#templateIsShown = false; - const template = this.#template.viewContainerRef.detach(); + const template = this.#template!.viewContainerRef.detach(); if (template) { if (this.cacheEnabled()) { - this.#viewCache.storeTemplate(this, template); + this.#viewCache!.storeTemplate(this, template); } else { template.destroy(); } @@ -345,10 +346,10 @@ export class RxVirtualView if (this.#placeholder) { const placeholderRef = - this.#viewCache.getPlaceholder(this) ?? + this.#viewCache!.getPlaceholder(this) ?? this.#placeholder.templateRef.createEmbeddedView({}); - this.#template.viewContainerRef.insert(placeholderRef); + this.#template!.viewContainerRef.insert(placeholderRef); placeholderRef.detectChanges(); } } From ce873aed2ba692909d252f276e2dac20ef91c020 Mon Sep 17 00:00:00 2001 From: Enea Jahollari Date: Mon, 23 Dec 2024 20:43:46 +0100 Subject: [PATCH 40/46] chore: disable non null assertion completely --- libs/template/.eslintrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/template/.eslintrc.json b/libs/template/.eslintrc.json index 5ddf9ea84..2d1a9e831 100644 --- a/libs/template/.eslintrc.json +++ b/libs/template/.eslintrc.json @@ -21,7 +21,7 @@ } ], "@angular-eslint/no-input-rename": "off", - "@typescript-eslint/no-non-null-assertion": "warn" + "@typescript-eslint/no-non-null-assertion": "off" } }, { From 70f7426c693a8353002aec8bd9991996cf9d005c Mon Sep 17 00:00:00 2001 From: Enea Jahollari Date: Mon, 23 Dec 2024 21:00:59 +0100 Subject: [PATCH 41/46] feat(template): rename template to content --- .../virtual-view/virtual-item.component.ts | 6 +- .../virtual-view-demo.component.ts | 16 ++--- .../template/api/virtual-view-directive.mdx | 44 ++++++------ libs/template/virtual-view/src/index.ts | 2 +- libs/template/virtual-view/src/lib/model.ts | 4 +- .../lib/tests/virtual-view.directive.spec.ts | 8 +-- .../src/lib/virtual-view-cache.ts | 54 +++++++------- ...e.ts => virtual-view-content.directive.ts} | 14 ++-- .../lib/virtual-view-observer.directive.ts | 2 +- .../lib/virtual-view-placeholder.directive.ts | 2 +- .../src/lib/virtual-view.config.ts | 22 +++--- .../src/lib/virtual-view.directive.ts | 72 +++++++++---------- 12 files changed, 123 insertions(+), 123 deletions(-) rename libs/template/virtual-view/src/lib/{virtual-view-template.directive.ts => virtual-view-content.directive.ts} (59%) diff --git a/apps/demos/src/app/features/template/virtual-view/virtual-item.component.ts b/apps/demos/src/app/features/template/virtual-view/virtual-item.component.ts index 0c2cddbeb..a9d8506a7 100644 --- a/apps/demos/src/app/features/template/virtual-view/virtual-item.component.ts +++ b/apps/demos/src/app/features/template/virtual-view/virtual-item.component.ts @@ -1,13 +1,13 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core'; import { RxVirtualViewPlaceholder, - RxVirtualViewTemplate, + RxVirtualViewContent, } from '@rx-angular/template/virtual-view'; @Component({ selector: 'virtual-item', template: ` -
+
{{ item().content }}
@@ -15,7 +15,7 @@ import {
`, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [RxVirtualViewTemplate, RxVirtualViewPlaceholder], + imports: [RxVirtualViewContent, RxVirtualViewPlaceholder], }) export class VirtualItem { item = input<{ id: number; content: string }>(); diff --git a/apps/demos/src/app/features/template/virtual-view/virtual-view-demo.component.ts b/apps/demos/src/app/features/template/virtual-view/virtual-view-demo.component.ts index 0f4952860..e69f95466 100644 --- a/apps/demos/src/app/features/template/virtual-view/virtual-view-demo.component.ts +++ b/apps/demos/src/app/features/template/virtual-view/virtual-view-demo.component.ts @@ -8,7 +8,7 @@ import { RxVirtualView, RxVirtualViewObserver, RxVirtualViewPlaceholder, - RxVirtualViewTemplate, + RxVirtualViewContent, } from '@rx-angular/template/virtual-view'; import { VirtualContent } from './virtual-content.component'; import { VirtualItem } from './virtual-item.component'; @@ -34,7 +34,7 @@ import { VirtualPlaceholder } from './virtual-placeholder.component';

Inline, no placeholder, keepLastKnownSize

@for (item of values; track item.id) {
-
+
{{ item.content }}
@@ -45,7 +45,7 @@ import { VirtualPlaceholder } from './virtual-placeholder.component'; @for (item of values; track item.id) {
content before
-
+
{{ item.content }}
content after
@@ -60,7 +60,7 @@ import { VirtualPlaceholder } from './virtual-placeholder.component'; @for (item of values; track item.id) {
content before
-
+
{{ item.content }}
content after
@@ -77,7 +77,7 @@ import { VirtualPlaceholder } from './virtual-placeholder.component'; Category 3 @for (item of values; track item.id) {
-
+
{{ item.content }}
@@ -109,7 +109,7 @@ import { VirtualPlaceholder } from './virtual-placeholder.component';

Category 4

@for (item of values; track item.id) {
-
+
{{ item.content }}
@@ -156,7 +156,7 @@ import { VirtualPlaceholder } from './virtual-placeholder.component'; imports: [ RxVirtualViewObserver, RxVirtualView, - RxVirtualViewTemplate, + RxVirtualViewContent, RxVirtualViewPlaceholder, VirtualPlaceholder, VirtualContent, diff --git a/apps/docs/docs/template/api/virtual-view-directive.mdx b/apps/docs/docs/template/api/virtual-view-directive.mdx index dd9d3f242..90ac1a374 100644 --- a/apps/docs/docs/template/api/virtual-view-directive.mdx +++ b/apps/docs/docs/template/api/virtual-view-directive.mdx @@ -41,7 +41,7 @@ RxVirtualView is designed to work in combination with related directives: - `rxVirtualViewObserver`: Defines the node being used for the `IntersectionObserver`. Provides cache & other services. - `rxVirtualView`: Defines the DOM node being observed for visibility. -- `rxVirtualViewTemplate`: Defines the template shown when the observed node is visible. +- `rxVirtualViewContent`: Defines the content shown when the observed node is visible. - `rxVirtualViewPlaceholder`: (Optional) Defines the placeholder shown when the observed node isn't visible. ### Show a widget when it's visible, otherwise show a placeholder @@ -52,7 +52,7 @@ RxVirtualView is designed to work in combination with related directives:
- +
Placeholder @@ -65,13 +65,13 @@ RxVirtualView is designed to work in combination with related directives: This setup will: 1. Use rxVirtualViewObserver to monitor the visibility of the rxVirtualView element. -2. Render the content of rxVirtualViewTemplate when the element is visible. +2. Render the content of rxVirtualViewContent when the element is visible. 3. Show the rxVirtualViewPlaceholder when the element is not visible. :::tip Define placeholder dimensions The placeholder is what makes or breaks your experience with `RxVirtualView`. In best case it's just -an empty container which has just the same dimensions as its template it should replace. +an empty container which has just the same dimensions as its content it should replace. This will make sure you don't run into stuttery scrolling behavior and layout shifts. @@ -86,7 +86,7 @@ We are only rendering the `item` component when it's visible to the user. Otherw
@for (item of items; track item.id) {
- +
} @@ -105,41 +105,41 @@ We are only rendering the `item` component when it's visible to the user. Otherw ### RxVirtualView Inputs -| Input | Type | description | -| -------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `cacheEnabled` | `boolean` | Useful when we want to cache the templates and placeholders to optimize view rendering. | -| `startWithPlaceholderAsap` | `boolean` | Whether to start with the placeholder asap or not. If `true`, the placeholder will be rendered immediately, without waiting for the template to be visible. This is useful when you want to render the placeholder immediately, but you don't want to wait for the template to be visible. This is to counter concurrent rendering, and to avoid flickering. | -| `keepLastKnownSize` | `boolean` | This will keep the last known size of the host element while the template is visible. It sets 'minHeight' to the host node | -| `useContentVisibility` | `boolean` | It will add the `content-visibility` CSS class to the host element, together with `contain-intrinsic-width` and `contain-intrinsic-height` CSS properties. | -| `useContainment` | `boolean` | It will add `contain` css property with:
- `size layout paint`: if `useContentVisibility` is `true` && placeholder is visible
- `content`: if `useContentVisibility` is `false` or template is visible | -| `placeholderStrategy` | `boolean` | The strategy to use for rendering the placeholder.
Defaults to: `low`
[Read more about strategies](../../cdk/render-strategies/strategies/concurrent-strategies) | -| `templateStrategy` | `boolean` | The strategy to use for rendering the template.
Defaults to: `normal`
[Read more about strategies](../../cdk/render-strategies/strategies/concurrent-strategies) | +| Input | Type | description | +| -------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `cacheEnabled` | `boolean` | Useful when we want to cache the contents and placeholders to optimize view rendering. | +| `startWithPlaceholderAsap` | `boolean` | Whether to start with the placeholder asap or not. If `true`, the placeholder will be rendered immediately, without waiting for the content to be visible. This is useful when you want to render the placeholder immediately, but you don't want to wait for the content to be visible. This is to counter concurrent rendering, and to avoid flickering. | +| `keepLastKnownSize` | `boolean` | This will keep the last known size of the host element while the content is visible. It sets 'minHeight' to the host node | +| `useContentVisibility` | `boolean` | It will add the `content-visibility` CSS class to the host element, together with `contain-intrinsic-width` and `contain-intrinsic-height` CSS properties. | +| `useContainment` | `boolean` | It will add `contain` css property with:
- `size layout paint`: if `useContentVisibility` is `true` && placeholder is visible
- `content`: if `useContentVisibility` is `false` or content is visible | +| `placeholderStrategy` | `boolean` | The strategy to use for rendering the placeholder.
Defaults to: `low`
[Read more about strategies](../../cdk/render-strategies/strategies/concurrent-strategies) | +| `contentStrategy` | `boolean` | The strategy to use for rendering the content.
Defaults to: `normal`
[Read more about strategies](../../cdk/render-strategies/strategies/concurrent-strategies) | ### RxVirtualViewConfig Defines an interface representing all configuration that can be adjusted on provider level. ```typescript -interface RxVirtualViewConfig { +export interface RxVirtualViewConfig { keepLastKnownSize: boolean; useContentVisibility: boolean; useContainment: boolean; placeholderStrategy: RxStrategyNames; - templateStrategy: RxStrategyNames; + contentStrategy: RxStrategyNames; cacheEnabled: boolean; startWithPlaceholderAsap: boolean; cache: { /** - * The maximum number of templates that can be stored in the cache. + * The maximum number of contents that can be stored in the cache. * Defaults to 20. */ - maxTemplates: number; + contentCacheSize: number; /** * The maximum number of placeholders that can be stored in the cache. * Defaults to 20. */ - maxPlaceholders: number; + placeholderCacheSize: number; }; } ``` @@ -172,12 +172,12 @@ This is the default configuration which will be used when no other config was pr useContentVisibility: false, useContainment: true, placeholderStrategy: 'low', - templateStrategy: 'normal', + contentStrategy: 'normal', startWithPlaceholderAsap: false, cacheEnabled: true, cache: { - maxTemplates: 20, - maxPlaceholders: 20, + contentCacheSize: 20, + placeholderCacheSize: 20, }, }; diff --git a/libs/template/virtual-view/src/index.ts b/libs/template/virtual-view/src/index.ts index a05cea76c..11bfc12b9 100644 --- a/libs/template/virtual-view/src/index.ts +++ b/libs/template/virtual-view/src/index.ts @@ -1,4 +1,4 @@ export { RxVirtualView } from './lib/virtual-view.directive'; +export { RxVirtualViewContent } from './lib/virtual-view-content.directive'; export { RxVirtualViewObserver } from './lib/virtual-view-observer.directive'; export { RxVirtualViewPlaceholder } from './lib/virtual-view-placeholder.directive'; -export { RxVirtualViewTemplate } from './lib/virtual-view-template.directive'; diff --git a/libs/template/virtual-view/src/lib/model.ts b/libs/template/virtual-view/src/lib/model.ts index 2ae8f53fe..6f19c8fe2 100644 --- a/libs/template/virtual-view/src/lib/model.ts +++ b/libs/template/virtual-view/src/lib/model.ts @@ -4,7 +4,7 @@ import { Observable } from 'rxjs'; /** * @internal */ -export interface _RxVirtualViewTemplate { +export interface _RxVirtualViewContent { viewContainerRef: ViewContainerRef; templateRef: TemplateRef; } @@ -33,6 +33,6 @@ export abstract class _RxVirtualViewObserver { * @internal */ export abstract class _RxVirtualView { - abstract registerTemplate(template: _RxVirtualViewTemplate): void; + abstract registerContent(content: _RxVirtualViewContent): void; abstract registerPlaceholder(placeholder: _RxVirtualViewPlaceholder): void; } diff --git a/libs/template/virtual-view/src/lib/tests/virtual-view.directive.spec.ts b/libs/template/virtual-view/src/lib/tests/virtual-view.directive.spec.ts index ee6e9425a..17273c71e 100644 --- a/libs/template/virtual-view/src/lib/tests/virtual-view.directive.spec.ts +++ b/libs/template/virtual-view/src/lib/tests/virtual-view.directive.spec.ts @@ -5,15 +5,15 @@ import { RX_RENDER_STRATEGIES_CONFIG } from '@rx-angular/cdk/render-strategies'; import { tap } from 'rxjs'; import { provideVirtualViewConfig } from '../virtual-view.config'; import { RxVirtualView } from '../virtual-view.directive'; +import { RxVirtualViewContent } from '../virtual-view-content.directive'; import { RxVirtualViewObserver } from '../virtual-view-observer.directive'; import { RxVirtualViewPlaceholder } from '../virtual-view-placeholder.directive'; -import { RxVirtualViewTemplate } from '../virtual-view-template.directive'; @Component({ template: `
-
ze-template
+
ze-template
@if (withPlaceholder()) {
ze-placeholder @@ -27,7 +27,7 @@ import { RxVirtualViewTemplate } from '../virtual-view-template.directive'; RxVirtualViewObserver, RxVirtualView, RxVirtualViewPlaceholder, - RxVirtualViewTemplate, + RxVirtualViewContent, ], }) class VirtualViewTestComponent { @@ -98,7 +98,7 @@ describe('RxVirtualView', () => { }, provideVirtualViewConfig({ placeholderStrategy: 'sync', - templateStrategy: 'sync', + contentStrategy: 'sync', }), ], }); diff --git a/libs/template/virtual-view/src/lib/virtual-view-cache.ts b/libs/template/virtual-view/src/lib/virtual-view-cache.ts index c337d196a..3a20b6d51 100644 --- a/libs/template/virtual-view/src/lib/virtual-view-cache.ts +++ b/libs/template/virtual-view/src/lib/virtual-view-cache.ts @@ -11,14 +11,14 @@ import { VIRTUAL_VIEW_CONFIG_TOKEN } from './virtual-view.config'; export class VirtualViewCache implements OnDestroy { #config = inject(VIRTUAL_VIEW_CONFIG_TOKEN); - // Maximum number of templates that can be stored in the cache. - #maxTemplates = this.#config.cache.maxTemplates; + // Maximum number of content that can be stored in the cache. + #contentCacheSize = this.#config.cache.contentCacheSize; - // Cache for storing template views, identified by a unique key, which is the directive instance. - #templateCache = new Map(); + // Cache for storing content views, identified by a unique key, which is the directive instance. + #contentCache = new Map(); // Maximum number of placeholders that can be stored in the cache. - #maxPlaceholders = this.#config.cache.maxPlaceholders; + #placeholderCacheSize = this.#config.cache.placeholderCacheSize; // Cache for storing placeholder views, identified by a unique key. #placeholderCache = new Map(); @@ -31,11 +31,11 @@ export class VirtualViewCache implements OnDestroy { * @param view - The ViewRef of the placeholder to cache. */ storePlaceholder(key: unknown, view: ViewRef) { - if (this.#maxPlaceholders <= 0) { + if (this.#placeholderCacheSize <= 0) { view.destroy(); return; } - if (this.#placeholderCache.size >= this.#maxPlaceholders) { + if (this.#placeholderCache.size >= this.#placeholderCacheSize) { this.#removeOldestEntry(this.#placeholderCache); } this.#placeholderCache.set(key, view); @@ -54,43 +54,43 @@ export class VirtualViewCache implements OnDestroy { } /** - * Stores a template view in the cache. When the cache reaches its limit, + * Stores a content view in the cache. When the cache reaches its limit, * the oldest entry is removed. * - * @param key - The key used to identify the template in the cache. - * @param view - The ViewRef of the template to cache. + * @param key - The key used to identify the content in the cache. + * @param view - The ViewRef of the content to cache. */ - storeTemplate(key: unknown, view: ViewRef) { - if (this.#maxTemplates <= 0) { + storeContent(key: unknown, view: ViewRef) { + if (this.#contentCacheSize <= 0) { view.destroy(); return; } - if (this.#templateCache.size >= this.#maxTemplates) { - this.#removeOldestEntry(this.#templateCache); + if (this.#contentCache.size >= this.#contentCacheSize) { + this.#removeOldestEntry(this.#contentCache); } - this.#templateCache.set(key, view); + this.#contentCache.set(key, view); } /** - * Retrieves a cached template view using the specified key. + * Retrieves a cached content view using the specified key. * - * @param key - The key of the template to retrieve. - * @returns The ViewRef of the cached template, or undefined if not found. + * @param key - The key of the content to retrieve. + * @returns The ViewRef of the cached content, or undefined if not found. */ - getTemplate(key: unknown) { - const view = this.#templateCache.get(key); - this.#templateCache.delete(key); + getContent(key: unknown) { + const view = this.#contentCache.get(key); + this.#contentCache.delete(key); return view; } /** - * Clears both template and placeholder caches for a given key. + * Clears both content and placeholder caches for a given key. * - * @param key - The key of the template and placeholder to remove. + * @param key - The key of the content and placeholder to remove. */ clear(key: unknown) { - this.#templateCache.get(key)?.destroy(); - this.#templateCache.delete(key); + this.#contentCache.get(key)?.destroy(); + this.#contentCache.delete(key); this.#placeholderCache.get(key)?.destroy(); this.#placeholderCache.delete(key); } @@ -99,9 +99,9 @@ export class VirtualViewCache implements OnDestroy { * Clears all cached resources when the service is destroyed. */ ngOnDestroy() { - this.#templateCache.forEach((view) => view.destroy()); + this.#contentCache.forEach((view) => view.destroy()); this.#placeholderCache.forEach((view) => view.destroy()); - this.#templateCache.clear(); + this.#contentCache.clear(); this.#placeholderCache.clear(); } diff --git a/libs/template/virtual-view/src/lib/virtual-view-template.directive.ts b/libs/template/virtual-view/src/lib/virtual-view-content.directive.ts similarity index 59% rename from libs/template/virtual-view/src/lib/virtual-view-template.directive.ts rename to libs/template/virtual-view/src/lib/virtual-view-content.directive.ts index e1b6f11cb..088c8f59d 100644 --- a/libs/template/virtual-view/src/lib/virtual-view-template.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view-content.directive.ts @@ -4,13 +4,13 @@ import { TemplateRef, ViewContainerRef, } from '@angular/core'; -import { _RxVirtualViewTemplate } from './model'; +import { _RxVirtualViewContent } from './model'; import { RxVirtualView } from './virtual-view.directive'; /** - * The RxVirtualViewTemplate directive is a directive that allows you to create a template for the virtual view. + * The RxVirtualViewTemplate directive is a directive that allows you to create a content template for the virtual view. * - * It can be used on an element/component to create a template for the virtual view. + * It can be used on an element/component to create a content template for the virtual view. * * It needs to be a sibling of the `rxVirtualView` directive. * @@ -18,7 +18,7 @@ import { RxVirtualView } from './virtual-view.directive'; * ```html *
*
- *
Virtual View 1
+ *
Virtual View 1
*
Loading...
*
*
@@ -26,11 +26,11 @@ import { RxVirtualView } from './virtual-view.directive'; * * @developerPreview */ -@Directive({ selector: '[rxVirtualViewTemplate]', standalone: true }) -export class RxVirtualViewTemplate implements _RxVirtualViewTemplate { +@Directive({ selector: '[rxVirtualViewContent]', standalone: true }) +export class RxVirtualViewContent implements _RxVirtualViewContent { #virtualView = inject(RxVirtualView); viewContainerRef = inject(ViewContainerRef); constructor(public templateRef: TemplateRef) { - this.#virtualView.registerTemplate(this); + this.#virtualView.registerContent(this); } } diff --git a/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts b/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts index e3340d128..de77ec39c 100644 --- a/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view-observer.directive.ts @@ -29,7 +29,7 @@ import { VirtualViewCache } from './virtual-view-cache'; * ```html *
*
- *
Virtual View 1
+ *
Virtual View 1
*
Loading...
*
*
diff --git a/libs/template/virtual-view/src/lib/virtual-view-placeholder.directive.ts b/libs/template/virtual-view/src/lib/virtual-view-placeholder.directive.ts index 744f7283b..c6cefb367 100644 --- a/libs/template/virtual-view/src/lib/virtual-view-placeholder.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view-placeholder.directive.ts @@ -12,7 +12,7 @@ import { _RxVirtualView, _RxVirtualViewPlaceholder } from './model'; * ```html *
*
- *
Virtual View 1
+ *
Virtual View 1
*
Loading...
*
*
diff --git a/libs/template/virtual-view/src/lib/virtual-view.config.ts b/libs/template/virtual-view/src/lib/virtual-view.config.ts index bfcbdc25a..358b36b3f 100644 --- a/libs/template/virtual-view/src/lib/virtual-view.config.ts +++ b/libs/template/virtual-view/src/lib/virtual-view.config.ts @@ -12,21 +12,21 @@ export interface RxVirtualViewConfig { useContentVisibility: boolean; useContainment: boolean; placeholderStrategy: RxStrategyNames; - templateStrategy: RxStrategyNames; + contentStrategy: RxStrategyNames; cacheEnabled: boolean; startWithPlaceholderAsap: boolean; cache: { /** - * The maximum number of templates that can be stored in the cache. + * The maximum number of contents that can be stored in the cache. * Defaults to 20. */ - maxTemplates: number; + contentCacheSize: number; /** * The maximum number of placeholders that can be stored in the cache. * Defaults to 20. */ - maxPlaceholders: number; + placeholderCacheSize: number; }; } @@ -35,12 +35,12 @@ export const VIRTUAL_VIEW_CONFIG_DEFAULT: RxVirtualViewConfig = { useContentVisibility: false, useContainment: true, placeholderStrategy: 'low', - templateStrategy: 'normal', + contentStrategy: 'normal', startWithPlaceholderAsap: false, cacheEnabled: true, cache: { - maxTemplates: 20, - maxPlaceholders: 20, + contentCacheSize: 20, + placeholderCacheSize: 20, }, }; @@ -50,8 +50,8 @@ export const VIRTUAL_VIEW_CONFIG_DEFAULT: RxVirtualViewConfig = { * Can be used to customize the behavior of the `VirtualView` service. * * Default configuration: - * - maxTemplates: 20 - * - maxPlaceholders: 20 + * - contentCacheSize: 20 + * - placeholderCacheSize: 20 * * Example usage: * @@ -61,8 +61,8 @@ export const VIRTUAL_VIEW_CONFIG_DEFAULT: RxVirtualViewConfig = { * const appConfig: ApplicationConfig = { * providers: [ * provideVirtualViewConfig({ - * maxTemplates: 50, - * maxPlaceholders: 50, + * contentCacheSize: 50, + * placeholderCacheSize: 50, * }), * ], * }; diff --git a/libs/template/virtual-view/src/lib/virtual-view.directive.ts b/libs/template/virtual-view/src/lib/virtual-view.directive.ts index c33baa48b..3f8750b6b 100644 --- a/libs/template/virtual-view/src/lib/virtual-view.directive.ts +++ b/libs/template/virtual-view/src/lib/virtual-view.directive.ts @@ -26,9 +26,9 @@ import { } from 'rxjs/operators'; import { _RxVirtualView, + _RxVirtualViewContent, _RxVirtualViewObserver, _RxVirtualViewPlaceholder, - _RxVirtualViewTemplate, } from './model'; import { VIRTUAL_VIEW_CONFIG_TOKEN } from './virtual-view.config'; import { VirtualViewCache } from './virtual-view-cache'; @@ -39,7 +39,7 @@ import { VirtualViewCache } from './virtual-view-cache'; * It can be used on an element/component to create a virtual view. * * It works by using 3 directives: - * - `rxVirtualViewTemplate`: The template to render when the virtual view is visible. + * - `rxVirtualViewContent`: The content to render when the virtual view is visible. * - `rxVirtualViewPlaceholder`: The placeholder to render when the virtual view is not visible. * - `rxVirtualViewObserver`: The directive that observes the virtual view and emits a boolean value indicating whether the virtual view is visible. * @@ -50,7 +50,7 @@ import { VirtualViewCache } from './virtual-view-cache'; * ```html *
*
- *
Virtual View 1
+ *
Virtual View 1
*
Loading...
*
*
@@ -82,7 +82,7 @@ export class RxVirtualView readonly #destroyRef = inject(DestroyRef); readonly #config = inject(VIRTUAL_VIEW_CONFIG_TOKEN); - #template: _RxVirtualViewTemplate | null = null; + #content: _RxVirtualViewContent | null = null; #placeholder: _RxVirtualViewPlaceholder | null = null; /** @@ -97,8 +97,8 @@ export class RxVirtualView /** * Whether to start with the placeholder asap or not. * - * If `true`, the placeholder will be rendered immediately, without waiting for the template to be visible. - * This is useful when you want to render the placeholder immediately, but you don't want to wait for the template to be visible. + * If `true`, the placeholder will be rendered immediately, without waiting for the content to be visible. + * This is useful when you want to render the placeholder immediately, but you don't want to wait for the content to be visible. * * This is to counter concurrent rendering, and to avoid flickering. */ @@ -110,7 +110,7 @@ export class RxVirtualView ); /** - * This will keep the last known size of the host element while the template is visible. + * This will keep the last known size of the host element while the content is visible. */ readonly keepLastKnownSize = input(this.#config.keepLastKnownSize, { transform: booleanAttribute, @@ -131,7 +131,7 @@ export class RxVirtualView * * It will add `contain` css property with: * - `size layout paint`: if `useContentVisibility` is `true` && placeholder is visible - * - `content`: if `useContentVisibility` is `false` || template is visible + * - `content`: if `useContentVisibility` is `false` || content is visible */ readonly useContainment = input(this.#config.useContainment, { transform: booleanAttribute, @@ -145,10 +145,10 @@ export class RxVirtualView ); /** - * The strategy to use for rendering the template. + * The strategy to use for rendering the content. */ - readonly templateStrategy = input>( - this.#config.templateStrategy, + readonly contentStrategy = input>( + this.#config.contentStrategy, ); /** @@ -166,7 +166,7 @@ export class RxVirtualView readonly #placeholderVisible = signal(false); - #templateIsShown = false; + #contentIsShown = false; readonly #visible$ = new ReplaySubject(1); @@ -222,9 +222,9 @@ export class RxVirtualView } ngAfterContentInit() { - if (!this.#template) { + if (!this.#content) { throw new Error( - 'RxVirtualView expects you to provide a RxVirtualViewTemplate', + 'RxVirtualView expects you to provide a RxVirtualViewContent', ); } if (this.startWithPlaceholderAsap()) { @@ -241,9 +241,9 @@ export class RxVirtualView distinctUntilChanged(), switchMap((visible) => { if (visible) { - return this.#templateIsShown + return this.#contentIsShown ? NEVER - : this.showTemplate$().pipe( + : this.showContent$().pipe( switchMap((view) => { const resize$ = this.#observer!.observeElementSize( this.#elementRef.nativeElement, @@ -267,12 +267,12 @@ export class RxVirtualView } ngOnDestroy() { - this.#template = null; + this.#content = null; this.#placeholder = null; } - registerTemplate(template: _RxVirtualViewTemplate) { - this.#template = template; + registerContent(content: _RxVirtualViewContent) { + this.#content = content; } registerPlaceholder(placeholder: _RxVirtualViewPlaceholder) { @@ -280,29 +280,29 @@ export class RxVirtualView } /** - * Shows the template using the configured rendering strategy (by default: normal). + * Shows the content using the configured rendering strategy (by default: normal). * @private */ - private showTemplate$(): Observable> { + private showContent$(): Observable> { return this.#strategyProvider.schedule( () => { - this.#templateIsShown = true; + this.#contentIsShown = true; this.#placeholderVisible.set(false); - const placeHolder = this.#template!.viewContainerRef.detach(); + const placeHolder = this.#content!.viewContainerRef.detach(); if (this.cacheEnabled() && placeHolder) { this.#viewCache!.storePlaceholder(this, placeHolder); } else if (!this.cacheEnabled() && placeHolder) { placeHolder.destroy(); } const tmpl = - (this.#viewCache!.getTemplate(this) as EmbeddedViewRef) ?? - this.#template!.templateRef.createEmbeddedView({}); - this.#template!.viewContainerRef.insert(tmpl); + (this.#viewCache!.getContent(this) as EmbeddedViewRef) ?? + this.#content!.templateRef.createEmbeddedView({}); + this.#content!.viewContainerRef.insert(tmpl); placeHolder?.detectChanges(); return tmpl; }, - { scope: this, strategy: this.templateStrategy() }, + { scope: this, strategy: this.contentStrategy() }, ); } @@ -318,9 +318,9 @@ export class RxVirtualView } /** - * Renders a placeholder within the view container, and hides the template. + * Renders a placeholder within the view container, and hides the content. * - * If we already have a template and cache enabled, we store the template in + * If we already have a content and cache enabled, we store the content in * the cache, so we can reuse it later. * * When we want to render the placeholder, we try to get it from the cache, @@ -330,18 +330,18 @@ export class RxVirtualView */ private renderPlaceholder() { this.#placeholderVisible.set(true); - this.#templateIsShown = false; + this.#contentIsShown = false; - const template = this.#template!.viewContainerRef.detach(); + const content = this.#content!.viewContainerRef.detach(); - if (template) { + if (content) { if (this.cacheEnabled()) { - this.#viewCache!.storeTemplate(this, template); + this.#viewCache!.storeContent(this, content); } else { - template.destroy(); + content.destroy(); } - template?.detectChanges(); + content?.detectChanges(); } if (this.#placeholder) { @@ -349,7 +349,7 @@ export class RxVirtualView this.#viewCache!.getPlaceholder(this) ?? this.#placeholder.templateRef.createEmbeddedView({}); - this.#template!.viewContainerRef.insert(placeholderRef); + this.#content!.viewContainerRef.insert(placeholderRef); placeholderRef.detectChanges(); } } From 4fa670ebe1abf528ad2e64fc6e721a4baf524acc Mon Sep 17 00:00:00 2001 From: Enea Jahollari Date: Mon, 23 Dec 2024 21:57:18 +0100 Subject: [PATCH 42/46] chore: add another demo for virtual views --- .../virtual-view-cool-demo.component.ts | 458 ++++++++++++++++++ .../virtual-view/virtual-view.routes.ts | 7 + 2 files changed, 465 insertions(+) create mode 100644 apps/demos/src/app/features/template/virtual-view/virtual-view-cool-demo.component.ts diff --git a/apps/demos/src/app/features/template/virtual-view/virtual-view-cool-demo.component.ts b/apps/demos/src/app/features/template/virtual-view/virtual-view-cool-demo.component.ts new file mode 100644 index 000000000..37b8b60ea --- /dev/null +++ b/apps/demos/src/app/features/template/virtual-view/virtual-view-cool-demo.component.ts @@ -0,0 +1,458 @@ +import { + ChangeDetectionStrategy, + Component, + ViewEncapsulation, +} from '@angular/core'; +import { + RxVirtualView, + RxVirtualViewContent, + RxVirtualViewObserver, + RxVirtualViewPlaceholder, +} from '@rx-angular/template/virtual-view'; + +@Component({ + selector: 'virtual-view-cool-demo', + template: ` +
+

Movies

+
+ @for (movie of movies; track movie.id) { +
+
+ {{ movie.title }} +
+

{{ movie.title }}

+
+ {{ movie.release_date }} +
+

{{ movie.overview.slice(0, 100) }}...

+
+
+
+ {{ movie.title }} +
+
+ } +
+
+ `, + styles: [ + ` + .container { + padding: 20px; + height: 100%; + max-height: 100%; + overflow-y: scroll; + } + + .item-wrapper { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + grid-gap: 20px; + } + + .movie-card { + background-color: #242333; + border-radius: 10px; + overflow: hidden; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + transition: transform 0.2s ease; + color: white; + height: 680px; + } + + .movie-card img { + width: 100%; + height: 505px; + object-fit: cover; + } + + .card-details { + padding: 15px; + } + + .movie-title { + margin: 0; + font-size: 1.2rem; + margin-bottom: 10px; + } + + .movie-info { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + font-size: 0.9rem; + } + + .overview { + font-size: 0.9rem; + line-height: 1.4; + } + + .placeholder { + background-color: #242333; + height: 680px; + } + `, + ], + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ + RxVirtualViewObserver, + RxVirtualView, + RxVirtualViewContent, + RxVirtualViewPlaceholder, + ], +}) +export class VirtualViewCoolDemoComponent { + movies = movies; +} + +const movies = [ + { + adult: false, + backdrop_path: '/bQXAqRx2Fgc46uCVWgoPz5L5Dtr.jpg', + genre_ids: [28, 14, 878], + id: 436270, + original_language: 'en', + original_title: 'Black Adam', + overview: + 'Nearly 5,000 years after he was bestowed with the almighty powers of the Egyptian gods—and imprisoned just as quickly—Black Adam is freed from his earthly tomb, ready to unleash his unique form of justice on the modern world.', + popularity: 6579.615, + poster_path: '/pFlaoHTZeyNkG83vxsAJiGzfSsa.jpg', + release_date: '2022-10-19', + title: 'Black Adam', + video: false, + vote_average: 7.3, + vote_count: 2508, + }, + { + adult: false, + backdrop_path: '/7zQJYV02yehWrQN6NjKsBorqUUS.jpg', + genre_ids: [28, 18, 36], + id: 724495, + original_language: 'en', + original_title: 'The Woman King', + overview: + 'The story of the Agojie, the all-female unit of warriors who protected the African Kingdom of Dahomey in the 1800s with skills and a fierceness unlike anything the world has ever seen, and General Nanisca as she trains the next generation of recruits and readies them for battle against an enemy determined to destroy their way of life.', + popularity: 3881.892, + poster_path: '/438QXt1E3WJWb3PqNniK0tAE5c1.jpg', + release_date: '2022-09-15', + title: 'The Woman King', + video: false, + vote_average: 7.9, + vote_count: 615, + }, + { + adult: false, + backdrop_path: '/au4HUSWDRadIcl9CqySlw1kJMfo.jpg', + genre_ids: [80, 28, 53], + id: 829799, + original_language: 'en', + original_title: 'Paradise City', + overview: + 'Renegade bounty hunter Ryan Swan must carve his way through the Hawaiian crime world to wreak vengeance on the kingpin who murdered his father.', + popularity: 1796.896, + poster_path: '/xdmmd437QdjcCls8yCQxrH5YYM4.jpg', + release_date: '2022-11-11', + title: 'Paradise City', + video: false, + vote_average: 6.3, + vote_count: 40, + }, + { + adult: false, + backdrop_path: '/sUuzl04qNIYsnwCLQpZ2RSvXA1V.jpg', + genre_ids: [35, 28, 53], + id: 792775, + original_language: 'is', + original_title: 'Leynilögga', + overview: + "When Bússi, Iceland's toughest cop, is forced to work with a new partner to solve a series of bank robberies, the pressure to close the case as soon as possible proves too much for him.", + popularity: 1405.479, + poster_path: '/jnWyZsaCl3Ke6u6ReSmBRO8S1rX.jpg', + release_date: '2022-05-23', + title: 'Cop Secret', + video: false, + vote_average: 6.3, + vote_count: 33, + }, + { + adult: false, + backdrop_path: '/kmzppWh7ljL6K9fXW72bPN3gKwu.jpg', + genre_ids: [14, 28, 35, 80], + id: 1013860, + original_language: 'en', + original_title: 'R.I.P.D. 2: Rise of the Damned', + overview: + 'When Sheriff Roy Pulsipher finds himself in the afterlife, he joins a special police force and returns to Earth to save humanity from the undead.', + popularity: 2530.599, + poster_path: '/g4yJTzMtOBUTAR2Qnmj8TYIcFVq.jpg', + release_date: '2022-11-15', + title: 'R.I.P.D. 2: Rise of the Damned', + video: false, + vote_average: 6.7, + vote_count: 207, + }, + { + adult: false, + backdrop_path: '/707thQazLJiYLBhCrZlRoV05NNL.jpg', + genre_ids: [28, 18, 53], + id: 948276, + original_language: 'fr', + original_title: 'Balle perdue 2', + overview: + 'Having cleared his name, genius mechanic Lino has only one goal in mind: getting revenge on the corrupt cops who killed his brother and his mentor.', + popularity: 1277.701, + poster_path: '/uAeZI1JJbLPq7Bu5dziH7emHeu7.jpg', + release_date: '2022-11-10', + title: 'Lost Bullet 2', + video: false, + vote_average: 6.6, + vote_count: 148, + }, + { + adult: false, + backdrop_path: '/90ZZIoWQLLEXSVm0ik3eEQBinul.jpg', + genre_ids: [28, 27, 53], + id: 988233, + original_language: 'en', + original_title: 'Hex', + overview: + 'Following a mysterious disappearance on a jump, a group of skydivers experience paranormal occurrences that leave them fighting for their lives.', + popularity: 1977.125, + poster_path: '/xFJHb43ZAnnuiDztxZYsmyopweb.jpg', + release_date: '2022-11-01', + title: 'Hex', + video: false, + vote_average: 5.1, + vote_count: 13, + }, + { + adult: false, + backdrop_path: '/jCY35GkjwWUmoPO9EV1lWL6kuyj.jpg', + genre_ids: [28, 12, 53], + id: 855440, + original_language: 'es', + original_title: 'Polar', + overview: + 'MG, a policewoman who has been expelled from the Corps due to the problems with alcohol and drugs that she has had since the loss of her son, receives a call from a man asking her to look for Macarena Gómez, a popular TV actress.', + popularity: 1881.197, + poster_path: '/efuKHH9LqBZB67AS87kprLgaYO8.jpg', + release_date: '2022-10-26', + title: 'Polar', + video: false, + vote_average: 7.5, + vote_count: 2, + }, + { + adult: false, + backdrop_path: '/vmDa8HijINCAFYKqsMz0YM3sVyE.jpg', + genre_ids: [80, 28, 53], + id: 747803, + original_language: 'en', + original_title: 'One Way', + overview: + 'On the run with a bag full of cash after a robbing his former crime boss—and a potentially fatal wound—Freddy slips onto a bus headed into the unrelenting California desert. With his life slipping through his fingers, Freddy is left with very few choices to survive.', + popularity: 1875.044, + poster_path: '/uQCxOziq79P3wDsRwQhhkhQyDsJ.jpg', + release_date: '2022-09-02', + title: 'One Way', + video: false, + vote_average: 6.5, + vote_count: 22, + }, + { + adult: false, + backdrop_path: '/8Tr79lfoCkOYRg8SYwWit4OoQLi.jpg', + genre_ids: [878, 28], + id: 872177, + original_language: 'en', + original_title: 'Corrective Measures', + overview: + "Set in San Tiburon, the world's most dangerous maximum-security penitentiary and home to the world's most treacherous superpowered criminals, where tensions among the inmates and staff heighten, leading to anarchy that engulfs the prison and order is turned upside down.", + popularity: 1196.661, + poster_path: '/aHFq9NMhavOL0jtQvmHQ1c5e0ya.jpg', + release_date: '2022-04-29', + title: 'Corrective Measures', + video: false, + vote_average: 5.1, + vote_count: 35, + }, + { + adult: false, + backdrop_path: '/sP1ShE4BGLkHSRqG9ZeGHg6C76t.jpg', + genre_ids: [53, 80], + id: 934641, + original_language: 'en', + original_title: 'The Minute You Wake Up Dead', + overview: + 'A stockbroker in a small southern town gets embroiled in an insurance scam with a next-door neighbor that leads to multiple murders when a host of other people want in on the plot. Sheriff Thurmond Fowler, the by-the-book town sheriff for over four decades, works earnestly to try and unravel the town’s mystery and winds up getting more than he bargained for.', + popularity: 1785.183, + poster_path: '/pUPwTbnAqfm95BZjNBnMMf39ChT.jpg', + release_date: '2022-11-04', + title: 'The Minute You Wake Up Dead', + video: false, + vote_average: 4.9, + vote_count: 21, + }, + { + adult: false, + backdrop_path: '/rfnmMYuZ6EKOBvQLp2wqP21v7sI.jpg', + genre_ids: [35, 878, 12], + id: 774752, + original_language: 'en', + original_title: 'The Guardians of the Galaxy Holiday Special', + overview: + 'On a mission to make Christmas unforgettable for Quill, the Guardians head to Earth in search of the perfect present.', + popularity: 1329.347, + poster_path: '/8dqXyslZ2hv49Oiob9UjlGSHSTR.jpg', + release_date: '2022-11-25', + title: 'The Guardians of the Galaxy Holiday Special', + video: false, + vote_average: 7.5, + vote_count: 607, + }, + { + adult: false, + backdrop_path: '/xDMIl84Qo5Tsu62c9DGWhmPI67A.jpg', + genre_ids: [28, 12, 878], + id: 505642, + original_language: 'en', + original_title: 'Black Panther: Wakanda Forever', + overview: + 'Queen Ramonda, Shuri, M’Baku, Okoye and the Dora Milaje fight to protect their nation from intervening world powers in the wake of King T’Challa’s death. As the Wakandans strive to embrace their next chapter, the heroes must band together with the help of War Dog Nakia and Everett Ross and forge a new path for the kingdom of Wakanda.', + popularity: 1798.687, + poster_path: '/ps2oKfhY6DL3alynlSqY97gHSsg.jpg', + release_date: '2022-11-09', + title: 'Black Panther: Wakanda Forever', + video: false, + vote_average: 7.5, + vote_count: 1213, + }, + { + adult: false, + backdrop_path: '/c1bz69r0v065TGFA5nqBiKzPDys.jpg', + genre_ids: [35, 10751, 10402], + id: 830784, + original_language: 'en', + original_title: 'Lyle, Lyle, Crocodile', + overview: + 'When the Primm family moves to New York City, their young son Josh struggles to adapt to his new school and new friends. All of that changes when he discovers Lyle — a singing crocodile who loves baths, caviar and great music — living in the attic of his new home. But when Lyle’s existence is threatened by evil neighbor Mr. Grumps, the Primms must band together to show the world that family can come from the most unexpected places.', + popularity: 1131.919, + poster_path: '/irIS5Tn3TXjNi1R9BpWvGAN4CZ1.jpg', + release_date: '2022-10-07', + title: 'Lyle, Lyle, Crocodile', + video: false, + vote_average: 7.8, + vote_count: 137, + }, + { + adult: false, + backdrop_path: '/kpUre8wWSXn3D5RhrMttBZa6w1v.jpg', + genre_ids: [35, 10751, 14], + id: 338958, + original_language: 'en', + original_title: 'Disenchanted', + overview: + 'Disillusioned with life in the city, feeling out of place in suburbia, and frustrated that her happily ever after hasn’t been so easy to find, Giselle turns to the magic of Andalasia for help. Accidentally transforming the entire town into a real-life fairy tale and placing her family’s future happiness in jeopardy, she must race against time to reverse the spell and determine what happily ever after truly means to her and her family.', + popularity: 1120.736, + poster_path: '/4x3pt6hoLblBeHebUa4OyiVXFiM.jpg', + release_date: '2022-11-16', + title: 'Disenchanted', + video: false, + vote_average: 7.3, + vote_count: 492, + }, + { + adult: false, + backdrop_path: '/olPXihyFeeNvnaD6IOBltgIV1FU.jpg', + genre_ids: [27, 9648, 53], + id: 882598, + original_language: 'en', + original_title: 'Smile', + overview: + "After witnessing a bizarre, traumatic incident involving a patient, Dr. Rose Cotter starts experiencing frightening occurrences that she can't explain. As an overwhelming terror begins taking over her life, Rose must confront her troubling past in order to survive and escape her horrifying new reality.", + popularity: 1120.904, + poster_path: '/aPqcQwu4VGEewPhagWNncDbJ9Xp.jpg', + release_date: '2022-09-23', + title: 'Smile', + video: false, + vote_average: 6.8, + vote_count: 1043, + }, + { + adult: false, + backdrop_path: '/5aSvzECXrtABcIh7fZYkH2K6ttC.jpg', + genre_ids: [28, 53, 80], + id: 972313, + original_language: 'en', + original_title: 'Blowback', + overview: + "When a master thief is sabotaged during a bank heist and left for dead, he seeks revenge on his former crew one target at a time. Now, with the cops and the mob closing in, he's in the race of his life to reclaim an untold fortune in cryptocurrency from those who double-crossed him.", + popularity: 1324.392, + poster_path: '/fHQHC32dhom8u0OxC2hs2gYQh0M.jpg', + release_date: '2022-06-17', + title: 'Blowback', + video: false, + vote_average: 6, + vote_count: 21, + }, + { + adult: false, + backdrop_path: null, + genre_ids: [10749], + id: 485470, + original_language: 'ko', + original_title: '착한 형수2', + overview: + "If you give it once, a good brother-in-law who gives everything generously will come! At the house of her girlfriend Jin-kyung, who lives with pumice stone, her brother and his wife suddenly visit and the four of them live together. At first, Kyung-seok, who was burdened by his girlfriend's brother, began to keep his eyes on his wife, Yeon-su. A bold brother-in-law who walks around in no-bra and panties without hesitation even at his sister-in-law's house. Besides, from a certain moment, he starts to send a hand of temptation to Pyeong-seok first...", + popularity: 545.569, + poster_path: '/3pEs4hmeHvTAsmx09whEaPDOQpq.jpg', + release_date: '2017-10-08', + title: 'Nice Sister-In-Law 2', + video: false, + vote_average: 6, + vote_count: 2, + }, + { + adult: false, + backdrop_path: '/eyiSLRh44SKKWIJ6bxWq8z1sscB.jpg', + genre_ids: [53, 27, 80], + id: 899294, + original_language: 'en', + original_title: 'Frank and Penelope', + overview: + 'A tale of love and violence when a man on his emotional last legs finds a savior seductively dancing in a run-down strip club. And a life most certainly headed off a cliff suddenly becomes redirected - as everything is now worth dying for.', + popularity: 879.196, + poster_path: '/5NpXoAi3nEQkEgLO09nmotPfyNa.jpg', + release_date: '2022-06-03', + title: 'Frank and Penelope', + video: false, + vote_average: 7.8, + vote_count: 44, + }, + { + adult: false, + backdrop_path: '/yNib9QAiyaopUJbaayKQ2xK7mYf.jpg', + genre_ids: [18, 28, 10752], + id: 966220, + original_language: 'uk', + original_title: 'Снайпер. Білий ворон', + overview: + 'Mykola is an eccentric pacifist who wants to be useful to humanity. When the war begins at Donbass, Mykola’s naive world is collapsing as the militants kill his pregnant wife and burn his home to the ground. Recovered, he makes a cardinal decision and gets enlisted in a sniper company. Having met his wife’s killers, he emotionally breaks down and arranges “sniper terror” for the enemy. He’s saved from a senseless death by his instructor who himself gets mortally wounded. The death of a friend leaves a “scar” and Mykola is ready to sacrifice his life.', + popularity: 960.86, + poster_path: '/lZOODJzwuQo0etJJyBBZJOSdZcW.jpg', + release_date: '2022-05-03', + title: 'Sniper: The White Raven', + video: false, + vote_average: 7.7, + vote_count: 146, + }, +]; diff --git a/apps/demos/src/app/features/template/virtual-view/virtual-view.routes.ts b/apps/demos/src/app/features/template/virtual-view/virtual-view.routes.ts index c0453c32f..07b2aa75b 100644 --- a/apps/demos/src/app/features/template/virtual-view/virtual-view.routes.ts +++ b/apps/demos/src/app/features/template/virtual-view/virtual-view.routes.ts @@ -13,4 +13,11 @@ export const VIRTUAL_VIEW_ROUTES: Routes = [ (m) => m.VirtualViewDemoComponent, ), }, + { + path: 'cool-example', + loadComponent: () => + import('./virtual-view-cool-demo.component').then( + (m) => m.VirtualViewCoolDemoComponent, + ), + }, ]; From 46388b4f31fb2ff3b664dd11af39aef30e7e3663 Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Mon, 23 Dec 2024 23:44:01 +0100 Subject: [PATCH 43/46] release(state): 19.0.1 --- libs/state/CHANGELOG.md | 9 +++++++++ libs/state/package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/libs/state/CHANGELOG.md b/libs/state/CHANGELOG.md index ba823edf5..9f2516823 100644 --- a/libs/state/CHANGELOG.md +++ b/libs/state/CHANGELOG.md @@ -2,6 +2,15 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +## [19.0.1](https://github.com/rx-angular/rx-angular/compare/state@19.0.0...state@19.0.1) (2024-12-23) + + +### Bug Fixes + +* replace toObservableMicrotask private API with proper solution ([339b2e3](https://github.com/rx-angular/rx-angular/commit/339b2e3e69e2ed49d368f33c45fa0bdaac8820f4)) + + + # [19.0.0](https://github.com/rx-angular/rx-angular/compare/state@18.1.0...state@19.0.0) (2024-12-05) diff --git a/libs/state/package.json b/libs/state/package.json index 38fcf39ea..a292fe8ad 100644 --- a/libs/state/package.json +++ b/libs/state/package.json @@ -1,6 +1,6 @@ { "name": "@rx-angular/state", - "version": "19.0.0", + "version": "19.0.1", "description": "@rx-angular/state is a light-weight, flexible, strongly typed and tested tool dedicated to reduce the complexity of managing component state and side effects in angular", "publishConfig": { "access": "public" From 6d6fabed2a650058fd075a7deefea07954b4de6d Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Mon, 23 Dec 2024 23:48:43 +0100 Subject: [PATCH 44/46] release(cdk): 19.0.1 --- libs/cdk/CHANGELOG.md | 9 +++++++++ libs/cdk/package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/libs/cdk/CHANGELOG.md b/libs/cdk/CHANGELOG.md index c5a301eee..bbd03c3f5 100644 --- a/libs/cdk/CHANGELOG.md +++ b/libs/cdk/CHANGELOG.md @@ -2,6 +2,15 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +## [19.0.1](https://github.com/rx-angular/rx-angular/compare/cdk@19.0.0...cdk@19.0.1) (2024-12-23) + + +### Bug Fixes + +* replace toObservableMicrotask private API with proper solution ([339b2e3](https://github.com/rx-angular/rx-angular/commit/339b2e3e69e2ed49d368f33c45fa0bdaac8820f4)) + + + # [19.0.0](https://github.com/rx-angular/rx-angular/compare/cdk@18.0.0...cdk@19.0.0) (2024-12-05) diff --git a/libs/cdk/package.json b/libs/cdk/package.json index c8c33f973..11171abbb 100644 --- a/libs/cdk/package.json +++ b/libs/cdk/package.json @@ -1,6 +1,6 @@ { "name": "@rx-angular/cdk", - "version": "19.0.0", + "version": "19.0.1", "description": "@rx-angular/cdk is a Component Development Kit for ergonomic and highly performant angular applications. It helps to to build Large scale applications, UI libs, state management, rendering systems and much more. Furthermore the unique way of mixing reactive as well as imperative code leads to best DX and speed.", "publishConfig": { "access": "public" From 58cd4b02384dce345e82c49bbbfaea08cdcf9735 Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Mon, 23 Dec 2024 23:52:38 +0100 Subject: [PATCH 45/46] release(template): 19.1.0 --- libs/template/CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ libs/template/package.json | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/libs/template/CHANGELOG.md b/libs/template/CHANGELOG.md index 348b075ca..d1ff19cc7 100644 --- a/libs/template/CHANGELOG.md +++ b/libs/template/CHANGELOG.md @@ -2,6 +2,36 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +# [19.1.0](https://github.com/rx-angular/rx-angular/compare/template@19.0.0...template@19.1.0) (2024-12-23) + + +### Bug Fixes + +* replace toObservableMicrotask private API with proper solution ([339b2e3](https://github.com/rx-angular/rx-angular/commit/339b2e3e69e2ed49d368f33c45fa0bdaac8820f4)) +* **template:** destroy cached views ([ce61bd9](https://github.com/rx-angular/rx-angular/commit/ce61bd9fd01e8856471b6f4147a648741f87ab14)) +* **template:** fix API usage ([7c21631](https://github.com/rx-angular/rx-angular/commit/7c21631ddde6b53d92734696c5c1bb20c68ba8f0)) +* **template:** fix memory leaks in observers ([c15f3fd](https://github.com/rx-angular/rx-angular/commit/c15f3fdde25a39ea9ed94e51fcea099f4db09169)) +* **template:** properly handle subscriptions & cleanup ([8accd30](https://github.com/rx-angular/rx-angular/commit/8accd30f36236ee1032b46ae69ceec73e1f2d279)) +* **template:** rxjs 6 compat ([061b4fa](https://github.com/rx-angular/rx-angular/commit/061b4fa453eff5748fe58aebde1f9876f3575a8b)) + + +### Features + +* **template:** add developer preview jsdoc ([4bc010f](https://github.com/rx-angular/rx-angular/commit/4bc010fd058f38b7b95156c9c3decb390bcfbc35)) +* **template:** add jsdocs and token-based configuration ([a8460cf](https://github.com/rx-angular/rx-angular/commit/a8460cf1c08852c7c619c7ba56b3b1f546b6e14a)) +* **template:** add more docs for directives ([441a2e7](https://github.com/rx-angular/rx-angular/commit/441a2e7865c85013f17f2f40ef982cf7a030bce0)) +* **template:** introduce rx-virtual-view ([ca4c7d0](https://github.com/rx-angular/rx-angular/commit/ca4c7d0153a9419c76b585cd1e923fdbf9629655)) +* **template:** make virtual-view use config defaults ([caf5cf4](https://github.com/rx-angular/rx-angular/commit/caf5cf4df559d954d442f347b83e7173881b4396)) +* **template:** rename template to content ([70f7426](https://github.com/rx-angular/rx-angular/commit/70f7426c693a8353002aec8bd9991996cf9d005c)) +* **template:** slightly better types ([98a447e](https://github.com/rx-angular/rx-angular/commit/98a447e0bd427ac062e5a9a8f2f6fc0d148d8f45)) + + +### Performance Improvements + +* **template:** fix memory leaks in view cache ([4c496b4](https://github.com/rx-angular/rx-angular/commit/4c496b447056f97ff02cbffbf472274671d8cbdb)) + + + # [19.0.0](https://github.com/rx-angular/rx-angular/compare/template@18.0.3...template@19.0.0) (2024-12-05) diff --git a/libs/template/package.json b/libs/template/package.json index 705274c66..3fa4c2c31 100644 --- a/libs/template/package.json +++ b/libs/template/package.json @@ -1,6 +1,6 @@ { "name": "@rx-angular/template", - "version": "19.0.0", + "version": "19.1.0", "description": "**Fully** Reactive Component Template Rendering in Angular. @rx-angular/template aims to be a reflection of Angular's built in renderings just reactive.", "publishConfig": { "access": "public" From d0096fca680eb38a5895a9808508d46fa62228b6 Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Tue, 24 Dec 2024 00:02:28 +0100 Subject: [PATCH 46/46] chore: fix template changelog --- libs/template/CHANGELOG.md | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/libs/template/CHANGELOG.md b/libs/template/CHANGELOG.md index d1ff19cc7..13065c508 100644 --- a/libs/template/CHANGELOG.md +++ b/libs/template/CHANGELOG.md @@ -8,28 +8,11 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/s ### Bug Fixes * replace toObservableMicrotask private API with proper solution ([339b2e3](https://github.com/rx-angular/rx-angular/commit/339b2e3e69e2ed49d368f33c45fa0bdaac8820f4)) -* **template:** destroy cached views ([ce61bd9](https://github.com/rx-angular/rx-angular/commit/ce61bd9fd01e8856471b6f4147a648741f87ab14)) -* **template:** fix API usage ([7c21631](https://github.com/rx-angular/rx-angular/commit/7c21631ddde6b53d92734696c5c1bb20c68ba8f0)) -* **template:** fix memory leaks in observers ([c15f3fd](https://github.com/rx-angular/rx-angular/commit/c15f3fdde25a39ea9ed94e51fcea099f4db09169)) -* **template:** properly handle subscriptions & cleanup ([8accd30](https://github.com/rx-angular/rx-angular/commit/8accd30f36236ee1032b46ae69ceec73e1f2d279)) -* **template:** rxjs 6 compat ([061b4fa](https://github.com/rx-angular/rx-angular/commit/061b4fa453eff5748fe58aebde1f9876f3575a8b)) ### Features -* **template:** add developer preview jsdoc ([4bc010f](https://github.com/rx-angular/rx-angular/commit/4bc010fd058f38b7b95156c9c3decb390bcfbc35)) -* **template:** add jsdocs and token-based configuration ([a8460cf](https://github.com/rx-angular/rx-angular/commit/a8460cf1c08852c7c619c7ba56b3b1f546b6e14a)) -* **template:** add more docs for directives ([441a2e7](https://github.com/rx-angular/rx-angular/commit/441a2e7865c85013f17f2f40ef982cf7a030bce0)) -* **template:** introduce rx-virtual-view ([ca4c7d0](https://github.com/rx-angular/rx-angular/commit/ca4c7d0153a9419c76b585cd1e923fdbf9629655)) -* **template:** make virtual-view use config defaults ([caf5cf4](https://github.com/rx-angular/rx-angular/commit/caf5cf4df559d954d442f347b83e7173881b4396)) -* **template:** rename template to content ([70f7426](https://github.com/rx-angular/rx-angular/commit/70f7426c693a8353002aec8bd9991996cf9d005c)) -* **template:** slightly better types ([98a447e](https://github.com/rx-angular/rx-angular/commit/98a447e0bd427ac062e5a9a8f2f6fc0d148d8f45)) - - -### Performance Improvements - -* **template:** fix memory leaks in view cache ([4c496b4](https://github.com/rx-angular/rx-angular/commit/4c496b447056f97ff02cbffbf472274671d8cbdb)) - +* **template:** introduce virtual-view subpackage ([0bfa4fe9](https://github.com/rx-angular/rx-angular/commit/0bfa4fe9b2e395d0df7a534f8277e37134f2d5ff)) # [19.0.0](https://github.com/rx-angular/rx-angular/compare/template@18.0.3...template@19.0.0) (2024-12-05)