From 0745bfc92e5e1c843da17e8a737f4af25f18ef1a Mon Sep 17 00:00:00 2001 From: Bartek Fabiszewski Date: Tue, 17 Dec 2019 20:01:59 +0100 Subject: [PATCH] Extend bindings with one-way content update bind --- js/src/configviewmodel.js | 2 - js/src/trackviewmodel.js | 9 ++-- js/src/viewmodel.js | 80 +++++++++++++++++++++++---------- js/test/configviewmodel.test.js | 2 +- js/test/trackviewmodel.test.js | 3 +- js/test/viewmodel.test.js | 16 +++++++ 6 files changed, 79 insertions(+), 33 deletions(-) diff --git a/js/src/configviewmodel.js b/js/src/configviewmodel.js index a219138..e7ff918 100644 --- a/js/src/configviewmodel.js +++ b/js/src/configviewmodel.js @@ -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); }); } diff --git a/js/src/trackviewmodel.js b/js/src/trackviewmodel.js index 2ba5285..2b883b7 100644 --- a/js/src/trackviewmodel.js +++ b/js/src/trackviewmodel.js @@ -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}
` : ''; const timeString = `${dateTime.time}${dateTime.zone}`; - this.summaryEl.innerHTML = ` + this.model.summary = ` ${dateString} ${timeString}`; } else { - this.summaryEl.innerHTML = ` + this.model.summary = `
${lang.strings['tdistance']} ${lang.getLocaleDistanceMajor(last.totalMeters, true)}
${lang.strings['ttime']} ${lang.getLocaleDuration(last.totalSeconds)}
`; diff --git a/js/src/viewmodel.js b/js/src/viewmodel.js index 6a7fbc4..a25a725 100644 --- a/js/src/viewmodel.js +++ b/js/src/viewmodel.js @@ -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); } - } diff --git a/js/test/configviewmodel.test.js b/js/test/configviewmodel.test.js index 7691de6..c66f5a5 100644 --- a/js/test/configviewmodel.test.js +++ b/js/test/configviewmodel.test.js @@ -52,7 +52,7 @@ describe('ConfigViewModel tests', () => { const fixture = `
diff --git a/js/test/trackviewmodel.test.js b/js/test/trackviewmodel.test.js index c937564..3672180 100644 --- a/js/test/trackviewmodel.test.js +++ b/js/test/trackviewmodel.test.js @@ -65,7 +65,7 @@ describe('TrackViewModel tests', () => { reload
-
+
kml gpx @@ -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); diff --git a/js/test/viewmodel.test.js b/js/test/viewmodel.test.js index b889da9..3e0f077 100644 --- a/js/test/viewmodel.test.js +++ b/js/test/viewmodel.test.js @@ -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(`
`); + document.body.appendChild(divElement); + const newContent = 'new value'; + // 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