Dynamically ADDING and REMOVING Components in Angular - html

The current official docs only shows how to dynamically change components within an <ng-template> tag. https://angular.io/guide/dynamic-component-loader
What I want to achieve is, let's say I have 3 components: header, section, and footer with the following selectors:
<app-header>
<app-section>
<app-footer>
And then there are 6 buttons that will add or remove each component: Add Header, Add Section, and Add Footer
and when I click Add Header, the page will add <app-header> to the page that renders it, so the page will contain:
<app-header>
And then if I click Add Section twice, the page will now contain:
<app-header>
<app-section>
<app-section>
And if I click Add Footer, the page will now contain all these components:
<app-header>
<app-section>
<app-section>
<app-footer>
Is it possible to achieve this in Angular? Note that ngFor is not the solution I'm looking for, as it only allows to add the same components, not different components to a page.
EDIT: ngIf and ngFor is not the solution I'm looking for as the templates are already predetermined. What I am looking for is something like a stack of components or an array of components where we can add, remove, and change any index of the array easily.
EDIT 2: To make it more clear, let's have another example of why ngFor does not work. Let's say we have the following components:
<app-header>
<app-introduction>
<app-camera>
<app-editor>
<app-footer>
Now here comes a new component, <app-description>, which the user wants to insert in between and <app-editor>. ngFor works only if there is one same component that I want to loop over and over. But for different components, ngFor fails here.

What you're trying to achieve can be done by creating components dynamically using the ComponentFactoryResolver and then injecting them into a ViewContainerRef. One way to do this dynamically is by passing the class of the component as an argument of your function that will create and inject the component.
See example below:
import {
Component,
ComponentFactoryResolver, Type,
ViewChild,
ViewContainerRef
} from '#angular/core';
// Example component (can be any component e.g. app-header app-section)
import { DraggableComponent } from './components/draggable/draggable.component';
#Component({
selector: 'app-root',
template: `
<!-- Pass the component class as an argument to add and remove based on the component class -->
<button (click)="addComponent(draggableComponentClass)">Add</button>
<button (click)="removeComponent(draggableComponentClass)">Remove</button>
<div>
<!-- Use ng-template to ensure that the generated components end up in the right place -->
<ng-template #container>
</ng-template>
</div>
`
})
export class AppComponent {
#ViewChild('container', {read: ViewContainerRef}) container: ViewContainerRef;
// Keep track of list of generated components for removal purposes
components = [];
// Expose class so that it can be used in the template
draggableComponentClass = DraggableComponent;
constructor(private componentFactoryResolver: ComponentFactoryResolver) {
}
addComponent(componentClass: Type<any>) {
// Create component dynamically inside the ng-template
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(componentClass);
const component = this.container.createComponent(componentFactory);
// Push the component so that we can keep track of which components are created
this.components.push(component);
}
removeComponent(componentClass: Type<any>) {
// Find the component
const component = this.components.find((component) => component.instance instanceof componentClass);
const componentIndex = this.components.indexOf(component);
if (componentIndex !== -1) {
// Remove component from both view and array
this.container.remove(this.container.indexOf(component));
this.components.splice(componentIndex, 1);
}
}
}
Notes:
If you want to make it easier to remove the components later on, you can keep track of them in a local variable, see this.components. Alternatively you can loop over all the elements inside the ViewContainerRef.
You have to register your component as an entry component. In your module definition register your component as an entryComponent (entryComponents: [DraggableComponent]).
Running example:
https://plnkr.co/edit/mrXtE1ICw5yeIUke7wl5
For more information:
https://angular.io/guide/dynamic-component-loader

