Add a component dynamically to a child element using a directive - javascript

Trying to place a component dynamically to a child element, using a directive.
The component (as template):
#Component({
selector: 'ps-tooltip',
template: `
<div class="ps-tooltip">
<div class="ps-tooltip-content">
<span>{{content}}</span>
</div>
</div>
`
})
export class TooltipComponent {
#Input()
content: string;
}
the directive:
import { TooltipComponent } from './tooltip.component';
#Directive({
selector: '[ps-tooltip]',
})
export class TooltipDirective implements AfterViewInit {
#Input('ps-tooltip') content: string;
private tooltip: ComponentRef<TooltipComponent>;
constructor(
private viewContainerRef: ViewContainerRef,
private resolver: ComponentFactoryResolver,
private elRef: ElementRef,
private renderer: Renderer
) { }
ngAfterViewInit() {
// add trigger class to el
this.renderer.setElementClass(this.elRef.nativeElement, 'ps-tooltip-trigger', true); // ok
// factory comp resolver
let factory = this.resolver.resolveComponentFactory(TooltipComponent);
// create component
this.tooltip = this.viewContainerRef.createComponent(factory);
console.log(this.tooltip);
// set content of the component
this.tooltip.instance.content = this.content as string;
}
}
The problem is that this is creating a sibling and I want a child (see bellow)
result:
<a class="ps-btn ps-tooltip-trigger" ng-reflect-content="the tooltip">
<span>Button</span>
</a>
<ps-tooltip>...</ps-tooltip>
wanted result:
<a class="ps-btn ps-tooltip-trigger" ng-reflect-content="the tooltip">
<span>Button</span>
<ps-tooltip>...</ps-tooltip>
</a>
Thanks in advance for your help!

Even dynamic component is inserted as sibling element you can still move element to desired place by using:
this.elRef.nativeElement.appendChild(this.tooltip.location.nativeElement);
Plunker Example

A better approach would be to have a nested ng-template with template reference variable on it such that the component is added as a sibling to ng-template but is now child to ng-template's parent.
Your template should be
<div class="ps-tooltip">
<div class="ps-tooltip-content">
<span>{{content}}</span>
<ng-template #addHere></ng-template>
</div>
</div>
And in your component
#ViewChild('addHere') addHere: ViewContainerRef;
ngAfterViewInit() {
...
this.tooltip = addHere.createComponent(factory)
...
}

Related

Set some background color only for login page

