Display <my-component> by [innerHTML] - javascript

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

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.

Angular 2 Routes - Is it possible to create dynamic routerLinks from routes, using an ngFor?

I would like to take all of the Routes in a specific module that have a defined path, and use ngFor to loop through them and create a dynamic list of links in my component html.
ngFor Example (overview.component.html):
<li *ngFor="let item of items; let i = index;">
<a routerLink="/{route.path}">{route.path}</a>
</li>
module example (base.module.ts):
...
const routerConfig = [
{
path: '',
component: BaseComponent,
children: [
{
path: '',
component: OverviewComponent
},
{
path: 'group-one',
component: OverviewComponent
},
{
path: 'group-one/:id',
component: DetailComponent
},
{
path: 'group-two',
component: OverviewComponent
},
{
path: 'group-two/:id',
component: DetailComponent
}
]
}
];
#NgModule({
imports: [
RouterModule.forChild(routerConfig)
]
...
})
export class BaseModule { }
After trying various experiments with ActivatedRoute, Router, etc, I have not been able to find an object where this information is made available to use.
Is this currently possible via the Angular 2 Router?
How could I go about solving this problem?
This code
<a routerLink="/{route.path}">
passes /{route.path} literally as route path
You probably either want
<a [routerLink]="'/' + route.path">
or
<a routerLink="/{{route.path}}">
For Angular to evaluate expressions, bindings need to have exactly one of [] or {{}} (never both at the same time)
[propName]="..."
or
propName="{{...}}"
The later always stringifies the result and is therefore not suitable to pass objects.

List of different components in Angular 2 ngFor

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.

In Angular2 how do you change a sidebar based on the active route?

I'm writing an SPA using Angular 2 (RC5) and Typescript. I have a main navbar on top (using Bootstrap 4 Alpha 3), and a custom-designed sidebar. Here's a pic:
The intention here is to navigate to a main "application" using the top navbar. Currently I have 3 main apps mapped out - Tracker, Proboard and Equipment. Once a main application has been activated, the links on the sidebar would change. In the picture above, the sidebar is showing links for the 'Proboard' application.
How can I change the contents of the sidebar based on the active route without having a bunch of '*ngIf' declarations in my HTML?
I would like to have a JSON object in my code somewhere (perhaps retrieved from a backend API) that would list the links for each app. Something like:
[
{ proboard: [
{ label: 'Home', link: '/proboard', icon: 'fa-home'},
{ label: 'Candidates', link: '/proboard/candidates', icon: 'fa-users'},
{ label: 'New', link: '/proboard/candidate/new', icon: 'fa-new-user; } ]
},
{ tracker: [...] },
{ equipment: [...] }
]
Then when the user would navigate to one of the links on the top navbar, the sidebar would be populated with the contents in the array above, using an NgFor in the HTML.
Any suggestions or links would be appreciated.
I've done the same thing and here is how I solved it.
Created the menu in the top-most app component
Created a service and kept a common routing table in the service
Created a subscribable subject stream for changing the route
Subscribed to that stream from the component where the menu resides
Issue subcription.next from the current component which sets the selected route in the component with the menu.
You can achieve this with other type of component interactions. Here is more info on that from the Angular 2 docs: https://angular.io/docs/ts/latest/cookbook/component-communication.html
Here's one possible solution.
Since each of your three apps is meant to be completely separate (as you've defined it), you could make a completely independent 'SideBarComponent' that you can reuse in each app.
Since you can give a component any information you want with #Input() variables, you can provide it the list of labels, links, and icons it needs. For example:
EquipmentComponent.ts
sideBarLinks: any = { proboard: [
{ label: 'Home', link: '/proboard', icon: 'fa-home'},
{ label: 'Candidates', link: '/proboard/candidates', icon: 'fa-users'},
{ label: 'New', link: '/proboard/candidate/new', icon: 'fa-new-user; } ]
}
EquipmentComponent.html
<side-bar [buttons]='sideBarLinks'></side-bar>
SideBarComponent.ts
#Input()
buttons: any;
SideBarComponent.html
// Use 'buttons' to construct your buttons in the sidebar. Possibly using an ng repeater in this template.
Even if you don't end up implementing this exactly, I hope this gets you thinking in the direction of using reusable components with Angular 2.
Hopefully this is helpful!
Here's more than everything you would need to know about component interactions: https://angular.io/docs/ts/latest/cookbook/component-communication.html.
2 possible ways
1 - In sidebar component, subscribe to router changes, but you will end up using *ngIf
#Input()
data: any;
constructor(private router: Router) {
router.changes.subscribe((val) => {
if(val === 'noSidebarRoute'){
this.navButtons= false;
}
else {
this.navButtons= navData;
}
})
}
then in parent component template
#Component({
selector: 'parent',
template: `<side-bar [data]='navData'></side-bar>`
});
export class Parent{
navData = [
{ proboard: [
{ label: 'Home', link: '/proboard', icon: 'fa-home'},
{ label: 'Candidates', link: '/proboard/candidates', icon: 'fa-users'},
{ label: 'New', link: '/proboard/candidate/new', icon: 'fa-new-user; }
]},
{ tracker: [...] },
{ equipment: [...] }
]}
2 - in your app component (or sidebar parent component), use resolver.resolveComponent to dynamically add the sidebar component to your app, this way you won't be using data binding (*ngIf).
sidebarCmpRef:ComponentRef<any>;
constructor(private router: Router,
private viewContainerRef:ViewContainerRef,
private resolver:ComponentResolver) {
router.changes.subscribe((val) => {
if(val === 'noSidebarRoute'){
if(this.sidebarCmpRef){
this.sidebarCmpRef.destroy();
this.sidebarCmpRef
}
}
else{
this.AddSidebar();
}
})
}
AddSidebar(){
if(!this.cmpRef){
this.resolver.resolveComponent(<Type>SideBar)
.then((factory:ComponentFactory<any>) => {
this.cmpRef = this.viewContainerRef.createComponent(factory);
this.cmpRef.instance.buttons= navData;
});
}
}
I would prefer to go with angular basic approach i.e loading children inside the routes.
Define your main.route as follows
import { NgModule } from '#angular/core';
import { Routes, RouterModule } from '#angular/router';
import { PageNotFoundComponent } from './shared/components/pagenotfound.component';
export const appRoutes: Routes = [
{
path: 'tracker',
loadChildren: 'app/tracker/module#trackermodule',
},
{
path: 'proboard',
loadChildren: 'app/proboard/module#proboard',
},
{
path: 'equiment',
loadChildren: 'app/equiment/module#equiment',
},
{
path: '**',
component: PageNotFoundComponent
}
];
#NgModule({
imports: [RouterModule.forRoot(appRoutes)],
declarations: [
PageNotFoundComponent
],
exports: [RouterModule]
})
export class AppRouterModule {
}
then in your child/sub-module routing configuration should be
import { RouterModule } from '#angular/router';
import { CssComponent } from './view';
import { RadioAndCheckBoxComponent } from './radio-and-checkbox/view';
export const RouteComponents = [
CssComponent
];
export const Routes = RouterModule.forChild([
{
path: '',
component: equimentComponent,
children: [
{
path: 'Home',
component: HomeComoponent
},
{
path: 'Candidates',
component: CandidatesComoponent
}
]
}
]);
I have same situation (Main Menu Links and their respected sub menu items), I have done this in following manner.
Inside the root component (app.component.ts) constructor, I subscribe to router events, and based on the redirect route URL, I hide and show the Main application sub menus. Here is code:
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
this.currentUrl = event.urlAfterRedirects;
console.log(event.urlAfterRedirects);
if (this.currentUrl.includes("/MainMenuLink1")) {
this.link1SubMenu = true;
this.link2SubMenu = false;
} else if (this.currentUrl.includes("/MainMenuLink2")) {
this.link2SubMenu = true;
this.link1SubMenu = false;
}
}
});
And based on that [link1SubMenu] boolean values, I show and hide the Sub menu in app.component.html using *ngIf='link1SubMenu'
It needs to be refactored, but this is quick win.

