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.getLocaleDistanceMajor(last.totalMeters, true)}
${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 = `
-
+
@@ -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