I am trying to give my login page some background color(only for my login page)
For that, I have built a directive in my shared folder
import {
Directive,
ElementRef,
Renderer2,
Input,
AfterViewInit
} from "#angular/core";
#Directive({
selector: "[appBackground]"
})
export class BackgroundDirective implements AfterViewInit {
constructor(private renderer: Renderer2, private el: ElementRef) {}
#Input() backgroundColor: string;
ngAfterViewInit() {
this.renderer.setStyle(
this.el.nativeElement,
"background-color",
this.backgroundColor
);
}
}
and then in my login component, I am doing this
export class LoginComponent implements OnInit {
color: string;
emailFormControl = new FormControl("", [
Validators.required,
Validators.pattern(EMAIL_REGEX)
]);
passwordFormControl = new FormControl("", [Validators.required]);
constructor(private dir: BackgroundDirective) {}
ngOnInit() {
console.log(this.dir);
this.dir.backgroundColor = "lightblue";
}
}
and then in login.component.html
<div BackgroundDirective>
// my login page content
</div>
But I see no change in my login page background color, what I am missing?
There are multiple errors in your example. here is a stackblitz implementing your code
1/ Directive are meant to be used in an HTML declarative way. You don't have to inject it in your constructor (for simple cases)
2/ your selector is [appBackground] so you have to use your directive this way. You have to use the chosen selector, not the directive ClassName
<div appBackground>...</div>
3/ your added an Input on your directive (#Input() backgroundColor: string;)
so if you want to set the color you can use it this way
<div appBackground backgroundColor="red"></div>
4/ don't forget to add your directive in the declarations parts of your module.

Angular 2 eventEmitter dosen't work

I need to make something simple, I want to display a dialog when I click on an help icon.
I have a parent component:
#Component({
selector: 'app-quotation',
templateUrl: './quotation.component.html'
})
export class QuotationComponent implements OnInit {
public quotation: any = {};
public device: string;
public isDataAvailable = false;
#Output() showPopin: EventEmitter<string> = new EventEmitter<string>();
constructor(private quotationService: QuotationService,
private httpErrors: HttpErrorsService,
private popinService: PopinService) {}
moreInfo(content: string) {
console.log('here');
this.showPopin.emit('bla');
}
}
And his html:
<ul>
<li *ngFor="let item of quotation.HH_Summary_TariffPageDisplay[0].content">
<label></label>
<i class="quotation-popin" (click)="moreInfo()"></i>
<div class="separator"></div>
</li>
</ul>
My popin component:
#Component({
selector: 'app-popin',
templateUrl: './popin.component.html',
styleUrls: ['./popin.component.scss']
})
export class PopinComponent implements OnInit {
public popinTitle: string;
public popinContent: string;
public hidden: boolean = true;
constructor() { }
openPopin(event):void {
console.log("here");
this.hidden = false;
}
}
His HTML:
<div class="card-block popin-container" (showPopin)="openPopin($event)" *ngIf="!hidden">
<div class="card">
<div class="popin-title">
{{ popinTitle }}
<i class="icon icon-azf-cancel"></i>
</div>
<div class="popin-content">
{{ popinContent }}
</div>
</div>
</div>
My parent component is loaded in a router-outlet and my popin is loaded on the same level than the router-outlet, like this:
<app-nav-bar></app-nav-bar>
<app-breadcrumb></app-breadcrumb>
<div class="container">
<router-outlet></router-outlet>
</div>
<app-popin></app-popin>
My problem is the eventEmitter doesn't work and i don't know why, someone can explain me ?
thx,
regards
EventEmitters only work for direct Parent-Child component relationships. You do not have this relationship with the components you are describing here.
In a parent-child relatonship, we will see the child's component element within the parent's template. We do not see this in your example.
You have two options:
Refactor to use a parent-child relationship
Use a service for communication
If you go with option 2, the service should just contain an observable that one component calls next on, and the other component subscribes to.
#Injectable()
export class PopinService {
showPopin = new ReplaySubject<string>(1);
}
Inject this in QuotationComponent and modify moreInfo
moreInfo(content: string): void {
this.popinService.showPopin.next('bla' + content);
}
In PopinComponent inject the popinService and add the following:
ngOnInit() {
this.popinService.showPopin.subscribe(content => {
this.hidden = false;
this.popinContent = content;
});
}
It's because you misuse it.
In your popin component, you just call the function and do a log, and set a variable to false.
And nowhere I can see that you use the app-quotation selector, so you don't really use it, do you ?
Looks like you are sending an output to a child component (popin) . Ideally if you give output that means it should be from child to parent and from parent to child, it is Input.

Accessing DOM element in Angular 2 and change the element's class attribute

I'm new in angular2. my code is like this:
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'main',
template: `
<div class="current">
</div>
`
})
export class MainComponent implements OnInit {
ngOnInit(): void {
//change the div class from current to next...
}
}
i'd like to change the div class from 'current' to 'next'.
i appropriate if you let me know what is the best way do that?
One option is to use a template reference variable.
In the example below, the reference variable #target is added to the desired element and then the decorator #ViewChild (#ViewChild('target') target) allows you to access the variable in your component.
From there, you can get a reference to the DOM element by accessing the nativeElement property on the variable.
Here is an example where the class name is updated:
import { Component, AfterViewInit, ViewChild } from '#angular/core';
#Component({
selector: 'main',
template: `
<div #target class="current">
</div>
`
})
export class MainComponent implements AfterViewInit {
#ViewChild('target') target;
constructor() { }
ngAfterViewInit(): void {
let element = this.target.nativeElement;
element.className = 'next';
}
}
However, it's worth pointing out that you can handle most DOM manipulation with the build-in DOM directives. In this case you could just use the ngClass directive to bind a variable with the class attribute:
import { Component, AfterViewInit } from '#angular/core';
#Component({
selector: 'main',
template: `
<div [ngClass]="targetClass">
</div>
`
})
export class MainComponent implements AfterViewInit {
private targetClass: string = 'current';
constructor() { }
ngAfterViewInit(): void {
this.targetClass = 'next';
}
}

