7

I am currently working on a calendar in my angular v8 application.

This is the plugin I use: https://fullcalendar.io

this is the component which I include in my html template:

    <full-calendar
        defaultView="dayGridMonth"
        [editable]="true"
        [eventLimit]="5"
        [nowIndicator]="true"
        [slotLabelFormat]="timeFormat"
        [eventTimeFormat]="timeFormat"
        [eventClassName]="'fc-event-brand'"
        [minTime]="'08:00:00'"
        [maxTime]="'24:00:00'"
        [header]="{
          left: 'prev,next today',
          center: 'title',
          right: 'dayGridMonth, timeGridWeek, timeGridDay, listWeek'
        }"
        [plugins]="calendarPlugins"
        [events]="calendarEvents"
        (eventMouseEnter)="showPopover($event)"
        (eventMouseLeave)="hidePopover($event)"
        (eventRender)="renderTooltip($event)"></full-calendar>

But how can I add a ngBootstrap popover or tooltip to an element?

this is the renderTooltip():

renderTooltip(event) {
    // bind ngBootstrap tooltip or popover to $event.el
}
Zoe
  • 27,060
  • 21
  • 118
  • 148
Sireini
  • 4,142
  • 12
  • 52
  • 91
  • you can see the demo here : [link](https://fullcalendar.io/docs/event-tooltip-demo) search for 'tooltip' in page source and you will see the implementation. – Mazdak Aug 04 '19 at 10:49

1 Answers1

12

I would create one simple component which is basically just a popover wrapper:

@Component({
  template: `
    <div class="fc-content" [ngbPopover]="template" container="body" triggers="manual">
      <ng-content></ng-content>
    </div>
  `,
})
export class PopoverWrapperComponent {
  template: TemplateRef<any>;

  @ViewChild(NgbPopover, { static: true }) popover: NgbPopover;
}
  • template property will be passed from our main component so we can create any template we want
  • we also hold of NgbPopover instance so we can use popover.open(context) later.

Also make sure you've added this component to entryComponents array of your NgModule:

@NgModule({
  imports:      [ BrowserModule, FullCalendarModule, NgbPopoverModule ],
  declarations: [ AppComponent, PopoverWrapperComponent ],
  entryComponents: [PopoverWrapperComponent],
                              \/
                           like this
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

Now we're going to dynamically render this component into $event.el element and project child nodes in ng-content.

import { Component, ComponentRef,
 TemplateRef, ViewChild, ComponentFactoryResolver, 
 Injector, ApplicationRef } from '@angular/core';
import dayGridPlugin from '@fullcalendar/daygrid';
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap';


@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  calendarPlugins = [dayGridPlugin];

  calendarEvents = [
    { title: 'event 1', date: '2019-08-09', customProp1: 'customProp1', customProp2: 'customProp2' },
    { title: 'event 2', date: '2019-08-12', customProp1: 'customProp3', customProp2: 'customProp4' }
  ];

  @ViewChild('popoverTmpl', { static: true }) popoverTmpl: TemplateRef<any>;

  popoversMap = new Map<any, ComponentRef<PopoverWrapperComponent>>();

  popoverFactory = this.resolver.resolveComponentFactory(PopoverWrapperComponent);

  constructor(
    private resolver: ComponentFactoryResolver, 
    private injector: Injector,
    private appRef: ApplicationRef) {
  }

  renderTooltip(event) {
    const projectableNodes = Array.from(event.el.childNodes)

    const compRef = this.popoverFactory.create(this.injector, [projectableNodes], event.el);
    compRef.instance.template = this.popoverTmpl;

    this.appRef.attachView(compRef.hostView);
    this.popoversMap.set(event.el, compRef);
  }

  destroyTooltip(event) {
    const popover = this.popoversMap.get(event.el); 
    if (popover) {
      this.appRef.detachView(popover.hostView);
      popover.destroy();
      this.popoversMap.delete(event.el);
    }
  }

  showPopover(event) {
    const popover = this.popoversMap.get(event.el);
    if (popover) {
      popover.instance.popover.open({ event: event.event });
    }
  }

  hidePopover(event) {
    const popover = this.popoversMap.get(event.el);
    if (popover) {
      popover.instance.popover.close();
    }
  }
}

The key part here is how we're rendering component dynamically with projectable nodes:

renderTooltip(event) {
  const projectableNodes = Array.from(event.el.childNodes)

  const compRef = this.popoverFactory.create(this.injector, [projectableNodes], event.el);
  compRef.instance.template = this.popoverTmpl;

  this.appRef.attachView(compRef.hostView)
  this.popoversMap.set(event.el, compRef)
}

Rendered dynamically component has no relation to the Angular change detection tree so we have to add its view to ApplicationRef views so change detection should work there.

Make sure you've subscribed to the following event in your template:

(eventRender)="renderTooltip($event)"
(eventDestroy)="destroyTooltip($event)"
(eventMouseEnter)="showPopover($event)"
(eventMouseLeave)="hidePopover($event)"

You should also define template for popover, i.e.:

<ng-template #popoverTmpl let-event="event">
  <h6>{{ event.title }}</h6>
  <div>
    <p>{{ event.extendedProps.customProp1 }}</p>
    <p>{{ event.extendedProps.customProp2 }}</p>
  </div>
</ng-template>

Stackblitz Example

Stackblitz Example with @fullcalendar/angular/5.5.0

yurzui
  • 205,937
  • 32
  • 433
  • 399
  • 1
    @yurzui, clap,clap,clap – Eliseo Aug 04 '19 at 16:44
  • Okay there is a problem with this solution - if you use interactionPlugin it breaks resizing functionality, which can be fixed by removing `fc-content` class from `PopoverWrapperComponent`. It adds two nested `fc-content` divs, instead of one. Otherwise it works perfect, thank you! – Majesty Dec 05 '19 at 09:56
  • Great solution, thumbs up! The conflicting 'fc-event' class issue can be resolved in providing your own css class with the popover property popoverClass. So in the PopoverWrapperComponent template could be like:
    – Robert Dalla May 26 '20 at 14:40
  • The StackBlitz example appears to be broken – Daniel Flippance Nov 14 '20 at 01:29
  • 1
    @DanielFlippance No, you didn't see anything there because it used old dates. I updated it to use current days https://stackblitz.com/edit/angular-z8cwaq?file=src%2Fapp%2Fapp.component.ts – yurzui Nov 14 '20 at 04:42
  • Can you update your code for fullcalendar 5.x? – tala9999 Jan 29 '21 at 03:05
  • @tala9999 I added it to my answer. You can check that out – yurzui Jan 29 '21 at 11:22
  • @yurzui. Thanks for quick response. It works great until I hook `eventContent:this.renderEventContent`, then the popover shows at incorrect position, sometimes at the top-left corner. Any ideas? `renderEventContent(arg) {return 'test';}` – tala9999 Jan 29 '21 at 16:00
  • Very nice solution, a shame it has to be so complicated. In Angular 13, `ComponentFactoryResolver` is deprecated. Is there a future-proof alternative? – andreas Apr 08 '22 at 11:46
  • After some investigation, this solutions unfortunately leads to new problems: The text in events in grid view will print outside the box if long (overflow issue) and the events in the list view will collapse in an odd way. – andreas Apr 08 '22 at 13:01