Skip to content

Commit 36ba43b

Browse files
authored
Merge pull request #1 from namnguyen2k1/facade
feat(post): implement based signal store
2 parents a6f7b94 + cfee7bf commit 36ba43b

File tree

9 files changed

+275
-55
lines changed

9 files changed

+275
-55
lines changed

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"editor.formatOnSave": true,
3+
}
Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { HttpClient } from "@angular/common/http";
2-
import { Injectable, inject } from "@angular/core";
2+
import { inject, Injectable } from "@angular/core";
33
import { API_SERVICE } from "@core/constants";
4+
import { map, Observable } from "rxjs";
45
import { Post } from "../models";
56

67
@Injectable({ providedIn: "root" })
@@ -9,10 +10,42 @@ export class PostApi {
910
private readonly baseUrl = API_SERVICE.POST;
1011

1112
getAll({ limit }: { limit: number }) {
12-
return this.http.get<Post[]>(`${this.baseUrl}?_limit=${limit}`);
13+
const url = `${this.baseUrl}?_limit=${limit}`;
14+
15+
return this.http.get<Post[]>(url).pipe(
16+
map((body) => {
17+
return body.map((data) => {
18+
return Post.create(data);
19+
});
20+
}),
21+
);
1322
}
1423

1524
getById(id: number) {
16-
return this.http.get<Post>(`${this.baseUrl}/${id}`);
25+
const url = `${this.baseUrl}/${id}`;
26+
27+
return this.http.get<Post>(url).pipe(
28+
map((body) => {
29+
return Post.create(body);
30+
}),
31+
);
32+
}
33+
34+
addPost(post: Omit<Post, "id">): Observable<Post> {
35+
const url = `${this.baseUrl}`;
36+
37+
return this.http.post<Post>(url, post);
38+
}
39+
40+
updatePost(id: number, body: Partial<Post>): Observable<Post> {
41+
const url = `${this.baseUrl}/${id}`;
42+
43+
return this.http.put<Post>(url, body);
44+
}
45+
46+
deletePost(id: number): Observable<void> {
47+
const url = `${this.baseUrl}/${id}`;
48+
49+
return this.http.delete<void>(url);
1750
}
1851
}

src/app/features/post/components/post.component.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
import { ChangeDetectionStrategy, Component, inject, input } from "@angular/core";
1+
import { ChangeDetectionStrategy, Component, inject, input, output } from "@angular/core";
22
import { Router } from "@angular/router";
33
import { Post } from "../models";
44