How to get width of (DOM) Element in Angular2

there a some similiar threads but I couldn't find a suitable answer
for my needs. So that direct DOM access should be strictly avoided
in angular2 I'm just wondering whats best practice for this.
What I wan't to achieve is a resize of a element based on the current width.
workitemresize.component.ts
import { Directive, ElementRef, HostListener, Renderer, Input } from '#angular/core';
#Directive({
selector: '[workItemResize]'
})
export class WorkItemResizeDirective implements ngAfterViewInit {
private el: HTMLElement;
private renderer: Renderer;
constructor(el: ElementRef, public renderer: Renderer)
{
this.el = el.nativeElement;
this.renderer = renderer;
}
#HostListener('window:resize', ['$event.target'])
onResize()
{
this.resizeWorks();
}
ngAfterViewInit() {
this.resizeWorks();
}
private resizeWorks(): void {
this.renderer.setElementStyle(this.el, 'height', this.el.width); // <-- doesn't work I kow that this.el.width doesn't exist but just for demonstration purpose
this.renderer.setElementStyle(this.el, 'height', '500'); // <-- works
}
}
projects.template.html
<div class="posts row">
<div class="work-item col-xs-12 col-sm-6 col-no-padding" workItemResize *ngFor="let post of posts">
<!-- show some fancy image -->
<div class="img" [ngStyle]="{'background-image':'url('+post.better_featured_image.source_url+')'}"></div>
</div>
</div>
Related:
https://github.com/angular/angular/issues/6515
I don't know a way to get the width from the host element without accessing nativeElement but setting could be done like:
#HostListener('window:resize', ['$event.target'])
onResize() {
this.resizeWorks();
}
#HostBinding('style.height.px')
elHeight:number;
private resizeWorks(): void {
this.elHeight = this.el.nativeElement.width;
}
If you can add an element inside your components template like
<div style="width: 100%;" #div (window:resize)="elHeight = div.getBoundingClientRect()">
<!-- your template here -->
</div>
then this would work without direct DOM access at all (but not after init).
I tried out #GünterZöchbauer solution and refined it. You do not need HostListener to get bounds of div. Like with 'click' or other event (event)="function()", it will fire this function. I hope someone will find this helpful.
<div #div (window:resize)="onResize(div.getBoundingClientRect())">
<!-- your template here -->
</div>
onResize() {
this.resizeWorks();
}
#HostBinding('style.height.px') elHeight:number;
private resizeWorks(): void {
this.elHeight = this.el.nativeElement.width;
}
(#div) - this is variable needed to get measurement target. It does not have to be same name as element, it can be any name.
One more way to get element dimensions is to use ElementRef class from Angular Core, but then you have to find that child element which has width and height properties. I used this in Angular 2 Maps to get container width and height, but i found out that, using this method i had to find in element tree element which had these properties and it was second child of root element. It is more messy.
ElementDimensions - is just a class with height and width which I made to contain those properties.
<div (window:resize)="onResize()">
<!-- your template here -->
</div>
private getContainerDimensions(): ElementDimensions{
var rootElement = this.elementRef.nativeElement;
var childElement = rootElement.firstElementChild;
var contentElement = childElement.firstElementChild;
return {
height:contentElement.clientHeight,
width: contentElement.clientWidth
};
}
I tested this way and simply worked! It seems that binding did this job.
<div #foto [style.height.px]="foto.getBoundingClientRect().width / 2">
You can get the width of the component itself if that one has a display property defined (such as 'block' in the example below). Note that you should be able to define this style (using dom shadowing):
:host {
display:block;
}
If you are not using dom shadowing (Ionic 2-3):
my-component {
display:block;
}
Here is the controller (exposing the width as an observable):
import {AfterViewInit, Component, ElementRef, HostListener, Input} from '#angular/core';
import {Subject} from 'rxjs/Subject';
#Component({
selector: 'my-component',
template: '{{$width|async}}',
style: 'my-component {display: block;}'
})
export class MyComponent implements AfterViewInit {
#Input() public gridWidth: number;
private $width: Subject<number> = new Subject();
constructor(private elementRef: ElementRef) {
}
public ngAfterViewInit(): void {
this.emitWidth();
}
#HostListener('window:resize', ['$event.target'])
public onResize(): void {
this.emitWidth();
}
private emitWidth(): void {
this.$width.next(this.getWidth());
}
private getWidth(): number {
return this.getNativeElement().clientWidth;
}
private getNativeElement(): HTMLElement {
return this.elementRef.nativeElement;
}
}

