Search code examples
angularmodal-dialogangular-componentsviewchildangular-dynamic-components

Angular - Dynamically adding / removing multiple instances of a single component without having to refresh the page to update the view


This is my 3rd attempt at asking this question.

This question will be long since I see no way of making it short.

Objective

What I’m trying to do is to have a simple web-app that has a header (which I’ve created as HeaderComponent) with buttons, a single page (which I’ve created as LiveSummaryComponent) that will display multiple instances of a component (which I’ve created as StatusBoxComponent) that will display different data.

The header will have a button that will open up a modal dialogue (which I’ve created as AddStatusBoxComponent) which has a form and a submit button on. When the form is submitted the form data is saved to the browsers local storage as a map data structure, but also at that point I would like to create a single instance of a StatusBoxComponent that will be displayed on the LiveSummaryComponent page.

That particular StatusBoxComponent instance should then retrieve the values in local storage to display unique values. I would also like to be able to remove StatusBoxComponent instances by clicking a button on each instance of StatusBoxComponent as well as an edit button.

Ideally I’d also like to be able to not have to refresh the page to update LiveSummaryComponent page each time a new StatusBoxComponent instance is added, edited or removed.

How it currently works

I do have this partially working at the moment thanks to an answer on this question I asked a while back, but it has limitations in that I couldn’t find a way to remove and edit information in each instance of a StatusBoxComponent. I’m only able to add when submitting the form. Also you have to refresh the page to update the LiveSummaryComponent page each time a new StatusBoxComponent instance is added which is annoying.

The way this works is that when the AddStatusBoxComponent form is submitted it only puts the form data in the local storage in a map data structure and given a unique name/key. The way in which StatusBoxComponent instances are created is that in the ngInit() function in the LiveSummaryComponent all of the local storage map data structures are iterated through and passed to an Array called myConfigs. Each Map within the myConfigs array is then accessed in a directive called DynamicStatusBoxDirective which actually creates the instances of the StatusBoxComponent using the ComponentFactoryResolver. That particular StatusBoxComponent is then populated with it’s data from the passed in config variable which comes from the myConfigs array in LiveSummaryComponent.

This way of doing it means that the DynamicStatusBoxDirective simply loads as many StatusBoxComponent instances as there are are local storage maps. And it does this within the ngInit() function so it loads everything in at once rather than dynamically adding at the click of a button, which also isn't ideal.

LiveSummaryComponent ts code:

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

@Component({
  selector: 'app-live-summary',
  templateUrl: './live-summary.component.html',
  styleUrls: ['./live-summary.component.css']
})
export class LiveSummaryComponent implements OnInit 
{
  subscription!: Subscription;
  myConfigs: any[] = [];
  localStorageKeys: string[] = ['Old', 'New', 'Current', 'Test']

  constructor() { }

  ngOnInit(): void 
  {
    for (var key of this.localStorageKeys) // iterates though each type of device
      for(let i = 1; i < 5; i++) // for each of those device types 
        if (localStorage.getItem(key+i+'Topics') != null) // if the device + topics string is not null in local storage
          this.myConfigs.push(new Map(JSON.parse(localStorage.getItem(key+i+'Topics')!))) 
        else
          console.log(key+i + ' currently not deployed');
  }
 }

LiveSummaryComponent html code:

<div class='status-box-container'>
    <ng-container *ngFor="let config of myConfigs; let i=index" appDynamicStatusBox [config]="config"></ng-container>
</div>

DynamicStatusBoxDirective ts code:

import { ComponentFactoryResolver, ComponentRef, Directive, Input, OnDestroy, OnInit, ViewContainerRef } from '@angular/core';
import { StatusBoxComponent } from '../components/status-box/status-box.component';

@Directive({selector: '[appDynamicStatusBox]'})

export class DynamicStatusBoxDirective implements OnInit, OnDestroy 
{
  @Input() config: any;
  componentRef!: ComponentRef<StatusBoxComponent>;

