Dynamically add elements to the editable div with Angular and ANYWHERE

2k views Asked by At

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. enter image description here

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.

1

There are 1 answers

0
Eliseo On

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