Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to let a component delete itself on a button click with in angular

I can't make a component delete itself with angular.

I am currently learning angular and started a small greeting project for the start. How the App should work:

  1. Type your name
  2. Child component is created that is greeting you.
  3. Child component contains button to delete itself

Currently i fulfilled the first two steps and everything works fine. But i have no idea how i can make the child component delete itself. Coming from React i know, that there was the possibility to delete a "component" with the lifecycle methods somehow. Is there something similiar in angular? At the moment i can't find it, but i found the method "OnDestroy()" that is called, before a component is destroyed. But how do i destroy it properly?

Parent:

import { Component, OnInit, ViewChild, Input } from '@angular/core';

@Component({
  selector: 'app-greeter-service',
  templateUrl: './greeter-service.component.html'
})
export class GreeterServiceComponent implements OnInit {
  title = '';
  currentUser = '';
  isVisible = false;

  currentUsers: any[] = [];

  @ViewChild('newUser') inputField;

  constructor() {}

  greetingFunc(newUser : string) {
      if(newUser) {
      this.currentUsers.push(newUser);
      console.log(this.currentUsers);
      this.inputField.nativeElement.value='';
    }
  }

  ngOnInit() {
      this.title = 'Welcome to the Greeter!';
  }

}

Child:

import { Component, OnInit, Input } from '@angular/core';

@Component({
  selector: 'app-was-greeted',
  templateUrl: './was-greeted.component.html',
  styleUrls: ['./was-greeted.component.scss']
})

export class WasGreetedComponent implements OnInit {
  @Input() user: string;

  constructor() { }

  deleteMe() {
    console.log("here should be the action");
  }

  ngOnInit() {
  }
}

How i add a component to the app "dynamically":

<div class="column" *ngFor="let user of currentUsers">
    <app-was-greeted [user]="user"></app-was-greeted>
</div>

So for every "push" in the array "currentUsers" a component is created.

like image 641
Adam Avatar asked Jan 09 '19 14:01

Adam


3 Answers

As @cgTag commented there are many ways to handle this. One way is to add an @Output to your WasGreetedComponent which will emit to the parent component.

Then in you GreeterServiceComponent you can find the element in the array and remove it (remember that your array should be immutable so you want to create a new instance of the array), this will cause the ngFor to reevaluate and update the view

@Component({
  selector: 'app-was-greeted',
  templateUrl: './was-greeted.component.html',
  styleUrls: ['./was-greeted.component.scss']
})

export class WasGreetedComponent implements OnInit {
  @Input() user: string;

  @Output() delete: EventEmitter<string> = new EventEmitter();

  constructor() { }

  deleteMe() {
    console.log("here should be the action");
    this.delete.emit(user);
  }
}

Your parent component template would then subscribe to this emitter

<div class="column" *ngFor="let user of currentUsers">
    <app-was-greeted [user]="user" (delete)="deleteUser($event)"></app-was-greeted>
</div>

And the component will need to handle the deleteUser callback and remove the user from the array

@Component({
  selector: 'app-greeter-service',
  templateUrl: './greeter-service.component.html'
})
export class GreeterServiceComponent implements OnInit {
  title = '';
  currentUser = '';
  isVisible = false;

  currentUsers: any[] = [];

  @ViewChild('newUser') inputField;

  constructor() {}

  ...

  deleteUser(user: string) {
    this.currentUsers = this.currentUsers.filter(x => x !== user);
  }

}

Like I said this is just one of many ways to skin a cat. Hope this helps.

like image 132
Neil Stevens Avatar answered Oct 21 '22 04:10

Neil Stevens


I can't make a component delete itself with angular.

Let's say that the rendered HTML represents the current state of an application. There is a state where the component exists, and then there is a state where it doesn't exist. Don't think of it as an act of deleting, but that the state changed and afterwards the component is no longer rendered.

  1. Type your name

The state doesn't have any greetings.

  1. Child component is created that is greeting you.

The state has at least one getting.

  1. Child component contains button to delete itself

The state returns to not having any greetings.

Currently i fulfilled the first two steps and everything works fine. But i have no idea how i can make the child component delete itself. Coming from React i know, that there was the possibility to delete a "component" with the lifecycle methods somehow.

If you deleted the component forcefully, then the components on the page would no longer represent the current state of the application. This is fine where you have microstates and an example would be something like a modal dialog. Where the dialog itself has its own state and the displaying of the dialog has a life-cycle relative to that microstate.

I don't think this is the case given your example.

currentUsers: any[] = [];

