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 @
relative size of software
set of critical computer applications perceived as vital to running an enterprise
Source: Wikipedia
The Pareto Principal
A way to
Infrastructure-as-Code closes the Configuration Gap
implement a Login experience
achieved iteratively and incrementally
Managers
Warehouse
Cashiers
Developers
What you avoid implementing is more important than what you implement
Source: A wise developer
*Source: Angular Team, Google Analytics, 2018
app.ts
rootRouter
services
pipes
modules
/a: default
/master
/detail
/b/...
/c
childRouter
/d
/e
/f
App.js
Presentational
Container
Provider
Router
Component Legend
react-router
react-redux
User Roles
Modules
duluca/lemon-mart/src/app/app-routing.module.ts
{
path: 'user',
loadChildren: () =>
import('./user/user.module')
.then(m => m.UserModule),
},
{
path: 'manager',
loadChildren: () =>
import('./manager/manager.module')
.then(m => m.ManagerModule),
canLoad: [AuthGuard],
},
duluca/lemon-mart/src/app/user/user/user.service.ts
readonly currentUser$ = new BehaviorSubject<IUser>(
this.getItem('user') ||
new User()
)
duluca/lemon-mart/src/app/user/view-user/view-user.component.ts
<div *ngIf="currentUser$ | async as currentUser">
<div>{{currentUser.firstName}}</div>
...
</div>
duluca/lemon-mart/src/app/user/user/user.service.ts
updateResponse.pipe(
tap(
res => {
this.currentUser$.next(res)
this.removeItem('draft-user')
},
err => observableThrowError(err)
)
)
Master/Detail
Bound or Routed
duluca/lemon-mart/src/app/manager/user—table/user-table.component.ts
<a mat-button mat-icon-button
[routerLink]="[
'/manager/users',
{ outlets: { detail: ['user', { userId: row.id }] } }
]"
skipLocationChange>
<mat-icon>visibility</mat-icon>
</a>
duluca/lemon-mart/src/app/user/view-user/view-user.component.ts
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { BehaviorSubject } from 'rxjs'
import { IUser, User } from '../user/user'
@Component({
selector: 'app-view-user',
template: `
<div *ngIf="currentUser$ | async as currentUser">
<mat-card>
<mat-card-header>
<div mat-card-avatar><mat-icon>account_circle</mat-icon></div>
<mat-card-title>{{ currentUser.fullName }}</mat-card-title>
<mat-card-subtitle>{{ currentUser.role }}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<p><span class="mat-input bold">E-mail</span></p>
<p>{{ currentUser.email }}</p>
<p><span class="mat-input bold">Date of Birth</span></p>
<p>{{ currentUser.dateOfBirth | date: 'mediumDate' }}</p>
</mat-card-content>
<mat-card-actions *ngIf="editMode">
<button mat-button mat-raised-button>Edit</button>
</mat-card-actions>
</mat-card>
</div>
`,
styles: [
`
.bold {
font-weight: bold;
}
`,
],
})
export class ViewUserComponent implements OnInit, OnChanges {
@Input() user: IUser
readonly currentUser$ = new BehaviorSubject(new User())
get editMode() {
return !this.user
}
constructor(private route: ActivatedRoute) {}
ngOnInit() {
if (this.route.snapshot.data.user) {
const snapshotUser = User.Build(this.route.snapshot.data.user)
if (!snapshotUser.dateOfBirth) {
snapshotUser.dateOfBirth = Date.now() // for data mocking purposes only
}
this.currentUser$.next(snapshotUser)
}
}
ngOnChanges(changes: SimpleChanges): void {
this.currentUser$.next(User.Build(changes.user.currentValue))
}
}
duluca/lemon-mart/user/profile/profile.component.ts
<div fxLayout="row" fxLayout.lt-sm="column" class="margin-top"
fxLayoutGap="10px">
<mat-label class="mat-body-1">Select the Limoncu level:
<app-lemon-rater formControlName="level">
</app-lemon-rater>
</mat-label>
</div>
<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>
User Controls
Components
duluca/lemon-mart/src/app/user/user/user.ts
export interface IUser {
id: string
email: string
name: IName
…
address: {
line1: string
line2: string
city: string
state: string
zip: string
}
phones: IPhone[]
}
export interface IName {
first: string
middle?: string
last: string
}
duluca/lemon-mart/src/app/user/user/user.ts
export interface IPhone {
type: string
number: string
id: number
}
export class User implements IUser
duluca/lemon-mart/src/app/user/user/user.service.ts
getUser(id): Observable<IUser> {
return this.httpClient.get<IUser>(`${environment.baseUrl}/v1/user/${id}`)
}
duluca/local-weather-app/src/app/weather/weather.service.ts
private getCurrentWeatherHelper(uriParams: string):
Observable<ICurrentWeather> {
return this.httpClient
.get<ICurrentWeatherData>(
`${environment.baseUrl}api.openweathermap.org/data/2.5/weather?` +
`${uriParams}&appid=${environment.appId}`
)
.pipe(map(data => this.transformToICurrentWeather(data)))
}
duluca/lemon-mart/src/app/auth/role.enum.ts
export enum Role {
None = 'none',
Clerk = 'clerk',
Cashier = 'cashier',
Manager = 'manager',
}
duluca/lemon-mart/src/app/user/user/user.ts
export class User implements IUser {
static Build(user: IUser) {
return new User(
user.id,
user.email,
user.name,
…
)
}
get fullName() {
return this.name ?
`${this.name.first} ${this.name.middle} ${this.name.last}` : ''
}
toJSON() {
return JSON.stringify(...)
}
}
duluca/lemon-mart/src/app/common/base-form.class.ts
export abstract class BaseFormComponent<TFormData> {
@Input() initialData: TFormData
@Input() disable: boolean
@Output() formReady: EventEmitter<AbstractControl>
formGroup: FormGroup
private registeredForms: string[] = []
constructor() {
this.formReady = new EventEmitter<AbstractControl>(true)
}
abstract buildForm(initialData?: TFormData): FormGroup
patchUpdatedData(data) {
this.formGroup.patchValue(data, { onlySelf: false })
}
…
}
duluca/lemon-mart/src/app/common/base-form.class.ts
export class NameInputComponent extends BaseFormComponent<IName>
implements OnInit, OnChanges {
constructor(private formBuilder: FormBuilder) {
super()
}
buildForm(initialData?: IName): FormGroup {
const name = initialData
return this.formBuilder.group({ … })
}
ngOnInit() {
this.formGroup = this.buildForm(this.initialData)
this.formReady.emit(this.formGroup)
}
…
}
Feedback loop cycle reduced from 20 minutes to 1 minute
~25% reduction in package dependencies
Eliminated variations in versions of dependencies
~25% reduction in software bugs reported
~25% increase in velocity
Before
After
10 projects (with libraries, multiple Angular apps)
50 developers
With 33% of project time used, only 11% of functionality implemented
Digitizing 60+ field form (a small feature of larger application)
2 sprints to reach MVP with a small team
Other projects spend 6-12 sprints with large teams
Avoid going too deep
Defer fine tuning
Later, brought in component library to accelerate development
New project
Angular Evergreen
VS Code Extension
@duluca
linkedin.com/in/duluca
github.com/duluca
By Doguhan Uluca
The key ingredients to architecting for scalability are thinking router-first, keeping it simple, mastering the fundamentals and being reactive. A router-first approach to SPA design saving development teams significant waste in duplicative work, and re-architecting of the codebase; enabling better collaboration and achieving sub-second first meaningful paints in your application
Author of the best-selling Angular for Enterprise-Ready Web Apps. Google Developers Expert in Angular. Agile, JavaScript and Cloud expert, Go player.