Can't effectively re-render parent view from child component in Angular

Issue

So, I have parent component :

@Component({
    selector: 'app-root',
    template: `
        <app-header
            (filterItems)="filterItems($event)"
            [length]="items.length"
        ></app-header>
    `,
})

export class AppComponent implements OnInit {
    items: ItemT[] = [];
    constructor(private mainService: MainService) {}

    filterItems(filter: string) {
        this.items = this.mainService.filterItems(filter);
    }

    async ngOnInit() {
        this.items = await this.mainService.getItems();
    }
}

Then header:

@Component({
    selector: 'app-header',
    template: `
    <app-filtration (filterItems)="filterItems.emit($event)"></app-filtration>
    <h2>
        <span>{{ length }}</span>
        <ng-template #one>item</ng-template>
        <ng-template #many>items</ng-template>
        <ng-container *ngIf="length === 1; then one; else many"></ng-container>
    </h2>`,
})

export class HeaderComponent {
    @Input() length!: number;
    @Output() filterItems = new EventEmitter<string>();
}

Filtration-component:

@Component({
    selector: 'app-filtration',
    template: `
        <div>insert a filter here</div>
        <input
            class="input filter-input"
            [(ngModel)]="filter"
            (input)="filterItems.emit(filter)"
        />
        <button (click)="clear()"></button>
    `,
})
export class FiltrationComponent {
    constructor(private mainService: MainService) {}

    filter: 'all' | 'active' | 'done' = 'all';

    @Output() filterItems = new EventEmitter<string>();

    clear() {
        this.mainService.items = [];
    }
}

And finally, I have a service:

const ITEMS_API = 'http://localhost:3000/base';
export class MainService {
    items: ItemT[] = [];

    constructor(private http: HttpClient) {}

    async getItems() {
        const json = await fetch(ITEMS_API);
        this.items = await json.json();

        return this.items;
    }

    filterItems(filter: string) {
        if (filter === 'all') {
            return this.items;
        }
        return this.items.filter((item) => {
            return filter === 'done' ? item.done : !item.done;
        });
    }
}

So, I can emit events from child components to parent and by this way parent component props changes, and component re-renders. But I’ve to send listeners up the tree from every child to a parent.

Is there another way to do this? I was trying to do this in FiltrationComponent in "clear" function by clearing prop in the service, but it doesn’t work.

Could you help, please 🦔

Solution

Ok so, as @Edward have mentioned, I’ve got actually 2 options – ng-content (content projection, something like children in React) and Observable. I’ve chosen Observables since it seems more flexible for me.

That is a final result:

Api service:

export class MainService {
    private items$ = new BehaviorSubject<ItemT[]>([]);
    items: ItemT[] = [];

    constructor(private http: HttpClient) {}

    public init() {
        this.http.get<ItemT[]>(ITEMS_API).subscribe((items) => {
            this.items$.next(items);
            this.items = items;
        });
    }

    clear() {
        this.items$.next([]);
    }

    filter(filter: string) {
        let result;
        if (filter === 'all') {
            result = this.items;
        } else {
            result = this.items.filter((item) => {
                return filter === 'done' ? item.done : !item.done;
            });
        }
        this.items$.next(result);
    }

    getItems() {
        return this.items$;
    }
}

App component:

@Component({
    selector: 'app-root',
    template: `
        <app-header [length]="(items$ | async)?.length"></app-header>
        <div>{{ items$ | async | json }}</div>
    `,
})
export class AppComponent implements OnInit {
    items$!: Observable<ItemT[]>;
    constructor(private mainService: MainService) {}

    ngOnInit() {
        this.items$ = this.mainService.getItems();
        this.mainService.init();
    }
}

And finally, Filtration component:

@Component({
    selector: 'app-filtration',
    template: `
        <div>insert a filter here</div>
        <input
            class="input filter-input"
            [(ngModel)]="filter"
            (input)="filterItems()"
        />
        <button (click)="clear()"></button>
    `,
})
export class FiltrationComponent {
    constructor(private mainService: MainService) {}

    filter: 'all' | 'active' | 'done' = 'all';

    filterItems() {
        this.mainService.filter(this.filter);
    }
    clear() {
        this.mainService.clear();
    }
}

Eventually, I’ve created a BehaviorSubject in MainService and shared it in Filtration and App components. In App I’ve just subscribed with "async" pipe on getting new values and in Filtration I’m invoking "clear" function, which push empty array in a stream. The "filter" function itself is pushing into the stream filtered values on input change.

I’m a very new to Angular, and especially to rx.js, so please correct me if I’m wrong with my solution.

Answered By – Kirill Kazakov

This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply

(*) Required, Your email will not be published