Modify component with Renderer and ElementRef - javascript

1 ISSUE
I am trying to implement the following:
I have a container component ContainerComponent and child components ChildComponent. I want to modify the rendering and overall behaviour of the child components via the controlling ContainerComponent.
2 TECHNOLOGIES USED
Angular2, HTML, CSS, Javascript, Typescript, ES6
3 CODE
ContainerComponent.ts
export class ContainerComponent {
children: Array<Child>;
constructor(
private _el: ElementRef,
private _dcl: DynamicComponentLoader,
private _childService: ChildService) {
}
ngOnInit() {
let index = 0; // index of child component in container
this._childService.getChildren().then( // get the children models
(children) => {
this.children = children;
this.children.forEach((child, index) => {
this._dcl.loadIntoLocation(ChildComponent, this._el, 'dynamicChild')
.then(function(el){
el.instance.child = child; // assign child model to child component
el.instance.index = index;
});
});
}
);
}
}
ChildComponent.ts
export class ChildComponent {
child: Child;
index: number;
constructor(private _renderer: Renderer, private _el: ElementRef) {
}
ngOnInit() {
let delay = (this.index + 1) * 0.5; // calculate animation delay
this._renderer.setElementStyle(this._el, '-webkit-animation-delay', delay + 's !important');
this._renderer.setElementStyle(this._el, 'animation-delay', delay + 's !important');
}
}
4 CODE EXPLANATION
In the above code, the ContainerComponent dynamically inserts ChildComponents (granted, this could be done without the DynamicContentLoader).
The ChildComponents should dynamically add css properties, in this case, the animation delay once it is displayed. So based on the index of the child, the animation delay increases.
However the modifications from the renderer do not take effect, the css properties are not there at runtime.

I tried to reproduce your problem. In fact, I have problem to add styles like -webkit-animation-delay and animation-delay.
If I try with another style like color, it works fine and the style is taken into account at runtime.
ngOnInit() {
this._renderer.setElementStyle(this._el, 'color', 'yellow');
}
So it seems to be linked to animation styles... I see these links that could interest you:
How to set CSS3 transition using javascript?
http://www.javascriptkit.com/javatutors/setcss3properties.shtml
Otherwise it seems that there is some support for animation in Angular2 but it's not really documented... See this file: https://github.com/angular/angular/blob/master/modules/angular2/src/animate/animation.ts.
Hope it helps you,
Thierry

This seems to be a bug in angular2 itself. Adding !important to a style will result in an illegal value to the style and it is not applied to the element. The correct way in plain js is to use another parameter which implies if the style is important.
So the correct answer is to use:
this._renderer.setElementStyle(this._el, 'animation-delay', delay + 's'); //sans !important
and if you want to add !important you have to use:
this._el.nativeElement.style.setProperty('animation-delay', delay + 's', 'important');
The -webkit- prefix gets added (or removed) if necessary, so there is no need to add that as well

From here:
https://angular.io/docs/ts/latest/api/core/ElementRef-class.html
You should only use ElementRef as an absolute last resource. The whole idea of Angular 2 is that you don't have to mess with the dom at all. What you are trying to do can be acomplished very easy using a template:
import {NgStyle} from 'angular2/common';
import {Component} from "angular2/core";
#Component({
selector: 'app',
template: `
<div *ngFor="#child of children; #i = index">
<div [ngStyle]="{ 'z-index': i * multiplier,
'-webkit-animation-delay': i * multiplier + 's',
'animation-delay': i * multiplier + 's' }"> {{i}} - {{child}} </div>
</div>
`,
directives: [NgStyle]
})
export class AppComponent{
public children:string[] = [ "Larry", "Moe", "Curly" ];
public multiplier:number = 2;
}
Depending on the browser you might see those css properties or not, that's why I added the z-index which is more common and old so you can see you can render the css value dynamically using the index variable from ngFor inside a template.
I hope this helps !

Related

Is there a way to move to a specific index when an item is added in the Angular?