The above is your state variable. The value of that array represents what will be rendered in the HTML. You can ask some questions and the answers help guide you.

  1. who owns the state?
  2. who can mutate that state?
  3. how do I ask the state owner to change it?

Let's answer these questions

  1. who owns the state?

The GreeterServiceComponent component is the owner of the state, and in this case the state is represented as an array. In web components we refer to this as the component's state. It is an internal thing to the component.

  1. who can mutate that state?

We only want GreeterServiceComponent to make changes to this state. Source code outside of the class should not access it directly and mutate it. We can say that the component handles the storage and life-cycle of this internal state.

  1. how do I ask the state owner to change it?

This is where we get into Angular specifics. In Angular we have multiple ways to communicate between components. In this case, we want the child component to communicate to the parent component that it should change it's internal state. We want to tell the parent not to show the greeting anymore. Each approach to this problem has it's benefits and drawbacks, and it's up to you to decide which approach works for you.

@Output() bindings

A component in Angular can have an @Output() that executes an expression in the parent component's template. This is the same as passing a callback function to the properties of a React component.

In the WasGreetedComponent you would add:

@Output()
public closed: Subject<void>() = new Subject();

deleteMe() { this.closed.next(); }

In the GreeterServiceComponent template you would change:

<div class="column" *ngFor="let user of currentUsers">
    <app-was-greeted [user]="user" 
                     (closed)="currentUsers = currentUsers.filter(u=>u!==user)">
    </app-was-greeted>
</div>

Parent injection

A child component in Angular can inject its own parent component via the constructor, and you can then notify the parent directly.

This approach by-passes the template, and any changes made in the parent might require the view to be updated. Therefore, it's recommended that the parent use ChangeDetectorRef to mark it's view as dity.

In the GreeterServiceComponent you would add:

 public deleteUser(user: any) {
      this.currentUsers = this.currentUsers.filter(u=>u !== user);
      this.changeDetectorRef.markForCheck();
 }

In the WasGreetedComponent you would add:

 constructor(parent: GreeterServiceComponent) {}
 deleteMe() { this.parent.deleteUser(this.user); }

Global state via reactive programming

The final approach uses reactive programming where observables are used that allow consumers to watch for changes in the applications state. This has the advantage that the state is owned and managed by an external service. Popular services such as Redux/NGRX/NGXS are used heavily in Angular/React/Vue frameworks. There are many advantages to using a state store, but these are difficult frameworks to master and for a small project it's often overkill. Once you start using them they are difficult to quit using.

We can create our own small version as a demonstration.

You would add a service to represent your application state.

 @Injectable({provideIn: 'root'})
 export class StateService {
      public users: BehaviorSubject<any[]> = new BehaviorSubject([]);

      public addUser(user: any) {
           this.users
             .pipe(first())
             .subject(users => this.users.next([...users, user]));
      }

      public removeUser(user: any) {
           this.users
              .pipe(first())
              .subject(users => this.users.next(users.filter(u=>u !== user)));
      }
 }

Now, in your GreeterServiceComponent it will no longer be the owner of the state. We will inject the above service and allow that service to manage it.

@Component({...})
export class GreeterServiceComponent implements OnInit {
  constructor(public state: StateService) {}
  greetingFunc(newUser : string) {
      if(newUser) {
          this.state.addUser(newUser);
      }
}

In the GreeterServiceComponent template we will use the async pipe to display the current state of users directly from the StateService. We do this because the service holds the truth about the current state. The GreeterServiceComponent component will not care about how it changes, because it does not own or manage it.

<div class="column" *ngFor="let user of state.users | async">
    <app-was-greeted [user]="user"></app-was-greeted>
</div>

The WasGreetedComponent component will use the StateService to make changes to the current state. This component does not care about it's parent component. You can move this component around in your application and it will still function correctly. This is an important benefit, because the other above approaches depend upon the structure of the DOM itself. So moving a component would require changes elsewhere to keep the component working.

@Component({...})
export class WasGreetedComponent implements OnInit {
  constructor(public state: StateService) {}
  deleteMe() {
     this.state.removeUser(this.user);
  }
like image 6
Reactgular Avatar answered Oct 21 '22 06:10

Reactgular


Let the component have an @Output parameter that the parent listens to and removes it from currentUsers, or add additional logic for the *ngFor to ignore it when drawing elements in the loop. Something like this maby?

<div class="column" *ngFor="let user of currentUsers; let i = index">
<app-was-greeted *ngIf="shouldDisplay(i)" (buttonClick)="removeMe(i)" [user]="user"> 
</app-was-greeted>
</div>
like image 1
Stol Avatar answered Oct 21 '22 06:10

Stol