@danielkalen/simplybind
Version:
Magically simple, framework-less one-way/two-way data binding for frontend/backend in ~5kb.
175 lines (155 loc) • 4.77 kB
JavaScript
import {subscriberCollection} from './subscriber-collection';
import {DOM} from 'aurelia-pal';
const selectArrayContext = 'SelectValueObserver:array';
export class SelectValueObserver {
constructor(element, handler, observerLocator) {
this.element = element;
this.handler = handler;
this.observerLocator = observerLocator;
}
getValue() {
return this.value;
}
setValue(newValue) {
if (newValue !== null && newValue !== undefined && this.element.multiple && !Array.isArray(newValue)) {
throw new Error('Only null or Array instances can be bound to a multi-select.');
}
if (this.value === newValue) {
return;
}
// unsubscribe from old array.
if (this.arrayObserver) {
this.arrayObserver.unsubscribe(selectArrayContext, this);
this.arrayObserver = null;
}
// subscribe to new array.
if (Array.isArray(newValue)) {
this.arrayObserver = this.observerLocator.getArrayObserver(newValue);
this.arrayObserver.subscribe(selectArrayContext, this);
}
// assign and sync element.
this.oldValue = this.value;
this.value = newValue;
this.synchronizeOptions();
this.notify();
// queue up an initial sync after the bindings have been evaluated.
if (!this.initialSync) {
this.initialSync = true;
this.observerLocator.taskQueue.queueMicroTask(this);
}
}
call(context, splices) {
// called by task queue and array observer.
this.synchronizeOptions();
}
synchronizeOptions() {
let value = this.value;
let clear;
let isArray;
if (value === null || value === undefined) {
clear = true;
} else if (Array.isArray(value)) {
isArray = true;
}
let options = this.element.options;
let i = options.length;
let matcher = this.element.matcher || ((a, b) => a === b);
while (i--) {
let option = options.item(i);
if (clear) {
option.selected = false;
continue;
}
let optionValue = option.hasOwnProperty('model') ? option.model : option.value;
if (isArray) {
option.selected = value.findIndex(item => !!matcher(optionValue, item)) !== -1; // eslint-disable-line no-loop-func
continue;
}
option.selected = !!matcher(optionValue, value);
}
}
synchronizeValue() {
let options = this.element.options;
let count = 0;
let value = [];
for (let i = 0, ii = options.length; i < ii; i++) {
let option = options.item(i);
if (!option.selected) {
continue;
}
value.push(option.hasOwnProperty('model') ? option.model : option.value);
count++;
}
if (this.element.multiple) {
// multi-select
if (Array.isArray(this.value)) {
let matcher = this.element.matcher || ((a, b) => a === b);
// remove items that are no longer selected.
let i = 0;
while (i < this.value.length) {
let a = this.value[i];
if (value.findIndex(b => matcher(a, b)) === -1) { // eslint-disable-line no-loop-func
this.value.splice(i, 1);
} else {
i++;
}
}
// add items that have been selected.
i = 0;
while (i < value.length) {
let a = value[i];
if (this.value.findIndex(b => matcher(a, b)) === -1) { // eslint-disable-line no-loop-func
this.value.push(a);
}
i++;
}
return; // don't notify.
}
} else {
// single-select
if (count === 0) {
value = null;
} else {
value = value[0];
}
}
if (value !== this.value) {
this.oldValue = this.value;
this.value = value;
this.notify();
}
}
notify() {
let oldValue = this.oldValue;
let newValue = this.value;
this.callSubscribers(newValue, oldValue);
}
subscribe(context, callable) {
if (!this.hasSubscribers()) {
this.disposeHandler = this.handler.subscribe(this.element, this.synchronizeValue.bind(this, false));
}
this.addSubscriber(context, callable);
}
unsubscribe(context, callable) {
if (this.removeSubscriber(context, callable) && !this.hasSubscribers()) {
this.disposeHandler();
this.disposeHandler = null;
}
}
bind() {
this.domObserver = DOM.createMutationObserver(() => {
this.synchronizeOptions();
this.synchronizeValue();
});
this.domObserver.observe(this.element, { childList: true, subtree: true });
}
unbind() {
this.domObserver.disconnect();
this.domObserver = null;
if (this.arrayObserver) {
this.arrayObserver.unsubscribe(selectArrayContext, this);
this.arrayObserver = null;
}
}
}