Technique for jquery change events and aurelia

梦想与她 提交于 2019-12-23 15:44:26

问题


I need to find a reliable solution to making the two frameworks play nicely.

Using materialize-css, their select element uses jquery to apply the value change. However that then does not trigger aurelia in seeing the change. Using the technique of... $("select") .change((eventObject: JQueryEventObject) => { fireEvent(eventObject.target, "change"); }); I can fire an event aurelia sees, however, aurelia then cause the event to be triggered again while it's updating it's bindings and I end up in an infinite loop.... Stack Overflow :D

Whats the most reliable way of getting the two to play together in this respect?


回答1:


I have worked with materialize-css + aurelia for a while and I can confirm that the select element from materialize is quite problematic.

I just wanted to share one of my solutions here in case anyone wants some additional examples. Ashley's is probably cleaner in this case. Mine uses a bindable for the options instead of a slot.

Other than that the basic idea is the same (using a guard variable and a micro task).

One lesson I learned in dealing with 3rd party plugins and two-way data binding is that it helps to make a more clear, distinct separation between handling changes that originate from the binding target (the select element on the DOM) and changes that originate from the binding source (e.g. the ViewModel of the page containing the element).

I tend to use change handlers with names like onValueChangedByBindingSource and onValueChangedByBindingTarget to deal with the different ways of syncing the ViewModel with the DOM in a way that results in less confusing code.

Example: https://gist.run?id=6ee17e333cd89dc17ac62355a4b31ea9

src/material-select.html

<template>
    <div class="input-field">
        <select value.two-way="value" id="material-select">
            <option repeat.for="option of options" model.bind="option">
                ${option.displayName}
            </option>
        </select>
    </div>
</template>

src/material-select.ts

import {
    customElement,
    bindable,
    bindingMode,
    TaskQueue,
    Disposable,
    BindingEngine,
    inject,
    DOM
} from "aurelia-framework";

@customElement("material-select")
@inject(DOM.Element, TaskQueue, BindingEngine)
export class MaterialSelect {
    public element: HTMLElement;
    public selectElement: HTMLSelectElement;

    @bindable({ defaultBindingMode: bindingMode.twoWay })
    public value: { name: string, value: number };

    @bindable({ defaultBindingMode: bindingMode.oneWay })
    public options: { displayName: string }[];

    constructor(
        element: Element,
        private tq: TaskQueue,
        private bindingEngine: BindingEngine
    ) {
      this.element = element;
    }

    private subscription: Disposable;
    public isAttached: boolean = false;
    public attached(): void {
        this.selectElement = <HTMLSelectElement>this.element.querySelector("select");
        this.isAttached = true;

        $(this.selectElement).material_select();
        $(this.selectElement).on("change", this.handleChangeFromNativeSelect);

        this.subscription = this.bindingEngine.collectionObserver(this.options).subscribe(() => {
            $(this.selectElement).material_select();
        });
    }

    public detached(): void {
        this.isAttached = false;
        $(this.selectElement).off("change", this.handleChangeFromNativeSelect);
        $(this.selectElement).material_select("destroy");
        this.subscription.dispose();
    }

    private valueChanged(newValue, oldValue): void {
        this.tq.queueMicroTask(() => {
            this.handleChangeFromViewModel(newValue);
        });
    }


    private _suspendUpdate = false;

    private handleChangeFromNativeSelect = () => {
        if (!this._suspendUpdate) {
            this._suspendUpdate = true;
            let event = new CustomEvent("change", {
                bubbles: true
            });
            this.selectElement.dispatchEvent(event)

            this._suspendUpdate = false;
        }
    }

    private handleChangeFromViewModel = (newValue) => {
        if (!this._suspendUpdate) {
            $(this.selectElement).material_select();
        }
    }
}

EDIT

How about a custom attribute?

Gist: https://gist.run?id=b895966489502cc4927570c0beed3123

src/app.html

<template>
  <div class="container">
    <div class="row"></div>
    <div class="row">
      <div class="col s12">
        <div class="input-element" style="position: relative;">
          <select md-select value.two-way="currentOption">
            <option repeat.for="option of options" model.bind="option">${option.displayName}</option>
          </select>
          <label>Selected: ${currentOption.displayName}</label>
        </div>
      </div>
      </div>
    </div>
</template>

src/app.ts

export class App {
  public value: string;
  public options: {displayName: string}[];

  constructor() {
    this.options = new Array<any>();
    this.options.push({ displayName: "Option 1" });
    this.options.push({ displayName: "Option 2" });
    this.options.push({ displayName: "Option 3" });
    this.options.push({ displayName: "Option 4" });
  }

  public attached(): void {

    this.value = this.options[1];
  }
}

src/md-select.ts

import {
    customAttribute,
    bindable,
    bindingMode,
    TaskQueue,
    Disposable,
    BindingEngine,
    DOM,
    inject
} from "aurelia-framework";

@inject(DOM.Element, TaskQueue, BindingEngine)
@customAttribute("md-select")
export class MdSelect {
    public selectElement: HTMLSelectElement;

    @bindable({ defaultBindingMode: bindingMode.twoWay })
    public value;

    constructor(element: Element, private tq: TaskQueue) {
      this.selectElement = element;
    }

    public attached(): void {
        $(this.selectElement).material_select();
        $(this.selectElement).on("change", this.handleChangeFromNativeSelect);
    }

    public detached(): void {
        $(this.selectElement).off("change", this.handleChangeFromNativeSelect);
        $(this.selectElement).material_select("destroy");
    }

    private valueChanged(newValue, oldValue): void {
        this.tq.queueMicroTask(() => {
            this.handleChangeFromViewModel(newValue);
        });
    }


    private _suspendUpdate = false;

    private handleChangeFromNativeSelect = () => {
        if (!this._suspendUpdate) {
            this._suspendUpdate = true;
            const event = new CustomEvent("change", { bubbles: true });
            this.selectElement.dispatchEvent(event)
            this.tq.queueMicroTask(() => this._suspendUpdate = false);
        }
    }

    private handleChangeFromViewModel = (newValue) => {
        if (!this._suspendUpdate) {
            $(this.selectElement).material_select();
        }
    }
}



回答2:


Ok, I spent entirely too long getting this one answered the way I wanted, but more on that later. The actual answer to stop the infinite loop is fairly simple, so let's look at it first. You need to have a guard property, and you'll need to use Aurelia's TaskQueue to help unset the guard property.

Your code will look a little something like this:

$(this.selectElement).change(evt => {
  if(!this.guard) {
    this.guard = true;
    const changeEvent = new Event('change');
    this.selectElement.dispatchEvent(changeEvent);
    this.taskQueue.queueMicroTask(() => this.guard = false);
  }
});

Notice that I'm using queueing up a microtask to unset the guard. This makes sure that everything will work the way you want.

Now that we've got that out of the way, let's look at a gist I created here. In this gist I created a custom element to wrap the Materialize select functionality. While creating this, I learned that select elements and content projection via slot elements don't go together. So you'll see in the code that I have to do some coding gymnastics to move the option elements over from a dummy div element into the select element. I'm going to file an issue so we can look in to this and see if this is a bug in the framework or simply a limitation of the browser.

Normally, I would highly recommend creating a custom element to wrap this functionality. Given the code I had to write to shuffle nodes around, I can't say that I highly recommend creating a custom element. I just really recommend it in this case.

But anyways, there you go!



来源:https://stackoverflow.com/questions/40042667/technique-for-jquery-change-events-and-aurelia

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