Slide 1

Slide 1 text

My biggest Angular mistakes And how to Avoid Them

Slide 2

Slide 2 text

I was a Dumb Developer

Slide 3

Slide 3 text

I Still am a Dumb Developer

Slide 4

Slide 4 text

I know a little bit more now

Slide 5

Slide 5 text

My biggest Angular mistakes

Slide 6

Slide 6 text

My biggest Angular mistakes And how to Avoid Them

Slide 7

Slide 7 text

fabiangosebrink.bsky.social Fabian Gosebrink

Slide 8

Slide 8 text

Lessons from a developer’s journey

Slide 9

Slide 9 text

Opinionated

Slide 10

Slide 10 text

Components

Slide 11

Slide 11 text

Components Relationship of

Slide 12

Slide 12 text

Components Container

Slide 13

Slide 13 text

Components Presentational

Slide 14

Slide 14 text

Awesome Todo List ({{ store.doneCount() }}/{{ store.undoneCount() }} {{ store.percentageDone() | number : "1.0-0" }}% done)
Add
    @for (item of store.items(); track item.id) {
  • {{ item.value }}
    Delete
  • }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67

Slide 15

Slide 15 text

Awesome Todo List ({{ store.doneCount() }}/{{ store.undoneCount() }} {{ store.percentageDone() | number : "1.0-0" }}% done)
Add
    @for (item of store.items(); track item.id) {
  • {{ item.value }}
    Delete
  • }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 That's a lot! Doing multiple Things

Slide 16

Slide 16 text

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Separation Container Presentational

Slide 17

Slide 17 text

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
1 2 3 4 5 6 7 8 10 11 16 17
18 Separation Container Presentational

Slide 18

Slide 18 text

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
1 2 3 4 5 6 7 8 10 11 16 17
18
1 2 7 8 9 10 11 16 17
18 Separation Container Presentational

Slide 19

Slide 19 text

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
1 2 3 4 5 6 7 8 10 11 16 17
18
1 2 7 8 9 10 11 16 17
18
1 2 7 8 10 11 12 13 14 15 16 17
18 Separation Container Presentational

Slide 20

Slide 20 text

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
1 2 3 4 5 6 7 8 10 11 16 17
18
1 2 7 8 9 10 11 16 17
18
1 2 7 8 10 11 12 13 14 15 16 17
18
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Separation Container Presentational

Slide 21

Slide 21 text

Container Presentational Presentational

Slide 22

Slide 22 text

Main Form List

Slide 23

Slide 23 text

Where HOw HOw

Slide 24

Slide 24 text

state "no" state "No" state

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

Easier to test

Slide 27

Slide 27 text

