Using Angular 11.1.2 and rxjs 6.6.2
I would like to produce an app which displays a list of components dynamically. I have achieved this piece independently. The issues I am now facing revolve around transitioning to state management via NgRx.
I am using the following model:
import { Type } from '@angular/core';
export enum Position { Default, Top, Bottom }
export class NodeModel {
constructor(
public type: Type<any>,
public position: Position
) {}
}
Which is fed through a series of *ngFor
and @Input()
s to be used in a service to generate dynamic components:
@Component({
selector: 'app-node-body',
template: '<ng-container #container></ng-container>',
styleUrls: ['./node-body.component.scss']
})
export class NodeBodyComponent implements AfterViewInit {
@ViewChild('container', { read: ViewContainerRef, static: false }) viewRef;
@Input() type: Type<any>;
constructor(private nodeBodyService: NodeBodyService) { }
ngAfterViewInit(): void {
this.nodeBodyService.createDynamicComponent(this.type, this.viewRef);
}
}
@Injectable()
export class NodeBodyService {
constructor(private cfr: ComponentFactoryResolver) {}
createDynamicComponent<T>(component: Type<T>, viewRef: ViewContainerRef): ComponentRef<any> {
const factory = this.cfr.resolveComponentFactory<T>(component);
return viewRef.createComponent(factory);
}
}
At this point the app produces the dynamic components correctly.
I am facing issues when switching my approach to using NgRx to start monitoring the state of my application. The following approach does not allow me to see the Type<any>
value neither in the state, nor in the respective body.component.ts
and thus the dynamic components fail to render.
// node-list.actions.ts
export const SET_NODES = '[Node List] Set Nodes';
export class SetNodes implements Action {
readonly type = SET_NODES;
constructor(public payload: NodeModel[]) {}
}
export type NodeListActions = SetNodes;
// node-list.reducer.ts
export interface State {
nodes: NodeModel[];
}
export function nodeListReducer(
state: State,
action: NodeListActions.NodeListActions
) {
switch (action.type) {
case NodeListActions.SET_NODES:
return {
...state,
nodes: action.payload
};
default:
return state;
}
}
import * as fromApp from './store/app.reducer';
import * as NodeListActions from './node-list/store/node-list.actions';
@Component({
selector: 'app-root',
template: '<app-node-list [nodes]="nodes"></app-node-list>',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
nodes: NodeModel[] = [
new NodeModel(SuperHeaderComponent, Position.Default),
new NodeModel(IntroductionComponent, Position.Bottom)
];
constructor(private store: Store<fromApp.AppState>) {}
ngOnInit(): void {
this.store.dispatch(new NodeListActions.SetNodes(this.nodes));
}
}
This is what shows up after the SET_NODES
actions is dispatched:
{
nodeList: {
nodes: [
{
position: 0
},
{
position: 2
}
]
}
}
I recognize using Type<any>
at runtime yields { position: 0, type: Function }
which is not "state-like." So I went the route of trying a type map such as Map<string, Type<any>>
along with changing my NodeModel
import { Type } from '@angular/core';
export enum Position { Default, Top, Bottom }
export class NodeModel {
constructor(
public type: string, // <-- Switched this so that it is visible in state
public position: Position
) {}
}
This DID resolve my issue with respect to now displaying what I want to see in my app state, but I was still not able to render my dynamic components as the following error shows up:
ERROR TypeError: Cannot add property __NG_ELEMENT_ID__, object is not extensible
From what I have read so far it appears I could turn this error off if I wish but would not be best practice.
Am I approaching state management correctly by attempting to store Type<any>
or even the idea of referencing the type to begin with?
If this is not appropriate state management, how might I monitor dynamic components and their respective state; for when I resolve this issue I would seek to have state communicated from the dynamic components back up to the root?
I managed to resolve my issue without sacrificing state immutability. Where I ended up going wrong was assuming I had not stored Type<any>
in my state object.
Type<any>
was stored in my app state, but due to it being a Function
I could not see it in the redux devtools. I resorted to adjusting my state object to the following:
export interface Node {
type: string;
position: Position;
}
I then updated my service to use a type map:
@Injectable()
export class NodeBodyService {
constructor(private cfr: ComponentFactoryResolver) {}
nodeTypes: Map<string, Type<any>> = new Map([
[SuperHeaderComponent.name, SuperHeaderComponent],
[IntroductionComponent.name, IntroductionComponent]
]);
createDynamicComponent(component: string, viewRef: ViewContainerRef): void {
const nodeType: Type<any> = this.nodeTypes.get(component);
viewRef.createComponent(this.cfr.resolveComponentFactory(nodeType));
}
}
I am now able to see my dynamic components rendered, and the appropriate state