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
Principal Fellow at
@duluca
"Highest priority is ... early and continuous delivery
of valuable software"
"changing requirements, even late in
development"
"Deliver working software frequently"
"Working software is the primary measure of progress"
"... promote sustainable development"
"Continuous attention to technical excellence"
"Simplicity--the art of maximizing the amount
of work not done--is essential"
https://agilemanifesto.org/principles.html
Ron Jeffries, Chet Hendrickson, deliver:Agile 2018.
achieved iteratively and incrementally
read-only preview
simple layout
responsive layout
validations, fancy user controls
achieved iteratively and incrementally
simple inputs, static entry
auto complete fields
dynamic entry and validation
achieved iteratively and incrementally
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
Open-Closed
Principle
Open-Closed
Principle
duluca/lemon-mart/src/app/auth/auth.service.ts
export interface IAuthService {
readonly authStatus$: BehaviorSubject<IAuthStatus>
readonly currentUser$: BehaviorSubject<IUser>
login(email: string, password: string): Observable<void>
logout(clearToken?: boolean): void
getToken(): string
}
Open-Closed Principle
duluca/lemon-mart/src/app/auth/auth.service.ts
export abstract class AuthService implements IAuthService {
authStatus$: BehaviorSubject<IAuthStatus>
currentUser$: BehaviorSubject<IUser>
constructor() {}
login(email: string, password: string): Observable<void> {
throw new Error('Method not implemented.')
}
logout(clearToken?: boolean): void {
throw new Error('Method not implemented.')
}
getToken(): string {
throw new Error('Method not implemented.')
}
}
Open-Closed Principle
duluca/lemon-mart/src/app/auth/auth.service.ts
export abstract class AuthService implements IAuthService {
protected abstract authProvider(
email: string,
password: string
): Observable<IServerAuthResponse>
protected abstract transformJwtToken(token: unknown): IAuthStatus
protected abstract getCurrentUser(): Observable<User>
...
}
Open-Closed Principle
duluca/lemon-mart/src/app/auth/auth.service.ts
login(email: string, password: string): Observable<void> {
this.clearToken()
const loginResponse$ = this.authProvider(email, password).pipe(
map((value) => {
this.setToken(value.accessToken)
const token = decode(value.accessToken)
return this.transformJwtToken(token)
}),
tap((status) => this.authStatus$.next(status)),
this.getAndUpdateUserIfAuthenticated
)
loginResponse$.subscribe({
error: (err) => {
this.logout()
return throwError(err)
},
})
return loginResponse$
}
Liskov Subsitution
duluca/lemon-mart/src/app/auth/auth.inmemory.service.ts
export class InMemoryAuthService extends AuthService {
// LemonMart Server User Id: 5da01751da27cc462d265913
private defaultUser = User.Build({...})
constructor() {
super()
console.warn(
"You're using the InMemoryAuthService. Do not use this service in production."
)
}
protected authProvider(email: string, password: string): Observable<IServerAuthResponse> {
const authStatus = { isAuthenticated: true, ... } as IAuthStatus
const authResponse = {
accessToken: sign(authStatus, 'secret', {
expiresIn: '1h',
algorithm: 'none',
}),
} as IServerAuthResponse
return of(authResponse)
}
protected transformJwtToken(token: IAuthStatus): IAuthStatus {
return token
}
protected getCurrentUser(): Observable<User> {
return of(this.defaultUser)
}
}
Open-Closed Principle
duluca/lemon-mart/src/app/auth/auth.firebase.service.ts
@Injectable()
export class FirebaseAuthService extends AuthService {
constructor(private afAuth: AngularFireAuth) {
super()
}
protected authProvider(email: string, password: string): Observable<IServerAuthResponse> {
return this.afAuth.signInWithEmailAndPassword(email, password)
}
protected transformJwtToken(token: IJwtToken): IAuthStatus {
return { isAuthenticated: token.email ? true : false, userId: token.sub, userRole: Role.None }
}
protected getCurrentUser(): Observable<User> {
return this.afAuth.user.pipe(map(this.transformFirebaseUser))
}
logout() {
if (this.afAuth) {
this.afAuth.signOut()
}
this.clearToken()
this.authStatus$.next(defaultAuthStatus)
}
}
Open-Closed Principle
duluca/lemon-mart/src/app/auth/auth.custom.service.ts
export class CustomAuthService extends AuthService {
constructor(private httpClient: HttpClient) {
super()
}
protected authProvider(email: string, password: string): Observable<IServerAuthResponse> {
return this.httpClient.post<IServerAuthResponse>(
`${environment.baseUrl}/v1/auth/login`, { email, password }
)
}
protected transformJwtToken(token: IJwtToken): IAuthStatus {
return {
isAuthenticated: token.email ? true : false,
userId: token.sub,
userRole: $enum(Role).asValueOrDefault(token.role, Role.None),
userEmail: token.email,
userPicture: token.picture,
} as IAuthStatus
}
protected getCurrentUser(): Observable<User> {
return this.httpClient
.get<IUser>(`${environment.baseUrl}/v1/auth/me`)
.pipe(map(User.Build, catchError(transformError)))
}
}
Open-Closed Principle
duluca/lemon-mart/src/app/auth/auth.factory.ts
function authFactory(afAuth: AngularFireAuth, httpClient: HttpClient) {
switch (environment.authMode) {
case AuthMode.InMemory:
return new InMemoryAuthService()
case AuthMode.CustomServer:
return new CustomAuthService(httpClient)
case AuthMode.Firebase:
return new FirebaseAuthService(afAuth)
}
}
duluca/lemon-mart/src/app/app.module.ts
providers: [
{
provide: AuthService,
useFactory: authFactory,
deps: [AngularFireAuth, HttpClient],
}
]
Dependency Inversion
Pick a component library
JavaScript Class
Angular Binding
Angular Template
Uses
TypeScript
Uses
HTML, CSS
Bootstrap
Stick to it
HELL
Should you?
Know what you're doing
Leverage Angular CDK
Establish a design system
Test and stress components individually
Robust automated pipeline
Don't do it
Doguhan Uluca, Angular for Enterprise-Ready Web Apps, 2020.
duluca/local-weather-app/weather/weather.service.spec.ts
describe('getCurrentWeather', () => {
it('should return value given zip code', () => {
// Arrange
const httpMock = TestBed.inject(HttpTestingController)
const uriParams = 'lat=38.887103&lon=-77.093197'
postalCodeServiceMock.resolvePostalCode.and.returnValue(
of({
postalCode: '22201',
lat: 38.887103,
lng: -77.093197,
countryCode: 'US',
placeName: 'Arlington',
} as IPostalCode)
)
// Act
weatherService.getCurrentWeather('22201').subscribe()
// Assert
const request = httpMock.expectOne(
`${environment.baseUrl}api.openweathermap.org/data/2.5/weather?` +
`${uriParams}&appid=01ff1417eeb4a81b09ac68b15958d453`,
'call to api'
)
expect(request.request.method).toBe('GET')
})
})
Arrange, Act, Assert
weatherApi
weatherComponent
api.openweathermap.org/data/2.5/weather
weatherServiceMock
getCurrentWeather
return(32)
api.openweathermap.org/data/2.5/weather
hasBeenCalled(1)
weatherService
getCurrentWeather
Test Harness
duluca/local-weather-app/weather/weather.service.spec.ts
describe('getCurrentWeather', () => {
it('should return value given zip code', () => {
// Arrange
const httpMock = TestBed.inject(HttpTestingController)
const uriParams = 'lat=38.887103&lon=-77.093197'
postalCodeServiceMock.resolvePostalCode.and.returnValue(
of({
postalCode: '22201',
lat: 38.887103,
lng: -77.093197,
countryCode: 'US',
placeName: 'Arlington',
} as IPostalCode)
)
// Act
weatherService.getCurrentWeather('22201').subscribe()
// Assert
const request = httpMock.expectOne(
`${environment.baseUrl}api.openweathermap.org/data/2.5/weather?` +
`${uriParams}&appid=01ff1417eeb4a81b09ac68b15958d453`,
'call to api'
)
expect(request.request.method).toBe('GET')
})
})
Mocks and Spies
38%
50%
88%
?
1
2
3
4
5
6
7
8
1
2
3
4
5
6
7
8
Unit Tests
1
2
3
4
5
6
7
8
1
2
3
4
5
6
7
8
e2e Tests
1
2
3
4
5
6
7
8
1
2
3
4
5
6
7
8
75%
Don't repeat yourself!
Don't repeat yourself!
Don't repeat yourself!
keep it simple, stupid!
Avoid complexity, depend (on your own) code
Single Responsibility Principle
export class LemonRaterComponent {
internalValue!: number
setRating(lemon: any) {
let text = ''
if (lemon.value) {
text = this.ratings.find((i) => i.value === value)?.text || ''
}
if (!this.disabled) {
this.internalValue = lemon.value
this.displayTextRef.nativeElement.textContent = text
this.onChanged(lemon.value)
this.onTouched()
}
}
setDisplayText() {
let text = ''
if (this.internalValue) {
this.displayTextRef.nativeElement.textContent =
this.ratings.find((i) => i.value === value)?.text || ''
}
}
}
Single Responsibility Principle
export class LemonRaterComponent {
private internalValue!: number
get value() {
return this.internalValue
}
setRating(lemon: any) {
if (!this.disabled) {
this.internalValue = lemon.value
this.setDisplayText()
this.onChanged(lemon.value)
this.onTouched()
}
}
setDisplayText() {
this.setSelectedText(this.internalValue)
}
private setSelectedText(value: number) {
this.displayTextRef.nativeElement.textContent =
this.getSelectedText(value)
}
private getSelectedText(value: number) {
let text = ''
if (value) {
text = this.ratings.find((i) => i.value === value)?.text || ''
}
return text
}
}
new FormControl('',
[Validators.required, Validators.minLength(2)]
)
DRY, KISS, AC/DC
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 = ...
DRY, KISS, AC/DC
duluca/lemon-mart-server/server/services/authService.ts
export function createJwt(user: IUser): Promise<string> {
...
sanitizeToken(token)
}
export function authenticate(options?: {
requiredRole?: Role
permitIfSelf?: {
idGetter: (req: Request) => string
requiredRoleCanOverride: boolean
}
}) {
...
}
function sanitizeToken(authorization: string | undefined) {
...
}
DRY, KISS, AC/DC
duluca/lemon-mart-server/server/routes/userRouter.ts
router.get(
'/',
authenticate({ requiredRole: Role.Manager }),
async (req: Request, res: Response) => {
const query: Partial<IQueryParameters> = {
filter: req.query.filter,
limit: req.query.limit,
skip: req.query.skip,
sortKeyOrList: req.query.sortKey,
projectionKeyOrList: ['email', 'role', '_id', 'name'],
}
const users = await UserCollection.findWithPagination<User>(query)
res.send(users)
}
)
DRY, KISS, AC/DC
duluca/lemon-mart/auth/role.enum.ts
export enum Role {
None = 'none',
Clerk = 'clerk',
Cashier = 'cashier',
Manager = 'manager',
}
We're hiring, including fully remote positions!
By Doguhan Uluca
Almost everyone is doing Agile these days, however true agility comes from disciplined coding practices and flexible implementation choices. I’ll go over five ways to make sure your Angular code is agile. I’ll show you a few examples of iterative and incremental implementation. Then how to design your code leveraging Object-oriented principles, so it’s open to extension, but closed to modification. A sage advice on component libraries, testing smartly, and finally how you should think thrice and (no joke) code less.
Author of the best-selling Angular for Enterprise-Ready Web Apps. Google Developers Expert in Angular. Agile, JavaScript and Cloud expert, Go player.