How to Render Child Components Dynamically using loops if possible? (Angular 8) - javascript

I am trying to render my angular components dynamically using loops but I'm lost.
What I want to achieve is drag and re-arrange my components (I was able to achieve this using Dragula - ng2-dragula) but I want to save the arrangement to my local storage or session storage.
The tricky part is I am using child components
<section [dragula]="'VAMPIRES'">
<div class="first_element_class" id="first_element">
<my-first-component
[outputData]="outputData"></my-first-component>
</div>
<div class="second_element_class" id="second_element">
<my-second-component
[outputData]="outputData"></my-second-component>
</div>
</section>
I tried using the DOM sanitizer pipe to render them via for loops
import { Pipe, PipeTransform } from '#angular/core';
import { DomSanitizer, SafeHtml } from '#angular/platform-browser';
#Pipe({
name: 'trustHtml'
})
export class TrustHtmlPipe implements PipeTransform {
constructor(readonly sr: DomSanitizer){}
transform(html: string) : SafeHtml {
return this.sr.bypassSecurityTrustHtml(html);
}
}
Updated HTML Code (Let's assume I added my html elements to an array of object)
objectHTML: any[] = [{
toBeRender: `<div class="first_element_class" id="first_element">
<my-first-component
[outputData]="outputData"></my-first-component>
</div>`
}];
<section [dragula]="'VAMPIRES'">
<div *ngFor="let element of objectHTML">
<div [innerHTML]="element.toBeRender | trustHtml" ></div>
</div>
</section>
My question is is it really possible to render the html child components/elements using ngFor? I tried using the DOMSanitizer but I'm only getting a blank page (No error on console)

I have a similar solution for you. Firstly, in your <my-first-component> ts file declare "outputData" as #Input and use ngFor in HTML. Here is my sample code below.
CUSTOM Control
HTML: event.component.html
<h2>Event {{ event.id }}</h2>
<button (click)="event.hidden = true">Close me</button>
TS:event.component.ts
import { Component, Input } from '#angular/core';
import { Event } from './event';
#Component({
selector: 'my-event',
templateUrl: './event.component.html'
})
export class EventComponent {
#Input() event: Event;
}
export interface Event
{
id: number;
hidden: boolean;
}
**HTML:** app.component.html (You can use that code where you need to.)
<button (click)="addEvent()">Add event</button>
<div *ngFor="let event of events">
<my-event *ngIf="!event.hidden" [event]="event"></my-event>
</div>
Note: Here [event] is the #Input for your custom control and *ngFor is the loop.
TS: app.component.ts
import { Component } from '#angular/core';
import { Event } from './event';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
events: Array<Event> = [1, 2, 3, 4, 5].map(i => ({id: i, hidden: true}));
addEvent() {
const event = this.events.find(e => e.hidden);
if (event) {
event.hidden = false;
}
}
}
Note: Please check the code and let me know. This code is also available in Stackblitz
LINK .

You can iterate with ngFor and render many times the child component by his selector.
components = [1, 2, 3, 4, 5];
<ng-container *ngFor="let component of components">
<child-component></child-component>
</ng-container>

Related

How do I get inputs and raise events for the outside world in Angular?

