List of different components in Angular 2 ngFor - javascript

I know there are many similar questions and almost all of them end with DynamicComponentLoader answer but still, I think use case described below is so simple and common (IMO) that solution with Angular 2 should be straight forward.
Sample use case
I have an array of news items with property type describing what kind of item it is.
var items = [
{ id: 1, type: 'text', data: {} },
{ id: 2, type: 'text', data: {} },
{ id: 3, type: 'text-two-columns', data: {} },
{ id: 4, type: 'image-text', data: {} },
{ id: 5, type: 'image', data: {} },
{ id: 6, type: 'twitter', data: {} },
{ id: 7, type: 'text', data: {} }
]
Each different type has different view and quite different logic behind it. In other words - each type has its own angular2 Component.
So abstract code what I try to achieve is:
<div *ngFor="let item of items">
<item-{{item.type}} [data]="item.data"></item-{{item.type}}>
</div>
Of course it will not work.
Possible solution #1
<div *ngFor="let item of items">
<item-text *ngIf="item.type === 'text'" [data]="item.data"></item-text>
<item-image *ngIf="item.type === 'image'" [data]="item.data"></item-image>
...
</div>
I don't like this solution not only because it looks ugly and I will have to include this line every time I'll add new type but I wonder if this solution is good from performance perspective? I mean if I have 10,000 different types and only 3 items to display. So angular2 will have to remove from DOM 9,999 tags and leave only one for each of 3 items (3 * 9999 remove operations).
Possible solution #2
<div *ngFor="let item of items">
<dynamic-component-loader [item]="item"></dynamic-component-loader>
</div>
At the moment I don't remember how exactly DynamicComponentLoader works (I have tried it in similar problem in angular2 alpha long time ago). But as I remember the code looks like hack for me.. For such common task?..
Angular 1.x thinking
I don't know what I do wrong, maybe the problem is that I still think in Angular 1? Using it I would use ngInclude or custom directive with template function.
Guys, do you have other solutions how to do it? Don't stick to my two potential solutions, maybe I need to think out of the box and solve this problem completely in different part of my application.. I'm confused. Thanks:)
EDIT: One more real world example
Let's say your task is to write Facebook with Angular 2. I think you would face same issue trying to display news feed. Each news feed item has it's type (text, event, ads,.. )

This is my solution:
import { Component, OnInit, ViewContainerRef, TemplateRef, ComponentFactoryResolver, Input } from '#angular/core';
#Component({
selector: 'item',
template: '',
styleUrls: ['./item.component.scss']
})
export class ItemComponent implements OnInit {
#Input() type: string;
#Input() data: any;
constructor(
private viewContainerRef: ViewContainerRef,
private componentFactoryResolver: ComponentFactoryResolver,
private componentLookupService: YourComponentLookUpService
) { }
ngOnInit() {
const component = this.componentLookupService.findByType(type);
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(component);
// Look at the https://angular.io/docs/ts/latest/api/core/index/ViewContainerRef-class.html#!#createComponent-anchor for more information about how to use DI... in the createComponent function.
const componentRef =this.viewContainerRef.createComponent(componentFactory);
// Or you can then access the newly created component here: this.componentRef.instance
}
}
In your NgFor loop:
<div *ngFor="let item of items">
<item [type]="item.type" [data]="item.data"></item>
</div>

I guess you could use "ngComponentOutlet" that came with Angular 4 which creates component dynamically based on the value passed. I havent tested the code though.
#Component({
selector: 'my-app',
template: `
<h1>Angular version 4</h1>
<div *ngFor="let <component name> of <list of component name>">
<ng-container *ngComponentOutlet="<component name>">enter code here</ng-container>
</div>
`,
})
please refer url for more details : https://netbasal.com/a-taste-from-angular-version-4-50be1c4f3550

I'd write another component, say item-flex:
<item-flex [item]="item" *ngFor="let item of items"></item-flex>
And item-flex could use either ngSwitch:
<div [ngSwitch]="item.type">
<item-text *ngSwitchCase="'text'" [data]="item.data"></item-text>
<item-image *ngSwitchCase="'image'" [data]="item.data"></item-image>
<span *ngSwitchDefault >UNKNOWN TYPE:{{item.type}}</span>
</div>
or the "ugly ifs" (this way you can even get rid of the external tag/div/span that is present in ngSwitch solution):
<item-text *ngIf="item.type=='text'" [data]="item.data"></item-text>
<item-image *ngIf="item.type=='image'" [data]="item.data"></item-image>

