Dynamically add elements to the editable div with Angular and ANYWHERE - html

So, I am creating an HTML interface where user should be able to write a text and push it as a notification to our mobile app.
I am facing some troubleshoots with the text and the dynamic inserted elements using Angular 5;
The text can contain special elements like: phone number, Location and website URL. Those special elements will be inserted by pressing on a button that opens a dialog, and for each one its specific fields are displayed, like google maps for location and input fields for Web URL and mobile Phone. It is implemented this way in order to capture longitude, latitude and phone numbers on save button in order to add them as buttons to the received push on the devices.
Anyway, the above is implemented and could work successfully except the way of adding dynamically spans of special elements inside the div of the web interface. Spans added must have a class and a click event to display again the dialog in order to modify the data. Also they can be inserted anywhere inside the big div depending on user's choice.
Below is the image of above description.
The blue spans, are the ones that should be added dynamically inside the content editable div that can be filled by around 450 characters.
So how to solve the issue and enable the feature of adding clickable and designed spans with icons inside a content editable div, and be able in a final stage to retrieve data?
My code is the below, working but for a specific/predefined position:
Message.html
<div id="myMessage" contenteditable="true" dir="ltr" [innerHTML]="contentEN | safeHtml"
style=" height: 80px;border: 1px solid #c1c1c1; padding: 7px;">
</div>
<ng-container #vc>
</ng-container>
Message.ts
#ViewChild('vc', {read: ViewContainerRef}) target: ViewContainerRef;
createSpanPhone(spanIDNumber, phoneDescription, phoneValue ){
// here the span Phone is created dynamically outside the div
let phoneComponent = this.cfr.resolveComponentFactory(PhoneComponent);
this.componentRef = this.target.createComponent(phoneComponent);
}
PhoneComponent.ts
import { Component } from '#angular/core';
import { faPhone } from '#fortawesome/free-solid-svg-icons';
#Component({
selector: 'my-phone',
template: '<span contenteditable="false" (click) = "test()" class="BAN_Tags_IN_Text"> <fa-icon
[icon]="faPhone" class="faSpanIcon"> </fa-icon> <span class="phoneDesc"
data-attr="EN">hello</span> <span class="phoneVal" ><b>12346</b></span>
</span>'
})
export class PhoneComponent {
faPhone = faPhone; // trying the icon
constructor(){
}
test(){
console.log("Hiiii"); // trying the click event
}
}
The ViewContainerRef is filled successfully but I need to fill spans in the div above (id=myMessage) and not in a predefined position.

if your text are simple text (don't has html tags that can not enclosed by <span>, -I want to mean that is allowed e.g. <i> or <b>, but not <p> - you can create a component like
#Component({
selector: "html-content",
template: `
<span class="inline" [innerHTML]="value"></span>
`
})
export class HtmlComponent {
#Input() value;
constructor() {}
}
A directive like
#Directive({ selector: "[content]" })
export class ContentDirective {
#Input() set content(textHtml: string) {
this.viewContainerRef.clear();
if (!textHtml) return
//If not end with . or space, add an space
if (textHtml.slice(-1)!=" " && textHtml.slice(-1)!=".")
textHtml+=" "
//gets the "words"
//const parts = textHtml.match(/\ ?\S+\ |\ ?\S+\./gi);
const parts = textHtml.match(/<?[^\r\n\t\f\v< ]+\ ?/gi);
parts.forEach(h => {
let space = false;
let search = h.replace(/[\ .;,:]/gi, "")
let arg=null;
//to allow pass arguments to the components in the way, e.g.
// <phone=arguments -be carefull! the arguments can not contains spaces
//
if (search.match(/<phone=.+/))
{
arg=search.split("=")[1].split(">")[0]
search="<phone>"
}
if (search.match(/<location=.+/))
{
arg=search.split("=")[1].split(">")[0]
search="<location>"
}
switch (search) {
case "<phone>":
case "<location>":
const factory =
search == "<phone>"
? this.componentFactoryResolver.resolveComponentFactory(
PhoneComponent
)
: this.componentFactoryResolver.resolveComponentFactory(
LocationComponent
);
const phone=this.viewContainerRef.createComponent(factory);
//if our component has "#Input() arg"
(phone.instance as any).arg=arg||"";
break;
default:
const factoryHtml = this.componentFactoryResolver.resolveComponentFactory(
HtmlComponent
);
const html = this.viewContainerRef.createComponent(factoryHtml);
html.instance.value = h;
space = true;
break;
}
//this allow write space or dot after the component.
if (!space && h.match(/.+>[\ ;,:.]/gi)) {
const factoryDot = this.componentFactoryResolver.resolveComponentFactory(
HtmlComponent
);
const html = this.viewContainerRef.createComponent(factoryDot);
//we check if, after the component we has a "," or ";" or ":" or ". "
html.instance.value = h.slice(h.indexOf(">")+1)
}
});
//just for check the parts
console.log(textHtml, parts);
}
constructor(
private viewContainerRef: ViewContainerRef,
private componentFactoryResolver: ComponentFactoryResolver
) {}
}
You can see a stackblitz without warranty

