Skip to content

Commit

Permalink
feat: migrate on signals component
Browse files Browse the repository at this point in the history
  • Loading branch information
Nicoss54 committed May 7, 2024
1 parent ca07efd commit 6b0287d
Show file tree
Hide file tree
Showing 17 changed files with 115 additions and 105 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<section>
<ng-container *ngTemplateOutlet="headerTemplate"></ng-container>
<ng-container *ngTemplateOutlet="headerTemplate()"></ng-container>
</section>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NgTemplateOutlet } from '@angular/common';
import { Component, Input, TemplateRef } from '@angular/core';
import { Component, input, TemplateRef } from '@angular/core';
import { MatToolbar } from '@angular/material/toolbar';

@Component({
Expand All @@ -10,5 +10,5 @@ import { MatToolbar } from '@angular/material/toolbar';
imports: [NgTemplateOutlet],
})
export class HeaderComponent {
@Input() headerTemplate: TemplateRef<MatToolbar>;
headerTemplate = input.required<TemplateRef<MatToolbar>>();
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { Component, DebugElement, Input } from '@angular/core';
import { Component, DebugElement, input } from '@angular/core';
import { ComponentFixture, fakeAsync } from '@angular/core/testing';
import { MatButtonModule } from '@angular/material/button';
import { By } from '@angular/platform-browser';
Expand All @@ -16,7 +16,7 @@ import { HomeComponent } from './home.component';
standalone: true,
})
class MockCardComponent {
@Input() person: People;
person = input<People>();
}