i have some list and can add/remove item of the list.
Is there a way to cause an event to scroll to a specific index (where the item was added) when the item is added? In the current example, the item is added to the front of the list, so the scroll must be moved to the top
for example, when i'm in the middle(or bottom) of the list, if i add item to the list, the scroll move to the top of the list. (or move to some index, this case, index 0).
Tell me how to scroll from parent to child components without changing the structure of the example.
example: https://stackblitz.com/edit/angular-ivy-ymbsj7
Your problem is that ViewChild wont go into deeper levels when querying, so you cant query for a CdkVirtualScrollViewport in a child elements template. I could solve this with a custom change detection function in your list component.
You should remove this from your your app.ts -> addItem() function:
// want to move scroll to the top of the list
this.viewPort.scrollToIndex(0, 'smooth');
and instead create a custom change detection function in your list component, but first move the viewChild of the CdkVirtualScrollViewport to the list component:
export class ListComponent {
#ViewChild(CdkVirtualScrollViewport) viewPort: CdkVirtualScrollViewport;
#Input()
data: Favorite[];
#Output()
removeData = new EventEmitter<Favorite>();
remove($event) {
this.removeData.emit($event);
}
ngOnChanges(changes: SimpleChanges) {
if (
changes.data &&
changes.data.currentValue &&
changes.data.previousValue &&
changes.data.currentValue.length >changes.data.previousValue.length
) {
this.viewPort.scrollToIndex(0, 'smooth');
}
}
}
this works perfectly for me. Every time an item is added, it scrolls to the top.
Modified stackblitz link:
https://stackblitz.com/edit/angular-ivy-k5pve6?file=src/app/list/list.component.ts
Another solution(and maybe better) could be passing the ListComponent as a template reference to the addItem() function, then use the compononents viewPort property's scroll function.
List Component
...
export class ListComponent {
#ViewChild(CdkVirtualScrollViewport)
public viewPort: CdkVirtualScrollViewport;
...
}
AppComponentTemplate with template reference passing of the ListComponent:
<p>Start editing to see some magic happen :)</p>
<input #inputText />
<button #addButton (click)="addItem(list)">Add New</button>
<list-container #list [data]="favoriteList" (removeData)="remove($event)">
</list-container>
AppComponent-> addItem():
addItem(listComp: ListComponent) {
const newItem = {
id: Number(this.input.nativeElement.value) + 1,
title: `item ${Number(this.input.nativeElement.value) + 1}`,
};
this.favoriteList = [newItem, ...this.favoriteList];
listComp.viewPort.scrollToIndex(0, 'smooth');
}
StackBlitz for the second solution:
https://stackblitz.com/edit/angular-ivy-ofhubv?file=src/app/app.component.html

ngAfterViewInit not fired within ng-content

The ngAfterViewInit lifecycle hook is not being called for a Component that is transcluded into another component using <ng-content> like this:
<app-container [showContent]="showContentContainer">
<app-input></app-input>
</app-container>
However, it works fine without <ng-content>:
<app-input *ngIf="showContent"></app-input>
The container component is defined as:
#Component({
selector: 'app-container',
template: `
<ng-container *ngIf="showContent">
<ng-content></ng-content>
</ng-container>
`
})
export class AppContainerComponent {
#Input()
showContentContainer = false;
#Input()
showContent = false;
}
The input component is defined as:
#Component({
selector: 'app-input',
template: `<input type=text #inputElem />`
})
export class AppInputComponent implements AfterViewInit {
#ViewChild("inputElem")
inputElem: ElementRef<HTMLInputElement>;
ngAfterViewInit() {
console.info("ngAfterViewInit fired!");
this.inputElem.nativeElement.focus();
}
}
See a live example here: https://stackblitz.com/edit/angular-playground-vqhjuh
There are two issues at hand here:
Child components are instantiated along with the parent component, not when <ng-content> is instantiated to include them. (see https://github.com/angular/angular/issues/13921)
ngAfterViewInit does not indicate that the component has been attached to the DOM, just that the view has been instantiated. (see https://github.com/angular/angular/issues/13925)
In this case, the problem can be solved be addressing either one of them:
The container directive can be re-written as a structural directive that instantiates the content only when appropriate. See an example here: https://stackblitz.com/edit/angular-playground-mrcokp
The input directive can be re-written to react to actually being attached to the DOM. One way to do this is by writing a directive to handle this. See an example here: https://stackblitz.com/edit/angular-playground-sthnbr
In many cases, it's probably appropriate to do both.
However, option #2 is quite easy to handle with a custom directive, which I will include here for completeness:
#Directive({
selector: "[attachedToDom],[detachedFromDom]"
})
export class AppDomAttachedDirective implements AfterViewChecked, OnDestroy {
#Output()
attachedToDom = new EventEmitter();
#Output()
detachedFromDom = new EventEmitter();
constructor(
private elemRef: ElementRef<HTMLElement>
) { }
private wasAttached = false;
private update() {
const isAttached = document.contains(this.elemRef.nativeElement);
if (this.wasAttached !== isAttached) {
this.wasAttached = isAttached;
if (isAttached) {
this.attachedToDom.emit();
} else {
this.detachedFromDom.emit();
}
}
}
ngAfterViewChecked() { this.update(); }
ngOnDestroy() { this.update(); }
}
It can be used like this:
<input type=text
(attachedToDom)="inputElem.focus()"
#inputElem />
If you check the console of your stackblitz, you see that the event is fired before pressing any button.
I can only think of that everything projected as will be initialized/constructed where you declare it.
So in your example right between these lines
<app-container [showContent]="showContentContainer">
{{test()}}
<app-input></app-input>
</app-container>
If you add a test function inside the app-container, it will get called immediatly. So <app-input> will also be constructed immediatly. Since ngAfterVieWInit will only get called once (https://angular.io/guide/lifecycle-hooks), this is where it will be called already.
adding the following inside AppInputComponent is a bit weird however
ngOnDestroy() {
console.log('destroy')
}
the component will actually be destroyed right away and never initialized again (add constructor or onInit log to check).

DOM Manipulations in Angular 5

For example I have:
<div class="btn-wrapper-bt1">
<button>AAA</button>
</div>
This button is on the 3rd party element that exists in node_modules/somebt
I would like to do some simple class change within Angular environment.
Is there a simple way to change it in ngOnInit? Or I need to fork the source and change it within the source?
Thanks in advance.
In the html, add a #ref reference to the element containing your 3rd party component
yourComponent.html
<div #ref >
<your-3rd-party-component></your-3rd-party-component>
</div>
Then, in your component, retrieve the children of the containing element
yourComponent.ts
import { Component,Renderer2, ViewChild,ElementRef } from '#angular/core';
export class YourParentComponent {
#ViewChild('ref') containerEltRef: ElementRef;
constructor(private renderer: Renderer2)
{
}
ngAfterViewInit()
{
// retrieves element by class
let elt = this.containerEltRef.nativeElement.querySelector('.btn-wrapper-bt1');
this.renderer.addClass(elt, 'newClass'); //Adds new class to element
}
}
Here is a stacklblitz demo
Note: If you just want to change the 3rd party component's appearance, you could just override the class in your own component
yourComponent.scss
:host ::ng-deep .btn-wrapper-bt1
{
color: red;
}
Add a reference :
<div #myRef class="btn-wrapper-bt1">
<button>AAA</button>
</div>
And in your TS :
#ViewChild('myRef') myElement: ElementRef;
myFunc(){
// do whatever you want with it AFTER you third party module finished its job (that's your call)
//this.myElement.nativeElement.querySelector()
//this.myElement.nativeElement.classList.remove('toto')
}

Angular 2, Adding calc() as inline style. Unsafe interpolation using parentheses

Angular 2 rc3
I am trying to dynamically add calc() to an element in a template. I have something like this.
template : `<div attr.style.width="{{width}}></div>"`
export myClass
{
#Input() myInputObject:any;
private width:string;
ngOnInit() { this.setWidth()}
private setWidth()
{
let percent = myInputObject.percent;
this.width = 'calc(' + percent + '% - 20px)';
}
}
If I use the parenthesis the ouput looks like this in the DOM.
<div style="unsafe"></div>
If I take out the parenthesis it works (sort of) It looks like this.
<div style="calc10% - 20px"></div>
This also doesn't work.
<div attr.style.width="calc({{width}} - 20px)">
Any help on how to add calc() to the template is much appreciated. Note I also tried replacing the parenthesis with ( and ). That also came back as "unsafe".
Example: (rc1)
I am using rc3 in my environment. But I was able to reproduce the same issue with RC1 in plunker. I am assuming this is a security thing. But there must be a way to add calc() to a style attribute. Maybe there is a better way than this?
https://plnkr.co/edit/hmx5dL72teOyBWCOL0Jm?p=preview
Calculated styles should be sanitized.
Here is the solution for you:
import {DomSanitizationService} from '#angular/platform-browser';
#Component({
selector: 'my-app'
template: `
<div [style.width]="width">
<h2>Hello {{name}}</h2>
</div>
`
})
export class App {
private width:string;
constructor(sanitizer: DomSanitizationService) {
this.name = 'World'
this.width = sanitizer.bypassSecurityTrustStyle("calc(10% - 20px)");
}
}
You can also try using ngStyle instead:
[ngStyle]="{'width': 'calc(' + percent + '% - 20px)'}"
And just bind 'percent' value to the input value.

