Angular2: passing ALL the attributes to the child component - javascript

Don't even know the proper terminology to explain this problem
so, imagine this scenario...
There is a form-input-component and capturing some attributes and passing it down to the markup inside
<form-input placeholder="Enter your name" label="First name" [(ngModel)]="name" ngDefaultControl></form-input>
So, this is the markup, hope is pretty self explanatory...
obviously in the ts I have
#Input() label: string = '';
#Input() placeholder: string = '';
and then in the view I have something down these lines
<div class="form-group">
<label>{{label}}
<input type="text" [placeholder]="placeholder" [(ngModel)] = "ngModel">
</div>
Now, all works fine so far...
but let's say I want to add the validation rules around it...
or add other attributes that I don't capture via #Input()
How can I pass down anything else that comes down from <form-input> to my <input> in the view?

For the lazy ones:
export class FormInput implements OnInit {
#ViewChild(NgModel, {read: ElementRef}) inpElementRef: ElementRef;
constructor(
private elementRef: ElementRef
) {}
ngOnInit() {
const attributes = this.elementRef.nativeElement.attributes;
const inpAttributes = this.inpElementRef.nativeElement.attributes;
for (let i = 0; i < attributes.length; ++i) {
let attribute = attributes.item(i);
if (attribute.name === 'ngModel' ||
inpAttributes.getNamedItemNS(attribute.namespaceURI, attribute.name)
) {
continue;
}
this.inpElementRef.nativeElement.setAttributeNS(attribute.namespaceURI, attribute.name, attribute.value);
}
}
}
ngAfterViewInit won't work if you nest multiple components which pass attributes, because ngAfterViewInit will be called for inner elements before outer elements. I.e. the inner component will pass its attributes before it received attributes from the outer component, so the inner most element won't receive the attributes from the outer most element. That's why I'm using ngOnInit here.

You could iterate on the DOM attributes of your component :
import { ElementRef } from '#angular/core';
...
export class FormInput {
constructor(el: ElementRef) {
// Iterate here over el.nativeElement.attributes
// and add them to you child element (input)
}
}

Related

Pass variable within a function of a service to parent component - Angular & D3.js

Here is a stackBlitz demo.
I have a d3.js service file that builds my svg layout. Its a force directed d3 graph that has nodes. Each node carries its own data.
I have extracted that data into an array, capturing the ids of the nodes when selected. In my example, to select a node and capture the ID a user needs to hold/press Ctrl then click on a node.
This is done in a d3 .on click function within my angular service file.
Service.ts
export class DirectedGraphExperimentService {
public idArray = []
_update(_d3, svg, data): any {
...
svg.selectAll('.node-wrapper').on('click', function () {
if (_d3.event.ctrlKey) {
d3.select(this).classed(
'selected',
!d3.select(this).classed('selected')
);
const selectedSize = svg.selectAll('.selected').size();
if (selectedSize <= 2) {
svg
.selectAll('.selected')
.selectAll('.nodeText')
.style('fill', 'blue');
this.idArray = _d3.selectAll('.selected').data();
return this.idArray.filter((x) => x).map((d) => d.id);
}
}
});
...
}
}
My global variable this.idArray = [] does not update with the id strings therefore cant pass the array to the component like this this.directedGraphExperimentService.idArray its always [] empty.
component.ts
import { Component, OnInit, ViewChild, ElementRef, Input } from '#angular/core';
import { DirectedGraphExperimentService } from './directed-graph-experiment.service';
#Component({
selector: 'dge-directed-graph-experiment',
template: `
<style>
.selected .nodeText{
fill:red;
}
</style>
<body>
<button (click)="passValue()">Pass Value</button>
<svg #svgId width="500" height="700"><g [zoomableOf]="svgId"></g></svg>
</body>
`,
})
export class DirectedGraphExperimentComponent implements OnInit {
#ViewChild('svgId') graphElement: ElementRef;
constructor(
private directedGraphExperimentService: DirectedGraphExperimentService
) {}
ngOnInit() {}
#Input()
set data(data: any) {
this.directedGraphExperimentService.update(
data,
this.graphElement.nativeElement
);
}
passValue() {
console.log(this.directedGraphExperimentService.idArray); // returns []
}
}
I've also tried emitting an event with the value via the <svg></svg> container in the component template.
Is there another way I can get the updated this.array values into the parent component? A way to subscribe to the value in the function? Perhaps with a behaviourSubject from rxjs?
Here is a stackBlitz demo. In this demo you will see I have added a button that I press to trigger the update to my component file, obviously its only passing the empty global variable on my service. To add to the array you will need to press Ctrl and click on a node, you will see in console the array filling up(max of 2 string).

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).

Angular Dynamic Component Issue