Angular 2: Multiple Form

I have a component which contains child components. Each child contains a form, built using FormBuilder.
I have included JADE template inside the components just for clarity.
Further I load the child components using the component router.
#Component({
selector: 'parent',
template: `
.detail-page
.detail-header
.ui.secondary.menu.inverted.orange
.item Inquiry Details
.right.menu
a.item((click)='add($event)')
i.plus.icon
a.item((click)='save($event)')
i.save.icon
.detail-content
router-outlet
.detail-footer
a.item([routerLink]="['Child1']")
a.item([routerLink]="['Child2']")
`
})
#RouteConfig([
{ path: '/child1', name: 'Child1', component: Child1, useAsDefault: true },
{ path: '/child2', name: 'Child2', component: Child2 }
])
export class Parent {
save(event) {
event.preventDefault();
// validate all child forms
???????
}
}
#Component({
template: `
form([ngFormModel]='childForm1')
input(type='text', [control]="field1")
input(type='text', [control]="field2")
`
})
export class Child1 {
constructor(
private _formBuilder: FormBuilder
) {
this.childForm1 = this._formBuilder.group({
field1: this.field1,
field2: this.field2
});
}
}
#Component({
template: `
form([ngFormModel]='childForm2')
input(type='text', [control]="field1")
input(type='text', [control]="field2")
`
})
export class Child2 {
constructor(
private _formBuilder: FormBuilder
) {
this.childForm2 = this._formBuilder.group({
field1: this.field1,
field2: this.field2
});
}
}
I need to validate the all the child forms when the save button is pressed.
As I understand only one child component is active/initialized at the end of the routing. Thus I cannot loop over the components to validate the forms.
What is the best approach to design such a component where it is not user friendly to have a long vertical form but have it broken down into manageable child form components?
Is there a way to re-use the components created?
I was thinking I can use the dynamic component loader but still I only get access to the current loaded component.
Your suggestions/help is much appreciated.
Thank You
You can add all forms at once and just show a selection of them at once using
[hidden]="someExpression"
You can still wrap the parts in components to keep the size of the template of the parent small, but validation only runs for on elements that actually exist in the DOM. (If you wrap parts of the form into components, then the forms in these components would be validated individually and you have to collect the results to get the status for the whole.)

Categories

Resources