I want to create Dynamic components and previously when i was working with Angular 2, I used this piece of code from another stack-overflow answer which worked fine until i switched to Angular-cli. How can i make it work in angular cli?
import {
Component,
Directive,
NgModule,
Input,
ViewContainerRef,
Compiler
} from '#angular/core';
import { CommonModule } from '#angular/common';
#Directive({
selector: 'html-outlet'
})
export class HtmlOutlet {
#Input() html: string;
constructor(private vcRef: ViewContainerRef, private compiler: Compiler) {}
ngOnChanges() {
const html = this.html;
if (!html) return;
#Component({
selector: 'dynamic-comp',
templateUrl: html
})
class DynamicHtmlComponent { };
#NgModule({
imports: [CommonModule],
declarations: [DynamicHtmlComponent]
})
class DynamicHtmlModule {}
this.compiler.compileModuleAndAllComponentsAsync(DynamicHtmlModule)
.then(factory => {
const compFactory = factory.componentFactories.find(x => x.componentType === DynamicHtmlComponent);
this.vcRef.clear();
const cmpRef = this.vcRef.createComponent(compFactory, 0);
});
}
}
Related
I'm currently working on an Angular application. In one of my methods, I dynamically create a component, however I am unable to use ngClass, ngIf and other such directives from the CommonModule in the component.
Below is an example of the error:
Error when I use ngIf or ngClass inside the dynamically loaded logo component
WHAT I DID ALREADY:
I've imported commonModule in my project app.module.ts
I've imported commonModule in my display component and it works as I'm able to use ngIf and ngClass in every other component without any problem
Also I'm able to import any component without errors as long as I'm not using any directive from the CommonModule in my html
I've tried importing an instance of the NgModule through component factory createComponent function as shown in the angular documentation:
Quick view of angular documentation on component factory createcomponent
I have spent hours on this and believe it's related to my use of the createComponent method.
Please Help!
Here's my app.module.ts
import { NgModule } from '#angular/core';
import { BrowserModule, HammerGestureConfig, HammerModule,
HAMMER_GESTURE_CONFIG } from '#angular/platform-browser';
import { GoogleAnalyticsService } from
'./shared/services/googleanalytics'; // import our Google Analytics
service
import { BrowserAnimationsModule } from '#angular/platform-
browser/animations';
import { HttpClientModule } from '#angular/common/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { CommonModule } from '#angular/common';
#NgModule({
declarations: [AppComponent],
imports: [
CommonModule,
BrowserModule,
AppRoutingModule,
HttpClientModule,
BrowserAnimationsModule,
],
providers: [GoogleAnalyticsService],
bootstrap: [AppComponent],
})
export class AppModule {}
Here's my Display component (Bdoc-a.component.ts)
import {
Component, OnInit, Input, Output, Renderer2, AfterViewInit,
AfterContentChecked, ViewChild, ElementRef,
ComponentFactoryResolver, ViewContainerRef, ViewChildren, QueryList
} from '#angular/core';
import { StorageService } from
'../../../../shared/services/storage.service';
import { AuthService } from '../../../../auth/auth.service';
#Component({
selector: 'app-bdoc-a',
templateUrl: './bdoc-a.component.html',
styleUrls: ['./bdoc-a.component.scss'],
})
export class BdocAComponent implements OnInit, AfterViewInit,
AfterContentChecked {
#Input() docConfig = { aspectRatio: '4:3', width: 800, height: 500 };
#Input() bgStyles = { shadow: true, bgClr: '#ffcc00' };
#Input() showBtns = false;
fWidth = this.docConfig.width;
fMaxWidth = this.docConfig.width;
fHeight = this.docConfig.height;
fMaxHeight = this.docConfig.height;
#Input() url = '';
#Input() settings = { ... };
...
#ViewChildren('loadDynAssetElEditItms', { read: ViewContainerRef })
biEditEls: QueryList<ViewContainerRef>;
#ViewChildren('loadDynAssetElViewItms', { read: ViewContainerRef })
biViewEls: QueryList<ViewContainerRef>;
loadDynAssetEl: any;
#ViewChild('bieditorFloat', { read: ElementRef }) bieditorFloat:
ElementRef;
#ViewChild('bieditorFloatViewer', { read: ElementRef })
bieditorFloatViewer: ElementRef;
// variable to hold all document page elements
allDocPages: any;
allDocPageComp = [];
constructor(private storage: StorageService,
private resolver: ComponentFactoryResolver,
private authService: AuthService,
private elmRef: ElementRef) {
this.url = 'templates/logo/1/logo1a/logo1a.component';
}
ngAfterViewInit(): void {
this.initBrandAsset();
}
initBrandAsset(): void {
this.allDocPageComp = [];
setTimeout(() => {
for (let i = 0; i < this.pages; i++) {
this.loadDynAsset(this.url, i);
}
this.setDocPageVar();
}, 200);
}
async loadDynAsset(url, pgIndex) {
const impEl = await import( 'src/app/' + url);
const allKeys = Object.keys(impEl);
this.biEditEls.forEach((itm, i) => {
if (i === pgIndex) {
itm.clear();
const newComp = itm.createComponent(
this.resolver.resolveComponentFactory(impEl[allKeys[0]]));
newComp.instance['bol']['test'] = 'LOGO TEST TEXT HERE... ' + pgIndex;
this.allDocPageComp.push(newComp.instance);
}
});
}
}
Here's my Logo1a.component.html
<div class="baItem biLogo logo1a edit">
<div class="null bilNull">
<ng-container>
<div class="bilSymb">
<div class="null">
<div #forTxtLogo class="forTxtLogo" *ngIf="config.symb.mode ===
'txt'">
<div class="symbItm"><div class="nl">B</div></div><div
class="symbItm"><div class="nl">S</div></div>
</div>
<div #forImgSvgLogo class="forImgSvgLogo" *ngIf="config.symb.mode
=== 'svg' || config.symb.mode === 'img'">
<div class="symbItm"><div class="nl"></div></div>
</div>
</div>
</div><!-- end of bilSymb -->
</ng-container>
<ng-container *ngIf="config.body.show">
<div class="bilBody">
<div class="null">
<ng-container *ngIf="config.body.txt.show">
<div #forTxtArea class="forTxtArea">
<div class="null">Logo Body Text Area</div>
</div>
</ng-container>
<ng-container *ngIf="config.body.tag.show">
<div #forTagArea class="forTagArea"><div class="null">Logo Tag
Area...</div></div>
</ng-container>
</div>
</div><!-- end of bilBody -->
</ng-container>
</div><!-- end of biNull -->
</div><!-- end of biLogo-->
Here's my Logo1a.component.ts
import { NgModule, Component, OnInit, Input, Output, Renderer2,
AfterViewInit, AfterContentChecked, ViewChild, ElementRef,
ComponentFactoryResolver, ViewContainerRef, ViewChildren, QueryList
} from '#angular/core';
import { StorageService } from
'../../../../shared/services/storage.service';
import { AuthService } from '../../../../auth/auth.service';
#Component({
selector: "app-logo1a",
templateUrl: "./logo1a.component.html",
styleUrls: ["./logo1a.component.scss"],
})
export class Logo1aComponent implements OnInit, AfterViewInit {
#ViewChild('loadExtra', { read: ViewContainerRef }) loadExtra:
ViewContainerRef;
#Input() bol = { test: 'LOGO 1A LOADED!' }; // breadth of life
public config = {
symb: {
show: true,
mode: 'txt', // 'txt', 'symb', 'img'
},
body: {
show: true,
txt: { show: true },
tag: { show: true }
}
}
constructor(private storage: StorageService,
private resolver: ComponentFactoryResolver,
private vcRef: ViewContainerRef,
private authService: AuthService,
private elmRef: ElementRef) { }
ngOnInit(): void {}
ngAfterViewInit() {}
}
Here's my Logo1a.module.ts
import { NgModule } from '#angular/core';
import { CommonModule } from '#angular/common';
import { Logo1aComponent } from './logo1a.component';
#NgModule({
imports: [CommonModule],
declarations: [Logo1aComponent],
exports: [Logo1aComponent]
})
export class Logo1aModule {}
But this was the error that was thrown after I dynamically imported the module instead of the component in my Angular 10 app as #jburtondev suggested:
ERROR Error: Uncaught (in promise): Error: ASSERTION ERROR: Type passed in is not ComponentType, it does not have 'ɵcmp' property.
....
The component needs to be in its own module which declares the CommonModule. Otherwise, Angular cannot associate it with the CommonModule at runtime.
1. Create a component module
#NgModule({
declarations: [ YourDynamicComponent ],
imports: [ CommonModule ] // THIS IS WHAT WILL TELL ANGULAR TO LOAD IT INTO YOUR COMPONENT
exports: [ YourDynamicComponent ]
})
export class YourDynamicModule { }
2. Load the component
constructor(private compiler: Compiler, private viewContainerRef: ViewContainerRef) {}
createDynamicComponent(): void {
const componentModule = this.compiler.compileModuleAndAllComponentsSync(YourDynamicModule);
const factory = componentModule.componentFactories.find(c => comp.componentType === YourDynamicComponent);
this.viewContainerRef.createComponent(factory);
}
3. It should now be be decorated with the CommonModule
So I finally got a not-so-clean working approach to my question.
I created a new module, LogoModules, where I imported all the logo modules I will be displaying in my display component
import { NgModule } from '#angular/core';
import { Logo1aModule } from '../templates/logo/1/logo1a/logo1a.module';
import { Logo2aModule } from '../templates/logo/2/logo2a/logo2a.module';
import { Logo3aModule } from '../templates/logo/3/logo3a/logo3a.module';
#NgModule({
// imports: [],
// exports: [],
})
export class LogoModules {}
This was modified a bit from #jburtondev's suggestion to pre-import all the modules but I still think there should be a cleaner way to do this part. Dynamically loading the modules and components without pre-importing the modules still works but also fails sometimes. I think this is due to some settings with the compiler and webpack in tsconfig. I need more research to clarify this.
I imported the LogoModules module in my display component
import { LogoModules } from '../templates/logo/logos.module';
...
BUT ...
So I am able to store the relative paths of each logo template in my database and display them dynamically based on user clicks, I updated my dynamic component loader in my Display component (Bdoc-a.component.ts) as shown below:
....
async loadDynAsset(url, pgIndex) {
const mObj = await import('src/app/' + url2);
const mKeys = Object.keys(mObj);
const mName = mKeys[0];
this.compiler.compileModuleAndAllComponentsAsync(mObj[mName])
.then((factories) => {
const f = factories.componentFactories[0];
const newComp = this.testDynComp.createComponent(f);
newComp.instance['bol']['test'] = 'LOGO TEST TEXT HERE... ' + pgIndex;
});
....
}
....
And then I was able to get a consistent result without errors. Thanks again to #jburtondev for some suggestions. I'll update my answer as soon as I discover a better approach to my question.
I'm trying the set the templateUrl based on a module property that I imported it before the #Component , i.e -
import { details } from './details';
#Component({
selector: 'app-my-cmp',
templateUrl: details.typeA ? './pageA.html' : './pageB.html'
})
When I'm making that I'm getting an error - Module not found,
But when I use the imported module within the ngOnInit() I have an access to this module.
How can I use the imported module in the templateUrl line ?
Just another solution.
You can achieve this using ng-template and then dynamically update the template accordingly your condition like this -
import {
Compiler, Component, Injector, VERSION, ViewChild, NgModule, NgModuleRef,
ViewContainerRef
} from '#angular/core';
#Component({
selector: 'my-app',
template: `<ng-container #vc></ng-container>`,
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
name = 'Angular';
#ViewChild('vc', {read: ViewContainerRef}) vc;
conditionValue = 'myCondition';
constructor(
private _compiler: Compiler,
private _injector: Injector,
private _m: NgModuleRef<any>
) {
}
ngOnInit() {
let tmpCmp;
if (this.conditionValue === 'myCondition') {
tmpCmp = Component({
templateUrl: './e.html'})(class {
});
} else {
// something else
}
const tmpModule = NgModule({declarations: [tmpCmp]})(class { });
this._compiler.compileModuleAndAllComponentsAsync(tmpModule)
.then((factories) => {
const f = factories.componentFactories[0];
const cmpRef = f.create(this._injector, [], null, this._m);
cmpRef.instance.name = 'dynamic';
this.vc.insert(cmpRef.hostView);
})
}
}
#Example
For more info, refer -
https://blog.angularindepth.com/here-is-what-you-need-to-know-about-dynamic-components-in-angular-ac1e96167f9e
I have been trying to make a simple app in Angular, I was able to make it work in Plunker. Unfortunately, it gives me this error
Can't bind to 'joke' since it isn't a known property of 'app-root'.
that I don't know how to handle.
What is the problem?
joke.component.ts
import { Component, EventEmitter, Input, Output, OnInit } from '#angular/core';
import { Joke } from '../jokes'
#Component({
selector: 'app-joke',
templateUrl: './joke.component.html',
styleUrls: ['./joke.component.css']
})
export class JokeComponent implements OnInit {
constructor() {}
#Input("joke") joke: Joke;
#Output() jokeDeleted = new EventEmitter<Joke>();
deleteItem() {
this.jokeDeleted.emit(this.joke)
}
ngOnInit() {}
}
joke-form.component.spec
import { async, ComponentFixture, TestBed } from '#angular/core/testing';
import { JokeFormComponent } from './joke-form.component';
describe('JokeFormComponent', () => {
let component: JokeFormComponent;
let fixture: ComponentFixture<JokeFormComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ JokeFormComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(JokeFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});
joke-list.component
import { Component, OnInit } from '#angular/core';
import {Joke} from '../jokes';
#Component({
selector: 'app-joke-list',
templateUrl: './joke-list.component.html',
styleUrls: ['./joke-list.component.css']
})
export class JokeListComponent implements OnInit{
jokes: Joke[];
constructor() {
this.jokes = [
new Joke("I am telling a joke.", "Haha, that's funny!"),
new Joke("I am telling an even funnier joke.", "Hahahahaha!!"),
new Joke("I am telling the funniest joke.", "HAHAHAHAHAHA!!!!")
]
}
addJoke(joke) {
this.jokes.unshift(joke);
}
deleteJoke(joke) {
let indexToDelete = this.jokes.indexOf(joke)
if (indexToDelete !== -1) {
this.jokes.splice(indexToDelete, 1);
}
}
ngOnInit() {}
}
app.component
import { Component } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {}
app.module.ts
import { BrowserModule } from '#angular/platform-browser';
import { NgModule } from '#angular/core';
import { FormsModule, ReactiveFormsModule } from '#angular/forms';
import { HttpModule } from '#angular/http';
import { AppComponent } from './app.component';
import { JokeFormComponent } from './joke-form/joke-form.component';
import { JokeListComponent } from './joke-list/joke-list.component';
import { JokeComponent } from './joke/joke.component';
#NgModule({
declarations: [
AppComponent,
JokeFormComponent,
JokeListComponent,
JokeComponent,
],
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule,
HttpModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
From the code you have posted I see that your AppComponent class is empty :
export class AppComponent {}
Since you haven't posted your html code, I am guessing you are doing something similar to the plunker, where my-app in plunker is equivalent to app-root in your question's code:
<app-root *ngFor="let j of jokes" [joke]="j" (jokeDeleted)="deleteJoke($event)"></app-root>
Once you add #Input("joke") joke: Joke to AppComponent class, it should not throw that error anymore:
export class AppComponent {
#Input("joke") joke: Joke;
#Output() jokeDeleted = new EventEmitter<Joke>();
deleteItem() {
this.jokeDeleted.emit(this.joke)
}
}
You can try to delete this OnInit method that angular generates for us in this child joke.component.ts class that implements this #Input method for Property binding [property]. And also restart the server.
I have wheels.component nested to car.component.
wheels.component:
export class WheelsComponent {
#Output() onLoaded : EventEmitter<string>() = new EventEmitter<string>();
private downloadAllFiles(url: string) {
this.onLoaded.emit('Hello, World 1!');
//some operations to wait
this.onLoaded.emit('Hello, World 2!');
};
}
Component car.component is not written at html page, but called through routing at car-routing.module.ts:
#NgModule({
imports: [
RouterModule.forChild([
{
path: 'sfactmessage/:id',
component: CarComponent,
resolve: {
card: cardResolver
}
}
])
],
exports: [RouterModule]
})
export class CarRoutingModule {}
What I want is to handle event emitted from wheels.component, not at car.component, but at app.component.
Is it possible to handle event at app.component?
The plunker sample is not working (sorry, this is my first plunkr example), but gives a view how my app is arranged.
Hello_ friend.
So basically if you want to use events globally in your application you can use a Service in combination with EventEmitter
In this case you create a service for example car.service.ts
import { Injectable, EventEmitter } from '#angular/core';
#Injectable()
export class CarService {
onLoaded : EventEmitter<string> = new EventEmitter<string>();
}
Then use this service in a child component to emit events like this wheels.component.ts
import { Component, EventEmitter } from '#angular/core';
import { CarService } from './car.service';
#Component({
selector: 'wheels',
template: '<a (click)="sendValues()"> Click me to send value </a>'
})
export class WheelsComponent {
constructor(private carService:CarService ){}
sendValues() {
/* Use service to emit events that can be used everywhere in the application */
this.carService.onLoaded.emit('Button in WheelsComponent was clicked ...');
};
}
and then capture this event from AppComponent for example app.component.ts
import { Component, OnInit, OnDestroy } from '#angular/core';
import { CarService } from './cars/car.service';
import { Subscription } from 'rxjs';
#Component({
selector: 'my-app',
templateUrl: `src/app.component.html`
})
export class AppComponent implements OnInit, OnDestroy{
private subscription: Subscription;
private loading = true;
name = 'Angular';
constructor(private carService: CarService){}
ngOnInit(){
this.subscription = this.carService.onLoaded.subscribe((message) => {
/*
Here you receive events from anywhere where
carService.onLoaded.emit() is used
**/
alert(`From AppComponent -> ${message}`);
});
}
ngOnDestroy(){
/* Don't forget to unsubscribe when component is destroyed */
this.subscription.unsubscribe();
}
}
I M P O R T A N T______________
If you want your service to work globally you need to declare it in the top level providers for example app.module.ts is a good place:
import { NgModule } from '#angular/core';
import { BrowserModule } from '#angular/platform-browser';
import { FormsModule } from '#angular/forms';
import { AppComponent } from './app.component';
import { CarComponent} from './cars/car.component';
import { WheelsComponent} from './cars/wheels.component';
import { HomeComponent} from './home.component';
import { routing } from './app.routing';
import { CarService } from './cars/car.service';
#NgModule({
imports: [ BrowserModule, FormsModule, routing ],
declarations: [ AppComponent, CarComponent, WheelsComponent, HomeComponent ],
providers: [ CarService ], // <-------- SEE HERE
bootstrap: [ AppComponent ]
})
export class AppModule { }
CLICK HERE TO SEE THE DEMO
Every time I navigate from MainComponent to TestListComponent the TestListComponent constructor is triggered and a new instance of the ObservableServiceis created. When I click the link the console show the duplicated messages. Maybe is an angular issue, any help?
main.module.ts
import { NgModule } from '#angular/core';
import { BrowserModule } from '#angular/platform-browser';
import {MainRoutingModule} from "./main-routing.module";
import {MainComponent} from './main.component';
import {ObservableService} from "../../core/services/observable.service";
#NgModule({
imports: [
BrowserModule,
MainRoutingModule,
],
declarations: [MainComponent],
providers: [ObservableService],
bootstrap: [
MainComponent
]
})
export class MainModule { }
main.routing.module.ts
import { NgModule } from '#angular/core';
import { Routes, RouterModule } from '#angular/router';
export const routes: Routes = [
{ path: 'tests', loadChildren: 'angular/app/modules/test-list/test-list.module#TestListModule'},
{ path: '**', redirectTo: '' }
];
#NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class MainRoutingModule {}
observable.service.ts
import { Injectable } from '#angular/core';
import {Subject} from "rxjs/Rx";
import 'rxjs/add/operator/map'
#Injectable()
export class ObservableService {
// Observable string sources
private changeLanguageStatus = new Subject<Object>();
// Observable string streams
changeLanguageStatus$ = this.changeLanguageStatus.asObservable();
constructor(){}
/**
* Change language event
* #param params
*/
changeLanguageEvent(params: Object){
this.changeLanguageStatus.next(params);
}
}
test-list.module.ts
import { NgModule } from '#angular/core';
import {TestListComponent} from "./test-list.component";
#NgModule({
declarations: [
TestListComponent
]
})
export class TestListModule {}
test-list.component.ts
import {Component} from '#angular/core';
import 'rxjs/Rx';
import {ObservableService} from "../../core/services/observable.service";
#Component({
moduleId: module.id,
selector: 'st-test-list',
templateUrl: 'test-list.component.html'
})
export class TestListComponent {
constructor(private observableService:ObservableService) {
observableService.changeLanguageStatus$.subscribe(
data => {
console.log('Test', data);
});
}
}
main.component.ts
import {Component, ViewChild} from '#angular/core';
import 'rxjs/Rx';
import {ObservableService} from "../../core/services/observable.service";
#Component({
moduleId: module.id,
selector: 'st-main',
templateUrl: 'main.component.html'
})
export class MainComponent {
constructor(private observableService:ObservableService) {}
changeLanguage(lang){
this.observableService.changeLanguageEvent({type: lang});
}
}
main.component.html
<!--Dynamic content-->
<router-outlet></router-outlet>
It should be expected behavior that when you navigate to a component via routing it is created and when you navigate back it is destroyed. As far as I know you are experiencing this issue because you are creating what is called an Infinite Observable i.e. you are subscribing to it and waiting for a stream of events, in your case changing language. Because you never unsubscribe from your Observable, the function subscribed to it is kept alive for each new instance of your component. Therefore, rxjs won't handle disposing of your subscription and you will have to do it yourself.
First off I'd suggest you read about Lifecycle hooks. Check out the OnInit and OnDestroy lifecycle hooks.
Use ngOnInit to subscribe to your Observable and use ngOnDestroy to unsubscribe from it as such:
import { Component, OnInit, OnDestroy } from '#angular/core';
import { Subscription } from 'rxjs/Subscription';
#Component({ .... })
export class TestListComponent implements OnInit, OnDestroy
{
private _languageSubscription : Subscription;
ngOnInit(): void
{
this._languageSubscription = observableService.changeLanguageStatus$.subscribe(
data => {
console.log('Test', data);
});
}
ngOnDestroy() : void
{
this._languageSubscription.unsubscribe();
}
}
I hope this will solve your problem.