Related

how to refresh UI without reloading the page in Angular

I have a multiple charts in my page and I'm trying to make a delete call but some reason my chart UI is not updating immediately when I click the delete button. I always need to refresh the browser in order to see the changes.
I uploaded the full code for this two component her https://stackblitz.com/edit/angular-ivy-nnun96 so I would be really appreciated if I can get any suggestion on how to make the UI remove the Chart immediately when the user press Delete button.
Mc Chart List TS
deleteChart(){
this.chartService.deleteChart(this.chart.guid).subscribe((deleted) => {
console.log(deleted);
});
}
Mc Chart List HTML
<button mat-menu-item (click) = "deleteChart()" *ngIf = "chart.hasAccess && chart.canEdit && !chart.isPublished">Delete Chart</button>
Parent HTML
<mc-chart-list [chart]="chart" [editMode]="true" [wsType]="workspace.type"></mc-chart-list>
Parent TS
ngOnInit(): void {
this.charts = this.workspace.charts;
}
It look like this right now
You can use ChangeDetectorRef to detect changes on the view.
import {ChangeDetectorRef} from '#angular/core';
constructor(private ref: ChangeDetectorRef)
deleteChart(){
this.chartService.deleteChart(this.chart.guid).subscribe((deleted) => {
console.log(deleted);
this.ref.detectChanges();
});
}
Note: Remove changeDetection: ChangeDetectionStrategy.OnPush (if you are using it)

Custom Angular directive is not working with async value

We have a custom directive in our project which we use when we want trim the long text in some UI elements. In one case it fails to work. There are no errors, no warnings, it's just no there. Checking the code in the DevTools shows no signs of this directive triggering (no HTML changes, no CSS added). The directive looks like this:
ngAfterViewInit() {
let text = <string>this.elt.nativeElement.innerHTML.trim();
if (!text || text !== (<HTMLElement>this.elt.nativeElement).innerText) {
return;
}
const limit = this.value || DEFAULT_VISIBLE_ENDING_LENGTH; // default length = 4
if (text.length > limit && this.elt.nativeElement.scrollWidth > this.elt.nativeElement.clientWidth) {
const startText = text.substr(0, text.length - limit);
const endText = text.substr(-limit);
this.renderer.setProperty(
this.elt.nativeElement,
'innerHTML',
`<div class="part1"><span>${startText}</span></div><div class="part2"><span><span>${endText}</span></span></div>`
);
}
}
It fails to work when the text to display & trim is obtained from observable (store selector). It doesn't matter if I use Observable + async pipe or if I map the value to the component property in selector subscribe.
#Component({
...
changeDetection: ChangeDetectionStrategy.OnPush,
})
this.sampleInProgress$: Observable<string>;
this.sampleInProgress: string;
...
this.sampleInProgress$ = this.store.select(fromAutomation.getInfoPanelData).pipe(
map(({ sample, experiment }) => {
this.sampleInProgress = sample?.sampleName; // does not work either
this.experimentInProgress = experiment?.parameterSet;
return sample?.sampleName;
}),
);
And the HTML:
<span class="label" gs-ellipsis>{{ sampleInProgress$ | async }}</span>
<!-- In this case, subscribe is done in the component -->
<span class="label" gs-ellipsis>{{ sampleInProgress }}</span>
Sorry for the bit messy code, I just didn't wanted to post almost the same code twice. I'm either subscribing explicitly or assigning the observable using async with it. Not doing both at the same time. The other place in the code where we use this ellipsis (and where it works) also uses OnPush Detection strategy the but that the value is provided by #Input.
I have a feeling that it has something to do with the ngAfterViewInit() in the directive itself, but I'm not sure. Directives are not my strongest field.
Any idea what can be the cause and how to fix it?
your directive handling happends too early. I assume you can hack it a bit and render element just when its content is ok with the help of ngIf directive.
<span class="label" gs-ellipsis *ngIf="sampleInProgress$ | async as value">{{ value }}</span>

Display a link in dynamically obtained html text