Angular2 - two way databinding on a component variable / component class property?

In Angular2 (Beta 6) I have a component for a main menu.
<mainmenu></mainmenu>
I want to bind a boolean for wide or narrow. So I made it into this:
<mainmenu [(menuvisible)]="true"></mainmenu>
But what I want (I think) is to bind to a javascript class property (as I may have other things to bind but want to be tidy by using a single class in the component).
I get an error
EXCEPTION: Template parse errors: Invalid property name
'menumodel.visible' ("
][(menumodel.visible)]="menumodel.visible">
If I try the same with a single variable instead of a class I get:
Template parse errors: Parser Error: Unexpected token '='
However this (one way binding?) does seem to work (but I might want to trigger the menu to go wide/narrow from another component so felt this should be a two-way data bound property):
<menu [vis]="true"></menu>
This is a bit of my menu component:
#Component({
selector: 'menu',
templateUrl: './app/menu.html',
providers: [HTTP_PROVIDERS, ApplicationService],
directives: [ROUTER_DIRECTIVES, FORM_DIRECTIVES, NgClass, NgForm]
})
export class MenuComponent implements OnInit {
mainmenu: MainMenuVM;
constructor(private _applicationService: ApplicationService) {
this.mainmenu = new MainMenuVM();
}
// ...ngOnInit, various functions
}
Here is my MainMenu View Model class
export class MainMenuVM {
public visible: boolean;
constructor(
) { this.visible = true; }
}
I'm trying to create a menu which has icons and text, but can go narrow to just show icons. I will emit this event upwards to a parent component to alter the position of the container next to the menu. Triggering a content container to maximised will trigger the menu to go narrow - I am not saying this is the best way, but I would like to resolve this particular question before going deeper.
Please note: I am not databinding to an input control here - just databinding to a component so I can then modify the UI.
This is from the Angular cheatsheet
<my-cmp [(title)]="name">
Sets up two-way data binding. Equivalent to: <my-cmp [title]="name" (titleChange)="name=$event">
Thanks in advance!
UPDATE
Integrating the code from the accepted answer and adapting for my particular use case here the final working code:
app.html
...header html content
// This is what I started with
<!--<menu [menuvisible]="true" (menuvisibleChange)="menuvisible=$event"></menu>-->
// This is two way data binding
// 1. Banana-in-a-box is the input parameter
// 2. Banana-in-a-box is also the output parameter name (Angular appends it's usage with Change in code - to follow shortly)
// 3. Banana-in-a-box is the short hand way to declare the commented out code
// 4. First parameter (BIAB) refers to the child component, the second refers the variable it will store the result into.
// 5. If you just need an input use the remmed out code with just the first attribute / value
<menu [(menuvisible)]="menuvisible"></menu>
.. div content start
<router-outlet></router-outlet>
.. div content end
app.component.ts (root)
export class AppComponent implements OnInit{
menuvisible: Boolean;
}
menu.component.ts (child of root)
export class MenuComponent implements OnInit {
// Parameters - notice the appending of "Change"
#Input() menuvisible: boolean;
#Output() menuvisibleChange: EventEmitter<boolean> = new EventEmitter<boolean>();
// Init
ngOnInit() {
// Populate menu - fetch application list
this.getApplications();
// Initially we want to show/hide the menu depending on the input parameter
(this.menuvisible === true) ? this.showMenu() : this.hideMenu();
}
//...more code
}
menu.html
<div id="menu" [ngClass]="menuStateClass" style="position: absolute; top:0px; left: 0px;z-index: 800; height: 100%; color: #fff; background-color: #282d32">
<div style="margin-top: 35px; padding: 5px 0px 5px 0px;">
<ul class="menuList" style="overflow-x: hidden;">
<li>IsMenuVisible:{{menuvisible}}</li>
<li style="border-bottom: 1px solid #3d4247"><a (click)="toggleMenu()"><i class="fa fa-bars menuIcon" style="color: white; font-size: 16px;"></i></a></li>
<li *ngFor="#app of applications">
<a [routerLink]="[app.routerLink]">
<i class="menuIcon" [ngClass]="app.icon" [style.color]="app.iconColour" style="color: white;"></i>
<span [hidden]="menuStateTextHidden">{{ app.name }}</span>
</a>
</li>
</ul>
</div>
</div>
Remember to import what you need e.g.
import {Component, EventEmitter, OnInit, Input, Output} from
'angular2/core';
Highly recommend this video on You Tube:
Angular 2 Tutorial (2016) - Inputs and Outputs
For two-way binding you need something like:
#Component({
selector: 'menu',
template: `
<button (click)="menuvisible = !menuvisible; menuvisibleChange.emit(menuvisible)">toggle</button>
<!-- or
<button (click)="toggleVisible()">toggle</button> -->
`,
// HTTP_PROVIDERS should now be imports: [HttpModule] in #NgModule()
providers: [/*HTTP_PROVIDERS*/, ApplicationService],
// This should now be added to declarations and imports in #NgModule()
// imports: [RouterModule, CommonModule, FormsModule]
directives: [/*ROUTER_DIRECTIVES, FORM_DIRECTIVES, NgClass, NgForm*/]
})
export class MenuComponent implements OnInit {
#Input() menuvisible:boolean;
#Output() menuvisibleChange:EventEmitter<boolean> = new EventEmitter<boolean>();
// toggleVisible() {
// this.menuvisible = !this.menuvisible;
// this.menuvisibleChange.emit(this.menuvisible);
// }
}
And use it like
#Component({
selector: 'some-component',
template: `
<menu [(menuvisible)]="menuVisibleInParent"></menu>
<div>visible: {{menuVisibleInParent}}</div>
`
directives: [MenuComponent]
})
class SomeComponent {
menuVisibleInParent: boolean;
}
I've created a short plunkr.
ngModel Like Two-Way-Databinding for components
You have at least two possibilities to to create a two way databinding for components
V1: With ngModel Like Syntax, there you have to create a #Output property with the same name line the #Input property + "Change" at the end of the #Output property name
#Input() name : string;
#Output() nameChange = new EventEmitter<string>();
with V1 you can now bind to the Child Component with the ngModel Syntax
[(name)]="firstname"
V2. Just create one #Input and #Output property with the naming you prefer
#Input() age : string;
#Output() ageChanged = new EventEmitter<string>();
with V2 you have to create two attributes to get the two way databinding
[age]="alter" (ageChanged)="alter = $event"
Parent Component
import { Component } from '#angular/core';
#Component({
selector: 'my-app',
template: `<p>V1 Parentvalue Name: "{{firstname}}"<br/><input [(ngModel)]="firstname" > <br/><br/>
V2 Parentvalue Age: "{{alter}}" <br/><input [(ngModel)]="alter"> <br/><br/>
<my-child [(name)]="firstname" [age]="alter" (ageChanged)="alter = $event"></my-child></p>`
})
export class AppComponent {
firstname = 'Angular';
alter = "18";
}
Child Component
import { Component, Input, Output, EventEmitter } from '#angular/core';
#Component({
selector: 'my-child',
template: `<p>V1 Childvalue Name: "{{name}}"<br/><input [(ngModel)]="name" (keyup)="onNameChanged()"> <br/><br/>
<p>V2 Childvalue Age: "{{age}}"<br/><input [(ngModel)]="age" (keyup)="onAgeChanged()"> <br/></p>`
})
export class ChildComponent {
#Input() name : string;
#Output() nameChange = new EventEmitter<string>();
#Input() age : string;
#Output() ageChanged = new EventEmitter<string>();
public onNameChanged() {
this.nameChange.emit(this.name);
}
public onAgeChanged() {
this.ageChanged.emit(this.age);
}
}

Categories

Resources