Skip to content

Commit acf330e

Browse files
authored
Merge pull request #7 from actionanand/features/2-reactive-form
Features/2 reactive form
2 parents 4765340 + 2f03546 commit acf330e

File tree

8 files changed

+219
-3
lines changed

8 files changed

+219
-3
lines changed

README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,3 +258,56 @@ setTimeout(() => {
258258
```
259259

260260
See the code in this project [here](https://github.com/actionanand/angular-form/blob/master/src/app/auth/login/login.component.ts)
261+
262+
## Wiki
263+
264+
### Custom Form Controls `ControlValueAccessor`:
265+
266+
This interface lets you create custom input components that integrate seamlessly with Angular forms. You can define how your component reads and writes values to the form control, as well as how it handles validation and change detection.
267+
268+
### FormArrays:
269+
270+
Manage lists of form controls that can be added or removed dynamically.
271+
272+
### FormGroup:
273+
274+
Create nested form groups to represent complex data structures.
275+
276+
### Observables:
277+
278+
1. `valueChanges`: Use the valueChanges observable to track changes to form controls or the entire form.
279+
2. `patchValue` and `setValue`: Update form control values programmatically.
280+
281+
### onSubmit:
282+
283+
Handle form submission events and process the form data.
284+
285+
### `ControlValueAccessor` (CVA) - Explanation
286+
287+
1. `NG_VALUE_ACCESSOR`: This provider tells Angular that your component can act as a CVA.
288+
2. `writeValue`: This method is called by Angular when the form control's value changes.
289+
3. `registerOnChange`: This method is called by Angular to register a callback function that will be called when the component's value changes.
290+
4. `registerOnTouched`: This method is called by Angular to register a callback function that will be called when the component is touched.
291+
292+
The `NG_VALUE_ACCESSOR` is binding things to component's `:host` and linking to methods (`ControlValueAccessor` methods) there. Your module does not have any of those form methods (like `writeValue`, `registerOnTouched` etc). Your form element does. So providing at component level binds this for that specific element. Additionally, providing so deep down means each form control has it's own control value accessor and not a shared one.
293+
294+
Angular Form controls and its API is not the same as the DOM form controls. What angular does is binds to the inputs/outputs of the dom element and provides you with the results. Now, with your custom control, you must provide the same bindings there. By implementing `ControlValueAccessor` and providing `NG_VALUE_ACCESSOR`, you are telling Angular's Forms API how it can read and write values from/to your custom form control. - [Source](https://stackoverflow.com/questions/48085713/why-do-i-need-to-provide-ng-value-accessor-at-the-component-level)
295+
296+
`NG_VALUE_ACCESSOR` is just an injection token for ControlValueAccessor. You can refer the below one:
297+
298+
```ts
299+
const NG_VALUE_ACCESSOR: InjectionToken<readonly ControlValueAccessor[]>;
300+
```
301+
302+
### The expanded provider configuration is an object literal with two properties:
303+
304+
- The `provide` property holds the token that serves as the key for consuming the dependency value.
305+
- The second property is a provider definition object, which tells the injector how to create the dependency value. The provider-definition can be one of the following:
306+
1. `useClass` - this option tells Angular DI to instantiate a provided class when a dependency is injected
307+
2. `useExisting` - allows you to alias a token and reference any existing one.
308+
3. `useFactory` - allows you to define a function that constructs a dependency.
309+
4. `useValue` - provides a static value that should be used as a dependency.
310+
311+
## Sources
312+
313+
1. [How to PROPERLY implement ControlValueAccessor - Angular Form](https://blog.woodies11.dev/how-to-properly-implement-controlvalueaccessor/)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!-- eslint-disable @angular-eslint/template/mouse-events-have-key-events -->
2+
<i>{{ displayText }}</i>
3+
4+
<div class="stars" [ngClass]="{ disabled: disabled }">
5+
@for (star of ratings; track star.stars) {
6+
<svg
7+
title="{{ star.text }}"
8+
height="25"
9+
width="23"
10+
class="star rating"
11+
[ngClass]="{ selected: star.stars <= starVal }"
12+
(mouseover)="displayText = !disabled ? star.text : ''"
13+
(mouseout)="displayText = ratingText ? ratingText : ''"
14+
(click)="setRating(star)">
15+
<polygon points="9.9, 1.1, 3.3, 21.78, 19.8, 8.58, 0, 8.58, 16.5, 21.78" style="fill-rule: nonzero" />
16+
</svg>
17+
}
18+
</div>
19+
20+
<!-- <div class="stars" [ngClass]="{'disabled': disabled}">
21+
<ng-container *ngFor="let star of ratings" >
22+
<svg title="{{star.text}}"
23+
height="25" width="23" class="star rating" [ngClass]="{'selected': star.stars <= starVal}"
24+
(mouseover)="displayText = !disabled ? star.text : ''"
25+
(mouseout)="displayText = ratingText ? ratingText : ''"
26+
(click)="setRating(star)">
27+
<polygon points="9.9, 1.1, 3.3, 21.78, 19.8, 8.58, 0, 8.58, 16.5, 21.78" style="fill-rule:nonzero;"/>
28+
</svg>
29+
</ng-container>
30+
</div> -->
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
.stars {
2+
cursor: pointer;
3+
4+
&:hover {
5+
.star polygon {
6+
fill: #ffd055 !important;
7+
}
8+
}
9+
&.disabled:hover {
10+
cursor: not-allowed;
11+
.star {
12+
polygon {
13+
fill: #d8d8d8 !important;
14+
}
15+
}
16+
}
17+
18+
.star {
19+
float: left;
20+
margin: 0px 5px;
21+
22+
polygon {
23+
fill: #d8d8d8;
24+
}
25+
26+
&:hover ~ .star {
27+
polygon {
28+
fill: #d8d8d8 !important;
29+
}
30+
}
31+
&.selected {
32+
polygon {
33+
fill: #ffd055 !important;
34+
}
35+
}
36+
}
37+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { NgClass } from '@angular/common';
3+
import { Component, forwardRef } from '@angular/core';
4+
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
5+
6+
@Component({
7+
selector: 'app-rating',
8+
standalone: true,
9+
imports: [NgClass],
10+
templateUrl: './rating.component.html',
11+
styleUrl: './rating.component.less',
12+
providers: [
13+
{
14+
provide: NG_VALUE_ACCESSOR,
15+
useExisting: forwardRef(() => RatingComponent),
16+
multi: true,
17+
},
18+
],
19+
})
20+
export class RatingComponent implements ControlValueAccessor {
21+
protected readonly ratings = [
22+
{
23+
stars: 1,
24+
text: 'Feeling Sad 😔',
25+
},
26+
{
27+
stars: 2,
28+
text: 'Feeling Meh ☹️',
29+
},
30+
{
31+
stars: 3,
32+
text: 'Feeling Ok 🙂',
33+
},
34+
{
35+
stars: 4,
36+
text: 'Feeling Good 😀',
37+
},
38+
{
39+
stars: 5,
40+
text: 'Feeling Awesome 😍',
41+
},
42+
];
43+
44+
disabled = false;
45+
ratingText = '';
46+
displayText = '';
47+
starVal!: number;
48+
49+
private onChanged: any = () => {};
50+
private onTouched: any = () => {};
51+
52+
writeValue(val: number) {
53+
this.starVal = val;
54+
}
55+
56+
registerOnChange(fn: any) {
57+
this.onChanged = fn;
58+
}
59+
60+
registerOnTouched(fn: any) {
61+
this.onTouched = fn;
62+
}
63+
64+
setDisabledState(isDisabled: boolean): void {
65+
this.disabled = isDisabled;
66+
}
67+
68+
setRating(star: { stars: number; text: string }) {
69+
if (!this.disabled) {
70+
this.starVal = star.stars;
71+
this.ratingText = star.text;
72+
this.onChanged(star.stars);
73+
this.onTouched();
74+
}
75+
}
76+
}

src/app/reactive/signup/signup.component.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,15 @@ <h2>Welcome on board!</h2>
198198
}
199199
</div>
200200

201+
<br />
202+
203+
<div class="control-row">
204+
<label for="star-rating">
205+
Rating:
206+
<app-rating formControlName="rating" />
207+
</label>
208+
</div>
209+
201210
<div class="control-row">
202211
<div class="control">
203212
<label for="terms-and-conditions">

src/app/reactive/signup/signup.component.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from '@angular/forms';
1111

1212
import { debounceTime } from 'rxjs';
13+
import { RatingComponent } from '../../control-value-accessor/rating/rating.component';
1314

1415
type MyArray = {
1516
name: string;
@@ -49,7 +50,7 @@ function valueMustBeTrue(control: AbstractControl) {
4950
@Component({
5051
selector: 'app-signup',
5152
standalone: true,
52-
imports: [ReactiveFormsModule],
53+
imports: [ReactiveFormsModule, RatingComponent],
5354
templateUrl: './signup.component.html',
5455
styleUrl: './signup.component.scss',
5556
})
@@ -134,6 +135,7 @@ export class SignupComponent implements OnInit {
134135
sourceAr: new FormArray([new FormControl(false), new FormControl(false), new FormControl(false)]),
135136
fruitsAr: new FormArray([]),
136137
hobbies: new FormArray([]),
138+
rating: new FormControl(null),
137139
terms: new FormControl(false, [Validators.required, valueMustBeTrue]),
138140
});
139141

@@ -165,6 +167,7 @@ export class SignupComponent implements OnInit {
165167
sourceAr: this.fb.array([this.fb.control(false), this.fb.control(false), this.fb.control(false)]),
166168
fruitsAr: this.fb.array([]),
167169
hobbies: this.fb.array([]),
170+
rating: this.fb.control(null),
168171
terms: this.fb.control(false, [Validators.required, valueMustBeTrue])
169172
});
170173
*/

src/app/template-driven/login/login.component.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ <h2>Login</h2>
2222
<button class="button">Login</button>
2323
</div>
2424

25+
<div>
26+
<label for="star-rating">
27+
Rating:
28+
<app-rating name="rating" name="rating" ngModel #rating="ngModel" />
29+
</label>
30+
</div>
31+
2532
<!-- @if(formEl.form.invalid && formEl.form.controls['emailField'].touched) {
2633
<p class="control-error">
2734
Invalid email entered

src/app/template-driven/login/login.component.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { afterNextRender, Component, DestroyRef, inject, viewChild } from '@angu
22
import { FormsModule, NgForm } from '@angular/forms';
33

44
import { debounceTime } from 'rxjs/operators';
5+
import { RatingComponent } from '../../control-value-accessor/rating/rating.component';
56

67
@Component({
78
selector: 'app-template-driven',
89
standalone: true,
9-
imports: [FormsModule],
10+
imports: [FormsModule, RatingComponent],
1011
templateUrl: './login.component.html',
1112
styleUrl: './login.component.css',
1213
})
@@ -54,7 +55,7 @@ export class TemplateDrivenComponent {
5455
}
5556

5657
console.log('Form Obj(NgForm) : ', formEl);
57-
58+
console.log(formEl.value);
5859
console.log("formEl.form.controls['emailField'] => ", formEl.form.controls['emailField']);
5960
console.log("formEl.form.controls['emailField'].value => ", formEl.form.controls['emailField'].value);
6061
console.log("formEl.form.value['emailField'] => ", formEl.form.value['emailField']);

0 commit comments

Comments
 (0)