@danielkalen/simplybind
Version:
Magically simple, framework-less one-way/two-way data binding for frontend/backend in ~5kb.
134 lines (116 loc) • 3.97 kB
JavaScript
import {subscriberCollection} from './subscriber-collection';
const checkedArrayContext = 'CheckedObserver:array';
const checkedValueContext = 'CheckedObserver:value';
export class CheckedObserver {
constructor(element, handler, observerLocator) {
this.element = element;
this.handler = handler;
this.observerLocator = observerLocator;
}
getValue() {
return this.value;
}
setValue(newValue) {
if (this.value === newValue) {
return;
}
// unsubscribe from old array.
if (this.arrayObserver) {
this.arrayObserver.unsubscribe(checkedArrayContext, this);
this.arrayObserver = null;
}
// subscribe to new array.
if (this.element.type === 'checkbox' && Array.isArray(newValue)) {
this.arrayObserver = this.observerLocator.getArrayObserver(newValue);
this.arrayObserver.subscribe(checkedArrayContext, this);
}
// assign and sync element.
this.oldValue = this.value;
this.value = newValue;
this.synchronizeElement();
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, array observer, and model/value observer.
this.synchronizeElement();
// if the input's model or value property is data-bound, subscribe to it's
// changes to enable synchronizing the element's checked status when a change occurs.
if (!this.valueObserver) {
this.valueObserver = this.element.__observers__.model || this.element.__observers__.value;
if (this.valueObserver) {
this.valueObserver.subscribe(checkedValueContext, this);
}
}
}
synchronizeElement() {
let value = this.value;
let element = this.element;
let elementValue = element.hasOwnProperty('model') ? element.model : element.value;
let isRadio = element.type === 'radio';
let matcher = element.matcher || ((a, b) => a === b);
element.checked =
isRadio && !!matcher(value, elementValue)
|| !isRadio && value === true
|| !isRadio && Array.isArray(value) && value.findIndex(item => !!matcher(item, elementValue)) !== -1;
}
synchronizeValue() {
let value = this.value;
let element = this.element;
let elementValue = element.hasOwnProperty('model') ? element.model : element.value;
let index;
let matcher = element.matcher || ((a, b) => a === b);
if (element.type === 'checkbox') {
if (Array.isArray(value)) {
index = value.findIndex(item => !!matcher(item, elementValue));
if (element.checked && index === -1) {
value.push(elementValue);
} else if (!element.checked && index !== -1) {
value.splice(index, 1);
}
// don't invoke callbacks.
return;
}
value = element.checked;
} else if (element.checked) {
value = elementValue;
} else {
// don't invoke callbacks.
return;
}
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;
}
}
unbind() {
if (this.arrayObserver) {
this.arrayObserver.unsubscribe(checkedArrayContext, this);
this.arrayObserver = null;
}
if (this.valueObserver) {
this.valueObserver.unsubscribe(checkedValueContext, this);
}
}
}