Angular Dynamic Component Issue - javascript

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

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

Angular variables

I've got a line of text right now, and when that line of text is being overflown something gets set to true which let's me load in a tooltip! Code below:
Template
<div>
<p #tooltip [tooltip]="/* ShowToolTipSomeHow? ? name : null */" delay="300">{{name}}</p>
</div>
This is where it should check if it should show the tooltip or not. As you can see it should somehow detect if the tooltip should be shown or not, I have no idea how and that's my question right now.
Component
#ViewChildren('tooltip') private tooltips!: QueryList<ElementRef>;
ngAfterViewInit(): void {
this.tooltips.changes.subscribe((tts: QueryList<ElementRef>) => {
tts.forEach((tooltip, index) => {
this.checkTooltipTruncated(tooltip);
});
});
}
private checkTooltipTruncated(tooltip: ElementRef) {
// Checks if the text has overflown
const truncated = this.isTextTruncated(tooltip);
if (truncated) {
// Change the ShowToolTipSomehow? value?
}
}
In the component it somehow changes some value that the tooltip can detect so that it can update itself to hide the tooltip. The additional problem is that it's not 1 tooltip to change, but infinite tooltips (so basically 1 or more).
My question is, how would I do this because I'm pretty stuck.
Create an array that holds boolean values and using the tooltip elements index set the value in the array.
ts:
#ViewChildren('tooltip') private tooltips!: QueryList<ElementRef>;
tooltipsVisible: boolean[];
ngAfterViewInit(): void {
if (!this.tooltipsVisible) {
this.tooltipsVisible = new Array(this.tooltips.length).fill(false);
}
this.tooltips.changes.subscribe((tts: QueryList<ElementRef>) => {
tts.forEach((tooltip, index) => {
this.checkTooltipTruncated(tooltip, index);
});
});
}
private checkTooltipTruncated(tooltip: ElementRef, index) {
// Checks if the text has overflown
const truncated = this.isTextTruncated(tooltip);
if (truncated) {
this.tooltipsVisible[index] = true;
// Change the ShowToolTipSomehow? value?
}
}
html:
<div>
<p #tooltip [tooltip]="tooltipsVisible[i] ? ...." delay="300">{{name}}</p>
</div>
You should create your p elements using *ngFor you can have the index available in your template...

I want to replace text in component "x" by clicking a button from component "y"

I got two components that i want to connect. In component "x" i got some text in the template file and in component "y" i got a button that i want to click to replace/change the text in component "x".
this is "x" text i want to change:
<p> Text i want to replace </p>
this is "y" component.ts text i want to replace with:
changeTheText: string = "Text i want to change to";
showMyContainer2: boolean = false;
clearMe(){
this.showMyContainer2 = true;
this.UIS.clearStorage();
}
this is "y" component.template:
<button id="clearBtn" (click)="clearMe()">Change the text button</button>
<div *ngIf="showMyContainer2">
{{changeTheText}}
</div>
You can do this by using EventEmitters
https://angular.io/api/core/EventEmitter
Is x a direct child of y? Meaning is the HTML like this?
<y>
<x></x>
</y>
If so, you can use #Input() properties
In x component.ts, do this:
// import Output and EventEmitter from '#angular/core`;
#Input text: string;
And I assume the HTML is:
<p>{{ text }}</p>
Then in y.component.ts, do:
clearMe(){
this.showMyContainer2 = true;
this.UIS.clearStorage();
this.changeTheText = 'New Text you want'
}
And in y.html, do:
<div class="y">
<x [text]="changeTheText"></x>
</div>
You can possibly use EventEmitters like Mamosek mentioned but it depends on the heirarchy of x and y. Are they parent => child or child => parent.
If they don't have parent child relationship, you have to create a middle man Service that has a BehaviorSubject and both x and y have to inject this Service and communicate through that BehaviorSubject by doing .next to push a new value and .subscribe to listen to values.
====================== Edit ================================
Does it make sense for the text to live in component Z?
Component Z.ts
textToChange = 'Change this text';
changeTheText() {
this.textToChange = 'new text';
}
Component Z.html
<div class="z">
// feed the text down to the x component
<x [text]="textToChange"></x>
// listen to the textChanged event from component Y and every time it happens call the function changeTheText
<y (textChanged)="changeTheText()"></y>
</div>
Component X.ts
// take input of text from parent's location
#Input() text: string;
Component X.html
<div>{{ text }}</div>
Component Y.ts
#Output textChanged: EventEmitter<void> = new EventEmitter();
changeTheText() {
// emit the event so the text can change in component Z
this.textChanged.emit();
}
Component Y.html
<button (click)="changeTheText()">Change the text</button>
I freestyled all of that so there might be some errors but you get the idea.
If the text cannot live in component Z, you will have to have a centralized approach like I mentioned before this edit.
import { Injectable } from '#angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
#Injectable({
providedIn: 'root',
})
export class TextChangeService {
text = new BehaviorSubject('initial value of text');
constructor() { }
}
component X.ts (The HTML's for both components remain the same)
text: string;
constructor(textService: TextService){ }
ngOnInit() {
this.text.subscribe(text => this.text = text);
}
component Y.ts
constructor(textService: TextService){ }
changeTheText() {
this.textService.next('New text');
}
This last approach I showed you, I don't recommend because it turns into convoluted code the more you build on it. I suggest you learn the Redux pattern and use NGRX.

How to retain values of parent component in child on every click in angular 2?

I have written a code in which I am creating a treeview structure in angular 2.
Parent Component HTML:
<child-component [data]="tree"></child-component>
Parent Component.ts file has the following:
tree: TreeData[] = [];
treeData: TreeData;
ngOnInit() {
this.tree.push({answer:'first',child:[]});
this.treeData = this.tree[0];
}
I have created a class in the common.ts file which has answer(input in the textbox) and child(it will get populated recursively when add node is clicked) :
export class TreeData {
answer: string;
child: TreeData[];
constructor(answer: string, child: TreeData[]) {
this.answer = answer;
this.child = child;
}
}
Child component's html:
<li *ngFor="#eachChild of data ; let i = index">
<input type="text" [(ngModel)]="inputData" required/>
<child-component [data]="eachChild.child"></child-component>
</li>
Child component's ts file:
#Input() data : TreeData[];
inputData: any = null;
eachChild: TreeData;
add(inputData: any, index: number) {
this.data[index].child.push({answer: inputData, child: []});
}
Current output treeview
:The output looks like this when clicked on add Node, a new text box gets added below
The actual output should be like a tree structure (when add node is clicked, new textbox gets added as its child)
The issue is that the object gets refreshed on every click and is not able to retain the previous state. The contents should get appended but this seems like a new instance is created every time. Any insight on how I can proceed with this to create a treeView in angular 2?

Angular2: passing ALL the attributes to the child component

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

Categories