I would like to know how can one get inputs from the outside world and raise custom events that bubbles up to the outside world (i.e. outside of angular components). I understand I cannot get #Input() in the root component. Then how can I do the following:
Pass an array of object (say T[]) to my Angular components <app-root> from outside of angular.
raise an event from <app-grandchild> such that the event bubbles out of angular components and hits the DOM tree root or <body>.
Assume the following DOM hierarchy:
<body>
<app-root>
<app-child>
<app-grandchild></app-grandchild>
</app-child>
</app-root>
</body>
Your question requires an explanation of a couple of concepts from Angular: Component Interaction and content projection.
I've put together an example that combines both to meet what you described in your question:
#Component({
selector: 'app-child',
template: `
<ul>
<li *ngFor="let obj of arrayOfObjects"> {{ obj.id }} </li>
</ul>
<ng-content></ng-content>
`,
})
class AppChildComponent {
#Input() arrayOfObjects: object[];
}
#Component({
selector: 'app-grandchild',
template: `Am the grandchild!`,
})
class AppGrandChildComponent implements OnInit {
#Output() somethingHappened = new EventEmitter<string>();
ngOnInit(){
this.somethingHappened.emit('yellow');
}
}
#Component({
selector: 'app-root',
template: `
<app-child [arrayOfObjects]="[
{id: '123'},
{id: '456'}
]">
<app-grandchild
(somethingHappened)="handleSomething($event)">
</app-grandchild>
</app-child>
`,
})
class AppComponent {
handleSomething(event){
document.body.style.backgroundColor = event;
}
}
Full example here: https://jsfiddle.net/c945j08v/1/
I strongly recommend you reading more on Angular Docs & Blog.
https://angular.io/guide/component-interaction
https://blog.angular-university.io/angular-ng-content/
I hope this helps.

Two way binding on Array elements using <form>

I have an array of objects in a component. which I will iterate in the template.
app.component.ts
import {Component, OnInit} from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'sample-app';
classesData = [];
constructor() {
}
ngOnInit() {
this.classesData = [
{title: 'Hello0'}, {title: 'Hello1'}, {title: 'Hello2'}
];
}
duplicate() {
const newData = JSON.parse(JSON.stringify(this.classesData[1]));
newData.title += 'Copy';
this.classesData.splice(1, 0, newData);
}
}
app.template.html
<form #testingFrom="ngForm">
<p>{{classesData | json}}</p>
<div *ngFor="let classData of classesData; let i=index">
<input [(ngModel)]="classData.title" name="{{'title-' + i}}" type="text">
</div>
<button (click)="duplicate()">Duplicate</button>
</form>
My aim is when a user clicks on the duplicate button I simply add a new element at index 1 in an array. My initial state looks like (before user clicks)
And my state after a user clicks duplicate button
In the image above at 3rd input field, we are getting Hello1Copy instead of Hello1.
I completely suspect that this behavior is happening because of conflict in the name attribute value. For this case only, if you splice the newItem at first location, it only adds that's variable and other DOM's doesn't re-render. For cross verification you can try replacing input element with simple binding like {{classData.title}} and everything works fine.
This behavior can easily be solved by not conflicting name attribute value for all time. What that means is to assign a unique id variable with each collection item and use it.
this.classesData = [
{ id: 1, title: 'Hello0' },
{ id: 2, title: 'Hello1' },
{ id: 3, title: 'Hello2' }
];
duplicate() {
const newData = JSON.parse(JSON.stringify(this.classesData[1]));
newData.title += 'Copy';
newData.id = Date.now()
this.classesData.splice(1, 0, newData);
}
Template
<div *ngFor="let classData of classesData;let i=index">
<input [(ngModel)]="classData.title" [name]="'title_'+classData.id" type="text">
</div>
Stackblitz
You can also verify the same by removing name attribute from each input field. But that would not suffice, it would throw
ERROR Error: If ngModel is used within a form tag, either the name
attribute must be set or the form control must be defined as
'standalone' in ngModelOptions.
So add [ngModelOptions]="{standalone: true}" on each input field to make input working without name attribute. As suggested in another answer by #briosheje, you can also re-enforce rendering using trackBy.
PS: I'm investigating why this works differently when there is a combination of name and input, I suspect about form API wiring with input element. I'll update the answer as soon as I get something.
The problem is that you are using a form. Because you're using a form, you need to specify how angular should track the changes for your form items, if you're planning to alter the existing source. You can do such using the trackBy pipe:
<form #testingFrom="ngForm">
<p>{{classesData | json}}</p>
<div *ngFor="let classData of classesData; let i=index; trackBy: trackByFn">
<input [(ngModel)]="classData.title" [name]="'title-' + i" type="text">
</div>
<button (click)="duplicate()">Duplicate</button>
</form>
Typescript relevant part:
trackByFn(index: any) {
return index;
}
Please note that adding elements to the collection will work in your original example.
Working stackblitz: https://stackblitz.com/edit/angular-uabuya
Make another variable and iterate that variable to crate inputs
import { Component,OnInit } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit {
title = 'sample-app';
originalData=[];
classesData = [];
constructor() {
}
ngOnInit() {
this.classesData = [
{title: 'Hello0'}, {title: 'Hello1'}, {title: 'Hello2'}
];
this.originalData=[...this.classesData]; // changed here
}
duplicate() {
const newData = JSON.parse(JSON.stringify(this.classesData[1]));
newData.title += 'Copy';
this.classesData.splice(1, 0, newData);
}
}
Working Demo
You can solve your issue using "trackBy" feature. Please see below code sample.
app.component.ts
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
title = 'sample-app';
classesData = [];
constructor() {}
ngOnInit() {
this.classesData = [
{ title: 'Hello0' },
{ title: 'Hello1' },
{ title: 'Hello2' }
];
}
duplicate() {
const newData = JSON.parse(JSON.stringify(this.classesData[1]));
newData.title += 'Copy';
this.classesData.splice(1, 0, newData);
}
trackByIndex(index: number, obj: any): any {
return index;
}
}
app.component.html
<form>
<p>{{classesData | json}}</p>
<div *ngFor="let classData of classesData; let i=index;trackBy:trackByIndex;">
<input [(ngModel)]="classesData[i].title" name="{{'title-' + i}}" type="text" />
</div>
<button (click)="duplicate()">Duplicate</button>
</form>
Please let me know if this solution works for you!

