Doguhan Uluca
Technical Fellow @
set of critical computer applications perceived as vital to running an enterprise
Source: Wikipedia
A way to
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
User Roles
Modules
implement a Login experience
achieved iteratively and incrementally
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/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/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/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/user/user.service.ts readonly currentUser$ = new BehaviorSubject<IUser>( this.getItem('user') || new User() )
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/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/view-user/view-user.component.ts <div *ngIf="currentUser$ | async as currentUser"> <div>{{currentUser.firstName}}</div> ... </div>
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) ) )
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) ) )
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) ) )
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) ) )
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.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.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/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/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/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/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 { @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 { @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 { @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 { @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 { @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 { @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.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.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/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/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/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/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 <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 <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 <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 <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>
Master/Detail
Bound or Routed
duluca/lemon-mart/src/app/user/profile/profile.component.html <mat-step [stepControl]="formGroup"> <form [formGroup]="formGroup" (ngSubmit)="save(formGroup)"> <ng-template matStepLabel>Review</ng-template> <div class="stepContent"> Review and update your user profile. <app-view-user [user]="formGroup.value"></app-view-user> </div> … </form> </mat-step>
duluca/lemon-mart/src/app/user/profile/profile.component.html <mat-step [stepControl]="formGroup"> <form [formGroup]="formGroup" (ngSubmit)="save(formGroup)"> <ng-template matStepLabel>Review</ng-template> <div class="stepContent"> Review and update your user profile. <app-view-user [user]="formGroup.value"></app-view-user> </div> … </form> </mat-step>
duluca/lemon-mart/src/app/user/profile/profile.component.html <mat-step [stepControl]="formGroup"> <form [formGroup]="formGroup" (ngSubmit)="save(formGroup)"> <ng-template matStepLabel>Review</ng-template> <div class="stepContent"> Review and update your user profile. <app-view-user [user]="formGroup.value"></app-view-user> </div> … </form> </mat-step>
duluca/lemon-mart/src/app/user/profile/profile.component.html <mat-step [stepControl]="formGroup"> <form [formGroup]="formGroup" (ngSubmit)="save(formGroup)"> <ng-template matStepLabel>Review</ng-template> <div class="stepContent"> Review and update your user profile. <app-view-user [user]="formGroup.value"></app-view-user> </div> … </form> </mat-step>
duluca/lemon-mart/src/app/manager/manager-routing.module.ts { path: 'users', component: UserManagementComponent, children: [ { path: '', component: UserTableComponent, outlet: 'master' }, { path: 'user', component: ViewUserComponent, outlet: 'detail', resolve: { user: UserResolve, }, }, ], canActivate: [AuthGuard], canActivateChild: [AuthGuard], data: { expectedRole: Role.Manager, }, }
duluca/lemon-mart/src/app/manager/manager-routing.module.ts { path: 'users', component: UserManagementComponent, children: [ { path: '', component: UserTableComponent, outlet: 'master' }, { path: 'user', component: ViewUserComponent, outlet: 'detail', resolve: { user: UserResolve, }, }, ], canActivate: [AuthGuard], canActivateChild: [AuthGuard], data: { expectedRole: Role.Manager, }, }
duluca/lemon-mart/src/app/manager/manager-routing.module.ts { path: 'users', component: UserManagementComponent, children: [ { path: '', component: UserTableComponent, outlet: 'master' }, { path: 'user', component: ViewUserComponent, outlet: 'detail', resolve: { user: UserResolve, }, }, ], canActivate: [AuthGuard], canActivateChild: [AuthGuard], data: { expectedRole: Role.Manager, }, }
duluca/lemon-mart/src/app/manager/manager-routing.module.ts { path: 'users', component: UserManagementComponent, children: [ { path: '', component: UserTableComponent, outlet: 'master' }, { path: 'user', component: ViewUserComponent, outlet: 'detail', resolve: { user: UserResolve, }, }, ], canActivate: [AuthGuard], canActivateChild: [AuthGuard], data: { expectedRole: Role.Manager, }, }
duluca/lemon-mart/src/app/manager/manager-routing.module.ts { path: 'users', component: UserManagementComponent, children: [ { path: '', component: UserTableComponent, outlet: 'master' }, { path: 'user', component: ViewUserComponent, outlet: 'detail', resolve: { user: UserResolve, }, }, ], canActivate: [AuthGuard], canActivateChild: [AuthGuard], data: { expectedRole: Role.Manager, }, }
duluca/lemon-mart/src/app/manager/manager-routing.module.ts { path: 'users', component: UserManagementComponent, children: [ { path: '', component: UserTableComponent, outlet: 'master' }, { path: 'user', component: ViewUserComponent, outlet: 'detail', resolve: { user: UserResolve, }, }, ], canActivate: [AuthGuard], canActivateChild: [AuthGuard], data: { expectedRole: Role.Manager, }, }
duluca/lemon-mart/src/app/manager/manager-routing.module.ts { path: 'users', component: UserManagementComponent, children: [ { path: '', component: UserTableComponent, outlet: 'master' }, { path: 'user', component: ViewUserComponent, outlet: 'detail', resolve: { user: UserResolve, }, }, ], canActivate: [AuthGuard], canActivateChild: [AuthGuard], data: { expectedRole: Role.Manager, }, }
duluca/lemon-mart/src/app/manager/manager-routing.module.ts { path: 'users', component: UserManagementComponent, children: [ { path: '', component: UserTableComponent, outlet: 'master' }, { path: 'user', component: ViewUserComponent, outlet: 'detail', resolve: { user: UserResolve, }, }, ], canActivate: [AuthGuard], canActivateChild: [AuthGuard], data: { expectedRole: Role.Manager, }, }
duluca/lemon-mart/src/app/manager/manager-routing.module.ts { path: 'users', component: UserManagementComponent, children: [ { path: '', component: UserTableComponent, outlet: 'master' }, { path: 'user', component: ViewUserComponent, outlet: 'detail', resolve: { user: UserResolve, }, }, ], canActivate: [AuthGuard], canActivateChild: [AuthGuard], data: { expectedRole: Role.Manager, }, }
duluca/lemon-mart/src/app/manager/manager-routing.module.ts { path: 'users', component: UserManagementComponent, children: [ { path: '', component: UserTableComponent, outlet: 'master' }, { path: 'user', component: ViewUserComponent, outlet: 'detail', resolve: { user: UserResolve, }, }, ], canActivate: [AuthGuard], canActivateChild: [AuthGuard], data: { expectedRole: Role.Manager, }, }
duluca/lemon-mart/src/app/manager/manager-routing.module.ts { path: 'users', component: UserManagementComponent, children: [ { path: '', component: UserTableComponent, outlet: 'master' }, { path: 'user', component: ViewUserComponent, outlet: 'detail', resolve: { user: UserResolve, }, }, ], canActivate: [AuthGuard], canActivateChild: [AuthGuard], data: { expectedRole: Role.Manager, }, }
duluca/lemon-mart/src/app/manager/manager-routing.module.ts { path: 'users', component: UserManagementComponent, children: [ { path: '', component: UserTableComponent, outlet: 'master' }, { path: 'user', component: ViewUserComponent, outlet: 'detail', resolve: { user: UserResolve, }, }, ], canActivate: [AuthGuard], canActivateChild: [AuthGuard], data: { expectedRole: Role.Manager, }, }
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/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/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/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/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 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 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 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 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 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.ts export interface IPhone { type: string number: string id: number } export class User implements IUser
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/lemon-mart/src/app/user/user/user.service.ts getUser(id): Observable<IUser> { return this.httpClient.get<IUser>(`${environment.baseUrl}/v1/user/${id}`) }
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/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/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/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/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/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/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/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/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 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 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 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 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 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 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) } … }
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) } … }
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) } … }
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) } … }
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) } … }
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) } … }
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) } … }
Angular Evergreen
VS Code Extension
@duluca
linkedin.com/in/duluca
github.com/duluca
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