Search code examples
aurelia

Aurelia js providing dynamic content to popover body


I am following the structure to implement tool tip from Sean Hunter Blog . Now i want provide tool-tip content as a dynamic html content i.e I want to show one html pattern inside content. How can I provide using Aurelia framework. In knockout JS using custom binding handler I am providing content as with id of division like below code.

Knockout Structure is

 <button data-bind="tooltip: { template: 'ElementId', trigger: 'click', placement: 'right', container: 'body' }">Click Me</button>

    <div id="ElementId" style="display: none;">
        <div>Dynamic content will go here</div>
    </div>

How to achieve same with Aurelia Structure:

<template>
<require from="templates/popover/tooltip"></require>
<button data-toggle="tooltip" tooltip="placement:bottom;trigger:click;html:true;template:'ElementId';title:tooltip Header">Click Me</button>

<div id="ElementId" style="display: none;">
            <div>Dynamic content will go here</div>
        </div>

</template>

Custom Attribute code

import {customAttribute, inject, bindable} from 'aurelia-framework';
import $ from 'bootstrap';

@customAttribute('tooltip')
@inject(Element)
export class Tooltip {
    element: HTMLElement;
    @bindable title: any;
    @bindable placement: any;
    @bindable content: any;
    @bindable template: any;

    constructor(element) {
        this.element = element;
    }

    bind() {
        if (this.content) {
            $(this.element).tooltip({ title: this.title, placement: this.placement, content: this.content });
        }
        else {
            $(this.element).tooltip({ title: this.title, placement: this.placement, template: $('#'+this.template).html() });
        }


    }

    // gets fired when the provided value changes, although not needed in this example since the json from reddit is static
    titleChanged(newValue) {
        $(this.element).data('bs.tooltip').options.title = newValue;
    }
    contentChanged(newValue) {
        if (this.content) {
            $(this.element).data('bs.tooltip').options.content = newValue;
        }
        else {
            $(this.element).data('bs.tooltip').options.template = newValue;
        }
    }
    placementChanged(newValue) {
        $(this.element).data('bs.tooltip').options.placement = newValue;
    }
}

Solution

  • You would need to implement the rest of bootstrap's popover API in your custom attribute, and add some logic to turn a selector into a template.

    Here's an example: https://gist.run?id=909c7aa984477a465510abe2fd25c8a1

    Note: i've added the default values from bootstrap popovers for clarity

    With a custom attribute:

    src/app.html

    <template>
      <h1>${message}</h1>
    
     <button class="btn btn-block btn-default" popover="title.bind: message; placement: top">Default popover</button>
     <button class="btn btn-block btn-default" popover="title.bind: message; template-selector: #popoverTemplate; placement: bottom; html: true">Custom popover</button>
    
     <div id="popoverTemplate">
       <div class="popover" role="tooltip">
         <div class="arrow"></div>
         <h3 class="popover-title"></h3>
         <div>Some custom html</div>
       </div>
     </div>
    </template>
    

    src/app.ts

    export class App {
      message = "Hello world";
    }
    

    src/popover.ts

    import {inject, customAttribute, bindable, DOM} from "aurelia-framework";
    
    @customAttribute("popover")
    @inject(DOM.Element)
    export class Popover {
      public element: HTMLElement;
    
      constructor(element) {
        this.element = element;
      }
    
      @bindable({defaultValue: true})
      public animation: boolean;
    
      @bindable({defaultValue: false})
      public container: (string | false);
    
      @bindable({defaultValue: 0})
      public delay: (number | object);
    
      @bindable({defaultValue: false})
      public html: boolean;
    
      @bindable({defaultValue: "right"})
      public placement: (string | Function);
    
      @bindable({defaultValue: false})
      public selector: (string | false);
    
      @bindable({defaultValue: `<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>`})
      public template: string;
    
      @bindable({defaultValue: false})
      public templateSelector: (string | false);
    
      @bindable({defaultValue: ""})
      public title: (string | Function);
    
      @bindable({defaultValue: "click"})
      public trigger: string;
    
      @bindable({defaultValue: { selector: "body", padding: 0 }})
      public viewport: (string | Object | Function);
    
      public attached(): void {
        let template;
    
        if (this.templateSelector) {
          const templateElement = document.querySelector(this.templateSelector);
          template = templateElement.innerHTML;
        } else {
          template = this.template;
        }
    
        $(this.element).popover({
          animation: this.animation,
          container: this.container,
          delay: this.delay,
          html: this.html,
          placement: this.placement,
          selector: this.selector,
          template: template,
          title: this.title,
          trigger: this.trigger,
          viewport: this.viewport
        });
      }
    }
    

    With a custom element:

    This is in response to @Ashley Grant's comment. It could improve clarity if you used a custom element for this. I'm not sure of the implementation he had in mind, but this would be one way to make it work without really losing flexibility.

    src/app.html

    <template>
      <h1>${message}</h1>
    
     <popover-element title.bind="message" placement="bottom">
     </popover-element>
     <popover-element title.bind="message" placement="bottom">
       <button slot="popoverTarget" class="btn btn-block btn-default">
         Custom popover (custom element)
       </button>
       <div slot="popoverTemplate" class="popover" role="tooltip">
         <div class="arrow"></div>
         <h3 class="popover-title"></h3>
         <div>Some custom html</div>
         <div>Message: ${message}</div>
       </div>
     </popover-element>
    
    </template>
    

    src/app.ts

    export class App {
      message = "Hello world";
    }
    

    src/popover-element.html

    <template>
      <div ref="target">
        <slot name="popoverTarget">
          <button class="btn btn-block btn-default">Default popover (custom element)</button>
        </slot>
      </div>
      <div ref="template">
        <slot name="popoverTemplate">
          <div class="popover" role="tooltip">
            <div class="arrow"></div>
            <h3 class="popover-title"></h3>
            <div class="popover-content"></div>
          </div>
        </slot>
      </div>
    </template>
    

    src/popover-element.ts

    import {customElement, bindable} from "aurelia-framework";
    
    @customElement("popover-element")
    export class PopoverElement {
      public template: HTMLElement;
      public target: HTMLElement;
    
      @bindable({defaultValue: true})
      public animation: boolean;
    
      @bindable({defaultValue: false})
      public container: (string | false);
    
      @bindable({defaultValue: 0})
      public delay: (number | object);
    
      @bindable({defaultValue: false})
      public html: boolean;
    
      @bindable({defaultValue: "right"})
      public placement: (string | Function);
    
      @bindable({defaultValue: false})
      public selector: (string | false);
    
      @bindable({defaultValue: ""})
      public title: (string | Function);
    
      @bindable({defaultValue: "click"})
      public trigger: string;
    
      @bindable({defaultValue: { selector: "body", padding: 0 }})
      public viewport: (string | Object | Function);
    
      public attached(): void {
    
        $(this.target.firstElementChild).popover({
          animation: this.animation,
          container: this.container,
          delay: this.delay,
          html: this.html,
          placement: this.placement,
          selector: this.selector,
          template: this.template.firstElementChild.outerHTML,
          title: this.title,
          trigger: this.trigger,
          viewport: this.viewport
        });
      }
    }