How to refresh bindings?

蹲街弑〆低调 提交于 2019-12-08 09:23:32

问题


I need to know how to refresh the bindings in Aurelia. I have been "Googling" for some time now, but can't seem to find the answer. The reason I need to refresh bindings is because some of the html (with click.delegate bindings) is generated after a call to server to retrieve data. I am updating a grid with some "Edit" and "Delete" buttons. Anyway, when I was using Durandal / KnockoutJS, I did the following:

var body = this.element.find("tbody")[0];
if (body) {
    ko.cleanNode(body);
    ko.applyBindings(ko.dataFor(body), body);
}

How do I do the same thing in Aurelia?

UPDATE:

Thanks @fred-kleuver for your response. I am not sure that is relevant in my case and it does seem like overkill for what I want to do. It may be that I need to do as you've suggested, but before I delve into figuring that all out, let me provide further details here of exactly what I am doing, as you might have a simpler solution for me:

I am using Kendo UI (the old GPL version from early 2014), which unfortunately doesn't work with the Aurelia Kendo Bridge. Therefore, I have to initialize the KendoGrid myself. I am copying over code as follows into Aurelia's attached() lifecycle method:

$("#grid").kendoGrid({
    data: null,
    dataSource: {
        type: "odata",
        transport: {
            read: {
                url: this.apiUrl,
                dataType: "json"
            },
            parameterMap: function (options, operation) {
                var paramMap = kendo.data.transports.odata.parameterMap(options);
                if (paramMap.$inlinecount) {
                    if (paramMap.$inlinecount == "allpages") {
                        paramMap.$count = true;
                    }
                    delete paramMap.$inlinecount;
                }
                if (paramMap.$filter) {
                    paramMap.$filter = paramMap.$filter.replace(/substringof\((.+),(.*?)\)/, "contains($2,$1)");
                }
                return paramMap;
            }
        },
        schema: {
            data: function (data) {
                return data.value;
            },
            total: function (data) {
                return data["@odata.count"];
            },
            model: {
                fields: {
                    Name: { type: "string" }
                }
            }
        },
        pageSize: this.gridPageSize,
        serverPaging: true,
        serverFiltering: true,
        serverSorting: true,
        sort: { field: "Name", dir: "asc" }
    },
    dataBound: function (e) {
        var body = this.element.find("tbody")[0];
        if (body) {
            // TODO: Figure out how to do this in Aurelia
            //ko.cleanNode(body);
            //ko.applyBindings(ko.dataFor(body), body);
        }
    },
    filterable: true,
    sortable: {
        allowUnsort: false
    },
    pageable: {
        refresh: true
    },
    scrollable: false,
    columns: [{
        field: "Name",
        title: this.translations.columns.name,
        filterable: true
    }, {
        field: "Id",
        title: " ",
        template:
            '<div class="btn-group">' +
            '<button type="button" click.delegate="edit(#=Id#)" class="btn btn-default btn-xs">' + this.translations.edit + '</button>' +
            '<button type="button" click.delegate="remove(#=Id#)" class="btn btn-danger btn-xs">' + this.translations.delete + '</button>' +
            '</div>',
        attributes: { "class": "text-center" },
        filterable: false,
        width: 120
    }]
});

So for the grid's dataBound function, I want Aurelia to refresh it's bindings (to pickup the click bindings on the buttons in each row).


回答1:


If you're generating html you'll need to pass it through the ViewCompiler for all bindings (and custom elements, attributes, etc) to be processed and start working.

I wrote a custom element some time ago that I could use in a view and then pass generated html (as well as a binding context) to it via a bindable property. This may be just what you need, or it may be overkill. It's production code, hence all the try/catch stuff.

In the latter case just focus on what I'm doing in the render() method which contains the necessary steps to compile, bind and attach dynamic html.

TLDR: the "meat" is all the way at the bottom, in render()

import { bindingMode, createOverrideContext } from "aurelia-binding";
import { Container } from "aurelia-dependency-injection";
import { TaskQueue } from "aurelia-task-queue";
import { bindable, customElement, inlineView, ViewCompiler, ViewResources, ViewSlot } from "aurelia-templating";

@customElement("runtime-view")
@inlineView("<template><div></div></template>")
export class RuntimeView {
  @bindable({ defaultBindingMode: bindingMode.toView })
  public html: string;

  @bindable({ defaultBindingMode: bindingMode.toView })
  public context: any;

  public el: HTMLElement;
  public slot: ViewSlot;
  public bindingContext: any;
  public overrideContext: any;
  public isAttached: boolean;
  public isRendered: boolean;
  public needsRender: boolean;

  private tq: TaskQueue;
  private container: Container;
  private viewCompiler: ViewCompiler;

  constructor(el: Element, tq: TaskQueue, container: Container, viewCompiler: ViewCompiler) {
    this.el = el as HTMLElement;
    this.tq = tq;
    this.container = container;
    this.viewCompiler = viewCompiler;
    this.slot = this.bindingContext = this.overrideContext = null;
    this.isAttached = this.isRendered = this.needsRender = false;
  }

