Skip to content

Commit

Permalink
Merge pull request #7 from actionanand/features/2-reactive-form
Browse files Browse the repository at this point in the history
Features/2 reactive form
  • Loading branch information
actionanand authored Oct 6, 2024
2 parents 4765340 + 2f03546 commit acf330e
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 3 deletions.
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,56 @@ setTimeout(() => {
```

See the code in this project [here](https://github.com/actionanand/angular-form/blob/master/src/app/auth/login/login.component.ts)

## Wiki

### Custom Form Controls `ControlValueAccessor`:

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.

### FormArrays:

Manage lists of form controls that can be added or removed dynamically.

### FormGroup:

Create nested form groups to represent complex data structures.

### Observables:

1. `valueChanges`: Use the valueChanges observable to track changes to form controls or the entire form.
2. `patchValue` and `setValue`: Update form control values programmatically.

### onSubmit:

Handle form submission events and process the form data.

### `ControlValueAccessor` (CVA) - Explanation

1. `NG_VALUE_ACCESSOR`: This provider tells Angular that your component can act as a CVA.
2. `writeValue`: This method is called by Angular when the form control's value changes.
3. `registerOnChange`: This method is called by Angular to register a callback function that will be called when the component's value changes.
4. `registerOnTouched`: This method is called by Angular to register a callback function that will be called when the component is touched.

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.

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)

`NG_VALUE_ACCESSOR` is just an injection token for ControlValueAccessor. You can refer the below one:

```ts
const NG_VALUE_ACCESSOR: InjectionToken<readonly ControlValueAccessor[]>;
```

### The expanded provider configuration is an object literal with two properties:

- The `provide` property holds the token that serves as the key for consuming the dependency value.
- 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:
1. `useClass` - this option tells Angular DI to instantiate a provided class when a dependency is injected
2. `useExisting` - allows you to alias a token and reference any existing one.
3. `useFactory` - allows you to define a function that constructs a dependency.
4. `useValue` - provides a static value that should be used as a dependency.

## Sources

1. [How to PROPERLY implement ControlValueAccessor - Angular Form](https://blog.woodies11.dev/how-to-properly-implement-controlvalueaccessor/)
30 changes: 30 additions & 0 deletions src/app/control-value-accessor/rating/rating.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!-- eslint-disable @angular-eslint/template/mouse-events-have-key-events -->
<i>{{ displayText }}</i>

<div class="stars" [ngClass]="{ disabled: disabled }">
@for (star of ratings; track star.stars) {
<svg
title="{{ star.text }}"
height="25"
width="23"
class="star rating"
[ngClass]="{ selected: star.stars <= starVal }"
(mouseover)="displayText = !disabled ? star.text : ''"
(mouseout)="displayText = ratingText ? ratingText : ''"
(click)="setRating(star)">
<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" />
</svg>
}
</div>

<!-- <div class="stars" [ngClass]="{'disabled': disabled}">
<ng-container *ngFor="let star of ratings" >
<svg title="{{star.text}}"
height="25" width="23" class="star rating" [ngClass]="{'selected': star.stars <= starVal}"
(mouseover)="displayText = !disabled ? star.text : ''"
(mouseout)="displayText = ratingText ? ratingText : ''"
(click)="setRating(star)">
<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;"/>
</svg>
</ng-container>
</div> -->
37 changes: 37 additions & 0 deletions src/app/control-value-accessor/rating/rating.component.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.stars {
cursor: pointer;

&:hover {
.star polygon {
fill: #ffd055 !important;
}
}
&.disabled:hover {
cursor: not-allowed;
.star {
polygon {
fill: #d8d8d8 !important;
}
}
}

.star {
float: left;
margin: 0px 5px;

polygon {
fill: #d8d8d8;
}

&:hover ~ .star {
polygon {
fill: #d8d8d8 !important;
}
}
&.selected {
polygon {
fill: #ffd055 !important;
}
}
}
}
76 changes: 76 additions & 0 deletions src/app/control-value-accessor/rating/rating.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NgClass } from '@angular/common';
import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
selector: 'app-rating',
standalone: true,
imports: [NgClass],
templateUrl: './rating.component.html',
styleUrl: './rating.component.less',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RatingComponent),
multi: true,
},
],
})
export class RatingComponent implements ControlValueAccessor {
protected readonly ratings = [
{
stars: 1,
text: 'Feeling Sad 😔',
},
{
stars: 2,
text: 'Feeling Meh ☹️',
},
{
stars: 3,
text: 'Feeling Ok 🙂',
},
{
stars: 4,
text: 'Feeling Good 😀',
},
{
stars: 5,
text: 'Feeling Awesome 😍',
},
];

disabled = false;
ratingText = '';
displayText = '';
starVal!: number;

private onChanged: any = () => {};
private onTouched: any = () => {};

writeValue(val: number) {
this.starVal = val;
}

registerOnChange(fn: any) {
this.onChanged = fn;
}

registerOnTouched(fn: any) {
this.onTouched = fn;
}

setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}

setRating(star: { stars: number; text: string }) {
if (!this.disabled) {
this.starVal = star.stars;
this.ratingText = star.text;
this.onChanged(star.stars);
this.onTouched();
}
}
}
9 changes: 9 additions & 0 deletions src/app/reactive/signup/signup.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,15 @@ <h2>Welcome on board!</h2>
}
</div>

<br />

<div class="control-row">
<label for="star-rating">
Rating:
<app-rating formControlName="rating" />
</label>
</div>

<div class="control-row">
<div class="control">
<label for="terms-and-conditions">
Expand Down
5 changes: 4 additions & 1 deletion src/app/reactive/signup/signup.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '@angular/forms';

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

type MyArray = {
name: string;
Expand Down Expand Up @@ -49,7 +50,7 @@ function valueMustBeTrue(control: AbstractControl) {
@Component({
selector: 'app-signup',
standalone: true,
imports: [ReactiveFormsModule],
imports: [ReactiveFormsModule, RatingComponent],
templateUrl: './signup.component.html',
styleUrl: './signup.component.scss',
})
Expand Down Expand Up @@ -134,6 +135,7 @@ export class SignupComponent implements OnInit {
sourceAr: new FormArray([new FormControl(false), new FormControl(false), new FormControl(false)]),
fruitsAr: new FormArray([]),
hobbies: new FormArray([]),
rating: new FormControl(null),
terms: new FormControl(false, [Validators.required, valueMustBeTrue]),
});

Expand Down Expand Up @@ -165,6 +167,7 @@ export class SignupComponent implements OnInit {
sourceAr: this.fb.array([this.fb.control(false), this.fb.control(false), this.fb.control(false)]),
fruitsAr: this.fb.array([]),
hobbies: this.fb.array([]),
rating: this.fb.control(null),
terms: this.fb.control(false, [Validators.required, valueMustBeTrue])
});
*/
Expand Down
7 changes: 7 additions & 0 deletions src/app/template-driven/login/login.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ <h2>Login</h2>
<button class="button">Login</button>
</div>

<div>
<label for="star-rating">
Rating:
<app-rating name="rating" name="rating" ngModel #rating="ngModel" />
</label>
</div>

<!-- @if(formEl.form.invalid && formEl.form.controls['emailField'].touched) {
<p class="control-error">
Invalid email entered
Expand Down
5 changes: 3 additions & 2 deletions src/app/template-driven/login/login.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { afterNextRender, Component, DestroyRef, inject, viewChild } from '@angu
import { FormsModule, NgForm } from '@angular/forms';

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

@Component({
selector: 'app-template-driven',
standalone: true,
imports: [FormsModule],
imports: [FormsModule, RatingComponent],
templateUrl: './login.component.html',
styleUrl: './login.component.css',
})
Expand Down Expand Up @@ -54,7 +55,7 @@ export class TemplateDrivenComponent {
}

console.log('Form Obj(NgForm) : ', formEl);

console.log(formEl.value);
console.log("formEl.form.controls['emailField'] => ", formEl.form.controls['emailField']);
console.log("formEl.form.controls['emailField'].value => ", formEl.form.controls['emailField'].value);
console.log("formEl.form.value['emailField'] => ", formEl.form.value['emailField']);
Expand Down

0 comments on commit acf330e

Please sign in to comment.