const PEOPLE_SERVICE = {
Expand Down Expand Up @@ -57,7 +57,7 @@ describe('HomeComponent', () => {
});
test('should pass the input person', () => {
const sfeirCard: CardComponent = debugElement.query(By.css('sfeir-card')).componentInstance;
expect(sfeirCard.person).toEqual(PERSON);
expect(sfeirCard.person()).toEqual(PERSON);
});
test('should call the getRandomPerson', () => {
const spy = jest.spyOn(component, 'getRandomPerson');
Expand All @@ -80,6 +80,6 @@ describe('HomeComponent', () => {
await componentFixture.whenStable();
componentFixture.detectChanges();
const sfeirCard: CardComponent = debugElement.query(By.css('sfeir-card')).componentInstance;
expect(sfeirCard.person).toEqual(RANDOM_PERSON);
expect(sfeirCard.person()).toEqual(RANDOM_PERSON);
}));
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { fireEvent, render } from '@testing-library/angular';
import { SearchComponent } from './search.component';

const SEARCH_SPY = jest.fn();
import { fakeAsync, flush } from '@angular/core/testing';

describe('SearchComponent', () => {
let container: Element;
Expand All @@ -16,11 +15,8 @@ describe('SearchComponent', () => {
rerender: reload,
} = await render(SearchComponent, {
schemas: [NO_ERRORS_SCHEMA],
componentProperties: {
componentInputs: {
searchText: 'SFEIR',
search: {
emit: SEARCH_SPY,
} as any,
},
});
container = hostContainer;
Expand All @@ -37,12 +33,15 @@ describe('SearchComponent', () => {
});
test('should refresh the search control', async () => {
const spy = jest.spyOn(component.searchControl, 'patchValue');
await rerender({ componentProperties: { searchText: 'sfeir' } });
await rerender({ componentInputs: { searchText: 'sfeir' } });
expect(spy).toHaveBeenCalledWith('sfeir', { emitEvent: false });
});
test('should emit the search event', () => {
test('should emit the search event', fakeAsync(() => {
const element = container.querySelector<HTMLInputElement>('input[placeholder="Person Searcher"]');
fireEvent.input(element, { target: { value: 'test' } });
expect(SEARCH_SPY).toHaveBeenCalledWith('test');
});
flush();
component.search.subscribe(input => {
expect(input).toBe('test');
});
}));
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core';
import { Component, effect, input } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { Subject, takeUntil } from 'rxjs';
import { MatInputModule } from '@angular/material/input';
import { outputFromObservable } from '@angular/core/rxjs-interop';

@Component({
selector: 'sfeir-search',
Expand All @@ -10,19 +10,13 @@ import { MatInputModule } from '@angular/material/input';
standalone: true,
imports: [MatInputModule, ReactiveFormsModule],
})
export class SearchComponent implements OnChanges, OnInit {
@Input() searchText: string;
@Output() search: EventEmitter<string> = new EventEmitter();
searchControl: FormControl<string | null>;
private unsubscribe$: Subject<boolean> = new Subject();
export class SearchComponent {
searchControl: FormControl<string | null> = new FormControl(null);

ngOnChanges() {
this.searchControl
? this.searchControl.patchValue(this.searchText, { emitEvent: false })
: (this.searchControl = new FormControl(this.searchText));
}
searchText = input<string>('');
search = outputFromObservable(this.searchControl.valueChanges);

ngOnInit(): void {
this.searchControl.valueChanges.pipe(takeUntil(this.unsubscribe$)).subscribe(search => this.search.emit(search));
}
#updateSearchControlEffect = effect(() => {
this.searchControl.patchValue(this.searchText(), { emitEvent: false });
});
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { Component, DebugElement, Input, NO_ERRORS_SCHEMA, signal } from '@angular/core';
import { Component, DebugElement, input, NO_ERRORS_SCHEMA, signal } from '@angular/core';
import { ComponentFixture, fakeAsync, flush } from '@angular/core/testing';
import { MatDialog } from '@angular/material/dialog';
import { MatListModule } from '@angular/material/list';
Expand All @@ -20,7 +20,7 @@ import { PeopleComponent } from './people.component';
template: '',
})
class MockCardComponent {
@Input() person: People;
person = input<People | undefined>();
}

const PEOPLE = [{ id: '1' }, { id: '2' }] as Array<People>;
Expand Down Expand Up @@ -100,8 +100,8 @@ describe('PeopleComponent', () => {
});
test('should pass the correct person', () => {
const [sfeirCard1, sfeirCard2] = debugElement.queryAll(By.css('sfeir-card'));
expect(sfeirCard1.componentInstance.person).toEqual(PEOPLE[0]);
expect(sfeirCard2.componentInstance.person).toEqual(PEOPLE[1]);
expect(sfeirCard1.componentInstance.person()).toEqual(PEOPLE[0]);
expect(sfeirCard2.componentInstance.person()).toEqual(PEOPLE[1]);
});
test('should call the delete method', () => {
jest.spyOn(PEOPLE_STORE_SERVICE, 'deletePerson');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import { Component, input } from '@angular/core';
import { fireEvent, render } from '@testing-library/angular';
import { of } from 'rxjs';
import { PeopleService } from '../../core/providers/people.service';
Expand All @@ -12,7 +12,7 @@ import { UpdatePersonComponent } from './update-person.component';
template: '',
})
class MockFormComponent {
@Input() person: People;
person = input<People>();
}

const LOCATION = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { MatIconModule } from '@angular/material/icon';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { DisplayDirective } from '../../directives/display.directive';
import { ComponentFixture } from '@angular/core/testing';

const PEOPLE: People = {
firstname: 'John',
Expand All @@ -25,6 +26,7 @@ const PERSON_DELETE = jest.fn();

describe('CardComponent', () => {
let component: CardComponent;
let componentFixture: ComponentFixture<CardComponent>;
let debugElement: DebugElement;
let rerender: any;

Expand All @@ -33,8 +35,12 @@ describe('CardComponent', () => {
imports: [MatButtonModule],
componentImports: [NaPipe, MatCardModule, MatIconModule, MatButtonModule, CommonModule, DisplayDirective],
schemas: [NO_ERRORS_SCHEMA],
componentProperties: { _person: PEOPLE, personDelete: { emit: PERSON_DELETE } as any },
componentInputs: {
person: PEOPLE,
},
componentOutputs: { personDelete: { emit: PERSON_DELETE } as any },
});
componentFixture = fixture;
component = fixture.componentInstance;
debugElement = fixture.debugElement;
rerender = reload;
Expand Down Expand Up @@ -96,7 +102,7 @@ describe('CardComponent', () => {
});
test('should display another person', async () => {
const newPerson = { ...PEOPLE, firstname: 'Jane', lastname: 'Doe', photo: 'jane-doe.jpg' };
await rerender({ componentProperties: { _person: newPerson } });
await rerender({ componentInputs: { person: newPerson } });
const image: HTMLImageElement = screen.getByAltText('person-photo');
expect((image as any).ngSrc).toEqual(newPerson.photo);
});
Expand All @@ -111,7 +117,9 @@ describe('CardComponent', () => {
expect(PERSON_DELETE).toHaveBeenCalledWith(PEOPLE);
});
test('should not display the button edit and delete if the person is a manager', async () => {
await rerender({ componentProperties: { _person: { ...PEOPLE, isManager: true } } });
await rerender({ componentInputs: { person: { ...PEOPLE, isManager: true } } });
componentFixture.detectChanges();
await componentFixture.whenStable();
const editButton = screen.queryByTitle<HTMLAnchorElement>('Edit');
const deleteButton = screen.queryByTitle<HTMLAnchorElement>('Delete');
expect(editButton).toBeFalsy();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, Output, signal } from '@angular/core';
import { Component, input, output } from '@angular/core';
import { People } from '../../models/people.model';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
Expand All @@ -16,13 +16,8 @@ import { DatePipe, NgOptimizedImage } from '@angular/common';
imports: [MatCardModule, MatIconModule, MatButtonModule, DisplayDirective, NaPipe, RouterLink, NgOptimizedImage, DatePipe],
})
export class CardComponent {
@Input({ alias: 'person' }) set _person(person: People | undefined) {
person && this.person.set(person);
}

@Output() personDelete: EventEmitter<People> = new EventEmitter();

person = signal<People>({} as People);
person = input.required<People>();
personDelete = output<People>();

deletePerson(person: People): void {
this.personDelete.emit(person);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<div class="sfeir-input">
<mat-form-field appearance="outline">
<mat-label>{{ placeholder }}</mat-label>
<input #InputElement matInput [type]="inputType" [placeholder]="placeholder" />
<mat-label>{{ placeholder() }}</mat-label>
<input #InputElement matInput [type]="inputType()" [placeholder]="placeholder()" />
@if (userLoseFocus$()) {
<ng-content />
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, Renderer2, signal, ViewChild } from '@angular/core';
import { afterNextRender, AfterRenderPhase, Component, ElementRef, forwardRef, input, OnDestroy, Renderer2, signal, viewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { fromEvent, merge, Subject, takeUntil, tap } from 'rxjs';
import { MatFormFieldModule } from '@angular/material/form-field';
Expand All @@ -12,42 +12,46 @@ import { MatInputModule } from '@angular/material/input';
styleUrls: ['./custom-input.component.scss'],
providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CustomInputComponent), multi: true }],
})
export class CustomInputComponent implements OnInit, OnDestroy, ControlValueAccessor {
@Input() placeholder = '';
@Input() inputType = 'text';
@ViewChild('InputElement', { static: true }) inputElement: ElementRef<HTMLInputElement>;
export class CustomInputComponent implements OnDestroy, ControlValueAccessor {
placeholder = input<string>('');
inputType = input<string>('text');
inputElement = viewChild.required<ElementRef<HTMLInputElement>>('InputElement');
userLoseFocus$ = signal<boolean>(false);

private _onChange: (x: string | number) => void;
private _onTouched: () => void;
private unsubscribe$: Subject<boolean> = new Subject();

constructor(private readonly renderer: Renderer2) {}
#listener = afterNextRender(
() => {
const inputListener$ = fromEvent(this.inputElement().nativeElement, 'input').pipe(
tap(() => {
this._onChange(this.inputElement().nativeElement.value);
this._onTouched();
}),
);

ngOnInit(): void {
const inputListener$ = fromEvent(this.inputElement.nativeElement, 'input').pipe(
tap(() => {
this._onChange(this.inputElement.nativeElement.value);
this._onTouched();
}),
);

const blurListener$ = fromEvent(this.inputElement.nativeElement, 'blur').pipe(
tap(() => {
this._onTouched();
this.userLoseFocus$.set(true);
}),
);

merge(inputListener$, blurListener$).pipe(takeUntil(this.unsubscribe$)).subscribe();
}
const blurListener$ = fromEvent(this.inputElement().nativeElement, 'blur').pipe(
tap(() => {
this._onTouched();
this.userLoseFocus$.set(true);
}),
);

merge(inputListener$, blurListener$).pipe(takeUntil(this.unsubscribe$)).subscribe();
},
{ phase: AfterRenderPhase.Read },
);

constructor(private readonly renderer: Renderer2) {}

ngOnDestroy(): void {
this.unsubscribe$.next(true);
this.unsubscribe$.complete();
}

writeValue(value: string | number): void {
this.renderer.setProperty(this.inputElement.nativeElement, 'value', value ?? null);
this.renderer.setProperty(this.inputElement().nativeElement, 'value', value ?? null);
}

registerOnTouched(fn: () => void) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ describe('FormComponent', () => {
} = await render(FormComponent, {
componentImports: [MatInputModule, ReactiveFormsModule, CustomInputComponent, CommonModule, NgOptimizedImage],
schemas: [NO_ERRORS_SCHEMA],
componentProperties: {
componentInputs: {
person: null,
},
componentOutputs: {
cancel: {
emit: CANCEL_SPY,
} as any,
Expand Down Expand Up @@ -59,8 +61,10 @@ describe('FormComponent', () => {
const spy = jest.spyOn(component.personForm, 'patchValue');
const person = { lastname: 'Doe', firstname: 'John' } as People;
await reload({
componentProperties: {
componentInputs: {
person,
},
componentProperties: {
cancel: {
emit: CANCEL_SPY,
} as any,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { Component, effect, input, output } from '@angular/core';
import { People, PeopleForm } from '../../models/people.model';
import { PersonForm } from './form';
import { ReactiveFormsModule } from '@angular/forms';
Expand All @@ -14,18 +14,18 @@ import { MatButtonModule } from '@angular/material/button';
templateUrl: './form.component.html',
styleUrls: ['./form.component.scss'],
})
export class FormComponent implements OnChanges {
@Input() person: People;
@Output() cancel: EventEmitter<void> = new EventEmitter();
@Output() save: EventEmitter<PeopleForm> = new EventEmitter();
export class FormComponent {
person = input<People | undefined>();
cancel = output<void>();
save = output<PeopleForm>();
personForm = new PersonForm();

ngOnChanges(changes: SimpleChanges): void {
const { person } = changes;
if (person.currentValue !== person.previousValue) {
this.personForm.patchValue(this.person);
#updatePersonFormEffect = effect(() => {
const person = this.person();
if (person) {
this.personForm.patchValue(person);
}
}
});

onSave(): void {
this.save.emit(this.personForm.getRawValue());
Expand Down
Loading

0 comments on commit 6b0287d

Please sign in to comment.