  public bind(bindingContext: any, overrideContext: any): void {
    this.bindingContext = this.context || bindingContext.context || bindingContext;
    this.overrideContext = createOverrideContext(this.bindingContext, overrideContext);

    this.htmlChanged();
  }

  public unbind(): void {
    this.bindingContext = null;
    this.overrideContext = null;
  }

  public attached(): void {
    this.slot = new ViewSlot(this.el.firstElementChild || this.el, true);
    this.isAttached = true;

    this.tq.queueMicroTask(() => {
      this.tryRender();
    });
  }

  public detached(): void {
    this.isAttached = false;

    if (this.isRendered) {
      this.cleanUp();
    }
    this.slot = null;
  }

  private htmlChanged(): void {
    this.tq.queueMicroTask(() => {
      this.tryRender();
    });
  }

  private contextChanged(): void {
    this.tq.queueMicroTask(() => {
      this.tryRender();
    });
  }

  private tryRender(): void {
    if (this.isAttached) {
      if (this.isRendered) {
        this.cleanUp();
      }
      try {
        this.tq.queueMicroTask(() => {
          this.render();
        });
      } catch (e) {
        this.tq.queueMicroTask(() => {
          this.render(`<template>${e.message}</template>`);
        });
      }
    }
  }

  private cleanUp(): void {
    try {
      this.slot.detached();
    } catch (e) {}
    try {
      this.slot.unbind();
    } catch (e) {}
    try {
      this.slot.removeAll();
    } catch (e) {}

    this.isRendered = false;
  }

  private render(message?: string): void {
    if (this.isRendered) {
      this.cleanUp();
    }

    const template = `<template>${message || this.html}</template>`;
    const viewResources = this.container.get(ViewResources) as ViewResources;
    const childContainer = this.container.createChild();
    const factory = this.viewCompiler.compile(template, viewResources);
    const view = factory.create(childContainer);

    this.slot.add(view);
    this.slot.bind(this.bindingContext, this.overrideContext);
    this.slot.attached();

    this.isRendered = true;
  }
}

Usage (of course you'd use variables instead of inlines):

<runtime-view
    html.bind="'<some-element some-property.bind="value"></some-element>'"
    context.bind="{ value: 'text' }">
</runtime-view>

EDIT:

Ok, based on your updated answer it seems you don't have any html behaviors in the generated html, so you won't need to invoke the lifecycles.

I can't test this without spending a fair amount of time getting the same setup as you, so I'll give you a few things to try:

(as for this.somethings, just capitalize the first letter - that gives you the Aurelia component you need to inject)

Option 1

Use TemplatingEngine.enhance

dataBound: e => {
    const body = document.querySelector("#grid tbody");
    if (body) {
        this.templatingEngine.enhance({ element: body, bindingContext: this });
    }
}

Option 2

Manually enhance tbody in-place

dataBound: e => {
    const body = document.querySelector("#grid tbody");
    if (body) {
        const factory = this.viewCompiler.compile(body);
        factory.create(this.container, { enhance: true });
    }
}

Option 3

Completely replace the body's innerHTML

dataBound: e => {
    const body = document.querySelector("#grid tbody")
    if (body) {
        const html = body.innerHTML;
        body.innerHTML = "";
        const factory = this.viewCompiler.compile(html);
        const view = factory.create(this.container);
        const slot = new ViewSlot(body, true);
        slot.add(view);
    }
}

document.addEventListener

You're already largely bypassing Aurelia with the way you're using Kendo, and you're not even databinding to anything. Now you're kind of making your own brittle bridge.

If all you're using is click.delegate then why not just use .addEventListener("click", someFunction) on the buttons?

Find a working bridge or don't use kendo

I'm sure there are much cleaner ways to accomplish this in the context of your app, but it's impossible to make any "spot-on" suggestions if you don't provide a plunkr repro or something similar.

But I'd recommend to try and find components that work out-of-the-box if you can't invest too much time in learning Aurelia. aurelia-v-grid is a great example of a native Aurelia grid that can probably do a lot more for you than a semi-integrated kendo bridge.




回答2:


I agree with Fred's argument on using addEventListener. You only try to use Aurelia to hook up event handler, I think your approach itself is overkill for the problem.

Since you are already using jQuery, just use jQuery live event to hook up event handle for ever-changing grid DOM.

Take edit button for example,

In your kendoGrid template

'<button data-id="#=Id#" class="edit-btn ..." type="button" >' + ...

In your Aurelia component

@inject(Element)
export class YourComp {
  constructor(element) {
    this.element = element;
  }

  edit(id) { /* ... */ }

  attached() {
    // this can work across grid rebuild
    $(this.element).on('click', '.edit-btn', event => {
      this.edit(event.target.getAttribute('data-id');
    });
  }

  detached() {
    $(this.element).off('click', '.edit-btn');
  }
}


来源:https://stackoverflow.com/questions/50022746/how-to-refresh-bindings

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!