My first thought would be to create a directive and use the Renderer class to add the appropriate component conditionally.
<div app-item [type]="item.type" [data]="item.data"></div>
Directive
import { Directive, ElementRef, Input, Renderer, OnInit } from '#angular/core';
#Directive({
selector: '[app-item]'
})
export class ItemDirective implements OnInit {
#Input('type') type: string;
#Input('data') data: any[];
constructor(private el: ElementRef, private r: Renderer) { }
ngOnInit(): void {
switch(this.type){
case: 'text'
let self = this.r.createElement( this.el.nativeElement, 'item-text' );
this.r.setElementAttribute(self, 'data', 'this.data')
break;
case: 'image');
let self = this.r.createElement( this.el.nativeElement, 'item-image'
this.r.setElementAttribute(self, 'data', 'this.data')
break;
// ... so on ...
}
}
You can use more #Inputs to pass in parameters and attach them using other Renderer methods.
This keeps the view very simple and will not not load modules for items whose tyoes ar not required.

Related

how to pass form value to another component in angular

I want to show taxDetailsId in my child component Html page.
But when click submit button.
After click submit button then shows taxDetailsId in my child component Html page.
Parent Component
export class OnlinePaymentComponent implements OnInit {
HttpClient: any;
paymentForm: FormGroup = this.formBuilder.group({
taxDetailsId: ['', [Validators.required]]
});
constructor(
private formBuilder: FormBuilder,
private router: Router,
) {}
ngOnInit() {}
submitForm(): void {
if (!this.paymentForm.valid) {
this.router.navigate(['/home/online-payment/error']);
return;
}
}
}
Parent.Component.html
<form [formGroup]="paymentForm" (ngSubmit)="submitForm()">
<label>Tax Details Id</label>
<input type="text" formControlName="taxDetailsId" placeholder="Tax Details Id" />
<button>Pay Bill</button>
<form>
Child Component
export class OnlinePaymentErrorComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}
Child.Component.html
<div>
<button [routerLink]="['/home/online-payment']" >Back Home</button>
</div>
you can try this pattern this.router.navigate(['/heroes', { id: heroId }]);
https://angular.io/guide/router
you can use angular #Input() decorator for it.
Child Component
import { Component, Input } from '#angular/core';
export class ChileComponent {
#Input() public taxDetailsId: number;
}
Child Component HTML
enter code here
<div>
{{ taxDetailsId }}
<button [routerLink]="['/home/online-payment']" >Back Home</button>
</div>
Parent Component HTML
<app-child-component [taxDetailsId]="taxDetailsId"> </app-child-component>
https://angular.io/guide/inputs-outputs
You can pass components around using Angular's InjectionToken.
First you start off by creating the token:
export const ONLINE_PAYMENT_REF = new InjectionToken<OnlinePaymentComponent>('OnlinePaymentComponent');
Next you add the token to one of the root components as a provider, in this case it is the OnlinePaymentComponent. This way everything that is a child of this component, and everything that is a child of those components, and so on; will have a reference to the main parent that we create here:
#Component({
selector: 'online-payment',
template: `
<online-payment-error></online-payment-error>
`,
providers: [
{
provide: ONLINE_PAYMENT_REF,
// Forwards the instance of OnlinePaymentComponent when injected into
// the child components constructor.
useExisting: forwardRef(() => OnlinePaymentComponent)
}
]
})
export class OnlinePaymentComponent {
message = 'I am the Online Payment Component';
}
Now that we have the main component setup, we can access it through the constructor of anything that is a child of OnlinePaymentComponent (no matter how deep it is).
#Component({
selector: 'online-payment-error',
template: `
<h2>Child</h2>
<strong>Parent Message:</strong> {{parentMessage}}
`
})
export class OnlinePaymentErrorComponent implements OnInit {
parentMessage = '';
constructor(
#Inject(ONLINE_PAYMENT_REF) private parent: OnlinePaymentComponent
) {}
ngOnInit() {
this.parentMessage = this.parent.message;
}
}
When all is said and done, you will see the following:
The pros of this method are that you don't have to bind values to the elements in the template, and if those components have components that need to reference the parent you wouldn't have to bind to those either. Since components look up the hierarchy till they find the first instance of the provider that we are looking for they will find the one in OnlinePaymentComponent.
This becomes very helpful when components get deeper and deeper into the parent component (say 5 levels deep), that means every time you would have to pass a reference to the template element 5 times, and if it changes or gets deeper you would have to update all the templates.
With this method we no longer need to update templates to pass data from one component to another component, we just request it in our constructor as seen in OnlinePaymentErrorComponent.
There are two simple ways:
Using query params (without routerLink).
Using Observables.
Using query params, you can use the router.navigate and pass the params you need (Id) along with the route.
eg: this.route.navigate(['yourroute/route', { tId: variableWithId }])
Using Observable, when you click on the button, use the same router navigate without params and pass the required data to an observable. On successful routing to the next page, get the resolved data from the observable.

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

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>

