@mmstack/form-core 19.0.0 → 19.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +139 -0
  2. package/package.json +17 -1
package/README.md CHANGED
@@ -62,6 +62,145 @@
62
62
  }
63
63
  ```
64
64
 
65
+ ## Slightly more complex example
66
+
67
+ ```typescript
68
+ type Post = {
69
+ id: number;
70
+ title?: string;
71
+ body?: string;
72
+ };
73
+
74
+ @Injectable({
75
+ providedIn: 'root',
76
+ })
77
+ export class PostsService {
78
+ private readonly endpoint = 'https://jsonplaceholder.typicode.com/posts';
79
+
80
+ readonly id = signal(1);
81
+
82
+ readonly post = queryResource<Post>(
83
+ () => ({
84
+ url: `${this.endpoint}/${this.id()}`,
85
+ }),
86
+ {
87
+ keepPrevious: true,
88
+ cache: true,
89
+ },
90
+ );
91
+
92
+ next() {
93
+ this.id.update((id) => id + 1);
94
+ }
95
+
96
+ prev() {
97
+ this.id.update((id) => id - 1);
98
+ }
99
+
100
+ private readonly createPostResource = mutationResource(
101
+ () => ({
102
+ url: this.endpoint,
103
+ method: 'POST',
104
+ }),
105
+ {
106
+ onMutate: (post: Post) => {
107
+ const prev = untracked(this.post.value);
108
+ this.post.set({ ...prev, ...post });
109
+ return prev;
110
+ },
111
+ onError: (err, prev) => {
112
+ if (isDevMode()) console.error(err);
113
+ this.post.set(prev); // rollback on error
114
+ },
115
+ onSuccess: (next) => {
116
+ this.post.set(next);
117
+ },
118
+ },
119
+ );
120
+
121
+ readonly loading = computed(() => this.createPostResource.isLoading() || this.post.isLoading());
122
+
123
+ createPost(post: Post) {
124
+ this.createPostResource.mutate({
125
+ body: post,
126
+ });
127
+ }
128
+
129
+ updatePost(id: number, post: Partial<Post>) {
130
+ this.createPostResource.mutate({
131
+ body: { id, ...post },
132
+ method: 'PATCH',
133
+ });
134
+ }
135
+ }
136
+
137
+ type PostState = FormGroupSignal<
138
+ Post,
139
+ {
140
+ title: FormControlSignal<string | undefined, Post>;
141
+ body: FormControlSignal<string | undefined, Post>;
142
+ }
143
+ >;
144
+
145
+ function createPostState(post: Post, loading: Signal<boolean>): PostState {
146
+ const value = signal<Post>(post);
147
+
148
+ return formGroup(value, {
149
+ title: formControl(derived(value, 'title'), {
150
+ label: () => 'Title',
151
+ readonly: loading,
152
+ validator: () => (value) => (value ? '' : 'Title is required'),
153
+ }),
154
+ body: formControl(derived(value, 'body'), {
155
+ label: () => 'Body',
156
+ readonly: loading,
157
+ validator: () => (value) => {
158
+ if (value && value.length > 255) return 'Body is too long';
159
+ return '';
160
+ },
161
+ }),
162
+ });
163
+ }
164
+
165
+ @Component({
166
+ selector: 'app-root',
167
+ imports: [FormsModule],
168
+ template: `
169
+ <label>{{ formState().children().title.label() }}</label>
170
+ <input [(ngModel)]="formState().children().title.value" [class.error]="formState().children().title.touched() && formState().children().title.error()" />
171
+
172
+ <label>{{ formState().children().body.label() }}</label>
173
+ <textarea [(ngModel)]="formState().children().body.value" [class.error]="formState().children().body.touched() && formState().children().body.error()"></textarea>
174
+
175
+ <button (click)="submit()" [disabled]="svc.loading()">Submit</button>
176
+ `,
177
+ })
178
+ export class AppComponent {
179
+ protected readonly svc = inject(PostsService);
180
+
181
+ protected readonly formState = linkedSignal<Post, PostState>({
182
+ source: () => this.svc.post.value() ?? { title: '', body: '', id: -1, userId: -1 },
183
+ computation: (source, prev) => {
184
+ if (prev) {
185
+ prev.value.reconcile(source);
186
+ return prev.value;
187
+ }
188
+
189
+ return createPostState(source, this.svc.loading);
190
+ },
191
+ });
192
+
193
+ protected submit() {
194
+ if (untracked(this.svc.loading)) return;
195
+ const state = untracked(this.formState);
196
+ if (untracked(state.error)) return state.markAllAsTouched();
197
+ const value = untracked(state.value);
198
+ if (value.id === -1) this.svc.createPost(value);
199
+ else this.svc.updatePost(value.id, untracked(state.partialValue)); // only patch dirty values
200
+ }
201
+ }
202
+ ```
203
+
65
204
  ## In-depth
66
205
 
67
206
  For an in-depth explanation of the primitives & how they work check out this article: [Fun-grained Reactivity in Angular: Part 2 - Forms](https://dev.to/mihamulec/fun-grained-reactivity-in-angular-part-2-forms-e84)
package/package.json CHANGED
@@ -1,6 +1,22 @@
1
1
  {
2
2
  "name": "@mmstack/form-core",
3
- "version": "19.0.0",
3
+ "version": "19.0.1",
4
+ "keywords": [
5
+ "angular",
6
+ "signals",
7
+ "form",
8
+ "validation",
9
+ "formControl",
10
+ "formGroup",
11
+ "formArray",
12
+ "formBuilder",
13
+ "formState"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/mihajm/mmstack"
18
+ },
19
+ "homepage": "https://github.com/mihajm/mmstack/blob/master/packages/form/core",
4
20
  "peerDependencies": {
5
21
  "@angular/common": "~19.2.3",
6
22
  "@angular/core": "~19.2.3",