Angular v13 or above - simple way to add dynamic components to DOM
parent.component.html
<ng-template #viewContainerRef></ng-template>
parent.component.ts
#ViewChild("viewContainerRef", { read: ViewContainerRef }) vcr!: ViewContainerRef;
ref!: ComponentRef<YourChildComponent>
addChild() {
this.ref = this.vcr.createComponent(YourChildComponent)
}
removeChild() {
const index = this.vcr.indexOf(this.ref.hostView)
if (index != -1) this.vcr.remove(index)
}
Angular v12 or below
I have created a demo to show the dynamic add and remove process.
The parent component creates the child components dynamically and removes them.
Click for demo
Parent Component
// .ts
export class ParentComponent {
#ViewChild("viewContainerRef", { read: ViewContainerRef })
VCR: ViewContainerRef;
child_unique_key: number = 0;
componentsReferences = Array<ComponentRef<ChildComponent>>()
constructor(private CFR: ComponentFactoryResolver) {}
createComponent() {
let componentFactory = this.CFR.resolveComponentFactory(ChildComponent);
let childComponentRef = this.VCR.createComponent(componentFactory);
let childComponent = childComponentRef.instance;
childComponent.unique_key = ++this.child_unique_key;
childComponent.parentRef = this;
// add reference for newly created component
this.componentsReferences.push(childComponentRef);
}
remove(key: number) {
if (this.VCR.length < 1) return;
let componentRef = this.componentsReferences.filter(
x => x.instance.unique_key == key
)[0];
let vcrIndex: number = this.VCR.indexOf(componentRef as any);
// removing component from container
this.VCR.remove(vcrIndex);
// removing component from the list
this.componentsReferences = this.componentsReferences.filter(
x => x.instance.unique_key !== key
);
}
}
// .html
<button type="button" (click)="createComponent()">
I am Parent, Create Child
</button>
<div>
<ng-template #viewContainerRef></ng-template>
</div>
Child Component
// .ts
export class ChildComponent {
public unique_key: number;
public parentRef: ParentComponent;
constructor() {
}
remove_me() {
console.log(this.unique_key)
this.parentRef.remove(this.unique_key)
}
}
// .html
<button (click)="remove_me()">I am a Child {{unique_key}}, click to Remove</button>

Related

Get value of a variable in one component to another component using ngmodel

I have two components first and second. From the second component, I am calling the first component. In the first component, I have a matslider module and I want to get that slider on/off status to my second component ts file. So I am getting that value in first, but don't know how to pass that to the second component.
first.component.html
<div>
<mat-slide-toggle class="toggles"
(change)="OnToggle($event)
[(ngModel)]="selected">Toggle</mat-slide-toggle>
</div>
first.component.ts
#Input() selected=false;
public OnToggle(event)
{
this.selected = event.selected;
}
second.component.html
<div class="container">
<app-first> </app-first>
</div>
I think you can use an output event in the first component and bind to it in the second component.
here it is example:
First Component:
#Output() selectedChange = new EventEmitter<boolean>();
public OnToggle(event) {
this.selected = event.selected;
this.selectedChange.emit(this.selected);
}
SecondComponent:
<app-first (selectedChange)="onSelectedChange($event)"></app-first>
public onSelectedChange(selected: boolean) {
console.log(selected);
}

How to pass Angular directive by reference?

In an existing component template I have this (simplified) element:
<input type="button" #refreshPrice />
This is picked up (I don't know the correct term) by this property so we can subscribe to it's click event and call a function when the input element is clicked.
I want to replace this input element with a component I've developed, which would make it look (simplified) like this:
<spinner-button #refreshPrice></spinner-button>
This child component has this as its (simplified) template:
<button>
<mat-spinner></mat-spinner>
</button>
So now the button element, in the child component template, needs to have the #refreshPrice hash attribute (?) attached.
To do this, perhaps the spinner-button element should take the name of the hash attribute as an attribute value. Here is the complete spinner component class file:
import { Component, Input, OnInit } from "#angular/core";
#Component({
selector: "spinner-button",
templateUrl: "./spinner-button.component.html",
styleUrls: ["./spinner-button.component.css"]
})
export class SpinnerButtonComponent implements OnInit {
constructor() {}
#Input() targetElement: string;
ngOnInit() {
}
}
In theory, the targetElement property can then be attached to the button element as a hash attribute - but how is this done?
the #Input() attribute here allows you to bind a value to a variable on your component, if you want to have the parent do something based on your components data, you might want to use #Output() and emit a custom event. If the requirement is just listen to a click event then adding a (click)=functionToBeCalled() should help your cause here.
You can refer to the official docs as well:
https://angular.io/guide/inputs-outputs

Angular change html tag programmatically when inheriting template