Page transition animations with Angular 2.0 router

In Angular 2 I am trying to animated in new components via the Router onActivate method.
I have set up a Plunk with a demonstration of the issue here:
http://plnkr.co/FikHIEPONMYhr6COD9Ou
An example of the onActivate method in one of the page components:
routerOnActivate(next: ComponentInstruction, prev: ComponentInstruction) {
document.getElementsByTagName("page3")[0].className='animateinfromright';
}
The issue that I'm having is that I want the new components to animate in on top of the existing component, but the old component is removed from the DOM before the new component is added.
Is there any way to delay the removal of the previous page while the new one animates in?
I found this similar issue: Page transition animations with Angular 2.0 router and component interface promises
but the technique just delays the removal of the previous component before the new one is added.
Eventually I will have different animations depending on which page we are moving from / to, hence having the onActivate in each of the page components.
Many thanks for any help!
You could add an "EchoComponent" where your <router-outlet> is, create a <canvas> in it and drawImage() on routerOnDeactivate()... Something like:
#Component({
template: `<canvas #canvas *ngIf="visible"></canvas>`
})
class EchoComponent {
#ViewChild("canvas") canvas;
public visible = false;
constructor(private _shared: SharedEmitterService) {
this._shared.subscribe(el => el ? this.show(el) : this.hide(el));
}
show(el) {
this.canvas.drawImage(el);
this.visible = true;
}
hide() {
this.visible = false;
}
}
#Component({...})
class PrevRoute {
constructor(private _eref: ElementRef,
private _shared: SharedEmitterService) {}
routerOnDeactivate {
this._shared.emit(this._eref.nativeElement);
}
}
#Component({...})
class NextRoute {
constructor(private _eref: ElementRef,
private _shared: SharedEmitterService) {}
routerOnActivate {
this._shared.emit(false);
}
}
This is just a pseudo code (writing it from memory), but it should illustrate what would you need for this approach.

Categories