Doguhan Uluca PRO
Author of the best-selling Angular for Enterprise-Ready Web Apps. Google Developers Expert in Angular. Agile, JavaScript and Cloud expert, Go player.
Doguhan Uluca
Technical Fellow @
products$ = this.http.get<Product[]>(this.productsUrl)
.pipe(
tap(data => console.log('getProducts: ', JSON.stringify(data))),
shareReplay(),
catchError(this.handleError)
)
productsWithCategory$ = combineLatest(
this.products$,
this.productCategoryService.productCategories$
).pipe(
map(([products, categories]) =>
products.map(
p =>
({
...p,
category: categories.find(c => p.categoryId === c.id).name
} as Product) // <-- note the type here!
)
),
shareReplay()
)
Data Composition with RxJS | Deborah Kurata
Thinking Reactively: Most Difficult | Mike Pearson
duluca/local-weather-app/city-search.component.ts
search = new FormControl('', [Validators.required, Validators.minLength(2)])
this.search.valueChanges
.pipe(
debounceTime(1000),
filter(() => !this.search.invalid),
tap((searchValue: string) => this.doSearch(searchValue))
)
.subscribe()
duluca/local-weather-app/city-search.component.html
<form #searchForm>
<mat-form-field appearance="outline"
[ngClass]="search.value ? 'search-box-partial' : 'search-box-full'">
<mat-label>City Name or Postal Code</mat-label>
<mat-icon matPrefix>search</mat-icon>
<input matInput aria-label="City or Zip" [formControl]="search" />
<mat-hint>Specify country code like 'Paris, US'</mat-hint>
<mat-error *ngIf="search.invalid"> Type more than one character to search
</mat-error>
</mat-form-field>
<button mat-icon-button *ngIf="search.value" class="clear-button"
(click)="searchForm.reset(); search.reset();">
<mat-icon>close</mat-icon>
</button>
</form>
new FormControl('',
[Validators.required, Validators.minLength(2)]
)
duluca/lemon-mart/common/validations.ts
export const OptionalTextValidation = [Validators.minLength(2), Validators.maxLength(50)]
export const RequiredTextValidation = OptionalTextValidation.concat([Validators.required])
export const EmailValidation = [Validators.required, Validators.email]
export const PasswordValidation = ...
export const BirthDateValidation = [
Validators.required,
Validators.min(new Date().getFullYear() - 100),
Validators.max(new Date().getFullYear()),
]
export const USAZipCodeValidation = [
Validators.required,
Validators.pattern(/^\d{5}(?:[-\s]\d{4})?$/),
]
export const USAPhoneNumberValidation = ...
duluca/lemon-mart/user/profile/profile.component.ts
userForm: FormGroup
constructor(
private formBuilder: FormBuilder
) {}
ngOnInit() {
const user = ...
this.buildUserForm(user)
}
buildUserForm(user?: IUser, currentUserRole = UserRole.None) {
this.userForm = this.formBuilder.group({
...
})
}
duluca/lemon-mart/user/profile/profile.component.html
<mat-horizontal-stepper #stepper="matHorizontalStepper">
<mat-step [stepControl]="userForm">
<form [formGroup]="userForm">
<ng-template matStepLabel>Account Information</ng-template>
<div class="stepContent">
...
</div>
<div fxLayout="row" class="margin-top">
<div class="flex-spacer"></div>
<div *ngIf="userError" class="mat-caption error">{{ userError }}</div>
<button mat-raised-button matStepperNext color="accent">Next</button>
</div>
</form>
</mat-step>
<mat-step [stepControl]="userForm">
...
</mat-step>
<mat-step [stepControl]="userForm">
...
</mat-step>
</mat-horizontal-stepper>
duluca/lemon-mart/user/profile/profile.component.ts
buildUserForm(user?: IUser, currentUserRole = UserRole.None) {
this.userForm = this.formBuilder.group({
email: [{
value: (user && user.email) || '',
disabled: currentUserRole !== this.Role.Manager,
},
EmailValidation,
],
name: this.formBuilder.group({
first: [(user && user.name.first) || '', RequiredTextValidation],
middle: [(user && user.name.middle) || '', OneCharValidation],
last: [(user && user.name.last) || '', RequiredTextValidation],
}),
role: [{
value: (user && user.role) || '',
disabled: currentUserRole !== this.Role.Manager,
},
[Validators.required],
],
...
duluca/lemon-mart/user/profile/profile.component.html
<div class="stepContent">
<div fxLayout="row" fxLayout.lt-sm="column"
[formGroup]="userForm.get('name')" fxLayoutGap="10px">
<mat-form-field appearance="outline" fxFlex="40%">
<mat-label>First Name</mat-label>
<input matInput aria-label="First Name" formControlName="first" />
<mat-error *ngIf="userForm.get('name').get('first').hasError('required')">
First Name is required
</mat-error>
<mat-error *ngIf="userForm.get('name').get('first').hasError('minLength')">
Must be at least 2 characters
</mat-error>
<mat-error *ngIf="userForm.get('name').get('first').hasError('maxLength')">
Can't exceed 50 characters
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" fxFlex="20%">
<mat-label>MI</mat-label>
<input matInput aria-label="Middle Initial" formControlName="middle" />
<mat-error *ngIf="userForm.get('name').get('middle').invalid">
Only inital
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" fxFlex="40%">
<mat-label>Last Name</mat-label>
<input matInput aria-label="Last Name" formControlName="last" />
<mat-error *ngIf="userForm.get('name').get('last').hasError('required')">
Last Name is required
</mat-error>
<mat-error *ngIf="userForm.get('name').get('last').hasError('minLength')">
Must be at least 2 characters
</mat-error>
<mat-error *ngIf="userForm.get('name').get('last').hasError('maxLength')">
Can't exceed 50 characters
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayout.lt-sm="column" fxLayoutGap="10px">
<mat-form-field appearance="outline" fxFlex="50%">
<mat-label>Date of Birth</mat-label>
<input matInput aria-label="Date of Birth" formControlName="dateOfBirth"
[matDatepicker]="dateOfBirthPicker" />
<mat-hint *ngIf="userForm.get('dateOfBirth').touched">
{{ this.age }} year(s) old
</mat-hint>
<mat-datepicker-toggle matSuffix [for]="dateOfBirthPicker">
</mat-datepicker-toggle>
<mat-datepicker #dateOfBirthPicker></mat-datepicker>
<mat-error *ngIf="userForm.get('dateOfBirth').invalid">
Date must be with the last 100 years
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" fxFlex="50%">
<mat-label>E-mail</mat-label>
<input matInput aria-label="E-mail" formControlName="email" />
<mat-hint>Only your manager can update your e-mail.</mat-hint>
<mat-error *ngIf="userForm.get('email').invalid">
A valid E-mail is required
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayout.lt-sm="column" class="margin-top" fxLayoutGap="10px">
<div fxFlex>
<mat-label class="mat-body-1">Role</mat-label>
<mat-radio-group formControlName="role">
<mat-radio-button style="margin-right: 10px;" [value]="Role.None">
<span class="mat-body-1">None</span>
</mat-radio-button>
<mat-radio-button style="margin-right: 10px;" [value]="Role.Cashier">
<span class="mat-body-1">Cashier</span>
</mat-radio-button>
<mat-radio-button style="margin-right: 10px;" [value]="Role.Clerk">
<span class="mat-body-1">Clerk</span>
</mat-radio-button>
<mat-radio-button style="margin-right: 10px;" [value]="Role.Manager">
<span class="mat-body-1">Manager</span>
</mat-radio-button>
</mat-radio-group>
<mat-error *ngIf="
userForm.get('role').hasError('required') && userForm.get('role').touched
">
<span class="mat-caption">Role is required</span>
</mat-error>
</div>
</div>
</div>
duluca/lemon-mart/user/profile/profile.component.html
<div class="stepContent">
...
<div fxLayout="row" fxLayout.lt-sm="column" fxLayoutGap="10px">
...
<mat-form-field appearance="outline" fxFlex="50%">
<mat-label>E-mail</mat-label>
<input matInput aria-label="E-mail" formControlName="email" />
<mat-hint>Only your manager can update your e-mail.</mat-hint>
<mat-error *ngIf="userForm.get('email').invalid">
A valid E-mail is required
</mat-error>
</mat-form-field>
</div>
...
</div>
duluca/lemon-mart/user/profile/profile.component.html
<mat-form-field appearance="outline" fxFlex="50%">
<mat-label>Date of Birth</mat-label>
<input matInput aria-label="Date of Birth" formControlName="dateOfBirth"
[matDatepicker]="dateOfBirthPicker" />
<mat-hint *ngIf="userForm.get('dateOfBirth').touched">
{{ this.age }} year(s) old
</mat-hint>
<mat-datepicker-toggle matSuffix [for]="dateOfBirthPicker">
</mat-datepicker-toggle>
<mat-datepicker #dateOfBirthPicker></mat-datepicker>
<mat-error *ngIf="userForm.get('dateOfBirth').invalid">
Date must be with the last 100 years
</mat-error>
</mat-form-field>
duluca/lemon-mart/user/profile/profile.component.ts
name: this.formBuilder.group({
first: [(user && user.name.first) || '', RequiredTextValidation],
middle: [(user && user.name.middle) || '', OneCharValidation],
last: [(user && user.name.last) || '', RequiredTextValidation],
}),
duluca/lemon-mart/user/profile/profile.component.html
<div fxLayout="row" fxLayout.lt-sm="column"
[formGroup]="userForm.get('name')" fxLayoutGap="10px">
<mat-form-field appearance="outline" fxFlex="40%">
<mat-label>First Name</mat-label>
<input matInput aria-label="First Name" formControlName="first" />
<mat-error *ngIf="userForm.get('name').get('first').hasError('required')">
First Name is required
</mat-error>
<mat-error *ngIf="userForm.get('name').get('first').hasError('minLength')">
Must be at least 2 characters
</mat-error>
<mat-error *ngIf="userForm.get('name').get('first').hasError('maxLength')">
Can't exceed 50 characters
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" fxFlex="20%">
<mat-label>MI</mat-label>
<input matInput aria-label="Middle Initial" formControlName="middle" />
<mat-error *ngIf="userForm.get('name').get('middle').invalid">
Only inital
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" fxFlex="40%">
<mat-label>Last Name</mat-label>
<input matInput aria-label="Last Name" formControlName="last" />
<mat-error *ngIf="userForm.get('name').get('last').hasError('required')">
Last Name is required
</mat-error>
<mat-error *ngIf="userForm.get('name').get('last').hasError('minLength')">
Must be at least 2 characters
</mat-error>
<mat-error *ngIf="userForm.get('name').get('last').hasError('maxLength')">
Can't exceed 50 characters
</mat-error>
</mat-form-field>
</div>
duluca/lemon-mart/user/name-input/name-input.component.ts
export class NameInputComponent implements OnInit {
@Input() initialData: IName
@Input() disable: boolean
@Output() formReady = new EventEmitter<FormGroup>()
formGroup: FormGroup
constructor(private formBuilder: FormBuilder) {}
ngOnInit() {
this.formGroup = this.buildForm(this.initialData)
this.formReady.emit(this.formGroup)
if (this.disable) {
this.formGroup.disable()
}
}
...
}
duluca/lemon-mart/user/name-input/name-input.component.ts
export class NameInputComponent implements OnInit {
...
buildForm(initialData?: IName): FormGroup {
return this.formBuilder.group({
first: [name ? name.first : '', RequiredTextValidation],
middle: [name ? name.middle : '', OptionalTextValidation],
last: [name ? name.last : '', RequiredTextValidation],
})
}
}
duluca/lemon-mart/user/name-input/name-input.component.html
template: `
<form [formGroup]="formGroup">
<div fxLayout="row" fxLayout.lt-sm="column" fxLayoutGap="10px">
<mat-form-field appearance="outline" fxFlex="40%">
<mat-label>First Name</mat-label>
<input matInput aria-label="First Name" formControlName="first" required />
<mat-error *ngIf="formGroup.get('first').hasError('required')">
First Name is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" fxFlex="20%">
<mat-label>MI</mat-label>
<input matInput aria-label="Middle" formControlName="middle" />
</mat-form-field>
<mat-form-field appearance="outline" fxFlex="40%">
<mat-label>Last Name</mat-label>
<input matInput aria-label="Last Name" formControlName="last" required />
<mat-error *ngIf="formGroup.get('last').hasError('required')">
Last Name is required
</mat-error>
</mat-form-field>
</div>
</form>
`,
duluca/lemon-mart/user/profile/profile.component.ts
<form [formGroup]="userForm">
<ng-template matStepLabel>Contact Information</ng-template>
<div class="stepContent">
<app-name-input [initialData]="name$ | async"
(formReady)="userForm.setControl('name', $event)">
</app-name-input>
...
</div>
</form>
duluca/lemon-mart/user/profile/profile.component.ts
states$: Observable<IUSState[]>
buildUserForm(user?: IUser, currentUserRole = UserRole.None) {
this.userForm = this.formBuilder.group({
...
this.states$ = this.userForm
.get('address')
.get('state')
.valueChanges.pipe(
startWith(''),
map(value => USStateFilter(value))
)
})
}
duluca/lemon-mart/user/profile/profile.component.ts
export function USStateFilter(value: string): IUSState[] {
return USStates.filter(state => {
return (
(state.code.length === 2 && state.code.toLowerCase() === value.toLowerCase()) ||
state.name.toLowerCase().indexOf(value.toLowerCase()) === 0
)
})
}
duluca/lemon-mart/user/profile/profile.component.html
<mat-form-field appearance="outline" fxFlex="30%">
<mat-label>State</mat-label>
<input type="text" aria-label="State" matInput formControlName="state"
[matAutocomplete]="stateAuto" />
<mat-autocomplete #stateAuto="matAutocomplete">
<mat-option *ngFor="let state of (states$ | async)" [value]="state.name">
{{ state.name }}
</mat-option>
</mat-autocomplete>
<mat-error *ngIf="userForm.get('address').get('state').hasError('required')">
State is required
</mat-error>
</mat-form-field>
duluca/lemon-mart/user/profile/profile.component.ts
buildUserForm(user?: IUser, currentUserRole = UserRole.None) {
this.userForm = this.formBuilder.group({
...
phones: this.formBuilder.array(this.buildPhoneArray(user ? user.phones : [])),
})
}
private buildPhoneArray(phones: IPhone[]) {
const groups = []
if (!phones || (phones && phones.length === 0)) {
groups.push(this.buildPhoneFormControl(1))
} else {
phones.forEach(p => {
groups.push(this.buildPhoneFormControl(p.id, p.type, p.number))
})
}
return groups
}
duluca/lemon-mart/user/profile/profile.component.ts
private buildPhoneFormControl(id, type?: string, phoneNumber?: string) {
return this.formBuilder.group({
id: [id],
type: [type || '', Validators.required],
number: [phoneNumber || '', USAPhoneNumberValidation],
})
}
addPhone() {
this.phonesArray.push(this.buildPhoneFormControl(this.phonesArray.value.length + 1))
}
get phonesArray(): FormArray {
return this.userForm.get('phones') as FormArray
}
duluca/lemon-mart/user/profile/profile.component.html
<mat-list formArrayName="phones">
<h2 mat-subheader>Phone Number(s)
<button mat-button (click)="this.addPhone()">
<mat-icon>add</mat-icon>Add Phone
</button>
</h2>
<mat-list-item style="margin-top: 36px;"
*ngFor="let position of this.phonesArray.controls; let i = index"
[formGroupName]="i">
...
<button fxFlex="33px" mat-icon-button
(click)="this.phonesArray.removeAt(i)">
<mat-icon>close</mat-icon>
</button>
</mat-list-item>
</mat-list>
duluca/lemon-mart/user/profile/profile.component.ts
PhoneTypes = $enum(PhoneType).getKeys()
duluca/lemon-mart/user/profile/profile.component.html
<mat-form-field appearance="outline" fxFlex="100px">
<mat-label>Type</mat-label>
<mat-select formControlName="type">
<mat-option *ngFor="let type of this.PhoneTypes" [value]="type">
{{ type }}
</mat-option>
</mat-select>
</mat-form-field>
duluca/lemon-mart/user/user.module.ts
@NgModule({
imports: [
...
NgxMaskModule.forChild({ showMaskTyped: true, showTemplate: true }),
]
})
duluca/lemon-mart/user/profile/profile.component.ts
<mat-form-field appearance="outline" fxFlex fxFlexOffset="10px">
<mat-label>Number</mat-label>
<input matInput type="text" formControlName="number"
mask="(000) 000-0000" prefix="+1" />
<mat-error *ngIf="this.phonesArray.controls[i].invalid">
A valid phone number is required
</mat-error>
</mat-form-field>
The Control Value Accessor | Jennifer Wadella
tehfedaykin/galaxy-rating-app/star-rater.component.html
<i>{{displayText}}</i>
<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 <= _value}"
(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>
tehfedaykin/galaxy-rating-app/star-rater.component.ts
export class StarRaterComponent implements ControlValueAccessor {
public ratings = ...
public disabled: boolean;
public ratingText: string;
public _value: number;
writeValue(val) {
this._value = val;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
setRating(star: any) {
if(!this.disabled) {
this._value = star.stars;
this.ratingText = star.text
this.onChanged(star.stars);
this.onTouched();
}
}
}
By Doguhan Uluca
Reactive forms are the best way to write forms in Angular. In this workshop, we will implement a multi-step form with varied inputs including reusable form groups, date pickers, drop-downs with type-ahead support, form arrays, custom form controls with input masking implementing ControlValueAccessor and doing validation as your form spans multiple components.
Author of the best-selling Angular for Enterprise-Ready Web Apps. Google Developers Expert in Angular. Agile, JavaScript and Cloud expert, Go player.