I have a Component, DataGrid, which represents a table with expandable rows. Each row, when expanded, shows a child DataGrid table, which is very similar to the parent DataGrid component.
Therefore I defined a base class DataGridComponent, from which the child inherits the both the component and the template. however, I need to change one of the tags in the child's template. Do I have to rewrite the entire template, or could I just point the templateUrl to the parent's template and programmatically change the one html tag that I need to change?
Minimal Example:
base.component.ts
#Component({
selector: 'datagrid',
templateUrl: 'datagrid.component.html'
})
export class DataGridComponent {
childEnabled:boolean = true;
// stuff
}
datagrid.component.html
<div>...</div>
<div *ngIf="childEnabled">
<childgrid
[options]="childOptions"
>
</childgrid>
</div>
child.component.ts
#Component({
selector: 'childgrid',
templateUrl: 'datagrid.component.html' // <-- POINT TO BASECLASS TEMPLATE
})
export class ChildGridComponent extends DataGridComponent{
}
childgrid.component.html // <-- HOW THE (REAL) TEMPLATE SHOULD BE
<div>...</div>
<div *ngIf="childEnabled">
<grandchildgrid
[options]="childOptions"
>
</grandchildgrid>
</div>
grandchild.component.ts
#Component({
selector: 'grandchildgrid',
templateUrl: 'datagrid.component.html' // <-- POINT TO BASECLASS TEMPLATE
})
export class GrandChildGridComponent extends DataGridComponent{
constructor() {
super();
childEnable=false;
}
}
grandchildgrid.component.html // <-- HOW THE (REAL) TEMPLATE SHOULD BE
<div>...</div>
<div *ngIf="childEnabled">
<grandchildgrid
[options]="childOptions"
>
</grandchildgrid>
</div>
and so on until childEnabled is set to false. Is there any chance to do something like this and is it something that would make sense from an angularly point of view? Would ng-content be of any help in this case?
The content of DataGrid can go into a separate component and that can be used as a template in both parent and child DataGrid.
Alternative option is to have the same tags and control behavior using different class and id for parent and child

querySelector does not seem to find elements inside ng-container

I am developing a web application using angular dart.
I am trying to find a 'div' element inside the component using document.querySelector() and I am trying to modify(add some content to) its body.
But it doesn't seem to find the 'div' element.
Here is my html:
<ng-container *ngFor="let item of list">
<ng-container *ngIf="item.canShowChart">
<div [id]="item.elementID" class="chart"></div>
</ng-container>
</ng-container>
Here is my component method which tries to modify the 'div':
void drawChart() {
for (final item in list) {
if (!item.canShowChart) {
continue;
}
final DivElement _container = document.querySelector('#' + item.elementID);
print(_container);
}
}
It always prints the '_container' as 'null'
I tried removing the ng-container and having only the 'div' in the page like below and it seems to work!.
<div [id]="item.elementID" class="chart"></div>
What is the problem?
TIA.
It is not working because as at the time you used 'querySelectorAll', angular had not loaded ng-container to the DOM yet. You should put your code in the 'AfterViewChecked' lifecycle hook.
export class ImageModalComponent implements OnInit, AfterViewChecked{
//AfterViewChecked
ngAfterViewChecked {
void drawChart() {
for (final item in list) {
if (!item.canShowChart) {
continue;
}
final DivElement _container = document.querySelector('#' + item.elementID);
print(_container);
}
}
}
}
Make sure to import 'AfterViewChecked' like so;
import { Component, OnInit, AfterViewChecked } from '#angular/core';
You can make it a separate component, let's call it app-chart:
<ng-container *ngFor="let item of list">
<app-chart *ngIf="item.canShowChart" [item]="item">
</app-chart>
</ng-container>
In the AppChartComponent declare necessary input(s), and inject ElementRef in the constructor:
#Input() item: any;
constructor(private ref: ElementRef) {}
this.ref.nativeElement is how you can access the DOM element from inside.
Never use querySelector to find elements in your template. Angular and DOM are two seperate paradigms and you should not mix them.
To find an element in your template, use a reference to an element.
<div #chartContainer class="chart"></div>
Then you can reference the div from your code.
See https://itnext.io/working-with-angular-5-template-reference-variable-e5aa59fb9af for an explanation.
AfterViewChecked not worked. Use AfterViewInit

#ContentChildren not picking up items inside custom component