Display <my-component> by [innerHTML]

I'm trying to have my custom component tags in a string array and bind them by ngfor to the innerhtml property after sanitizing them by calling bypassSecurityTrustHtml... unfortunately the output is always empty, but there is also no sanitize error...
What am i doing wrong?
// adminpanel.component.ts
#Component({
selector: 'admin-panel',
templateUrl: './adminpanel.component.html'
})
export class AdminPanelComponent {
static GetRoutes(): Route[] {
return [
{ path: '', redirectTo: 'news', pathMatch: 'full' },
// 0
{ path: 'news', component: AdminNewsViewComponent },
// 1
{ path: 'users', component: AdminUsersViewComponent },
// 2
{ path: 'roles', component: AdminRolesViewComponent },
// 3
{
path: 'culturesettings',
redirectTo: 'culturesettings/wordvariables'
},
{
path: 'culturesettings',
component: AdminCultureSettingsViewComponent,
pathMatch: 'prefix',
children: AdminCultureSettingsViewComponent.GetRoutes()
},
// 4
{
path: 'account',
component: AdminAccountViewComponent
}
]
}
panels: AdminPanel[] = [];
routedTabs: RoutedTabs
constructor(private authService: AuthService, private routerService: RouterService, private sanitizer: DomSanitizer) {
this.routedTabs = new RoutedTabs("admin/panel", 2, authService, routerService);
var routes = AdminPanelComponent.GetRoutes().filter(x => x.component != undefined);
var comps = [
'<admin-news-view></admin-news-view>',
'<admin-users-view></admin-users-view>',
'<admin-roles-view></admin-roles-view>',
'<admin-culture-settings-view></admin-culture-settings-view>',
'<admin-account-view></admin-account-view>'
];
for (var i = 0; i < comps.length; i++) this.panels.push(new AdminPanel(i, routes[i], this.sanitizer.bypassSecurityTrustHtml(comps[i]) , this.sanitizer));
}
ngOnInit() {
this.routedTabs.MakeTabs(AdminPanelComponent.GetRoutes());
this.routedTabs.Subscribe();
this.routedTabs.Emit();
}
ngOnDestroy() {
this.routedTabs.Unsubscribe()
}
}
class AdminPanel {
index: number;
route: Route;
innerHtml: any = '';
constructor(index: number, route: Route, innerHtml: any, private sanitizer: DomSanitizer) {
this.index = index;
this.route = route;
this.innerHtml = innerHtml;
}
}
And in my adminpanel.component.html:
<mat-tab-group (selectedTabChange)="routedTabs.onTabChange($event)" [(selectedIndex)]="routedTabs.selectedTab">
<mat-tab *ngFor="let panel of panels" label="{{ routedTabs.tabs[panel.index].label }}">
<div [innerHTML]="panel.innerHtml">
</div>
</mat-tab>
</mat-tab-group>
From what I gather, this has not really been resolved in a satisfying or clean manner. I've been in the same boat and didn't find a good solution to loading components in dynamic strings either - so I've written my own with ngx-dynamic-hooks!
Some key points you might be interested in:
Finds all component selectors inside a string and autormatically loads the corresponding components in their place
Can even load components by other text pattern than their selectors, if that is what you need
Inputs and outputs can be set just like in a normal template and are automatically parsed from strings into actual variables for you
Components can be nested without restrictions and will appear in each others "ng-content"-slots as expected
You can pass live data from the parent component into the dynamically loaded components (and even use it to bind inputs/outputs in the content string)
You have meticulous control over which components are allowed to load on an outlet-to-outlet-basis and even which inputs/outputs you can give them
You can optionally configure components to lazy-load only when they are needed
The library uses Angular's built-in DOMSanitizer to be safe to use even with potentially unsafe input
The components are created with native Angular methods and behave just like any other component in your app. I hope this helps all who enounter the same problem.
See it in action in this Stackblitz.
Just using the tag as a string won't work, as angular doesn't just create component instances if an element with matching selector pops up in the dom.
You either
use the ComponentFactoryResolver
just have a type property on panel so you use ngSwitch based on type and render the corresponding in an ngSwitchCase. In that case you would have the tags in your template though

Angular: pass single object within ngFor to router linked component