I make a dynamic component in one of my components and it was made and here it's in the html I place it in the (ng-template) :
<div type="text" class="form-control" contenteditable="true" name="phrase" (input)="triggerChange($event)">
<ng-container #container></ng-container>
</div>
Code of triggerChange :
triggerChange(event) {
let newText = event.target.innerText;
this.updateLineAndParentLineAndCreateComponent(newText);
}
Which made what the function says literally update the line with the new text and update the parent component with this changes and also make the on the fly component
Code for create Component :
compileTemplate(line: any) {
// console.log(line[4]);
let metadata = {
selector: `runtime-component-sample`,
template: line[4]
};
let factory = this.createComponentFactorySync(this.compiler, metadata);
if (this.componentRef) {
this.componentRef.destroy();
this.componentRef = null;
}
this.componentRef = this.container.createComponent(factory);
let instance = <DynamicComponent>this.componentRef.instance;
instance.line = line;
instance.selectPhrase.subscribe(this.selectPhrase.bind(this));
}
private createComponentFactorySync(compiler: Compiler, metadata: Component, componentClass?: any): ComponentFactory<any> {
let cmpClass;
let decoratedCmp;
if (componentClass) {
cmpClass = componentClass;
decoratedCmp = Component(metadata)(cmpClass);
} else {
#Component(metadata)
class RuntimeComponent {
#Input() line: any;
#Output() selectPhrase: EventEmitter<any> = new EventEmitter<any>();
showEntities(lineIndex, entityIndex) {
this.selectPhrase.emit(entityIndex);
}
};
decoratedCmp = RuntimeComponent;
}
#NgModule({ imports: [CommonModule], declarations: [decoratedCmp] })
class RuntimeComponentModule { }
let module: ModuleWithComponentFactories<any> = compiler.compileModuleAndAllComponentsSync(RuntimeComponentModule);
return module.componentFactories.find(f => f.componentType === decoratedCmp);
}
and I display a text inside theis div based on the data I calculate and it's a string with html tags like that:
Data My name is foo
I trigger the blur event of the div that is contenteditable and I see the changes and based on that I generate a new string with new spans and render it again the same div
the problem comes when I delete all the text from the contenteditable div the component removed from the dom and can't be reinstantiated again even if I try to type again in the field but it just type inside the div not the created component
how I can solve this problem and can generate the component when the user delete all text from field and try to type again ?
Here is a stackblitz for the project :
https://stackblitz.com/edit/angular-dynamic-stack?file=src%2Fapp%2Fapp.component.ts
I found the solution is by handling keystrokes in the contenteditable div especially the DEL , BackSpace Strokes so when the input is empty and the stroke is one of them you just create a new component , It still has problems that dynamic components is not appearing when have it's empty or have only a tag but that's the workaround that I came up with untill now

Pass a div just created by ngFor to a function in Angular

I am producing some divs using ngFor:
<div *ngFor='let img of images'>
I want to pass the div just produced to a function. At first, I tried to just pass a number to the function
<div *ngFor='let img of images' (being)="ToRunEachTime(img)">
where being is a directive like:
#Directive({
selector: '[being]'
})
export class beingDirective {
#Output() being: EventEmitter<any> = new EventEmitter<any>();
constructor() {}
ngOnInit() {
this.being.emit('anything');
}
}
This code works well and I am able to pass img (which is a number) each time to the function ToRunEachTime. However, I wish to pass the whole div which is just created by *ngFor to this function. I tried:
<div *ngFor='let img of images' (being)="ToRunEachTime($event)">
but the function
ToRunEachTime(event: Event){
var currentdiv= <HTMLDivElement>event.target;
}
gives me undefined. Any Help is appreciated!
In your directive you are emitting a string which does not contain the target property
export class beingDirective {
#Output() being: EventEmitter<any> = new EventEmitter<any>();
constructor(private el:ElementRef,private renderer:Renderer2) {}
ngOnInit() {
this.being.emit(this.el.nativeElement);
}
}
If you are looking to emit any Browser event you should be adding a HostListener as below and emit that event
#HostListener('mouseover', ['$event']) onClick(event) {
this.being.emit(event);
}
This will give you the target element. Updated the demo.
LIVE DEMO

Angular 2, dynamic bind attribute name

I have a code block angular 2 like that:
<div *ngFor="let elm of elms; let i = index" [attr.name]="name" text-center>
{{elm }}
</div>
It works fine.
But when i want to dynamic set attribute name base on index like name-1="name" name-2="name"i dont know how to do it.
I tried [attr.name-{{i}}]="name"or [attr.name- + i]="name" but it does not work. Is there any way to do it?
Many thanks.
To start off thanks to OP for the question. I eventually learnt new things by solving this answer. Below explanation on how i achieved.
Important: One catch you cannot bind the dynamic attribute to your component scope. To achieve this you need to make each div as a dynamic component and compile it. Kinda hacky i guess.
Template:
<div #looped *ngFor="let elm of elms; let i = index;" text-center>
{{elm }}
</div>
and in the component import implements AfterViewInit and ViewChildren decorator to get children elements and its changes on rendering:
import { Component, ViewChildren, QueryList, AfterViewInit } from '#angular/core';
component:
export class ExamplePage implements AfterViewInit {
elms : Array<string> = ["d1", "d2", "d3"]
#ViewChildren('looped') things: QueryList<any>;
constructor() { }
ngAfterViewInit() {
this.things.forEach((t, index) => {
let el : HTMLDivElement = t.nativeElement;
el.setAttribute("name-" + index , "dynamicAttrString");
})
}
}
Output:
I don't know weather it is possible or not but I've alternate solution
<div *ngFor="let elm of elms; let i = index" [attr.name]="{id : index, data : name}">{{item}}</div>
then you'll get object as avalue with id and data keys, hope this helps.

Categories