I'm trying to use #ContentChildren to pick up all items with the #buttonItem tag.
#ContentChildren('buttonItem', { descendants: true })
This works when we have the ref item directly in the parent component.
<!-- #ContentChildren returns child item -->
<parent-component>
<button #buttonItem></button>
<parent-component>
But, if the element with the #buttonItem ref is wrapped in a custom component, that does not get picked by the #ContentChildren even when I set the {descendants: true} option.
<!-- #ContentChildren returns empty -->
<parent-component>
<child-component-with-button-ref></child-component-with-button-ref>
<parent-component>
I have created a simple StackBlitz example demonstrating this.
Doesn't appear to be a timeline for a resolution of this item via github... I also found a comment stating you cannot query across an ng-content boundary.
https://github.com/angular/angular/issues/14320#issuecomment-278228336
Below is possible workaround to get the elements to bubble up from the OptionPickerComponent.
in OptionPickerComponent count #listItem there and emit the array AfterContentInit
#Output() grandchildElements = new EventEmitter();
#ViewChildren('listItem') _items
ngAfterContentInit() {
setTimeout(() => {
this.grandchildElements.emit(this._items)
})
}
Set template reference #picker, register to (grandchildElements) event and set the $event to picker.grandchildElements
<app-option-picker #picker [optionList]="[1, 2, 3]" (grandchildElements)="picker.grandchildElements = $event" popup-content>
Create Input on PopupComponent to accept values from picker.grandchildElements
#Input('grandchildElements') grandchildElements: any
In app.component.html accept picker.grandchildElements to the input
<app-popup [grandchildElements]="picker.grandchildElements">
popup.component set console.log for open and close
open() {
if (this.grandchildElements) {
console.log(this.grandchildElements);
}
else {
console.log(this.childItems);
}
close() {
if (this.grandchildElements) {
console.log(this.grandchildElements);
}
else {
console.log(this.childItems);
}
popup.component change your ContentChildren back to listItem
#ContentChildren('listItem', { descendants: true }) childItems: Element;
popup.component.html set header expression
<h3>Child Items: {{grandchildElements ? grandchildElements.length : childItems.length}}</h3>
Stackblitz
https://stackblitz.com/edit/angular-popup-child-selection-issue-bjhjds?embed=1&file=src/app/option-picker/option-picker.component.ts
I had the same issue. We are using Kendo Components for angular. It is required to define Columns as ContentChilds of the Grid component. When I wanted to wrap it into a custom component and tried to provide additional columns via ng-content it simply didn't work.
I managed to get it working by resetting the QueryList of the grid component AfterViewInit of the custom wrapping component.
#ViewChild(GridComponent, { static: true })
public grid: GridComponent;
#ContentChildren(ColumnBase)
columns: QueryList<ColumnBase>;
ngAfterViewInit(): void {
this.grid.columns.reset([...this.grid.columns.toArray(), ...this.columns.toArray()]);
this.grid.columnList = new ColumnList(this.grid.columns);
}
One option is re-binding to the content child.
In the template where you are adding the content child you want picked up:
<outer-component>
<content-child [someBinding]="true" (onEvent)="someMethod($event)">
e.g. inner text content
</content-child>
</outer-component>
And inside of the example fictional <outer-component>:
#Component()
class OuterComponent {
#ContentChildren(ContentChild) contentChildren: QueryList<ContentChild>;
}
and the template for <outer-component> adding the <content-child> component, re-binding to it:
<inner-template>
<content-child
*ngFor="let child of contentChildren?.toArray()"
[someBinding]="child.someBinding"
(onEvent)="child.onEvent.emit($event)"
>
<!--Here you'll have to get the inner text somehow-->
</content-child>
</inner-template>
Getting that inner text could be impossible depending on your case. If you have full control over the fictional <content-child> component you could expose access to the element ref:
#Component()
class ContentChildComponent {
constructor(public element: ElementRef<HTMLElement>)
}
And then when you're rebinding to it, you can add the [innerHTML] binding:
<content-child
*ngFor="let child of contentChildren?.toArray()"
[someBinding]="child.someBinding"
(onEvent)="child.onEvent.emit($event)"
[innerHTML]="child.element.nativeElement.innerHTML"
></content-child>
You may have to sanitize the input to [innerHTML] however.