  constructor(private resolver: ComponentFactoryResolver, public viewContainerRef: ViewContainerRef) {}

  ngOnInit(): void 
  {
    const factory = this.resolver.resolveComponentFactory(StatusBoxComponent); 
    this.viewContainerRef.clear(); 
    this.componentRef = this.viewContainerRef.createComponent(factory); 

    // set each  StatusBoxComponent vars with values from myConfig
    this.componentRef.instance.deviceId = this.config.get('deviceId')
    this.componentRef.instance.infoExample1 = this.config.get('infoExample1')
    this.componentRef.instance. infoExample2 = this.config.get('infoExample2')
    this.componentRef.instance. infoExample3 = this.config.get('infoExample3')

  }
 }

What I’ve tried, to get remove / edit functionality on each StatusBoxComponent instance

I’ve found this code that enables you to add and remove instances of a Component, but it has a limitation in that if you remove a component out of order, it messes up the indexing ID system it uses. Meaning that you can end up with two component instances with the same ID. To get around this I’d rather use the deviceID value (which is a string) which gets set in each local storage Map to identify each instance of my StatusBoxComponent rather than an index. (Although I’m not sure how this would work yet).

The other problem with this code is that the button that calls the createComponnent() is all within one component. In my case I would want to have my LiveSummaryComponent define the createComponent() function that does the same as my current DynamicStatusBoxDirective. But be able to call / access the createComponent() from my form submission button in the AddStatusBoxComponent. Obviously they are two completely different components. And as you can see from this and this question I asked, accessing that createComponent() function outside of the LiveSummaryComponent gives me errors. And the solution in the latter question I wasn’t able to get working unfortunately.

Conclusion

I’m not sure if I should stick to my original approach of using the DynamicStatusBoxDirective or should I try to get the example code I linked working to be able to meet my objective. Or should I do something different?

It also seems difficult to be able to know which instance of the StatusBoxComponent to remove when clicking on a remove button on the component. Each map data structure in local storage does have a string deviceID value which I'd like to use. But I have no idea how to associate that deviceID string with a particular StatusBoxComponent instance.

I’m completely stuck on this.

I hope what I’m trying to achieve is at least clear. And please click the links to my other questions as this will provide a lot more context. Creating a stackblitz of my code will be very difficult due to sensitive information.

Thanks.


Solution

  • you can use service to store/read data from localStorage and a stream with array of current values.

        @Injectable()
        export class SomeService { 
            constructor() {
                this._initStream();        
            }
                   
            //your stream with statuses
            public statuses$ = new BehaviorSubject([]);
    
            private _initStream(){
                // get data from localStorage
                // transform Map to Array
                this.statuses$.next(transformedDataToArray);
            } 
            
            // add status, use it in your modal    
            addStatus(props) {
               ...your logic to add
               this._updateDataInLocalStorage();
               this._updateStream();
            }
            // use it in your StatusBoxComponent    
            removeStatus() {
                ...your logic to remove
                this._updateDataInLocalStorage();
                this._updateStream();
            }
            // use it in your StatusBoxComponent  
            updateStatus() {
                ...your logic to remove
                this._updateDataInLocalStorage();
                this._updateStream();
            }
            // set current value to stream
            _updateStream() {
                const statuses = localStorage.getItem();
                this.statuses$.next(statuses)
            }
            // update data in localStroage
            _updateDataInLocalStorage() {
                ...some logic here
            }
    

    LiveSummaryComponent html file

    <app-status-box *ngFor="let status of SomeService.statuses$ | async; let i=index" [props]="status" [index]="index"></app-status-box>
    

    Think of it like you have a simple array of statuses with a unique id (map key) and work with it. All your work with local storage inside your methods is to convert your array into a Map and save to local storage, and vice versa - convert Map from local storage to an array to display your statuses. And *ngFor will do all the work of displaying the components