diff --git a/README.md b/README.md index 0fe257b..e55cfe8 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ Todo MVC demo using [RxAngular](https://rx-angular.github.io/rx-angular/#/). +- Fully Zoneless for lightning-fast performance - Push-based Architecture - Standalone Components -- Fully Zoneless for lightning-fast performance - Angular v17 with Control-Flow Syntax -- Consumes an API via HTTP (to showcase a real-world app) +- Consumes an API via HTTP (uses `@tanstack/angular-query` under the hood) ![demo](./app.png) diff --git a/package.json b/package.json index d03cef0..dc051d3 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@rx-angular/cdk": "17.0.0", "@rx-angular/state": "17.0.0", "@rx-angular/template": "17.0.0", + "@tanstack/angular-query-experimental": "^5.17.0", "body-parser": "^1.20.1", "cors": "^2.8.5", "rxjs": "~7.4.0", @@ -45,4 +46,4 @@ "ts-node": "~8.3.0", "typescript": "~5.2.2" } -} \ No newline at end of file +} diff --git a/projects/todo-mvc/src/app/todo-list.service.ts b/projects/todo-mvc/src/app/todo-list.service.ts index 3b90e69..054885e 100644 --- a/projects/todo-mvc/src/app/todo-list.service.ts +++ b/projects/todo-mvc/src/app/todo-list.service.ts @@ -3,13 +3,7 @@ import { inject, Injectable } from '@angular/core'; import { rxState } from '@rx-angular/state'; import { eventValue, rxActions } from '@rx-angular/state/actions'; import { merge, MonoTypeOperatorFunction } from 'rxjs'; -import { - exhaustMap, - filter, - map, - tap, - withLatestFrom, -} from 'rxjs/operators'; +import { exhaustMap, filter, map, tap, withLatestFrom } from 'rxjs/operators'; import { Todo, TodoFilter } from './todo.model'; import { TodoResource } from './todo.resource'; @@ -56,30 +50,29 @@ export class TodoService { readonly #state = rxState(({ set, connect, select }) => { set({ filter: 'all' }); - const getAll$ = this.#todoResource - .getAll() - .pipe(map((todos) => ({ todos }))); + connect('todos', this.#todoResource.allTodos.data); + const setFilter$ = this.actions.setFilter$.pipe( map((filter) => ({ filter })) ); const create$ = this.actions.create$.pipe( filter(({ text }) => text.trim().length > 0), tap(({ callback }) => callback()), - exhaustMap((todo) => this.#todoResource.create(todo)), + exhaustMap((todo) => this.#todoResource.addOne.mutateAsync(todo)), map((todos) => ({ todos })) ); const remove$ = this.actions.remove$.pipe( - exhaustMap((todo) => this.#todoResource.removeOne(todo)), + exhaustMap((todo) => this.#todoResource.removeOne.mutateAsync(todo)), map((todos) => ({ todos })) ); const update$ = this.actions.update$.pipe( - exhaustMap((todo) => this.#todoResource.updateOne(todo)), + exhaustMap((todo) => this.#todoResource.updateOne.mutateAsync(todo)), map((todos) => ({ todos })) ); const toggleAll$ = this.actions.toggleAll$.pipe( withLatestFrom(select('todos')), exhaustMap(([, todos]) => - this.#todoResource.updateMany( + this.#todoResource.updateMany.mutateAsync( todos.map((todo) => ({ ...todo, done: todos.every(({ done }) => !done), @@ -90,7 +83,9 @@ export class TodoService { ); const clearCompleted$ = this.actions.clearCompleted$.pipe( withLatestFrom(select('todos').pipe(activeTodos)), - exhaustMap(([, todos]) => this.#todoResource.updateMany(todos)), + exhaustMap(([, todos]) => + this.#todoResource.updateMany.mutateAsync(todos) + ), map((todos) => ({ todos })) ); const drop$ = this.actions.drop$.pipe( @@ -102,13 +97,12 @@ export class TodoService { updatedTodos.splice(currentIndex, 0, todo); return updatedTodos; }), - exhaustMap((todos) => this.#todoResource.updateMany(todos)), + exhaustMap((todos) => this.#todoResource.updateMany.mutateAsync(todos)), map((todos) => ({ todos })) ); connect( merge( - getAll$, setFilter$, create$, remove$, diff --git a/projects/todo-mvc/src/app/todo.resource.ts b/projects/todo-mvc/src/app/todo.resource.ts index e3ed64f..f15dd61 100644 --- a/projects/todo-mvc/src/app/todo.resource.ts +++ b/projects/todo-mvc/src/app/todo.resource.ts @@ -1,6 +1,11 @@ -import { HttpClient } from "@angular/common/http"; -import { inject, Injectable } from "@angular/core"; -import { Todo } from "./todo.model"; +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { + injectMutation, + injectQuery, +} from '@tanstack/angular-query-experimental'; +import { lastValueFrom } from 'rxjs'; +import { Todo } from './todo.model'; @Injectable({ providedIn: 'root' }) export class TodoResource { @@ -8,23 +13,33 @@ export class TodoResource { private static endpoint = new URL('http://localhost:3000/todo').toString(); - getAll() { - return this.#http.get(TodoResource.endpoint); - } + allTodos = injectQuery(() => ({ + queryKey: ['todos'], + initialData: [], + queryFn: () => lastValueFrom(this.#http.get(TodoResource.endpoint)), + })); - create(todo: Pick) { - return this.#http.post(TodoResource.endpoint, todo); - } + addOne = injectMutation(() => ({ + mutationFn: (todo: Pick) => + lastValueFrom(this.#http.post(TodoResource.endpoint, todo)), + })); - removeOne(todo: Pick) { - return this.#http.delete(`${TodoResource.endpoint}/${todo.id}`); - } + removeOne = injectMutation(() => ({ + mutationFn: (todo: Pick) => + lastValueFrom( + this.#http.delete(`${TodoResource.endpoint}/${todo.id}`) + ), + })); - updateOne(todo: Todo) { - return this.#http.put(`${TodoResource.endpoint}/${todo.id}`, todo); - } + updateOne = injectMutation(() => ({ + mutationFn: (todo: Todo) => + lastValueFrom( + this.#http.put(`${TodoResource.endpoint}/${todo.id}`, todo) + ), + })); - updateMany(todos: Todo[]) { - return this.#http.put(TodoResource.endpoint, todos); - } + updateMany = injectMutation(() => ({ + mutationFn: (todos: Todo[]) => + lastValueFrom(this.#http.put(`${TodoResource.endpoint}`, todos)), + })); } diff --git a/projects/todo-mvc/src/main.ts b/projects/todo-mvc/src/main.ts index a19ef5d..5b956ee 100644 --- a/projects/todo-mvc/src/main.ts +++ b/projects/todo-mvc/src/main.ts @@ -1,15 +1,16 @@ import { HttpClientModule } from '@angular/common/http'; -import { - NgZone, - importProvidersFrom, - ɵNoopNgZone -} from '@angular/core'; +import { NgZone, importProvidersFrom, ɵNoopNgZone } from '@angular/core'; import { bootstrapApplication } from '@angular/platform-browser'; +import { + QueryClient, + provideAngularQuery, +} from '@tanstack/angular-query-experimental'; import { AppComponent } from './app/app.component'; bootstrapApplication(AppComponent, { providers: [ { provide: NgZone, useClass: ɵNoopNgZone }, importProvidersFrom(HttpClientModule), + provideAngularQuery(new QueryClient()), ], }).catch((err) => console.error(err)); diff --git a/yarn.lock b/yarn.lock index 81a2100..0069c1e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2418,6 +2418,19 @@ resolved "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz" integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== +"@tanstack/angular-query-experimental@^5.17.0": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@tanstack/angular-query-experimental/-/angular-query-experimental-5.17.0.tgz#b8e118f655358200b89f273f30e9c2f54131d47a" + integrity sha512-vcPqrJGhoMqWKVTtF7BPW6ZkRe+Lra06wkbbO/dU9+dY6Dwrt33DUw1V3437L7y5SEXDfzqY7AMCW9pHsckIsg== + dependencies: + "@tanstack/query-core" "5.17.0" + tslib "^2.6.2" + +"@tanstack/query-core@5.17.0": + version "5.17.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.17.0.tgz#3fd41e8557de9904c76e92b3820f62407223c990" + integrity sha512-LoBaPtbMY26kRS+ohII4thTsWkJJsXKGitOLikTo2aqPA4yy7cfFJITs8DRnuERT7tLF5xfG9Lnm33Vp/38Vmw== + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz" @@ -8494,7 +8507,7 @@ tsconfig-paths@^4.1.2: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@2.6.2, tslib@^2.4.0: +tslib@2.6.2, tslib@^2.4.0, tslib@^2.6.2: version "2.6.2" resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==