I am dynamically obtaining a list of strings.I display it in angular using ngFor. But when displayed, certain strings include few hyperlinks ,but they are displayed as normal strings. I want the hyperlink distinguished like underlined.
Eg: Refer https://support.google.com/accounts/answer/abc?hl=en# to 'Create a Google Account' using email
If I get it right, its really simple and you can do that like this :
{{Text Variable}}
you can make a pipe, but this pipe must be used in a [innerHtml]
#Pipe({name: 'linkPipe'})
export class LinkPipe implements PipeTransform {
constructor(private _domSanitizer: DomSanitizer){}
transform(value: string): any {
if (value.indexOf("http")>=0)
{
//search the "link"
const link=value.match(/(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9#:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9#:%_\+~#?&//=]*)?(\[.*\])?/)
if (link) //if has a link
{
const valueSplit=link[0].split('[') //check if is in the way:
//http://direccion[text to show]
value=value.replace(link[0],
"<a href='"+valueSplit[0]+"'>"+
(valueSplit[1]?valueSplit[1].slice(0,-1):valueSplit[0])+
"</a>")
}
}
return this._domSanitizer.bypassSecurityTrustHtml(value)
}
}
e.g. of use
<p [innerHTML]="'see the http://www.google.com[page of Google] for more information'|linkPipe "></p>
<p [innerHTML]="'http://www.google.com'|linkPipe"></p>
see stackblitz

checking how many lines catches some text

How could I check how many lines a rendered text takes, no matter if it's displayed on a large or small screen?
On the below component I want to show the anchor element only if the text has 3 lines.
import React from 'react';
type contentProps = {
content: string,
};
function subString(string: string): string {
return string.substring(0, string.length / 2);
}
export const Content = ({ content }: contentProps) =>
content ? (
<p className="content">
{subString(content)}
See More
</p>
) : null;
What should be the flow ?
First i need to render the component
Second check if it takes 3 lines and after this change the state ?

Get cursor position in a contenteditable div using innerHTML and pipes

I'm writing a simple WYSIWYG editor in Angular 5 to handle tags in the text. Those tags are like variables. For instance when doing: Hi (!--username--), welcome! it's rendered as Hi alex, welcome!. In order to be user-friendly for the non-technical, the WYSIWYG is transforming (!--username--) to a pretty HTML fragment showing directly "Alexandre" in its content.
This editor needs to handle simple HTML tags too (<b>, <i>, ...)
To do that, I've developed a component named editor which is using Angular's value accessors and showing a simple div like that:
<div class="editor" #editor [innerHTML]="content | prettytags: completions" (focus)="toogleToolbar()" (focusout)="toogleToolbar()"
(click)="onClick($event)" (keyup)="onKey($event)" [attr.contenteditable]="!readonly"></div>
The pipe looks like (for information, completions is the variable containing all tags values):
const pattern: RegExp = /(\(!--[^\s-]*--\))/;
#Pipe({
name: 'prettytags'
})
export class PrettyTagsPipe {
constructor(private sanitizer: DomSanitizer) {}
transform(value: string, completions: any[]): SafeHtml {
if (isNil(value)) return '';
const text = this.makeText(value, completions, 0);
return this.sanitizer.bypassSecurityTrustHtml(text);
}
private makeText(value: string, completions: any[], index: number): any {
const text = value
.split(pattern)
.map(word => {
const tag = completions.find(t => t.tag === word);
return isNil(tag)
? word
: this.getTagHtml(tag.value)
})
.join('');
return text;
}
private getTagHtml(text: any) {
return `<span class="chip" spellcheck="false">${text}</span> `;
}
}
In order to get the two-way data binding working as I'm using [innerHTML], I'm using the keyup event to get new characters but I need to get the caret position to append new characters. To do that I've copy/pasted a function found on Stack Overflow to get the caret position:
private getCaretPosition() {
const element = document.querySelector('.editor');
const range = window.getSelection().getRangeAt(0);
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(element);
preCaretRange.setEnd(range.endContainer, range.endOffset);
return preCaretRange.toString().length;
}
And on my onKeyUp: I do the following:
[...]
const position = this.getCaretPosition();
this.content += key.length === 1 ? this.content.slice(0, position) + key + this.content.slice(position) : '';
but it's not working as it gets the text position.
For instance, if the user wants to edit the content: from Hi (!--username--), welcome! to Hi (!--username--), I'm fine to see you back!, he will place his caret just after the comma, so I'll get 8 (for "Hi alex,") but with my content variable I'll get Hi (!--u.
I know I can get the position of the cursor with HTML tags, but I'll need to do many computations for each key pressed.
Do you have any idea to get this thing to work?