From b0448b11989e0400a09a1ebb53db1e0fa269d79b Mon Sep 17 00:00:00 2001 From: Bede Overend Date: Wed, 6 Dec 2017 18:44:42 +1100 Subject: [PATCH 1/3] Add element mixin --- mixins/element.js | 1 + rollup.config.js | 8 ++ src/mixins/element.js | 267 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 276 insertions(+) create mode 100644 mixins/element.js create mode 100644 src/mixins/element.js diff --git a/mixins/element.js b/mixins/element.js new file mode 100644 index 0000000..970287a --- /dev/null +++ b/mixins/element.js @@ -0,0 +1 @@ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.SimplaElement=e()}(this,function(){"use strict";function t(t,e,i){t.dispatchEvent(new CustomEvent(e+"-changed",{detail:{value:i}}))}var e={NO_SIMPLA:"Cannot find Simpla, ensure it is included before this component"};return function(i){return function(i){function a(){var t=this;if(i.call(this),!window.Simpla)throw new Error(e.NO_SIMPLA);this._loaded=!1,this.__simplaObservers={},this.__triggerBufferSync=function(){t.loaded&&t._protectedSetCallback()}}i&&(a.__proto__=i),(a.prototype=Object.create(i&&i.prototype)).constructor=a;var o={path:{configurable:!0},editable:{configurable:!0},readonly:{configurable:!0},loaded:{configurable:!0},_loaded:{configurable:!0}},n={observedAttributes:{configurable:!0}};return o.path.get=function(){return this.__path},o.path.set=function(t){var e=this.__path;this.__path=t,t!==e&&this._initPathIfReady(t)},o.editable.get=function(){return this.__editable},o.editable.set=function(e){var i=this.__editable;this.readonly||(this.__editable=e,e!==i&&t(this,"editable",e))},o.readonly.get=function(){return this.__readonly},o.readonly.set=function(t){var e=this.__readonly;this.__readonly=!1,t||!0!==e?t&&(this.editable=!1):this.editable=Simpla.getState("editable"),this.__readonly=t},o.loaded.get=function(){return this.__loaded},o._loaded.set=function(e){var i=this.__loaded;this.__loaded=e,e!==i&&t(this,"loaded",e)},n.observedAttributes.get=function(){return["path","editable","readonly","loaded"]},a.prototype.attributeChangedCallback=function(t,e,i){"path"===t?this.path=i:this[t]=null!==i},a.prototype.connectedCallback=function(){var t=this,e=this.constructor.simplaConfig.events;i.prototype.connectedCallback&&i.prototype.connectedCallback.call(this),e.forEach(function(e){return t.addEventListener(e,t.__triggerBufferSync)}),this.editable=!this.readonly&&(this.editable||Simpla.getState("editable")),this._observeSimplaEditable(),this._initPathIfReady(this.path)},a.prototype.disconnectedCallback=function(){var t=this,e=this.constructor.simplaConfig.events;i.prototype.disconnectedCallback&&i.prototype.disconnectedCallback.call(this),e.forEach(function(e){return t.removeEventListener(e,t.__triggerBufferSync)}),Object.keys(this.__simplaObservers).forEach(function(e){t.__simplaObservers[e].unobserve()}),this.__simplaObservers={}},a.prototype._initPathIfReady=function(t){this.isConnected&&void 0!==t&&this._initSimplaPath(t)},a.prototype._initSimplaPath=function(t){var e=this;this._loaded=!1,Simpla.get(t).then(function(i){var a=!(i&&i.data),o=e.path!==t,n=Simpla.getState("buffer"),r=n&&n[t]&&!n[t].modified;o||(a&&r?e._protectedSetCallback():e._protectedGetCallback(i),e._loaded=!0)}),this._observeSimplaBuffer(t)},a.prototype._observeSimplaBuffer=function(t){var e=this,i=this.__simplaObservers;t&&(i.buffer&&i.buffer.unobserve(),i.buffer=Simpla.observe(t,function(t){t&&t.data&&e._protectedGetCallback(t)}))},a.prototype.updateSimplaBuffer=function(){var t=this;return this.constructor.simplaConfig.dataProperties.reduce(function(e,i){return Object.assign(e,(a={},a[i]=t[i],a));var a},{})},a.prototype.updateFromSimpla=function(t){Object.assign(this,t.data)},a.prototype._protectedGetCallback=function(t){this.__loadingFromSimpla=!0,this.updateFromSimpla(t),this.__loadingFromSimpla=!1},a.prototype._protectedSetCallback=function(){if(!this.__loadingFromSimpla&&this.isConnected&&this.path){var t=this.updateSimplaBuffer(),e=this.constructor.simplaConfig.type;void 0===t&&null!==t||Simpla.set(this.path,{type:e,data:t})}},a.prototype._observeSimplaEditable=function(){var t=this,e=this.__simplaObservers;e.editable&&e.editable.unobserve(),e.editable=Simpla.observeState("editable",function(e){t.editable=e})},Object.defineProperties(a.prototype,o),Object.defineProperties(a,n),a}(i)}}); diff --git a/rollup.config.js b/rollup.config.js index 7419187..9cdad7e 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -37,4 +37,12 @@ export default [{ name: 'SimplaNetlify' }, plugins +}, { + input: 'src/mixins/element.js', + output: { + file: './mixins/element.js', + format: 'umd', + name: 'SimplaElement' + }, + plugins }]; diff --git a/src/mixins/element.js b/src/mixins/element.js new file mode 100644 index 0000000..d8f0296 --- /dev/null +++ b/src/mixins/element.js @@ -0,0 +1,267 @@ +function changed(from, prop, value) { + from.dispatchEvent(new CustomEvent(`${prop}-changed`, { detail: { value } })); +} + +const ERRORS = { + NO_SIMPLA: 'Cannot find Simpla, ensure it is included before this component' +}; + +export default function SimplaElement(Base) { + return class extends Base { + constructor() { + super(); + + if (!window.Simpla) { + throw new Error(ERRORS.NO_SIMPLA); + } + + this._loaded = false; + this.__simplaObservers = {}; + this.__triggerBufferSync = () => { + if (this.loaded) { + this._protectedSetCallback(); + } + }; + } + + get path() { + return this.__path; + } + + set path(value) { + let previous = this.__path; + this.__path = value; + + if (value !== previous) { + this._initPathIfReady(value); + } + } + + get editable() { + return this.__editable; + } + + set editable(value) { + let previous = this.__editable; + + if (!this.readonly) { + this.__editable = value; + + if (value !== previous) { + changed(this, 'editable', value); + } + } + } + + get readonly() { + return this.__readonly; + } + + set readonly(value) { + let previous = this.__readonly; + + // Disable to allow editable to be set however we like + this.__readonly = false; + + if (!value && previous === true) { + this.editable = Simpla.getState('editable'); + } else if (value) { + this.editable = false; + } + + this.__readonly = value; + } + + get loaded() { + return this.__loaded; + } + + set _loaded(value) { + let previous = this.__loaded; + this.__loaded = value; + + if (value !== previous) { + changed(this, 'loaded', value); + } + } + + static get observedAttributes() { + return [ 'path', 'editable', 'readonly', 'loaded' ]; + } + + attributeChangedCallback(attr, oldValue, newValue) { + if (attr === 'path') { + this.path = newValue; + } else { + // Cast as Boolean. + this[attr] = newValue !== null; + } + } + + /** + * Setup editable state observer on attach + * @return {undefined} + */ + connectedCallback() { + let { events } = this.constructor.simplaConfig; + + super.connectedCallback && super.connectedCallback(); + + events.forEach(event => this.addEventListener(event, this.__triggerBufferSync)); + + this.editable = this.readonly ? false : this.editable || Simpla.getState('editable'); + + this._observeSimplaEditable(); + this._initPathIfReady(this.path); + } + + /** + * Clean up Simpla observers on detach + * @return {undefined} + */ + disconnectedCallback() { + let { events } = this.constructor.simplaConfig; + + super.disconnectedCallback && super.disconnectedCallback(); + + events.forEach(event => this.removeEventListener(event, this.__triggerBufferSync)); + + Object.keys(this.__simplaObservers) + .forEach(observer => { + this.__simplaObservers[observer].unobserve(); + }); + + this.__simplaObservers = {}; + } + + /** + * Checks if connected and has a valid path. If so, calls _initSimplaPath + * @param {String} path Current value of path prop + * @return {undefined} + */ + _initPathIfReady(path) { + if (this.isConnected && typeof path !== 'undefined') { + this._initSimplaPath(path); + } + } + + /** + * Init the path observer + * @param {String} path Current value of path prop + * @return {undefined} + */ + _initSimplaPath(path) { + this._loaded = false; + + Simpla.get(path) + .then(item => { + let isEmpty = !(item && item.data), + pathChanged = this.path !== path, + buffer = Simpla.getState('buffer'), + bufferIsClean = buffer && buffer[path] && !buffer[path].modified; + + if (pathChanged) { + return; + } + + if (isEmpty && bufferIsClean) { + // Load static content + this._protectedSetCallback(); + } else { + this._protectedGetCallback(item); + } + + this._loaded = true; + + }); + + this._observeSimplaBuffer(path); + } + + /** + * Observe buffer for changes to update element + * @param {String} path Path to observe in buffer + * @return {undefined} + */ + _observeSimplaBuffer(path) { + let observers = this.__simplaObservers; + + if (!path) { + return; + } + + if (observers.buffer) { + observers.buffer.unobserve(); + } + + observers.buffer = Simpla.observe(path, item => { + if (item && item.data) { + this._protectedGetCallback(item); + } + }); + } + + /** + * Sets up the data property to send to Simpla based on given dataProperties + * @return {Object} Data to set to Simpla + */ + updateSimplaBuffer() { + let { dataProperties } = this.constructor.simplaConfig; + + return dataProperties.reduce((data, prop) => { + return Object.assign(data, { [ prop ]: this[prop] }); + }, {}); + } + + /** + * Update element from Simpla data + * @param {Object} item The data from Simpla to update with + * @return {undefined} + */ + updateFromSimpla(item) { + Object.assign(this, item.data); + } + + /** + * Calls the getCallback but protects against triggering an infinite loop + * @param {Object} item Item to give to getCallback + * @return {undefined} + */ + _protectedGetCallback(item) { + this.__loadingFromSimpla = true; + this.updateFromSimpla(item); + this.__loadingFromSimpla = false; + } + + /** + * Calls the setCallback but protects against triggering an infinite loop + * @return {undefined} + */ + _protectedSetCallback() { + if (!this.__loadingFromSimpla && this.isConnected && this.path) { + let data = this.updateSimplaBuffer(), + { type } = this.constructor.simplaConfig; + + if (typeof data !== 'undefined' || data === null) { + Simpla.set(this.path, { type, data }); + } + } + } + + /** + * Editable state observer + * @return {undefined} + */ + _observeSimplaEditable() { + let { __simplaObservers: observers } = this; + + if (observers.editable) { + observers.editable.unobserve(); + } + + observers.editable = Simpla.observeState('editable', editable => { + this.editable = editable; + }); + } + }; +} \ No newline at end of file From 4c171a2f6f2e28dfd1846ffd3af289689f3954df Mon Sep 17 00:00:00 2001 From: Bede Overend Date: Wed, 6 Dec 2017 18:51:41 +1100 Subject: [PATCH 2/3] Generalise rollup entry --- rollup.config.js | 45 ++++++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/rollup.config.js b/rollup.config.js index 9cdad7e..5f2cef6 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -6,6 +6,19 @@ import replace from 'rollup-plugin-replace'; import ownPackage from './package.json'; const debugging = process.argv.includes('--debug'); +const entries = [{ + input: 'src/index.js', + output: 'simpla.min.js', + name: 'Simpla', +}, { + input: 'src/adapters/netlify.js', + output: 'adapters/netlify.js', + name: 'SimplaNetlify' +}, { + input: 'src/mixins/element.js', + output: 'mixins/element.js', + name: 'SimplaElement' +}]; let plugins = [ buble({ @@ -21,28 +34,10 @@ if (!debugging) { plugins.push(uglify()); } -export default [{ - input: 'src/index.js', - output: { - file: 'simpla.min.js', - format: 'umd', - name: 'Simpla' - }, - plugins -}, { - input: 'src/adapters/netlify.js', - output: { - file: './adapters/netlify.js', - format: 'umd', - name: 'SimplaNetlify' - }, - plugins -}, { - input: 'src/mixins/element.js', - output: { - file: './mixins/element.js', - format: 'umd', - name: 'SimplaElement' - }, - plugins -}]; +export default entries.map(({ input, output: file, name }) => { + return { + output: { format: 'umd', file, name }, + input, + plugins + }; +}); \ No newline at end of file From 3231a5c27063bd9b0eaaee41d4a088b86ed9c2f3 Mon Sep 17 00:00:00 2001 From: Bede Overend Date: Wed, 6 Dec 2017 19:47:08 +1100 Subject: [PATCH 3/3] Allow global reference to Simpla in mixin --- src/mixins/element.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mixins/element.js b/src/mixins/element.js index d8f0296..e3a07b0 100644 --- a/src/mixins/element.js +++ b/src/mixins/element.js @@ -1,3 +1,4 @@ +/* global Simpla */ function changed(from, prop, value) { from.dispatchEvent(new CustomEvent(`${prop}-changed`, { detail: { value } })); }