How do I pass the data binded information within the <a> tag (Within the volume-links.component.html ) to my page-view component when the link is clicked.
I want to pass that particular diary object to my page-view.
I've looked into parent and child component interaction but I don't think that is the proper way to do it. I've looked into communicating via a service but I do not know how that would work for a problem such as this.
volume-links.component.html
<ul class="navigation">
<li *ngFor="let d of diary">
<a id={{d.notebook_id}} routerLink="/page-view" routerLinkActive="active">Volume {{d.notebook_id}}, {{ d.date }}, {{ d.volume_id }}, Add MS {{ d.ms_id }}</a>
</li>
</ul>
volume-links.component.ts
import { Component, OnInit } from '#angular/core';
import { Http } from '#angular/http';
import { HttpClient, HttpClientModule } from '#angular/common/http';
import 'rxjs/add/operator/map'
#Component({
selector: 'app-volume-links',
templateUrl: './volume-links.component.html',
styleUrls: ['./volume-links.component.scss'],
//encapsulation: ViewEncapsulation.None
})
export class VolumeLinksComponent implements OnInit {
diary : String;
constructor(private http: HttpClient) { }
ngOnInit() {
this.http.get('/api/diaries').subscribe(data => {
this.diary = data["data"]["docs"];
console.log(this.diary);
})
}
}
You want to look at https://angular.io/guide/component-interaction
There are several methods / ways to achieve this and depending on your use case choose one.
I would define an Input property in VolumeLinksComponent and pass the diary object in there (that's the first part "Pass data from parent to child with input binding").
This would look something like:
<a *ngFor='let diary of diaries' (click)='chooseDiary(diary)'>
<my-diary-container [diary]='selectedDiary'></my-diary-container>
and that parent component of course needs a property 'selectedDiary' and a method:
chooseDiary(diary: Diary): void {
this.selectedDiary = diary;
}
But in your provided case it seems like you just need the specific id since you want to retrieve details from the api? In that case you could just define a route with the id and when the route is accessed ask an additional DiaryService to retrieve what you need.

Cyclic dependency error when Binding to properties of child component in ngFor

I'm trying to bind to input properties on a child component inside of a ngFor loop to create a menu structure.
I haven't been able to find anything in my searches to help, so I'm posting this question.
I'm storing the menu structure in code in the sidebar component as an array of MenuItemComponents -> MenuStructure
sidebar.component.ts: (Parent component)
export class SidebarComponent implements AfterViewInit {
MenuStructure: MenuItemComponent[] = [];
ngAfterViewInit() {
//Define and Add Dashboard
var dashboard = new MenuItemComponent(null);
dashboard.title = 'Dashboard';
dashboard.iconName = 'dashboard';
//Define Documentation
var documentation = new MenuItemComponent(null);
documentation.title = 'Documentation';
documentation.iconName = 'insert drive file';
this.MenuStructure.push(dashboard);
this.MenuStructure.push(documentation);
}
}
This is the child component (aka. the building block of the menu):
menu-item.component.ts:
#Component({
selector: 'app-menu-item',
templateUrl: './menu-item.component.html',
styleUrls: ['./menu-item.component.css']
})
export class MenuItemComponent implements OnInit {
#Input() iconName: string;
#Input() title: string;
#Input() children: MenuItemComponent[] = [];
#Input() routeLink: string = '';
constructor(private parent: MenuItemComponent) { }
menu-item.component.html:
<a><i class="material-icons">{{iconName}}</i><span>{{title}}</span>
<i class="material-icons" ></i>
</a>
The above template is used in a ngFor loop in the sidebar template...
sidebar.component.html:
<ul>
<li *ngFor="let item of MenuStructure">
<app-menu-item [title]='item.title' [iconName]='item.iconName'>
</app-menu-item>
</li>
</ul>
However when trying to implement this, I get the following template error:
Template parse errors:
Cannot instantiate cyclic dependency! MenuItemComponent ("<ul>
<li *ngFor="let item of MenuStructure">
[ERROR ->]<app-menu-item [title]='item.title' [iconName]='item.iconName'>
</app-menu-item>
"): SidebarComponent#9:12
Can anyone shed some light on why it is complaining / a fix, and/or a better way to do this in angular 2?
As pointed out by Amit, the error I was getting was generated from passing private parent: MenuItemComponent into the MenuItemComponent constructor (it's own constructor).
I falsely assumed that the dependency injector would create a different reference/instance for it and stop there, but turns out it was creating an infinite loop.
At this stage, I still haven't found a better solution on how to implement a Menu with children in angular 2 using components / sub-components, so anyone with a better solution is welcome to add an answer, but this answer addresses the error directly.

Categories

Resources