How can I access an already transcluded ContentChild?

I have a angular component app-b that is used within a component app-a that is used in the app-component. The app-component has some content in app-a, app-a transcludes this with ng-content into app-b, app-b shows it with another ng-content - but how can I access this content within the component (and not it's template)?
I would think that ContentChild is the correct approach but appears to be wrong.
Example:
https://stackblitz.com/edit/angular-ddldwi
EDIT: Updated example
You cannot query by tag name with #ContentChild decorator. You can query either by template variable, component or directive selector.
app-a.component.html
<app-b>
<ng-content></ng-content>
<p #myContent>This is a content child for app-b.</p>
</app-b>
app-b.component.ts
import { Component, AfterContentInit, ContentChild } from '#angular/core';
#Component({
selector: 'app-b',
templateUrl: './b.component.html',
styleUrls: ['./b.component.css']
})
export class BComponent implements AfterContentInit {
#ContentChild('myContent') contentchild;
ngAfterContentInit() {
console.log(this.contentchild);
}
}
Live demo
I recommend sharing the data between components. For example, move your data (E.G. dummyName) into a service. Then add the service to each component (where you need the shared data).
Service:
import { Injectable } from '#angular/core';
#Injectable()
export class DataShareService {
public dummyName: String = 'one';
constructor() { }
}
Add the new service to app.module.ts:
providers: [DataShareService],
Child Component:
import { DataShareService } from './data-share.service';
import { Component } from '#angular/core';
#Component({
selector: 'app-child',
templateUrl: './child.component.html'
})
export class ChildComponent {
constructor(public ds: DataShareService) { }
toggle() {
this.ds.dummyName = this.ds.dummyName === 'one' ? 'two' : 'one';
}
}
Child Component template (html):
<p> {{ds.dummyName}}</p>
<button (click)="toggle()">Click Me to Toggle Value</button>
Parent Component:
import { Component, OnInit } from '#angular/core';
import { DataShareService } from './data-share.service';
#Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
constructor(public ds: DataShareService) {}
displayNumber() {
return this.ds.dummyName === 'one' ? 1 : 2;
}
}
Parent Component template (html):
<p> this is the parent component:</p>
<div> value as string: {{ ds.dummyName }} </div>
<div> value as number: <span [textContent]="displayNumber()"></span></div>
<hr>
<p> this is the child component:</p>
<app-child></app-child>
Note! The child component toggle() function demonstrates how you can change the data. The parent component displayNumber() function demonstrates how to use the data, independent of it's display (I.E. as just pure data).
This appears to be impossible due to a bug in Angular:
https://github.com/angular/angular/issues/20810
Further reference:
https://www.reddit.com/r/Angular2/comments/8fb3ku/need_help_how_can_i_access_an_already_transcluded/

