Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Technique for jquery change events and aurelia

Tags:

aurelia

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?

like image 895
Adam Avatar asked Jan 01 '26 19:01

Adam


1 Answers

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();
        }
    }
}
like image 121
Fred Kleuver Avatar answered Jan 05 '26 22:01

Fred Kleuver



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!