eric
eric

Reputation: 352

Binding Angular2 components inside of a Jquery plugin template

I'm working on using a kendo inside of an angular 2 project.

Getting the widget set up correctly is no problem:

ngOnInit() {
    let options = inputsToOptionObject(KendoUIScheduler, this);
    options.dataBound = this.bound;
    this.scheduler = $(this.element.nativeElement)
        .kendoScheduler(options)
        .data('kendoScheduler');

}

When that runs, the plugin modifies the DOM (and, to my knowleged, without modifiying the shadow DOM maintained by angular2). My issue is that if I want to use a component anywhere inside of the plugin, like in a template, Angular is unaware of it's existence and won't bind it.

Example:

public views:kendo.ui.SchedulerView[] = [{
    type: 'month',
    title: 'test',
    dayTemplate: (x:any) => {
        let date = x.date.getDate();
        let count = this.data[date];
        return `<monthly-scheduler-day [date]="test" [count]=${count}"></monthly-scheduler-day>`
    }
}];

The monthly-scheduler-day class:

@Component({
    selector: 'monthly-scheduler-day',
    template: `
            <div>{{date}}</div>
            <div class="badge" (click)=dayClick($event)>Available</div>
    `
})
export class MonthlySchedulerDayComponent implements OnInit{
    @Input() date: number;
    @Input() count: number;
    constructor() {
        console.log('constructed');
    }
    ngOnInit(){            
        console.log('created');
    }

    dayClick(event){
        console.log('clicked a day');
    }

}

Is there a "right" way to bind these components inside of the markup created by the widget? I've managed to do it by listening for the bind event from the widget and then looping over the elements it created and using the DynamicComponentLoader, but it feels wrong.

Upvotes: 4

Views: 613

Answers (2)

slowkot
slowkot

Reputation: 478

Well your solution works fine until the component needs to change its state and rerender some stuff. Because I haven't found yet any ability to get ViewContainerRef for an element generated outside of Angular (jquery, vanilla js or even server-side) the first idea was to call detectChanges() by setting up an interval. And after several iterations finally I came to a solution which works for me.

So far in 2017 you have to replace ComponentResolver with ComponentResolverFactory and do almost the same things:

    let componentFactory = this.factoryResolver.resolveComponentFactory(componentType),
        componentRef = componentFactory.create(this.injector, null, selectorOrNode);

    componentRef.changeDetectorRef.detectChanges();

After that you can emulate attaching component instance to the change detection cycle by subscribing to EventEmitters of its NgZone:

    let enumerateProperties = obj => Object.keys(obj).map(key => obj[key]),
        properties = enumerateProperties(injector.get(NgZone))
                         .filter(p => p instanceof EventEmitter);

    let subscriptions = Observable.merge(...properties)
                                  .subscribe(_ => changeDetectorRef.detectChanges());

Of course don't forget to unsubscribe on destroy:

    componentRef.onDestroy(_ => {
        subscriptions.forEach(x => x.unsubscribe());
        componentRef.changeDetectorRef.detach();
    });

UPD after stackoverflowing once more

Forget all the words above. It works but just follow this answer

Upvotes: 0

eric
eric

Reputation: 352

I found some of the details I needed in this thread: https://github.com/angular/angular/issues/6223

I whipped this service up to handle binding my components:

import { Injectable, ComponentMetadata, ViewContainerRef, ComponentResolver, ComponentRef, Injector } from '@angular/core';

declare var $:JQueryStatic;

@Injectable()
export class JQueryBinder {
    constructor(
        private resolver: ComponentResolver,
        private injector: Injector
    ){}

    public bindAll(
        componentType: any, 
        contextParser:(html:string)=>{}, 
        componentInitializer:(c: ComponentRef<any>, context: {})=>void): 
            void 
        {
        let selector = Reflect.getMetadata('annotations', componentType).find((a:any) => {
            return a instanceof ComponentMetadata
        }).selector;

        this.resolver.resolveComponent(componentType).then((factory)=> {
            $(selector).each((i,e) => {
                let context = contextParser($(e).html());
                let c = factory.create(this.injector, null, e);
                componentInitializer(c, context);
                c.changeDetectorRef.detectChanges();
                c.onDestroy(()=>{
                    c.changeDetectorRef.detach();
                })
            });
        });        
    }
}

Params:

  • componentType: The component class you want to bind. It uses reflection to pull the selector it needs
  • contextParser: callback that takes the existing child html and constructs a context object (anything you need to initialize the component state)
  • componentInitializer - callback that initializes the created component with the context you parsed

Example usage:

    let parser = (html: string) => {
        return {
            date: parseInt(html)
        };
    };

    let initer =  (c: ComponentRef<GridCellComponent>, context: { date: number })=>{
        let d = context.date;

        c.instance.count = this.data[d];
        c.instance.date = d;
    }

    this.binder.bindAll(GridCellComponent, parser, initer );

Upvotes: 1

Related Questions