*ngFor on elements of ng-content Angular

I was wondering if is it possible to create a iteration of certain elements of . I need a different css class to every element of the ng-content so i need to make a loop of every element of ng-content. Is it possible?
Right now i pass a parameter to the child element to enumerate him, but i would like to do it without the number. This is my code:
<sys-tab [tabs]="['Principal', 'Complementar']">
<sys-tab-content [num]="1">
<sys-panel header="Dados Gerais">
<sys-input-text header="Nome" tam="1"></sys-input-text>
<sys-input-mask header="CNPJ"></sys-input-mask>
<sys-input-mask header="CNES"></sys-input-mask>
<sys-input-mask header="Telefone"></sys-input-mask>
<sys-input-text header="Email"></sys-input-text>
</sys-panel>
</sys-tab-content>
<sys-tab-content [num]="2">
<sys-input-text header="Email"></sys-input-text>
</sys-tab-content>
</sys-tab>
As you can see, to the child i passs the number so i can recognize who is him, but i want to create a loop to the ng-coontent so i can add a different class to every "sys-tab-content"
Simple List Example with dynamic ngTemplate
app.component.ts
import { Component } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html'
})
export class AppComponent {
items = [ { name: 'abc' }, { name: 'cdf' }, { name: 'fgh' } ];
}
app.component.html
<nu-list [itemTpl]="itemTpl" [items]="items"></nu-list>
<ng-template let-item #itemTpl>
<h1>{{item.name}}</h1>
</ng-template>
list.component.ts
import { Component, Input } from '#angular/core';
#Component({
selector: 'nu-list',
templateUrl: './list.component.html'
})
export class ListComponent {
#Input() itemTpl;
#Input() items;
}
list.component.html
<ng-container *ngFor="let item of items">
<ng-container *ngTemplateOutlet="itemTpl; context: {$implicit: item}"></ng-container>
</ng-container>
Example Link: https://stackblitz.com/edit/angular-list-ngtemplateoutlet
There's two ways of adding a "ngFor" or iterate trough the childs.
One is by transclusion: and the other one is by checking the ViewChildren of the parent component.

Angular2 ngFor not working

I am trying to implement a basic shopping list, but my ngFor in Angular is not working.
import { Component, View } from 'angular2/angular2';
#Component({
selector: 'my-app'
})
#View({
template: '<h1>{{title}}</h1><ul><li *ngFor="let i of items"><span>{{i}}</span></li></ul>'
})
export default class MyAppComponent {
title = 'ShoppinList';
items = ['Milk','Ham','Eggs'];
}
The only thing that appears is "loading..." and I am getting more than 10 cryptic errors.
Without trying it first I noticed an error in the import statement at the top. Should be:
import { Component } from '#angular/core'
#View() was removed almost a year ago. If you see examples that use it, just move the content to the #Component() decorator while directives and pipes were moved from #Component() to #NgModule()s declaration.
You need to add CommonModule to #NgModule({ imports: [CommonModule], ...}) export class SomeModule{} in every module where you want to use ngFor (or other directives shippled with Angular - BrowserModule already includes CommonModule).
its good way to use ngIf and use your template inside your component properties.
import { Component, View } from 'angular2/angular2';
#Component({
selector: 'my-app',
template : '<div>
<h1>{{title}}</h1>
<ul *ngIf="items">
<li *ngFor="let i of items"><span>{{i}}</span></li>
</ul>
</div>'
})
export default class MyAppComponent {
title : string = 'ShoppinList';
items : Array<string> = ['Milk','Ham','Eggs'];
}
You don't have to use #View here.
#Component({
selector: 'my-app',
template: `
<div>
{{title}}
<ul><li *ngFor="let i of items"><span>{{i}}</span></li></ul>
</div>
`,
})
export class App {
title = 'ShoppinList';
items = ['Milk','Ham','Eggs'];
}
Working plunker

Categories

Resources