Data-binding implementation

This commit is contained in:
Jeremy Likness 2019-11-26 12:05:41 -08:00
parent 274dc9077b
commit e7d8aad7ba
6 changed files with 213 additions and 16 deletions

View File

@ -4,10 +4,28 @@ body {
font-size: 3vh; font-size: 3vh;
} }
button { br {
clear: both;
}
label {
display: block;
}
label > div, input {
float: left;
width: 40vw;
}
button, input {
font-size: 3vh; font-size: 3vh;
} }
input {
padding: 1px;
}
pre { pre {
background-color: black; background-color: black;
overflow-x: auto; overflow-x: auto;
@ -44,17 +62,21 @@ slide-deck img.expandable:hover {
body { body {
font-size: 14px; font-size: 14px;
} }
button { button, input {
font-size: 14px; font-size: 14px;
} }
label > div, input {
width: 90vw;
}
} }
@media only screen and (max-width: 1200px) { @media only screen and (max-width: 1200px) {
body { body {
font-size: 2vh; font-size: 2vh;
} }
button { button, input {
font-size: 14px; font-size: 2vh;
} }
} }
@ -98,7 +120,7 @@ img {
text-align: center; text-align: center;
} }
slide-deck div { slide-deck > div {
height: 80vh; height: 80vh;
padding: 1em; padding: 1em;
} }
@ -116,7 +138,7 @@ slide-deck div {
.left { .left {
text-align: center; text-align: center;
} }
slide-deck div { slide-deck > div {
height: 75vh; height: 75vh;
} }
} }

View File