Easier to test @Component({ selector: 'app-doggo-list', templateUrl: './doggo-list.component.html', styleUrls: ['./doggo-list.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class DoggoListComponent { doggos = input([]); doggoSelected = output(); selectDoggo(doggo: Doggo): void { this.doggoSelected.emit(doggo.id); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

Slide 28

Slide 28 text

Easier to test @Component({ selector: 'app-doggo-list', templateUrl: './doggo-list.component.html', styleUrls: ['./doggo-list.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class DoggoListComponent { doggos = input([]); doggoSelected = output(); selectDoggo(doggo: Doggo): void { this.doggoSelected.emit(doggo.id); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Component({ selector: 'app-doggo-list', templateUrl: './doggo-list.component.html', styleUrls: ['./doggo-list.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) 1 2 3 4 5 6 export class DoggoListComponent { 7 doggos = input([]); 8 9 doggoSelected = output(); 10 11 selectDoggo(doggo: Doggo): void { 12 this.doggoSelected.emit(doggo.id); 13 } 14 } 15

Slide 29

Slide 29 text

Easier to test @Component({ selector: 'app-doggo-list', templateUrl: './doggo-list.component.html', styleUrls: ['./doggo-list.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class DoggoListComponent { doggos = input([]); doggoSelected = output(); selectDoggo(doggo: Doggo): void { this.doggoSelected.emit(doggo.id); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Component({ selector: 'app-doggo-list', templateUrl: './doggo-list.component.html', styleUrls: ['./doggo-list.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) 1 2 3 4 5 6 export class DoggoListComponent { 7 doggos = input([]); 8 9 doggoSelected = output(); 10 11 selectDoggo(doggo: Doggo): void { 12 this.doggoSelected.emit(doggo.id); 13 } 14 } 15 export class DoggoListComponent { doggos = input([]); doggoSelected = output(); selectDoggo(doggo: Doggo): void { this.doggoSelected.emit(doggo.id); } } @Component({ 1 selector: 'app-doggo-list', 2 templateUrl: './doggo-list.component.html', 3 styleUrls: ['./doggo-list.component.scss'], 4 changeDetection: ChangeDetectionStrategy.OnPush, 5 }) 6 7 8 9 10 11 12 13 14 15

Slide 30

Slide 30 text

Easier to test @Component({ selector: 'app-doggo-list', templateUrl: './doggo-list.component.html', styleUrls: ['./doggo-list.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class DoggoListComponent { doggos = input([]); doggoSelected = output(); selectDoggo(doggo: Doggo): void { this.doggoSelected.emit(doggo.id); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Component({ selector: 'app-doggo-list', templateUrl: './doggo-list.component.html', styleUrls: ['./doggo-list.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) 1 2 3 4 5 6 export class DoggoListComponent { 7 doggos = input([]); 8 9 doggoSelected = output(); 10 11 selectDoggo(doggo: Doggo): void { 12 this.doggoSelected.emit(doggo.id); 13 } 14 } 15 export class DoggoListComponent { doggos = input([]); doggoSelected = output(); selectDoggo(doggo: Doggo): void { this.doggoSelected.emit(doggo.id); } } @Component({ 1 selector: 'app-doggo-list', 2 templateUrl: './doggo-list.component.html', 3 styleUrls: ['./doggo-list.component.scss'], 4 changeDetection: ChangeDetectionStrategy.OnPush, 5 }) 6 7 8 9 10 11 12 13 14 15 doggos = input([]); doggoSelected = output(); @Component({ 1 selector: 'app-doggo-list', 2 templateUrl: './doggo-list.component.html', 3 styleUrls: ['./doggo-list.component.scss'], 4 changeDetection: ChangeDetectionStrategy.OnPush, 5 }) 6 export class DoggoListComponent { 7 8 9 10 11 selectDoggo(doggo: Doggo): void { 12 this.doggoSelected.emit(doggo.id); 13 } 14 } 15

Slide 31

Slide 31 text

Easier to test @Component({ selector: 'app-doggo-list', templateUrl: './doggo-list.component.html', styleUrls: ['./doggo-list.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class DoggoListComponent { doggos = input([]); doggoSelected = output(); selectDoggo(doggo: Doggo): void { this.doggoSelected.emit(doggo.id); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Component({ selector: 'app-doggo-list', templateUrl: './doggo-list.component.html', styleUrls: ['./doggo-list.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) 1 2 3 4 5 6 export class DoggoListComponent { 7 doggos = input([]); 8 9 doggoSelected = output(); 10 11 selectDoggo(doggo: Doggo): void { 12 this.doggoSelected.emit(doggo.id); 13 } 14 } 15 export class DoggoListComponent { doggos = input([]); doggoSelected = output(); selectDoggo(doggo: Doggo): void { this.doggoSelected.emit(doggo.id); } } @Component({ 1 selector: 'app-doggo-list', 2 templateUrl: './doggo-list.component.html', 3 styleUrls: ['./doggo-list.component.scss'], 4 changeDetection: ChangeDetectionStrategy.OnPush, 5 }) 6 7 8 9 10 11 12 13 14 15 doggos = input([]); doggoSelected = output(); @Component({ 1 selector: 'app-doggo-list', 2 templateUrl: './doggo-list.component.html', 3 styleUrls: ['./doggo-list.component.scss'], 4 changeDetection: ChangeDetectionStrategy.OnPush, 5 }) 6 export class DoggoListComponent { 7 8 9 10 11 selectDoggo(doggo: Doggo): void { 12 this.doggoSelected.emit(doggo.id); 13 } 14 } 15 selectDoggo(doggo: Doggo): void { this.doggoSelected.emit(doggo.id); } @Component({ 1 selector: 'app-doggo-list', 2 templateUrl: './doggo-list.component.html', 3 styleUrls: ['./doggo-list.component.scss'], 4 changeDetection: ChangeDetectionStrategy.OnPush, 5 }) 6 export class DoggoListComponent { 7 doggos = input([]); 8 9 doggoSelected = output(); 10 11 12 13 14 } 15

Slide 32

Slide 32 text

Easier to test describe('DoggoListComponent', () => { let component: DoggoListComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [DoggoListComponent], }).compileComponents(); fixture = TestBed.createComponent(DoggoListComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

Slide 33

Slide 33 text

Easier to test describe('DoggoListComponent', () => { let component: DoggoListComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [DoggoListComponent], }).compileComponents(); fixture = TestBed.createComponent(DoggoListComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 beforeEach(async () => { await TestBed.configureTestingModule({ imports: [DoggoListComponent], }).compileComponents(); describe('DoggoListComponent', () => { 1 let component: DoggoListComponent; 2 let fixture: ComponentFixture; 3 4 5 6 7 8 9 fixture = TestBed.createComponent(DoggoListComponent); 10 component = fixture.componentInstance; 11 fixture.detectChanges(); 12 }); 13 14 it('should create', () => { 15 expect(component).toBeTruthy(); 16 }); 17 18

Slide 34

Slide 34 text

Easier to test describe('DoggoListComponent', () => { let component: DoggoListComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [DoggoListComponent], }).compileComponents(); fixture = TestBed.createComponent(DoggoListComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 beforeEach(async () => { await TestBed.configureTestingModule({ imports: [DoggoListComponent], }).compileComponents(); describe('DoggoListComponent', () => { 1 let component: DoggoListComponent; 2 let fixture: ComponentFixture; 3 4 5 6 7 8 9 fixture = TestBed.createComponent(DoggoListComponent); 10 component = fixture.componentInstance; 11 fixture.detectChanges(); 12 }); 13 14 it('should create', () => { 15 expect(component).toBeTruthy(); 16 }); 17 18 it('should emit event when method is called', () => { // arrange const spy = jest.spyOn(component.doggoSelected, 'emit'); // act component.selectDoggo({ id: 'my-id' } as Doggo); // assert expect(spy).toHaveBeenCalled(); }); fixture.detectChanges(); 12 }); 13 14 it('should create', () => { 15 expect(component).toBeTruthy(); 16 }); 17 18 19 20 21 22 23 24 25 26 27 28 }) 29

Slide 35

Slide 35 text

Easier to test describe('DoggoListComponent', () => { let component: DoggoListComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [DoggoListComponent], }).compileComponents(); fixture = TestBed.createComponent(DoggoListComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 beforeEach(async () => { await TestBed.configureTestingModule({ imports: [DoggoListComponent], }).compileComponents(); describe('DoggoListComponent', () => { 1 let component: DoggoListComponent; 2 let fixture: ComponentFixture; 3 4 5 6 7 8 9 fixture = TestBed.createComponent(DoggoListComponent); 10 component = fixture.componentInstance; 11 fixture.detectChanges(); 12 }); 13 14 it('should create', () => { 15 expect(component).toBeTruthy(); 16 }); 17 18 describe('DoggoListComponent', () => { 1 let component: DoggoListComponent; 2 let fixture: ComponentFixture; 3 4 beforeEach(async () => { 5 await TestBed.configureTestingModule({ 6 imports: [DoggoListComponent], 7 }).compileComponents(); 8 9 fixture = TestBed.createComponent(DoggoListComponent); 10 component = fixture.componentInstance; 11 fixture.detectChanges(); 12 }); 13 14 it('should create', () => { 15 expect(component).toBeTruthy(); 16 }); 17 18 describe('DoggoListComponent', () => { let component: DoggoListComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [DoggoListComponent], }).compileComponents(); fixture = TestBed.createComponent(DoggoListComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

Slide 36

Slide 36 text

Easier to test @Component({ selector: 'app-main-doggo', templateUrl: './main-doggo.component.html', styleUrls: ['./main-doggo.component.scss'], imports: [DoggoListComponent, DoggoRateComponent], }) export class MainDoggoComponent implements OnInit { doggoId = input(''); service1 = inject(Service1); service2 = inject(Service2); service3 = inject(Service3); service4 = inject(Service4); service5 = inject(Service5); private readonly destroyRef = inject(DestroyRef); ngOnInit(): void { 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

Slide 37

Slide 37 text

Easier to test @Component({ selector: 'app-main-doggo', templateUrl: './main-doggo.component.html', styleUrls: ['./main-doggo.component.scss'], imports: [DoggoListComponent, DoggoRateComponent], }) export class MainDoggoComponent implements OnInit { doggoId = input(''); service1 = inject(Service1); service2 = inject(Service2); service3 = inject(Service3); service4 = inject(Service4); service5 = inject(Service5); private readonly destroyRef = inject(DestroyRef); ngOnInit(): void { 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 service1 = inject(Service1); service2 = inject(Service2); service3 = inject(Service3); service4 = inject(Service4); service5 = inject(Service5); templateUrl: ./main-doggo.component.html , 3 styleUrls: ['./main-doggo.component.scss'], 4 imports: [DoggoListComponent, DoggoRateComponent], 5 }) 6 export class MainDoggoComponent implements OnInit { 7 doggoId = input(''); 8 9 10 11 12 13 14 15 private readonly destroyRef = inject(DestroyRef); 16 17 ngOnInit(): void { 18 this.service1.myMethod().subscribe(()=> ...); 19 this.service2.myMethod().subscribe(()=> ...); 20 // 21

Slide 38

Slide 38 text

Easier to test @Component({ selector: 'app-main-doggo', templateUrl: './main-doggo.component.html', styleUrls: ['./main-doggo.component.scss'], imports: [DoggoListComponent, DoggoRateComponent], }) export class MainDoggoComponent implements OnInit { doggoId = input(''); service1 = inject(Service1); service2 = inject(Service2); service3 = inject(Service3); service4 = inject(Service4); service5 = inject(Service5); private readonly destroyRef = inject(DestroyRef); ngOnInit(): void { 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 service1 = inject(Service1); service2 = inject(Service2); service3 = inject(Service3); service4 = inject(Service4); service5 = inject(Service5); @Component({ 1 selector: 'app-main-doggo', 2 templateUrl: './main-doggo.component.html', 3 styleUrls: ['./main-doggo.component.scss'], 4 imports: [DoggoListComponent, DoggoRateComponent], 5 }) 6 export class MainDoggoComponent implements OnInit { 7 doggoId = input(''); 8 9 10 11 12 13 14 15 private readonly destroyRef = inject(DestroyRef); 16 17 ngOnInit(): void { 18 ngOnInit(): void { this.service1.myMethod().subscribe(()=> ...); this.service2.myMethod().subscribe(()=> ...); // ... this.destroyRef.onDestroy(() => { this.store.stopListeningToRealtimeDoggoEvents(); }); } service4 = inject(Service4); 13 service5 = inject(Service5); 14 15 private readonly destroyRef = inject(DestroyRef); 16 17 18 19 20 21 22 23 24 25 26 27 rateDoggo(rating: number): void { 28 this.service3.myMethod().subscribe(()=> ...); 29 } 30

Slide 39

Slide 39 text

Easier to test @Component({ selector: 'app-main-doggo', templateUrl: './main-doggo.component.html', styleUrls: ['./main-doggo.component.scss'], imports: [DoggoListComponent, DoggoRateComponent], }) export class MainDoggoComponent implements OnInit { doggoId = input(''); service1 = inject(Service1); service2 = inject(Service2); service3 = inject(Service3); service4 = inject(Service4); service5 = inject(Service5); private readonly destroyRef = inject(DestroyRef); ngOnInit(): void { 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 service1 = inject(Service1); service2 = inject(Service2); service3 = inject(Service3); service4 = inject(Service4); service5 = inject(Service5); @Component({ 1 selector: 'app-main-doggo', 2 templateUrl: './main-doggo.component.html', 3 styleUrls: ['./main-doggo.component.scss'], 4 imports: [DoggoListComponent, DoggoRateComponent], 5 }) 6 export class MainDoggoComponent implements OnInit { 7 doggoId = input(''); 8 9 10 11 12 13 14 15 private readonly destroyRef = inject(DestroyRef); 16 17 ngOnInit(): void { 18 ngOnInit(): void { @Component({ 1 selector: 'app-main-doggo', 2 templateUrl: './main-doggo.component.html', 3 styleUrls: ['./main-doggo.component.scss'], 4 imports: [DoggoListComponent, DoggoRateComponent], 5 }) 6 export class MainDoggoComponent implements OnInit { 7 doggoId = input(''); 8 9 service1 = inject(Service1); 10 service2 = inject(Service2); 11 service3 = inject(Service3); 12 service4 = inject(Service4); 13 service5 = inject(Service5); 14 15 private readonly destroyRef = inject(DestroyRef); 16 17 18 rateDoggo(rating: number): void { this.service3.myMethod().subscribe(()=> ...); } skipDoggo(): void { this.service4.myMethod().subscribe(()=> ...); } selectDoggo(id: string): void { this.service5.myMethod().subscribe(()=> ...); } 22 this.destroyRef.onDestroy(() => { 23 this.store.stopListeningToRealtimeDoggoEvents(); 24 }); 25 } 26 27 28 29 30 31 32 33 34 35 36 37 38 } 39

Slide 40

Slide 40 text

Easier to test describe('MainDoggoComponent', () => { let component: MainDoggoComponent; let fixture: ComponentFixture; let service1: Service1; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ MainDoggoComponent, MockComponent(DoggoListComponent), MockComponent(DoggoRateComponent), ], providers: [ MockProvider(Service1), MockProvider(Service2), MockProvider(Service3), MockProvider(Service4), 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

Slide 41

Slide 41 text

Easier to test describe('MainDoggoComponent', () => { let component: MainDoggoComponent; let fixture: ComponentFixture; let service1: Service1; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ MainDoggoComponent, MockComponent(DoggoListComponent), MockComponent(DoggoRateComponent), ], providers: [ MockProvider(Service1), MockProvider(Service2), MockProvider(Service3), MockProvider(Service4), 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 let service1: Service1; describe('MainDoggoComponent', () => { 1 let component: MainDoggoComponent; 2 let fixture: ComponentFixture; 3 4 5 6 beforeEach(async () => { 7 await TestBed.configureTestingModule({ 8 imports: [ 9 MainDoggoComponent, 10 MockComponent(DoggoListComponent), 11 MockComponent(DoggoRateComponent), 12 ], 13 providers: [ 14 MockProvider(Service1), 15 MockProvider(Service2), 16 MockProvider(Service3), 17 MockProvider(Service4), 18

Slide 42

Slide 42 text

Easier to test describe('MainDoggoComponent', () => { let component: MainDoggoComponent; let fixture: ComponentFixture; let service1: Service1; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ MainDoggoComponent, MockComponent(DoggoListComponent), MockComponent(DoggoRateComponent), ], providers: [ MockProvider(Service1), MockProvider(Service2), MockProvider(Service3), MockProvider(Service4), 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 let service1: Service1; describe('MainDoggoComponent', () => { 1 let component: MainDoggoComponent; 2 let fixture: ComponentFixture; 3 4 5 6 beforeEach(async () => { 7 await TestBed.configureTestingModule({ 8 imports: [ 9 MainDoggoComponent, 10 MockComponent(DoggoListComponent), 11 MockComponent(DoggoRateComponent), 12 ], 13 providers: [ 14 MockProvider(Service1), 15 MockProvider(Service2), 16 MockProvider(Service3), 17 MockProvider(Service4), 18 MainDoggoComponent, MockComponent(DoggoListComponent), MockComponent(DoggoRateComponent), let component: MainDoggoComponent; 2 let fixture: ComponentFixture; 3 4 let service1: Service1; 5 6 beforeEach(async () => { 7 await TestBed.configureTestingModule({ 8 imports: [ 9 10 11 12 ], 13 providers: [ 14 MockProvider(Service1), 15 MockProvider(Service2), 16 MockProvider(Service3), 17 MockProvider(Service4), 18 MockProvider(Service5), 19 provideRouter([]) 20

Slide 43

Slide 43 text

Easier to test describe('MainDoggoComponent', () => { let component: MainDoggoComponent; let fixture: ComponentFixture; let service1: Service1; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ MainDoggoComponent, MockComponent(DoggoListComponent), MockComponent(DoggoRateComponent), ], providers: [ MockProvider(Service1), MockProvider(Service2), MockProvider(Service3), MockProvider(Service4), 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 let service1: Service1; describe('MainDoggoComponent', () => { 1 let component: MainDoggoComponent; 2 let fixture: ComponentFixture; 3 4 5 6 beforeEach(async () => { 7 await TestBed.configureTestingModule({ 8 imports: [ 9 MainDoggoComponent, 10 MockComponent(DoggoListComponent), 11 MockComponent(DoggoRateComponent), 12 ], 13 providers: [ 14 MockProvider(Service1), 15 MockProvider(Service2), 16 MockProvider(Service3), 17 MockProvider(Service4), 18 MainDoggoComponent, MockComponent(DoggoListComponent), MockComponent(DoggoRateComponent), describe('MainDoggoComponent', () => { 1 let component: MainDoggoComponent; 2 let fixture: ComponentFixture; 3 4 let service1: Service1; 5 6 beforeEach(async () => { 7 await TestBed.configureTestingModule({ 8 imports: [ 9 10 11 12 ], 13 providers: [ 14 MockProvider(Service1), 15 MockProvider(Service2), 16 MockProvider(Service3), 17 MockProvider(Service4), 18 providers: [ MockProvider(Service1), MockProvider(Service2), MockProvider(Service3), MockProvider(Service4), MockProvider(Service5), provideRouter([]), ], imports: [ 9 MainDoggoComponent, 10 MockComponent(DoggoListComponent), 11 MockComponent(DoggoRateComponent), 12 ], 13 14 15 16 17 18 19 20 21 }).compileComponents(); 22 23 fixture = TestBed.createComponent(MainDoggoComponent); 24 component = fixture.componentInstance; 25

Slide 44

Slide 44 text

Easier to test describe('MainDoggoComponent', () => { let component: MainDoggoComponent; let fixture: ComponentFixture; let service1: Service1; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ MainDoggoComponent, MockComponent(DoggoListComponent), MockComponent(DoggoRateComponent), ], providers: [ MockProvider(Service1), MockProvider(Service2), MockProvider(Service3), MockProvider(Service4), 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 let service1: Service1; describe('MainDoggoComponent', () => { 1 let component: MainDoggoComponent; 2 let fixture: ComponentFixture; 3 4 5 6 beforeEach(async () => { 7 await TestBed.configureTestingModule({ 8 imports: [ 9 MainDoggoComponent, 10 MockComponent(DoggoListComponent), 11 MockComponent(DoggoRateComponent), 12 ], 13 providers: [ 14 MockProvider(Service1), 15 MockProvider(Service2), 16 MockProvider(Service3), 17 MockProvider(Service4), 18 MainDoggoComponent, MockComponent(DoggoListComponent), MockComponent(DoggoRateComponent), describe('MainDoggoComponent', () => { 1 let component: MainDoggoComponent; 2 let fixture: ComponentFixture; 3 4 let service1: Service1; 5 6 beforeEach(async () => { 7 await TestBed.configureTestingModule({ 8 imports: [ 9 10 11 12 ], 13 providers: [ 14 MockProvider(Service1), 15 MockProvider(Service2), 16 MockProvider(Service3), 17 MockProvider(Service4), 18 providers: [ MockProvider(Service1), MockProvider(Service2), MockProvider(Service3), MockProvider(Service4), describe('MainDoggoComponent', () => { 1 let component: MainDoggoComponent; 2 let fixture: ComponentFixture; 3 4 let service1: Service1; 5 6 beforeEach(async () => { 7 await TestBed.configureTestingModule({ 8 imports: [ 9 MainDoggoComponent, 10 MockComponent(DoggoListComponent), 11 MockComponent(DoggoRateComponent), 12 ], 13 14 15 16 17 18 fixture = TestBed.createComponent(MainDoggoComponent); component = fixture.componentInstance; service1 = TestBed.inject(Service1); jest.spyOn(service1, "myMethod").mockReturnValue(of(...)) // Make everything ready fixture.detectChanges(); MockProvider(Service5), 19 provideRouter([]), 20 ], 21 }).compileComponents(); 22 23 24 25 26 27 28 29 30 31 32 }); 33 34 it('should create ', () => { 35 expect(component).toBeTruthy(); 36

Slide 45

Slide 45 text

Easier to test provides Overview

Slide 46

Slide 46 text

Easier to test provides Overview Exceptions

Slide 47

Slide 47 text

Exceptions

Slide 48

Slide 48 text

Exceptions Too much Bubbling

Slide 49

Slide 49 text

Exceptions Too much Bubbling Shell Component

Slide 50

Slide 50 text

Components

Slide 51

Slide 51 text

Components Inheritance of

Slide 52

Slide 52 text

@Component({ /* ... */ }) export class UserBaseComponent { // Shared Logic // Shared Inputs // ... } @Component({ /* ... */ }) export class AddUserComponent extends UserBaseComponent { // ... } @Component({ /* ... */ }) export class EditUserComponent extends UserBaseComponent { // ... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

Slide 53

Slide 53 text

No Single Responsibility Hard to Test Complexity

Slide 54

Slide 54 text

@Injectable({ providedIn: "root" }) export class UserService { // Shared Logic // ... } @Component({ /* ... */ }) export class AddUserComponent { private userService = inject(UserService); // ... } @Component({ /* ... */ }) export class EditUserComponent { private userService = inject(UserService); // ... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

Slide 55

Slide 55 text

Composition over inheritance

Slide 56

Slide 56 text

Composition over inheritance USe dependency INjection

Slide 57

Slide 57 text

Composition over inheritance USe dependency INjection Easier to Read, understand & Test

Slide 58

Slide 58 text

Code Organization

Slide 59

Slide 59 text

Code Organization Beauty of

Slide 60

Slide 60 text

project-root/ ├── app/ │ ├── header.component.ts │ ├── footer.component.ts │ ├── data.service.ts │ ├── utils.ts │ ├── old/ │ │ ├── old-helper.js │ │ └── unused-file.md │ ├── temp.component.ts │ ├── feature/ │ │ ├── feature.component.ts │ │ ├── styles.css │ │ └── random-script.js │ ├── helper.ts │ └── app.module.ts └── main.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

Slide 61

Slide 61 text

project-root/ └── src/ ├── app/ │ ├── features/ │ │ ├── user/ │ │ │ ├── container │ │ │ ├── ui │ │ │ └── ... │ │ ├── dashboard/ │ │ │ ├── container │ │ │ ├── ui │ │ │ └── ... │ │ └── shopping-cart/ │ │ ├── container │ │ ├── ui │ │ └── ... │ └── shared/ │ ├── util-auth │ ├── util-logging │ └── ... └── main.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

Slide 62

Slide 62 text

Easy to follow

Slide 63

Slide 63 text

Easy to follow Folders & Levels provide information!

Slide 64

Slide 64 text

Easy to follow Folders & Levels provide information! Lightweight and scalable

Slide 65

Slide 65 text

Architecture Route based

Slide 66

Slide 66 text

export const APP_ROUTES = [ ]; 1 2 3 project-root/ └── src/ ├── app/ │ └── shared/ │ ├── util-auth │ ├── util-logging │ └── ... └── main.ts 1 2 3 4 5 6 7 8

Slide 67

Slide 67 text

export const APP_ROUTES = [ { path: 'users', // ... }, ]; 1 2 3 4 5 6 project-root/ └── src/ ├── app/ │ ├── features/ │ │ ├── users/ │ └── shared/ │ ├── util-auth │ ├── util-logging │ └── ... └── main.ts 1 2 3 4 5 6 7 8 9 10

Slide 68

Slide 68 text

export const APP_ROUTES = [ { path: 'users', // ... }, { path: 'dashboard', // ... }, ]; 1 2 3 4 5 6 7 8 9 10 project-root/ └── src/ ├── app/ │ ├── features/ │ │ ├── users/ │ │ ├── dashboard/ │ └── shared/ │ ├── util-auth │ ├── util-logging │ └── ... └── main.ts 1 2 3 4 5 6 7 8 9 10 11

Slide 69

Slide 69 text

export const APP_ROUTES = [ { path: 'users', // ... }, { path: 'dashboard', // ... }, { path: 'shopping-cart', // ... }, ]; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 project-root/ └── src/ ├── app/ │ ├── features/ │ │ ├── users/ │ │ ├── dashboard/ │ │ └── shopping-cart/ │ └── shared/ │ ├── util-auth │ ├── util-logging │ └── ... └── main.ts 1 2 3 4 5 6 7 8 9 10 11 12

Slide 70

Slide 70 text

export const APP_ROUTES = [ { path: 'users', // ... }, { path: 'dashboard', // ... }, { path: 'shopping-cart', // ... }, ]; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 project-root/ └── src/ ├── app/ │ ├── features/ │ │ ├── users/ │ │ │ ├── container │ │ │ ├── ui │ │ │ └── ... │ │ ├── dashboard/ │ │ │ ├── container │ │ │ ├── ui │ │ │ └── ... │ │ └── shopping-cart/ │ │ ├── container │ │ ├── ui │ │ └── ... │ └── shared/ │ ├── util-auth │ ├── util-logging │ └── ... └── main.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

Slide 71

Slide 71 text

Forms

Slide 72

Slide 72 text

"right" Form Type Template Reactive

Slide 73

Slide 73 text

"right" Form Type Signal based

Slide 74

Slide 74 text

Typed Forms @Component({ /* ... */ }) export class FormGroupComponent { private readonly formBuilder = inject(FormBuilder); myForm = this.formBuilder.group({ // ... age: this.formBuilder.control(0), }); onSubmit() { // both values of the form are typed here console.log(this.myForm.getRawValue()); console.log(this.myForm.value); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

Slide 75

Slide 75 text

Typed Forms @Component({ /* ... */ }) export class FormGroupComponent { private readonly formBuilder = inject(FormBuilder); myForm = this.formBuilder.group({ // ... age: this.formBuilder.control(0), }); onSubmit() { // both values of the form are typed here console.log(this.myForm.getRawValue()); console.log(this.myForm.value); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 myForm = this.formBuilder.group({ // ... age: this.formBuilder.control(0), }); @Component({ 1 /* ... */ 2 }) 3 export class FormGroupComponent { 4 private readonly formBuilder = inject(FormBuilder); 5 6 7 8 9 10 11 onSubmit() { 12 // both values of the form are typed here 13 console.log(this.myForm.getRawValue()); 14 console.log(this.myForm.value); 15 } 16 } 17

Slide 76

Slide 76 text

Typed Forms @Component({ /* ... */ }) export class FormGroupComponent { private readonly formBuilder = inject(FormBuilder); myForm = this.formBuilder.group({ // ... age: this.formBuilder.control(0), }); onSubmit() { // both values of the form are typed here console.log(this.myForm.getRawValue()); console.log(this.myForm.value); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 myForm = this.formBuilder.group({ // ... age: this.formBuilder.control(0), }); @Component({ 1 /* ... */ 2 }) 3 export class FormGroupComponent { 4 private readonly formBuilder = inject(FormBuilder); 5 6 7 8 9 10 11 onSubmit() { 12 // both values of the form are typed here 13 console.log(this.myForm.getRawValue()); 14 console.log(this.myForm.value); 15 } 16 } 17 onSubmit() { // both values of the form are typed here console.log(this.myForm.getRawValue()); console.log(this.myForm.value); } @Component({ 1 /* ... */ 2 }) 3 export class FormGroupComponent { 4 private readonly formBuilder = inject(FormBuilder); 5 6 myForm = this.formBuilder.group({ 7 // ... 8 age: this.formBuilder.control(0), 9 }); 10 11 12 13 14 15 16 } 17

Slide 77

Slide 77 text

Abstract Control API

Slide 78

Slide 78 text

value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14

Slide 79

Slide 79 text

value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 status: FormControlStatus value: any | T 1 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14

Slide 80

Slide 80 text

value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 status: FormControlStatus value: any | T 1 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 valid: boolean invalid: boolean pending: boolean value: any | T 1 status: FormControlStatus 2 3 4 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14

Slide 81

Slide 81 text

value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 status: FormControlStatus value: any | T 1 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 valid: boolean invalid: boolean pending: boolean value: any | T 1 status: FormControlStatus 2 3 4 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 disabled: boolean enabled: boolean value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 6 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14

Slide 82

Slide 82 text

value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 status: FormControlStatus value: any | T 1 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 valid: boolean invalid: boolean pending: boolean value: any | T 1 status: FormControlStatus 2 3 4 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 disabled: boolean enabled: boolean value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 6 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 errors: ValidationErrors | null value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14

Slide 83

Slide 83 text

value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 status: FormControlStatus value: any | T 1 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 valid: boolean invalid: boolean pending: boolean value: any | T 1 status: FormControlStatus 2 3 4 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 disabled: boolean enabled: boolean value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 6 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 errors: ValidationErrors | null value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 pristine: boolean dirty: boolean touched: boolean untouched: boolean value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 9 10 11 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14

Slide 84

Slide 84 text

value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 status: FormControlStatus value: any | T 1 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 valid: boolean invalid: boolean pending: boolean value: any | T 1 status: FormControlStatus 2 3 4 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 disabled: boolean enabled: boolean value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 6 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 errors: ValidationErrors | null value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 pristine: boolean dirty: boolean touched: boolean untouched: boolean value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 9 10 11 12 valueChanges: Observable<...> 13 statusChanges: Observable<...> 14 valueChanges: Observable<...> statusChanges: Observable<...> value: any | T 1 status: FormControlStatus 2 valid: boolean 3 invalid: boolean 4 pending: boolean 5 disabled: boolean 6 enabled: boolean 7 errors: ValidationErrors | null 8 pristine: boolean 9 dirty: boolean 10 touched: boolean 11 untouched: boolean 12 13 14

Slide 85

Slide 85 text

Validation

Slide 86

Slide 86 text

Validation .State .Errors

Slide 87

Slide 87 text

Validation Errors { required: true; } 1 2 3

Slide 88

Slide 88 text

Validation Errors export class AgeValidator { static ageValidator(control: AbstractControl) { if (control.value < 0) { return { ageNotValid: true }; } if (control.value > 100) { return { ageNotValid: true }; } return null; } } 1 2 3 4 5 6 7 8 9 10 11 12 13

Slide 89

Slide 89 text

Validation Errors export class AgeValidator { static ageValidator(control: AbstractControl) { if (control.value < 0) { return { ageNotValid: true }; } if (control.value > 100) { return { ageNotValid: true }; } return null; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 return { ageNotValid: true }; return { ageNotValid: true }; export class AgeValidator { 1 static ageValidator(control: AbstractControl) { 2 if (control.value < 0) { 3 4 } 5 6 if (control.value > 100) { 7 8 } 9 10 return null; 11 } 12 } 13

Slide 90

Slide 90 text

Validation Errors { required: true, ageNotValid: true } 1 2 3 4

Slide 91

Slide 91 text

Validation @Component({ /* ... */ }) export class FormGroupComponent { private readonly formBuilder = inject(FormBuilder); myForm = this.formBuilder.group({ // ... age: this.formBuilder.control(-10, [AgeValidator.ageValidator]), }); } 1 2 3 4 5 6 7 8 9 10 11

Slide 92

Slide 92 text

Validation @Component({ /* ... */ }) export class FormGroupComponent { private readonly formBuilder = inject(FormBuilder); myForm = this.formBuilder.group({ // ... age: this.formBuilder.control(-10, [AgeValidator.ageValidator]), }); } 1 2 3 4 5 6 7 8 9 10 11 `myForm` is `INVALID` `myForm.errors` is `null` `myForm.get('age').errors` would be `{ ageNotValid: true }` `myForm.get('age').errors` is `INVALID` 1 2 3 4

Slide 93

Slide 93 text

Cross Control Validation @Component({ /* ... */ }) export class FormGroupComponent { private readonly formBuilder = inject(FormBuilder); myForm = this.formBuilder.group( { // ... age: this.formBuilder.control(-10, [AgeValidator.ageValidator]), }, { validators: [RestrictAgeRoomValidator.ageRoomValidator], } ); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

Slide 94

Slide 94 text

Cross Control Validation @Component({ /* ... */ }) export class FormGroupComponent { private readonly formBuilder = inject(FormBuilder); myForm = this.formBuilder.group( { // ... age: this.formBuilder.control(-10, [AgeValidator.ageValidator]), }, { validators: [RestrictAgeRoomValidator.ageRoomValidator], } ); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 myForm = this.formBuilder.group( { validators: [RestrictAgeRoomValidator.ageRoomValidator], } ); @Component({ 1 /* ... */ 2 }) 3 export class FormGroupComponent { 4 private readonly formBuilder = inject(FormBuilder); 5 6 7 { 8 // ... 9 age: this.formBuilder.control(-10, [AgeValidator.ageValidator]), 10 }, 11 12 13 14 15 } 16

Slide 95

Slide 95 text

Cross Control Validation export class RestrictAgeRoomValidator { static ageRoomValidator(formGroup: AbstractControl) { const ageControl = formGroup.get("age"); const roomControl = formGroup.get("room"); if (!ageControl || !roomControl) { return null; } // return error or null } } 1 2 3 4 5 6 7 8 9 10 11 12 `myForm` is `INVALID` `myForm.errors` is `{ ... }` `myForm.get('age').errors` would be `...` `myForm.get('age').errors` is `...` 1 2 3 4

Slide 96

Slide 96 text

State

Slide 97

Slide 97 text

State IMpact of

Slide 98

Slide 98 text

You have to deal with State

Slide 99

Slide 99 text

@Component({ selector: 'app-root', template: ` Increase counter {{ counter }} `, }) export class App { counter = 0; increaseCounter(){ this.counter++; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

Slide 100

Slide 100 text

@Component({ selector: 'app-root', template: ` Increase counter {{ counter }} `, }) export class App { counter = 0; increaseCounter(){ this.counter++; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 {{ counter }} counter = 0; @Component({ 1 selector: 'app-root', 2 template: ` 3 4 Increase counter 5 6 7 `, 8 }) 9 export class App { 10 11 12 increaseCounter(){ 13 this.counter++; 14 } 15 } 16

Slide 101

Slide 101 text

DOM IS State

Slide 102

Slide 102 text

@Component({ selector: 'app-root', template: ` Increase counter {{ counter() }} `, }) export class App { counter = signal(0); increaseCounter(){ this.counter.update((counter) => counter+1) } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

Slide 103

Slide 103 text

@Component({ selector: 'app-root', template: ` Increase counter {{ counter() }} `, }) export class App { counter = signal(0); increaseCounter(){ this.counter.update((counter) => counter+1) } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 {{ counter() }} counter = signal(0); @Component({ 1 selector: 'app-root', 2 template: ` 3 4 Increase counter 5 6 7 `, 8 }) 9 export class App { 10 11 12 increaseCounter(){ 13 this.counter.update((counter) => counter+1) 14 } 15 } 16

Slide 104

Slide 104 text

Component has State

Slide 105

Slide 105 text

export class SimpleFormControlComponent { valueAdded = output(); myControl = new FormControl("my value"); submitted() { console.log(this.myControl.value); this.valueAdded.emit(this.myControl.value); } } 1 2 3 4 5 6 7 8 9 10 11 Closed Component State

Slide 106

Slide 106 text

export class TodoMainComponent { todos: Todo[] = []; addTodo(todo: Todo) { this.todos.push(todo); } deleteTodo(todoId: number) { this.todos = this.todos.filter(...); } } 1 2 3 4 5 6 7 8 9 10 11 container Component State

Slide 107

Slide 107 text

@Injectable({ providedIn: 'root' }) export class DashboardService { private dashboard: Dashboard = ...; getDashboard(): Observable { return this.http.get('api/dashboard').pipe( tap(dashboard => this.dashboard = dashboard) ); } updateDashboard(...): void { this.dashboard = ...; // or return this.http.put('api/dashboard').pipe( tap(newDashboard => this.dashboard = newDashboard) ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Feature State

Slide 108

Slide 108 text

@Injectable({ providedIn: 'root' }) export class DashboardService { private dashboard: Dashboard = ...; getDashboard(): Observable { return this.http.get('api/dashboard').pipe( tap(dashboard => this.dashboard = dashboard) ); } updateDashboard(...): void { this.dashboard = ...; // or return this.http.put('api/dashboard').pipe( tap(newDashboard => this.dashboard = newDashboard) ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private dashboard: Dashboard = ...; @Injectable({ providedIn: 'root' }) 1 export class DashboardService { 2 3 4 getDashboard(): Observable { 5 return this.http.get('api/dashboard').pipe( 6 tap(dashboard => this.dashboard = dashboard) 7 ); 8 } 9 10 updateDashboard(...): void { 11 this.dashboard = ...; 12 13 // or 14 15 return this.http.put('api/dashboard').pipe( 16 tap(newDashboard => this.dashboard = newDashboard) 17 ); 18 } 19 } 20 Feature State

Slide 109

Slide 109 text

@Injectable({ providedIn: 'root' }) export class DashboardService { private dashboard: Dashboard = ...; getDashboard(): Observable { return this.http.get('api/dashboard').pipe( tap(dashboard => this.dashboard = dashboard) ); } updateDashboard(...): void { this.dashboard = ...; // or return this.http.put('api/dashboard').pipe( tap(newDashboard => this.dashboard = newDashboard) ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private dashboard: Dashboard = ...; @Injectable({ providedIn: 'root' }) 1 export class DashboardService { 2 3 4 getDashboard(): Observable { 5 return this.http.get('api/dashboard').pipe( 6 tap(dashboard => this.dashboard = dashboard) 7 ); 8 } 9 10 updateDashboard(...): void { 11 this.dashboard = ...; 12 13 // or 14 15 return this.http.put('api/dashboard').pipe( 16 tap(newDashboard => this.dashboard = newDashboard) 17 ); 18 } 19 } 20 tap(dashboard => this.dashboard = dashboard) @Injectable({ providedIn: 'root' }) 1 export class DashboardService { 2 private dashboard: Dashboard = ...; 3 4 getDashboard(): Observable { 5 return this.http.get('api/dashboard').pipe( 6 7 ); 8 } 9 10 updateDashboard(...): void { 11 this.dashboard = ...; 12 13 // or 14 15 return this.http.put('api/dashboard').pipe( 16 tap(newDashboard => this.dashboard = newDashboard) 17 ); 18 } 19 } 20 Feature State

Slide 110

Slide 110 text

@Injectable({ providedIn: 'root' }) export class DashboardService { private dashboard: Dashboard = ...; getDashboard(): Observable { return this.http.get('api/dashboard').pipe( tap(dashboard => this.dashboard = dashboard) ); } updateDashboard(...): void { this.dashboard = ...; // or return this.http.put('api/dashboard').pipe( tap(newDashboard => this.dashboard = newDashboard) ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private dashboard: Dashboard = ...; @Injectable({ providedIn: 'root' }) 1 export class DashboardService { 2 3 4 getDashboard(): Observable { 5 return this.http.get('api/dashboard').pipe( 6 tap(dashboard => this.dashboard = dashboard) 7 ); 8 } 9 10 updateDashboard(...): void { 11 this.dashboard = ...; 12 13 // or 14 15 return this.http.put('api/dashboard').pipe( 16 tap(newDashboard => this.dashboard = newDashboard) 17 ); 18 } 19 } 20 tap(dashboard => this.dashboard = dashboard) @Injectable({ providedIn: 'root' }) 1 export class DashboardService { 2 private dashboard: Dashboard = ...; 3 4 getDashboard(): Observable { 5 return this.http.get('api/dashboard').pipe( 6 7 ); 8 } 9 10 updateDashboard(...): void { 11 this.dashboard = ...; 12 13 // or 14 15 return this.http.put('api/dashboard').pipe( 16 tap(newDashboard => this.dashboard = newDashboard) 17 ); 18 } 19 } 20 this.dashboard = ...; @Injectable({ providedIn: 'root' }) 1 export class DashboardService { 2 private dashboard: Dashboard = ...; 3 4 getDashboard(): Observable { 5 return this.http.get('api/dashboard').pipe( 6 tap(dashboard => this.dashboard = dashboard) 7 ); 8 } 9 10 updateDashboard(...): void { 11 12 13 // or 14 15 return this.http.put('api/dashboard').pipe( 16 tap(newDashboard => this.dashboard = newDashboard) 17 ); 18 } 19 } 20 Feature State

Slide 111

Slide 111 text

@Injectable({ providedIn: 'root' }) export class DashboardService { private dashboard: Dashboard = ...; getDashboard(): Observable { return this.http.get('api/dashboard').pipe( tap(dashboard => this.dashboard = dashboard) ); } updateDashboard(...): void { this.dashboard = ...; // or return this.http.put('api/dashboard').pipe( tap(newDashboard => this.dashboard = newDashboard) ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private dashboard: Dashboard = ...; @Injectable({ providedIn: 'root' }) 1 export class DashboardService { 2 3 4 getDashboard(): Observable { 5 return this.http.get('api/dashboard').pipe( 6 tap(dashboard => this.dashboard = dashboard) 7 ); 8 } 9 10 updateDashboard(...): void { 11 this.dashboard = ...; 12 13 // or 14 15 return this.http.put('api/dashboard').pipe( 16 tap(newDashboard => this.dashboard = newDashboard) 17 ); 18 } 19 } 20 tap(dashboard => this.dashboard = dashboard) @Injectable({ providedIn: 'root' }) 1 export class DashboardService { 2 private dashboard: Dashboard = ...; 3 4 getDashboard(): Observable { 5 return this.http.get('api/dashboard').pipe( 6 7 ); 8 } 9 10 updateDashboard(...): void { 11 this.dashboard = ...; 12 13 // or 14 15 return this.http.put('api/dashboard').pipe( 16 tap(newDashboard => this.dashboard = newDashboard) 17 ); 18 } 19 } 20 this.dashboard = ...; @Injectable({ providedIn: 'root' }) 1 export class DashboardService { 2 private dashboard: Dashboard = ...; 3 4 getDashboard(): Observable { 5 return this.http.get('api/dashboard').pipe( 6 tap(dashboard => this.dashboard = dashboard) 7 ); 8 } 9 10 updateDashboard(...): void { 11 12 13 // or 14 15 return this.http.put('api/dashboard').pipe( 16 tap(newDashboard => this.dashboard = newDashboard) 17 ); 18 } 19 } 20 tap(newDashboard => this.dashboard = newDashboard) @Injectable({ providedIn: 'root' }) 1 export class DashboardService { 2 private dashboard: Dashboard = ...; 3 4 getDashboard(): Observable { 5 return this.http.get('api/dashboard').pipe( 6 tap(dashboard => this.dashboard = dashboard) 7 ); 8 } 9 10 updateDashboard(...): void { 11 this.dashboard = ...; 12 13 // or 14 15 return this.http.put('api/dashboard').pipe( 16 17 ); 18 } 19 } 20 Feature State

Slide 112

Slide 112 text

@Injectable({ providedIn: "root" }) export class AuthService { private isAuthenticated = signal(false); isAuthenticated = this.isAuthenticated.asReadOnly(); login() { // ... this.isAuthenticated.set(true); } logout() { // ... this.isAuthenticated.set(false); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Application State

Slide 113

Slide 113 text

Route has State

Slide 114

Slide 114 text

http://localhost:4200/users?page=1&count=30&sort=asc

Slide 115

Slide 115 text

State impacts Architecture

Slide 116

Slide 116 text

src/ └── app/ ├── features/ │ ├── dashboard/ │ │ └── ... │ └── profile/ │ └── ... └── shared/ ├── util-auth └── ... 1 2 3 4 5 6 7 8 9 10

Slide 117

Slide 117 text

src/ └── app/ ├── features/ │ ├── dashboard/ │ │ └── ... │ └── profile/ │ └── ... └── shared/ ├── util-auth └── ... 1 2 3 4 5 6 7 8 9 10 │ ├── dashboard/ src/ 1 └── app/ 2 ├── features/ 3 4 │ │ └── ... 5 │ └── profile/ 6 │ └── ... 7 └── shared/ 8 ├── util-auth 9 └── ... 10

Slide 118

Slide 118 text

src/ └── app/ ├── features/ │ ├── dashboard/ │ │ └── ... │ └── profile/ │ └── ... └── shared/ ├── util-auth └── ... 1 2 3 4 5 6 7 8 9 10 │ ├── dashboard/ src/ 1 └── app/ 2 ├── features/ 3 4 │ │ └── ... 5 │ └── profile/ 6 │ └── ... 7 └── shared/ 8 ├── util-auth 9 └── ... 10 │ └── profile/ src/ 1 └── app/ 2 ├── features/ 3 │ ├── dashboard/ 4 │ │ └── ... 5 6 │ └── ... 7 └── shared/ 8 ├── util-auth 9 └── ... 10

Slide 119

Slide 119 text

src/ └── app/ ├── features/ │ ├── dashboard/ │ │ └── ... │ └── profile/ │ └── ... └── shared/ ├── util-auth └── ... 1 2 3 4 5 6 7 8 9 10 │ ├── dashboard/ src/ 1 └── app/ 2 ├── features/ 3 4 │ │ └── ... 5 │ └── profile/ 6 │ └── ... 7 └── shared/ 8 ├── util-auth 9 └── ... 10 │ └── profile/ src/ 1 └── app/ 2 ├── features/ 3 │ ├── dashboard/ 4 │ │ └── ... 5 6 │ └── ... 7 └── shared/ 8 ├── util-auth 9 └── ... 10 └── shared/ ├── util-auth src/ 1 └── app/ 2 ├── features/ 3 │ ├── dashboard/ 4 │ │ └── ... 5 │ └── profile/ 6 │ └── ... 7 8 9 └── ... 10

Slide 120

Slide 120 text

"dashboard": { { 1 2 // ... 3 }, 4 "profile": { 5 // ... 6 }, 7 "shared": { 8 "auth": { ... } 9 }, 10 } 11 src/ └── app/ ├── features/ │ ├── dashboard/ │ │ └── ... │ └── profile/ │ └── ... └── shared/ ├── util-auth └── ... 1 2 3 4 5 6 7 8 9 10

Slide 121

Slide 121 text

"dashboard": { { 1 2 // ... 3 }, 4 "profile": { 5 // ... 6 }, 7 "shared": { 8 "auth": { ... } 9 }, 10 } 11 "profile": { { 1 "dashboard": { 2 // ... 3 }, 4 5 // ... 6 }, 7 "shared": { 8 "auth": { ... } 9 }, 10 } 11 src/ └── app/ ├── features/ │ ├── dashboard/ │ │ └── ... │ └── profile/ │ └── ... └── shared/ ├── util-auth └── ... 1 2 3 4 5 6 7 8 9 10

Slide 122

Slide 122 text

"dashboard": { { 1 2 // ... 3 }, 4 "profile": { 5 // ... 6 }, 7 "shared": { 8 "auth": { ... } 9 }, 10 } 11 "profile": { { 1 "dashboard": { 2 // ... 3 }, 4 5 // ... 6 }, 7 "shared": { 8 "auth": { ... } 9 }, 10 } 11 "shared": { "auth": { ... } { 1 "dashboard": { 2 // ... 3 }, 4 "profile": { 5 // ... 6 }, 7 8 9 }, 10 } 11 src/ └── app/ ├── features/ │ ├── dashboard/ │ │ └── ... │ └── profile/ │ └── ... └── shared/ ├── util-auth └── ... 1 2 3 4 5 6 7 8 9 10

Slide 123

Slide 123 text

State impacts components

Slide 124

Slide 124 text

Container & Presentational

Slide 125

Slide 125 text

Container & Presentational Makes container "Listening" to State Changes

Slide 126

Slide 126 text

Container & Presentational Makes container "Listening" to State Changes Less Logic in COmponents

Slide 127

Slide 127 text

Less Logic in COmponents

Slide 128

Slide 128 text

GET THE LOGIC Out of Components

Slide 129

Slide 129 text

export class ProductsComponent implements OnInit { private readonly store = inject(Store); readonly productsByCategories = this.store.selectSignal( selectProductsByCategories, ); ngOnInit(): void { this.store.dispatch(ProductsActions.loadProducts()); } onProductClicked(id: string): void { this.store.dispatch(ProductsActions.navigateToDetail({ id })); } onCartClicked(product: Product): void { this.store.dispatch(CheckoutActions.addProduct({ product })); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

Slide 130

Slide 130 text

export class ProductsComponent implements OnInit { private readonly store = inject(Store); readonly productsByCategories = this.store.selectSignal( selectProductsByCategories, ); ngOnInit(): void { this.store.dispatch(ProductsActions.loadProducts()); } onProductClicked(id: string): void { this.store.dispatch(ProductsActions.navigateToDetail({ id })); } onCartClicked(product: Product): void { this.store.dispatch(CheckoutActions.addProduct({ product })); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 readonly productsByCategories = this.store.selectSignal( selectProductsByCategories, ); this.store.dispatch(ProductsActions.loadProducts()); this.store.dispatch(ProductsActions.navigateToDetail({ id })); this.store.dispatch(CheckoutActions.addProduct({ product })); export class ProductsComponent implements OnInit { 1 private readonly store = inject(Store); 2 3 4 5 6 7 ngOnInit(): void { 8 9 } 10 11 onProductClicked(id: string): void { 12 13 } 14 15 onCartClicked(product: Product): void { 16 17 } 18 } 19

Slide 131

Slide 131 text

export class ProductsComponent implements OnInit { private readonly store = inject(Store); readonly productsByCategories = this.store.productsByCategories(); ngOnInit(): void { this.store.loadProducts(); } onProductClicked(id: string): void { this.store.navigateToDetail({ id }); } onCartClicked(product: Product): void { this.store.addProduct({ product }); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

Slide 132

Slide 132 text

describe('ProductsComponent', () => { let component: ProductsComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ProductsComponent], providers: [ MockProvider(Store, { productsByCategories: signal(...), loadProducts: jest.fn(), navigateToDetail: jest.fn(), addProduct: jest.fn(), }), ], }).compileComponents(); fixture = TestBed.createComponent(ProductsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

Slide 133

Slide 133 text

describe('ProductsComponent', () => { let component: ProductsComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ProductsComponent], providers: [ MockProvider(Store, { productsByCategories: signal(...), loadProducts: jest.fn(), navigateToDetail: jest.fn(), addProduct: jest.fn(), }), ], }).compileComponents(); fixture = TestBed.createComponent(ProductsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ProductsComponent], providers: [ MockProvider(Store, { productsByCategories: signal(...), loadProducts: jest.fn(), navigateToDetail: jest.fn(), addProduct: jest.fn(), }), ], }).compileComponents(); describe('ProductsComponent', () => { 1 let component: ProductsComponent; 2 let fixture: ComponentFixture; 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 fixture = TestBed.createComponent(ProductsComponent); 18 component = fixture.componentInstance; 19 fixture.detectChanges(); 20 }); 21 }); 22

Slide 134

Slide 134 text

describe('ProductsComponent', () => { let component: ProductsComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ProductsComponent], providers: [ MockProvider(Store, { productsByCategories: signal(...), loadProducts: jest.fn(), navigateToDetail: jest.fn(), addProduct: jest.fn(), }), ], }).compileComponents(); fixture = TestBed.createComponent(ProductsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ProductsComponent], providers: [ MockProvider(Store, { productsByCategories: signal(...), loadProducts: jest.fn(), navigateToDetail: jest.fn(), addProduct: jest.fn(), }), ], }).compileComponents(); describe('ProductsComponent', () => { 1 let component: ProductsComponent; 2 let fixture: ComponentFixture; 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 fixture = TestBed.createComponent(ProductsComponent); 18 component = fixture.componentInstance; 19 fixture.detectChanges(); 20 }); 21 }); 22 describe('ProductsComponent', () => { let component: ProductsComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ProductsComponent], providers: [ MockProvider(Store, { productsByCategories: signal(...), loadProducts: jest.fn(), navigateToDetail: jest.fn(), addProduct: jest.fn(), }), ], }).compileComponents(); fixture = TestBed.createComponent(ProductsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

Slide 135

Slide 135 text

Impacts State Routing

Slide 136

Slide 136 text

{ "dashboard": { // ... }, "profile": { // ... }, "shared": { "auth": { ... } }, } 1 2 3 4 5 6 7 8 9 10 11 export const APP_ROUTES: Routes = [ { path: 'dashboard', loadChildren: () => // ... }, { path: 'profile', loadChildren: () => // ... }, ]; 1 2 3 4 5 6 7 8 9 10

Slide 137

Slide 137 text

State provides Guidance

Slide 138

Slide 138 text

"Why are you the most expensive yet the cheapest?"

Slide 139

Slide 139 text

I wish I knew

Slide 140

Slide 140 text

I wish I knew Container Presentational

Slide 141

Slide 141 text

I wish I knew Code Organization

Slide 142

Slide 142 text

I wish I knew Forms & Validation

Slide 143

Slide 143 text

I wish I knew State Management

Slide 144

Slide 144 text

No content

Slide 145

Slide 145 text

Team Work

Slide 146

Slide 146 text

No content

Slide 147

Slide 147 text

Learning

Slide 148

Slide 148 text

Learning Together

Slide 149

Slide 149 text

Thank You