5+
export interface PostSubmitEvent {
6+
type: "delete" | "update";
7+
data: Post;
8+
}
9+
510
@Component({
611
standalone: true,
712
selector: "app-post",
@@ -17,6 +22,7 @@ import { Post } from "../models";
1722
<button class="app-btn-primary" (click)="router.navigateByUrl('/home/post/' + p.id)">
1823
Read more
1924
</button>
25+
<button class="app-btn-primary" (click)="submitDelete()">Delete</button>
2026
</div>
2127
</div>
2228
</div>
@@ -28,4 +34,12 @@ import { Post } from "../models";
2834
export class PostComponent {
2935
router = inject(Router);
3036
data = input.required<Post>();
37+
submitEvent = output<PostSubmitEvent>();
38+
39+
submitDelete() {
40+
this.submitEvent.emit({
41+
type: "delete",
42+
data: this.data(),
43+
});
44+
}
3145
}

src/app/features/post/models/post.model.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ export class Post {
44
title = "";
55
body = "";
66

7-
static create(input?: any) {
8-
const m = new Post();
7+
static create(input?: Partial<Post>) {
8+
const model = new Post();
99

10-
m.userId = input?.userId ?? m.userId;
11-
m.id = input?.id ?? m.id;
12-
m.title = input?.title ?? m.title;
13-
m.body = input?.body ?? m.body;
10+
model.userId = input?.userId ?? model.userId;
11+
model.id = input?.id ?? model.id;
12+
model.title = input?.title ?? model.title;
13+
model.body = input?.body ?? model.body;
1414

15-
return m;
15+
return model;
1616
}
1717
}

src/app/features/post/pages/post-detail.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@ export class PostDetailComponent implements OnInit {
3434
post$?: Observable<FetchState<Post>>;
3535

3636
ngOnInit() {
37-
this.post$ = this.postService.getPostById(this.id()).pipe(toFetchState());
37+
this.post$ = this.postService.getPostDetailById(this.id()).pipe(toFetchState());
3838
}
3939
}
Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,47 @@
1-
import { AsyncPipe } from "@angular/common";
2-
import { ChangeDetectionStrategy, Component, inject } from "@angular/core";
3-
import { toFetchState } from "@core/utils";
1+
import { ChangeDetectionStrategy, Component, inject, OnInit, Signal } from "@angular/core";
2+
import { StateType } from "@shared/services/base-store.service";
43
import { PostComponent } from "../components";
4+
import { PostSubmitEvent } from "../components/post.component";
5+
import { Post } from "../models";
56
import { PostService } from "../services";
67

78
@Component({
89
selector: "app-post-list",
9-
imports: [AsyncPipe, PostComponent],
10+
imports: [PostComponent],
1011
changeDetection: ChangeDetectionStrategy.OnPush,
1112
standalone: true,
1213
template: `
1314
<div class="styled-box">
1415
<div class="divider text-accent">Post Listing Page</div>
15-
@let posts = posts$ | async;
16-
@let data = posts?.data;
17-
@if (data) {
18-
@for (p of data; track $index) {
19-
<app-post [data]="p" />
16+
@let state = postState();
17+
@if (state.error) {
18+
<div class="text-[red]">Loading posts failed....</div>
19+
} @else if (state.loading) {
20+
<span class="loading loading-spinner loading-xl"></span>
21+
} @else {
22+
@for (p of state.data; track $index) {
23+
<app-post [data]="p" (submitEvent)="handlePostItemEvent($event)" />
2024
} @empty {
2125
<div class="text-center">No post</div>
2226
}
23-
} @else if (posts?.error) {
24-
<div class="text-[red]">Loading posts failed....</div>
25-
} @else {
26-
<span class="loading loading-spinner loading-xl"></span>
2727
}
2828
</div>
2929
`,
3030
})
31-
export class PostListingComponent {
31+
export class PostListingComponent implements OnInit {
3232
private readonly postService = inject(PostService);
33-
protected posts$ = this.postService.getAllPosts().pipe(toFetchState());
33+
protected postState: Signal<StateType<Post>> = this.postService.postState;
34+
35+
ngOnInit() {
36+
this.postService.fetchAllPosts();
37+
}
38+
39+
protected handlePostItemEvent(event: PostSubmitEvent) {
40+
switch (event.type) {
41+
case "delete":
42+
return this.postService.deletePostById(event.data.id, false);
43+
case "update":
44+
return this.postService.updatePost(event.data.id, event.data, false);
45+
}
46+
}
3447
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Injectable } from "@angular/core";
2+
import { BaseStore } from "@shared/services/base-store.service";
3+
import { Post } from "../models";
4+
5+
@Injectable({
6+
providedIn: "root",
7+
})
8+
export class PostStore extends BaseStore<Post> {
9+
// override methods...
10+
}
Lines changed: 84 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,107 @@
11
import { inject, Injectable } from "@angular/core";
2-
import { BehaviorSubject, map, of } from "rxjs";
2+
import { catchError, finalize, throwError } from "rxjs";
33
import { PostApi } from "../apis";
44
import { Post } from "../models";
5+
import { PostStore } from "./post-store.service";
56

67
@Injectable({
78
providedIn: "root",
89
})
910
export class PostService {
10-
private readonly PostApi = inject(PostApi);
11-
private _posts$ = new BehaviorSubject<{
12-
data: Post[];
13-
expired: number;
14-
}>({ data: [], expired: 0 });
15-
16-
get posts() {
17-
return this._posts$.value.data;
18-
}
11+
private readonly postApi = inject(PostApi);
12+
private readonly postStore = inject(PostStore);
1913

20-
clearCache() {
21-
this._posts$.next({ data: [], expired: 0 });
14+
get postState() {
15+
return this.postStore.state;
2216
}
2317

2418
isExpired() {
25-
const expired = Date.now() > this._posts$.value.expired;
19+
const expired = Date.now() > this.postStore.state().expired;
2620
if (expired) {
27-
this.clearCache();
21+
this.postStore.reset();
2822
}
2923
return expired;
3024
}
3125

32-
getAllPosts() {
33-
if (this.posts.length && !this.isExpired()) {
34-
return of(this.posts);
26+
fetchAllPosts() {
27+
if (!this.isExpired()) {
28+
console.log("data not expired");
29+
return;
3530
}
3631

37-
return this.PostApi.getAll({ limit: 100 }).pipe(
38-
map((data) => {
39-
this._posts$.next({
40-
data,
41-
expired: Date.now() + 3 * 60 * 1000, // expires in 3 minutes
42-
});
43-
return data;
44-
}),
45-
);
32+
this.postStore.setLoading(true);
33+
this.postApi
34+
.getAll({ limit: 20 })
35+
.pipe(
36+
catchError((error) => {
37+
console.log("fetch all post", error);
38+
return throwError(() => error);
39+
}),
40+
finalize(() => {
41+
this.postStore.setLoading(false);
42+
}),
43+
)
44+
.subscribe((data) => {
45+
const expired = Date.now() + 1 * 60 * 1000;
46+
this.postStore.setData(data, expired);
47+
});
48+
}
49+
50+
getPostDetailById(id: number) {
51+
return this.postApi.getById(id);
52+
}
53+
54+
createPost(data: Post, loading = true) {
55+
this.postStore.setLoading(loading);
56+
this.postApi
57+
.addPost(data)
58+
.pipe(
59+
catchError((error) => {
60+
console.log("create-post", error);
61+
return throwError(() => error);
62+
}),
63+
finalize(() => {
64+
this.postStore.setLoading(false);
65+
}),
66+
)
67+
.subscribe((post) => {
68+
this.postStore.addNewData(post);
69+
});
70+
}
71+
72+
updatePost(id: number, data: Partial<Post>, loading = true) {
73+
this.postStore.setLoading(loading);
74+
this.postApi
75+
.updatePost(id, data)
76+
.pipe(
77+
catchError((error) => {
78+
console.log("update-post", error);
79+
return throwError(() => error);
80+
}),
81+
finalize(() => {
82+
this.postStore.setLoading(false);
83+
}),
84+
)
85+
.subscribe((post) => {
86+
this.postStore.updateDataById(id, post);
87+
});
4688
}
4789

48-
getPostById(id: number) {
49-
return this.PostApi.getById(id);
90+
deletePostById(id: number, loading = true) {
91+
this.postStore.setLoading(loading);
92+
this.postApi
93+
.deletePost(id)
94+
.pipe(
95+
catchError((error) => {
96+
console.log("delete-post", error);
97+
return throwError(() => error);
98+
}),
99+
finalize(() => {
100+
this.postStore.setLoading(false);
101+
}),
102+
)
103+
.subscribe(() => {
104+
this.postStore.deleteDataById(id);
105+
});
50106
}
51107
}

0 commit comments

Comments
 (0)