@ -1,5 +1,10 @@
// @ts-check // @ts-check
import { Observable, Computed } from "./observable.js"
/**
* Class supports data-binding operations
*/
export class DataBinding { export class DataBinding {
/** /**
@ -16,10 +21,56 @@ export class DataBinding {
* @param {object} context The context (data) to evaluate with * @param {object} context The context (data) to evaluate with
* @returns {object} The result of the evaluation * @returns {object} The result of the evaluation
*/ */
executeInContext(src, context) { executeInContext(src, context, attachBindingHelpers = false) {
if (attachBindingHelpers) {
context.observable = this.observable;
context.computed = this.computed;
context.bindValue = this.bindValue;
}
return this.execute.call(context, src); return this.execute.call(context, src);
} }
/**
* A simple observable implementation
* @param {object} value Any value to observe
* @returns {Observable} The observable instance to use
*/
observable(value) {
return new Observable(value);
}
/**
* Creates an observed computed property
* @param {function} calculation Calculated value
* @param {Observable[]} deps The list of dependent observables
* @returns {Computed} The observable computed value
*/
computed(calculation, deps) {
return new Computed(calculation, deps);
}
/**
* Binds an input element to an observable value
* @param {HTMLInputElement} input The element to bind to
* @param {Observable} observable The observable instance to bind to
*/
bindValue(input, observable) {
let initialValue = observable.value;
input.value = initialValue;
observable.subscribe(() => input.value = observable.value);
/**
* Converts the values
* @param {object} value
*/
let converter = value => value;
if (typeof initialValue === "number") {
converter = num => isNaN(num = parseFloat(num)) ? 0 : num;
}
input.onkeyup = () => {
observable.value = converter(input.value);
};
}
/** /**
* Searches for "repeat" attribute to data-bind lists * Searches for "repeat" attribute to data-bind lists
* @param {HTMLElement} elem The parent element to search * @param {HTMLElement} elem The parent element to search
@ -39,7 +90,7 @@ export class DataBinding {
if (matches) { if (matches) {
matches.forEach(match => { matches.forEach(match => {
match = match.replace("{{", "").replace("}}", ""); match = match.replace("{{", "").replace("}}", "");
const value = this.executeInContext(`this.${match}`, {item}); const value = this.executeInContext(`this.${match}`, { item });
newTemplate = newTemplate.replace(`{{${match}}}`, value); newTemplate = newTemplate.replace(`{{${match}}}`, value);
}); });
parent.innerHTML += newTemplate; parent.innerHTML += newTemplate;
@ -48,3 +99,4 @@ export class DataBinding {
}); });
} }
} }

View File

@ -117,10 +117,12 @@ export class Navigator extends HTMLElement {
*/ */
get hasNext() { get hasNext() {
const host = this.querySelector("div"); const host = this.querySelector("div");
if (host) {
const appear = host.querySelectorAll(".appear"); const appear = host.querySelectorAll(".appear");
if (appear && appear.length) { if (appear && appear.length) {
return true; return true;
} }
}
return this._currentIndex < (this.totalSlides - 1); return this._currentIndex < (this.totalSlides - 1);
} }

99
js/observable.js Normal file
View File

@ -0,0 +1,99 @@
// @ts-check
/**
* @callback ListenerCallback
* @param {object} newVal The new value generated
*/
/**
* Represents an observable value
*/
export class Observable {
/**
* Creates a new observable and initializes with a value
* @param {object} value
*/
constructor(value) {
/**
* Subscriptions
* @type {ListenerCallback[]}
*/
this._listeners = [];
/**
* The value
* @type {object}
*/
this._value = value;
}
/**
* Notifies subscribers of new value
*/
notify() {
this._listeners.forEach(listener => listener(this._value));
}
/**
* Subscribe to listen for changes
* @param {ListenerCallback} listener
*/
subscribe(listener) {
this._listeners.push(listener);
}
/**
* The value of the observable
* @returns {object} The current value
*/
get value() {
return this._value;
}
/**
* Sets the value of the observable
* @param {object} val The new value
*/
set value(val) {
if (val !== this._value) {
this._value = val;
this.notify();
}
}
}
/**
* Observable computed properties
*/
export class Computed extends Observable {
/**
* Creates a new observable and initializes with a value
* @param {Function} value Initial computation
* @param {Observable[]} deps Dependencies
*/
constructor(value, deps) {
super(value());
const listener = () => {
this._value = value();
this.notify();
}
deps.forEach(dep => dep.subscribe(listener));
}
/**
* Gets the value of the observable
* @returns {object} The value
*/
get value() {
return this._value;
}
/**
* Sets the value of the observable
* @param {object} _ The new value
* @throws "Cannot set computed property"
*/
set value(_) {
throw "Cannot set computed property";
}
}

View File

@ -65,7 +65,7 @@ export class Slide {
// execute any scripts // execute any scripts
const script = this._html.querySelector("script"); const script = this._html.querySelector("script");
if (script) { if (script) {
this._dataBinding.executeInContext(script.innerText, this._context); this._dataBinding.executeInContext(script.innerText, this._context, true);
this._dataBinding.bindLists(this._html, this._context); this._dataBinding.bindLists(this._html, this._context);
} }
} }

View File

@ -1,8 +1,30 @@
<title>Data-Binding Example 1</title> <title>Data-Binding Example 1</title>
<h1>Data-Binding Example</h1> <h1>Data-Binding Example</h1>
<label for="first"><div>Number:</div><input type="text" id="first"/></label>
<label for="second"><div>Multiplied by Number:</div><input type="text" id="second"/></label>
<label for="result"><div>Result:</div><input type="text" id="result" disabled/></label>
<label for="firstName"><div>First Name:</div><input type="text" id="firstName"/></label>
<label for="lastName"><div>Last Name:</div><input type="text" id="lastName"/></label>
<label for="fullName"><div>Full Name:</div><input type="text" id="fullName" disabled/></label>
<br/>
<ul> <ul>
<li repeat="list" class="appear">{{item.idx}} &mdash; {{item.value}}</li> <li repeat="list" class="appear">{{item.idx}} &mdash; {{item.value}}</li>
</ul> </ul>
<script> <script>
this.list = [{idx: 0, value:"one"}, {idx: 1, value:"two"}, {idx: 2, value:"three"}]; this.list = [{idx: 0, value:"one"}, {idx: 1, value:"two"}, {idx: 2, value:"three"}];
const bindings = () => {
var n1 = this.observable(2);
var n2 = this.observable(2);
var result = this.computed(() => n1.value*n2.value, [n1, n2]);
var first = this.observable("Jeremy");
var last = this.observable("");
var full = this.computed(() => `${first.value} ${last.value}`.trim(), [first, last]);
this.bindValue(document.getElementById("first"), n1);
this.bindValue(document.getElementById("second"), n2);
this.bindValue(document.getElementById("result"), result);
this.bindValue(document.getElementById("firstName"), first);
this.bindValue(document.getElementById("lastName"), last);
this.bindValue(document.getElementById("fullName"), full);
};
setTimeout(bindings, 0);
</script> </script>