Extend bindings with one-way content update bind
This commit is contained in:
parent
9dd8ad007f
commit
0745bfc92e
@ -31,7 +31,6 @@ export default class ConfigViewModel extends ViewModel {
|
|||||||
constructor(state) {
|
constructor(state) {
|
||||||
super(config);
|
super(config);
|
||||||
this.state = state;
|
this.state = state;
|
||||||
this.intervalEl = document.querySelector('#interval');
|
|
||||||
this.model.onSetInterval = () => this.setAutoReloadInterval();
|
this.model.onSetInterval = () => this.setAutoReloadInterval();
|
||||||
this.bindAll();
|
this.bindAll();
|
||||||
this.onChanged('mapApi', (api) => {
|
this.onChanged('mapApi', (api) => {
|
||||||
@ -46,7 +45,6 @@ export default class ConfigViewModel extends ViewModel {
|
|||||||
ConfigViewModel.reload();
|
ConfigViewModel.reload();
|
||||||
});
|
});
|
||||||
this.onChanged('interval', (interval) => {
|
this.onChanged('interval', (interval) => {
|
||||||
this.intervalEl.innerHTML = interval.toString();
|
|
||||||
uUtils.setCookie('interval', interval);
|
uUtils.setCookie('interval', interval);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,8 @@ export default class TrackViewModel extends ViewModel {
|
|||||||
autoReload: false,
|
autoReload: false,
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
inputFile: false,
|
inputFile: false,
|
||||||
|
/** @type {string} */
|
||||||
|
summary: false,
|
||||||
// click handlers
|
// click handlers
|
||||||
/** @type {function} */
|
/** @type {function} */
|
||||||
onReload: null,
|
onReload: null,
|
||||||
@ -58,7 +60,6 @@ export default class TrackViewModel extends ViewModel {
|
|||||||
this.setClickHandlers();
|
this.setClickHandlers();
|
||||||
/** @type HTMLSelectElement */
|
/** @type HTMLSelectElement */
|
||||||
const listEl = document.querySelector('#track');
|
const listEl = document.querySelector('#track');
|
||||||
this.summaryEl = document.querySelector('#summary');
|
|
||||||
this.importEl = document.querySelector('#input-file');
|
this.importEl = document.querySelector('#input-file');
|
||||||
this.select = new uSelect(listEl);
|
this.select = new uSelect(listEl);
|
||||||
this.state = state;
|
this.state = state;
|
||||||
@ -279,7 +280,7 @@ export default class TrackViewModel extends ViewModel {
|
|||||||
|
|
||||||
renderSummary() {
|
renderSummary() {
|
||||||
if (!this.state.currentTrack || !this.state.currentTrack.hasPositions) {
|
if (!this.state.currentTrack || !this.state.currentTrack.hasPositions) {
|
||||||
this.summaryEl.innerHTML = '';
|
this.model.summary = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const last = this.state.currentTrack.positions[this.state.currentTrack.length - 1];
|
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 dateTime = uUtils.getTimeString(date);
|
||||||
const dateString = (date.toDateString() !== today.toDateString()) ? `${dateTime.date}<br>` : '';
|
const dateString = (date.toDateString() !== today.toDateString()) ? `${dateTime.date}<br>` : '';
|
||||||
const timeString = `${dateTime.time}<span style="font-weight:normal">${dateTime.zone}</span>`;
|
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>
|
<div class="menu-title">${lang.strings['latest']}:</div>
|
||||||
${dateString}
|
${dateString}
|
||||||
${timeString}`;
|
${timeString}`;
|
||||||
} else {
|
} else {
|
||||||
this.summaryEl.innerHTML = `
|
this.model.summary = `
|
||||||
<div class="menu-title">${lang.strings['summary']}</div>
|
<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['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>`;
|
<div><img class="icon" alt="${lang.strings['ttime']}" title="${lang.strings['ttime']}" src="images/time.svg"> ${lang.getLocaleDuration(last.totalSeconds)}</div>`;
|
||||||
|
@ -54,7 +54,7 @@ export default class ViewModel {
|
|||||||
* Creates bidirectional binding between model property and DOM element.
|
* Creates bidirectional binding between model property and DOM element.
|
||||||
* For input elements model property value change triggers change in DOM element and vice versa.
|
* 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.
|
* 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) {
|
bind(key) {
|
||||||
const dataProp = 'bind';
|
const dataProp = 'bind';
|
||||||
@ -63,34 +63,67 @@ export default class ViewModel {
|
|||||||
const name = element.dataset[dataProp];
|
const name = element.dataset[dataProp];
|
||||||
if (name === key) {
|
if (name === key) {
|
||||||
if (element instanceof HTMLInputElement || element instanceof HTMLSelectElement) {
|
if (element instanceof HTMLInputElement || element instanceof HTMLSelectElement) {
|
||||||
let prop = 'value';
|
this.onChangeBind(element, key);
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (element instanceof HTMLAnchorElement) {
|
} else if (element instanceof HTMLAnchorElement) {
|
||||||
element.addEventListener('click', (event) => {
|
this.onClickBind(element, key);
|
||||||
if (typeof this._model[key] !== 'function') {
|
} else {
|
||||||
throw new Error(`Property ${key} is not a callback`);
|
this.viewUpdateBind(element, key);
|
||||||
}
|
|
||||||
this._model[key](event);
|
|
||||||
event.preventDefault();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {string} property
|
||||||
* @param {ObserveCallback} callback
|
* @param {ObserveCallback} callback
|
||||||
@ -106,5 +139,4 @@ export default class ViewModel {
|
|||||||
unsubscribe(property, callback) {
|
unsubscribe(property, callback) {
|
||||||
uObserve.unobserve(this.model, property, callback);
|
uObserve.unobserve(this.model, property, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ describe('ConfigViewModel tests', () => {
|
|||||||
|
|
||||||
const fixture = `<div id="fixture">
|
const fixture = `<div id="fixture">
|
||||||
<div class="section">
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<label for="api">api</label>
|
<label for="api">api</label>
|
||||||
|
@ -65,7 +65,7 @@ describe('TrackViewModel tests', () => {
|
|||||||
<input id="auto-reload" type="checkbox" data-bind="autoReload">
|
<input id="auto-reload" type="checkbox" data-bind="autoReload">
|
||||||
<a id="force-reload" data-bind="onReload">reload</a>
|
<a id="force-reload" data-bind="onReload">reload</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="summary" class="section"></div>
|
<div id="summary" class="section" data-bind="summary"></div>
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<a id="export-kml" class="menu-link" data-bind="onExportKml">kml</a>
|
<a id="export-kml" class="menu-link" data-bind="onExportKml">kml</a>
|
||||||
<a id="export-gpx" class="menu-link" data-bind="onExportGpx">gpx</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);
|
const trackViewModel = new TrackViewModel(state);
|
||||||
// then
|
// then
|
||||||
expect(trackViewModel).toBeInstanceOf(ViewModel);
|
expect(trackViewModel).toBeInstanceOf(ViewModel);
|
||||||
expect(trackViewModel.summaryEl).toBeInstanceOf(HTMLDivElement);
|
|
||||||
expect(trackViewModel.importEl).toBeInstanceOf(HTMLInputElement);
|
expect(trackViewModel.importEl).toBeInstanceOf(HTMLInputElement);
|
||||||
expect(trackViewModel.select.element).toBeInstanceOf(HTMLSelectElement);
|
expect(trackViewModel.select.element).toBeInstanceOf(HTMLSelectElement);
|
||||||
expect(trackViewModel.state).toBe(state);
|
expect(trackViewModel.state).toBe(state);
|
||||||
|
@ -152,6 +152,22 @@ describe('ViewModel tests', () => {
|
|||||||
expect(model[propertyFunction].calls.mostRecent().args[0].target).toBe(anchorElement);
|
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', () => {
|
it('should start observing model property', () => {
|
||||||
// given
|
// given
|
||||||
// eslint-disable-next-line no-empty-function
|
// eslint-disable-next-line no-empty-function
|
||||||
|
Loading…
x
Reference in New Issue
Block a user