Extend bindings with one-way content update bind

This commit is contained in:
Bartek Fabiszewski 2019-12-17 20:01:59 +01:00
parent 9dd8ad007f
commit 0745bfc92e
6 changed files with 79 additions and 33 deletions

View File

@ -31,7 +31,6 @@ export default class ConfigViewModel extends ViewModel {
constructor(state) {
super(config);
this.state = state;
this.intervalEl = document.querySelector('#interval');
this.model.onSetInterval = () => this.setAutoReloadInterval();
this.bindAll();
this.onChanged('mapApi', (api) => {
@ -46,7 +45,6 @@ export default class ConfigViewModel extends ViewModel {
ConfigViewModel.reload();
});
this.onChanged('interval', (interval) => {
this.intervalEl.innerHTML = interval.toString();
uUtils.setCookie('interval', interval);
});
}

View File

@ -45,6 +45,8 @@ export default class TrackViewModel extends ViewModel {
autoReload: false,
/** @type {string} */
inputFile: false,
/** @type {string} */
summary: false,
// click handlers
/** @type {function} */
onReload: null,
@ -58,7 +60,6 @@ export default class TrackViewModel extends ViewModel {
this.setClickHandlers();
/** @type HTMLSelectElement */
const listEl = document.querySelector('#track');
this.summaryEl = document.querySelector('#summary');
this.importEl = document.querySelector('#input-file');
this.select = new uSelect(listEl);
this.state = state;
@ -279,7 +280,7 @@ export default class TrackViewModel extends ViewModel {
renderSummary() {
if (!this.state.currentTrack || !this.state.currentTrack.hasPositions) {
this.summaryEl.innerHTML = '';
this.model.summary = '';
return;
}
const last = this.state.currentTrack.positions[this.state.currentTrack.length - 1];
@ -290,12 +291,12 @@ export default class TrackViewModel extends ViewModel {
const dateTime = uUtils.getTimeString(date);
const dateString = (date.toDateString() !== today.toDateString()) ? `${dateTime.date}<br>` : '';
const timeString = `${dateTime.time}<span style="font-weight:normal">${dateTime.zone}</span>`;
this.summaryEl.innerHTML = `
this.model.summary = `
<div class="menu-title">${lang.strings['latest']}:</div>
${dateString}
${timeString}`;
} else {
this.summaryEl.innerHTML = `
this.model.summary = `
<div class="menu-title">${lang.strings['summary']}</div>
<div><img class="icon" alt="${lang.strings['tdistance']}" title="${lang.strings['tdistance']}" src="images/distance.svg"> ${lang.getLocaleDistanceMajor(last.totalMeters, true)}</div>
<div><img class="icon" alt="${lang.strings['ttime']}" title="${lang.strings['ttime']}" src="images/time.svg"> ${lang.getLocaleDuration(last.totalSeconds)}</div>`;

View File

@ -54,7 +54,7 @@ export default class ViewModel {
* Creates bidirectional binding between model property and DOM element.
* For input elements model property value change triggers change in DOM element and vice versa.
* In case of anchor element binding is one way. Model property is callback that will receive click event.
* @param key
* @param {string} key
*/
bind(key) {
const dataProp = 'bind';
@ -63,34 +63,67 @@ export default class ViewModel {
const name = element.dataset[dataProp];
if (name === key) {
if (element instanceof HTMLInputElement || element instanceof HTMLSelectElement) {
let prop = 'value';
let getVal = (val) => val;
if (element.type === 'checkbox') {
prop = 'checked';
getVal = (val) => !!val;
}
element.addEventListener('change', () => {
this._model[key] = element[prop];
});
uObserve.observe(this.model, key, (val) => {
val = getVal(val);
if (element[prop] !== val) {
element[prop] = val;
}
});
this.onChangeBind(element, key);
} else if (element instanceof HTMLAnchorElement) {
element.addEventListener('click', (event) => {
if (typeof this._model[key] !== 'function') {
throw new Error(`Property ${key} is not a callback`);
}
this._model[key](event);
event.preventDefault();
});
this.onClickBind(element, key);
} else {
this.viewUpdateBind(element, key);
}
}
});
}
/**
* One way bind: view element click event to view model event handler
* @param {HTMLAnchorElement} element
* @param {string} key
*/
onClickBind(element, key) {
element.addEventListener('click', (event) => {
if (typeof this._model[key] !== 'function') {
throw new Error(`Property ${key} is not a callback`);
}
this._model[key](event);
event.preventDefault();
});
}
/**
* Two way bind: view element change event to view model property
* @param {(HTMLInputElement|HTMLSelectElement)} element
* @param {string} key
*/
onChangeBind(element, key) {
let prop = 'value';
let getVal = (val) => val;
if (element.type === 'checkbox') {
prop = 'checked';
getVal = (val) => !!val;
}
element.addEventListener('change', () => {
this._model[key] = element[prop];
});
uObserve.observe(this.model, key, (val) => {
val = getVal(val);
if (element[prop] !== val) {
element[prop] = val;
}
});
}
/**
* One way bind: view model property to view element content
* @param {HTMLElement} element
* @param {string} key
*/
viewUpdateBind(element, key) {
uObserve.observe(this.model, key, (content) => {
if (element.innerHTML !== content) {
element.innerHTML = content;
}
});
}
/**
* @param {string} property
* @param {ObserveCallback} callback
@ -106,5 +139,4 @@ export default class ViewModel {
unsubscribe(property, callback) {
uObserve.unobserve(this.model, property, callback);
}
}

View File

@ -52,7 +52,7 @@ describe('ConfigViewModel tests', () => {
const fixture = `<div id="fixture">
<div class="section">
<a id="set-interval" data-bind="onSetInterval"><span id="interval">${config.interval}</span></a>
<a id="set-interval" data-bind="onSetInterval"><span id="interval" data-bind="interval">${config.interval}</span></a>
</div>
<div>
<label for="api">api</label>

View File

@ -65,7 +65,7 @@ describe('TrackViewModel tests', () => {
<input id="auto-reload" type="checkbox" data-bind="autoReload">
<a id="force-reload" data-bind="onReload">reload</a>
</div>
<div id="summary" class="section"></div>
<div id="summary" class="section" data-bind="summary"></div>
<div class="section">
<a id="export-kml" class="menu-link" data-bind="onExportKml">kml</a>
<a id="export-gpx" class="menu-link" data-bind="onExportGpx">gpx</a>
@ -113,7 +113,6 @@ describe('TrackViewModel tests', () => {
const trackViewModel = new TrackViewModel(state);
// then
expect(trackViewModel).toBeInstanceOf(ViewModel);
expect(trackViewModel.summaryEl).toBeInstanceOf(HTMLDivElement);
expect(trackViewModel.importEl).toBeInstanceOf(HTMLInputElement);
expect(trackViewModel.select.element).toBeInstanceOf(HTMLSelectElement);
expect(trackViewModel.state).toBe(state);

View File

@ -152,6 +152,22 @@ describe('ViewModel tests', () => {
expect(model[propertyFunction].calls.mostRecent().args[0].target).toBe(anchorElement);
});
it('should bind DOM div element to model property', () => {
// given
/** @type {HTMLDivElement} */
const divElement = uUtils.nodeFromHtml(`<div data-bind="${propertyString}"></div>`);
document.body.appendChild(divElement);
const newContent = '<span>new value</span>';
// when
vm.bind(propertyString);
// then
expect(uObserve.isObserved(vm.model, propertyString)).toBe(true);
// when
model[propertyString] = newContent;
// then
expect(divElement.innerHTML).toBe(newContent);
});
it('should start observing model property', () => {
// given
// eslint-disable-next-line no-empty-function