Display a link in dynamically obtained html text - html

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

Related

Directive to change the string inside of a paragraph to uppercase is not working

I am trying to write a directive that turns the content of a paragraph to uppercase when you hover your mouse over it. I am not getting any errors whatsoever - it just does not work. I have written a similar code before that highlights the text to a certain color, which worked. Why wouldn't it also work when changing the text to uppercase?
filter.component.html
<p appToUpperCase>String to uppercase</p>
to-upper-case.directive.ts
import { Directive, HostListener, ElementRef } from '#angular/core';
#Directive({
selector: '[appToUpperCase]'
})
export class ToUpperCaseDirective {
constructor(public el: ElementRef) {
}
#HostListener('mouseenter2') onMouseEnter() {
this.el.nativeElement.value = this.el.nativeElement.value.toUpperCase();
}
}
EDIT: As #Ilya Rachinsky suggested, I have changed the event name from mouseenter2 to mouseenter and it still does not work.
Your directive structure looks fine. I guess you forgot to include it into the list of declarations on the module, so the directive will be available for the templates. Additionally, there is no 'value' property on 'p' element, you need to use innerHTML as previously suggested.
Checkout my example: https://stackblitz.com/edit/angular-ivy-uppercase-directive?file=src%2Fapp%2Fto-upper-case.directive.ts
You have to use correct event name - mouseenter instead mouseenter2

Dynamically add elements to the editable div with Angular and ANYWHERE

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

Using 2 Pages to filter a table in angular

I'm quite new to angular and wanted to know how to make it so i can have 1 page that you put the info you want to filter in the table and when you press "search" it will lead you to the second page where you see the table after its filtered.
i my question is odd but i really couldn't find any answer how to do this online.
I cant share code as its confidential to my work.
Something that looks like this site : https://maerskcontainersales.com/
I have tried using mock data but still couldn't put my head into the right thing to do.
There can be multiple ways how you can achieve this.
Using Provider
Suppose you have two pages and , serach-page is where you will enter your filters and result-page is where the table renders.
In search-page, you will create inputs( ex: textbox, dropdown etc ) and have ngModels for all of them, or you can use Angular reactive forms i.e FormGroup and FormControls. Users will select their input and click on search button, which will read values from models or controls and store them in the provider.
search-page.component.html
<form [formGroup]="searchForm" (submit)="search()">
<input formControlName="country" />
<input formControlName="city" />
...
<input type="submit">
</form>
search-page.component.ts
export class SearchPage {
...
search() {
const country = this.searchForm.get('country').value
...
// get rest of the values
...
this.searchService.setData({ country, city });
this.router.navigate(['/result']); // '/result' is path on the result-page
}
...
}
search.service.ts
#Injectable()
export class SearchService {
_data : any;
set data(val) {
this._data = val;
}
get data() {
return this._data;
}
}
result-page.component.ts
export class ResultPage {
...
ngOnInit() {
const filters = this.searchService.getData();
// filters will be your data from previous page
}
...
}
Using RouterParams
search-page.component.html
// same as before
search-page.component.ts
export class SearchPage {
...
search() {
const country = this.searchForm.get('country').value
...
// get rest of the values
...
this.router.navigate(['/result', { country, city }]); // '/result' is path on the result-page
}
...
}
result-page.component.ts
export class ResultPage {
...
constructor(route:ActivatedRoute) {
this.country = route.snapshot.paramMap.get("country")
// alternatively you can also do below
route.paramMap.subscribe(filters => {
// you will have your filters here
});
}
...
}
And once you have values of filters in result-page, use them to get data or filter data if already fetched, then render the table accordingly.
Let me know if I wasn't clear.
The simple solution I would suggest you to use a filter component and a results component a third container component. This component will get the filter criteria as an input variable and will output the filter criteria (using an output variable) when you press the "filter" button.
The container app will look like this:
<filterComponent (onFilter)="changeFilter($event)" [data]="someDate" *ngIf="!filterCriteria"></filterComponent>
<resultsComponent [data]="someDate" [filterCriteria]="filterCriteria" *ngIf="!!filterCriteria"></resultsComponent>
The filterCriteria that is sent to the second tableComponent will come from the eventEmmiter of the first tableComponent. The filterCriteria variable will be initiate to null and this will allow you to switch from one table to the other.

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?

How many times does Angular 2 render a single page before showing it?

I'm using Angular 2 for my project. I have a simple div in my template which calls a function in my .ts file that outputs a simple text like this:
<div>{{ test() }}</div>
private test(): void {
console.log("Test text");
}
When I load a page I get the same output many times like this:
Test text
Test text
Test text
Test text
Test text
Does that mean that Angular 2 renders the template many times before it actually shows it and consequently calls function every time?
Angular renders the AppComponent and it's child components exactly once, except when you add remove parts of the DOM, then these added parts will be rendered again.
What you experience is Angulars change detection which runs quite frequently. See also Why event triggers ChangeDetection even if OnPush strategy is ON?.
It is usually a bad idea to use functions in value bindings because such functions will be called every time Angular runs change detection.
Prefer to assign the value to a property and bind to this property instead.
<div>{{ testVal }}</div>
ngOnInit() {
this.testVal = this.test();
}
private test(): string {
console.log("Test text");
return 'some string';
}
Yes It renders multiple time since ChangeDetectionStrategy is always "Default" means it check always(multiple times) for UI update
ChangeDetectionStrategy.OnPush
Use OnPush: OnPush means that the change detector's mode will be set to CheckOnce during hydration.
If you use ChangeDetectionStrategy.OnPush then it will print only once
changeDetection: ChangeDetectionStrategy.OnPush
https://angular.io/api/core/ChangeDetectionStrategy
https://plnkr.co/edit/lNXNsS?p=preview
Code Snippet
#Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'my-app',
template: `
<div>
Check Console
<h2>{{print()}}</h2>
</div>
`,
})
export class App {
name:string;
constructor() {
this.name = `Angular! v${VERSION.full}`
console.log("Called Once")
}
print(): void {
console.log("I am printing only one time"):
}
}