From 5e8d38042c37f8b45f5e5be35ee6f6ea8bc6b11c Mon Sep 17 00:00:00 2001 From: Adrian Ottiker Date: Tue, 30 May 2017 15:16:13 +0200 Subject: [PATCH 1/5] combine element with arguments selector --- lib/state.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/state.js b/lib/state.js index ae187b3..65b5e28 100644 --- a/lib/state.js +++ b/lib/state.js @@ -39,8 +39,8 @@ module.exports = function activateState (_options, callback) { let selector = config.sel || options.selector; // handle element config let element = config.element || options.element; - if (element && !selector) { - selector = "[data-element='" + element + "']"; + if (element) { + selector = "[data-element='" + element + "']" + " " + (selector || ""); } // return with error if state has no selector From cec70a333dabda5c6c5ccfadb56b746de5c93384 Mon Sep 17 00:00:00 2001 From: Adrian Ottiker Date: Tue, 30 May 2017 21:05:32 +0200 Subject: [PATCH 2/5] make rendering async with promises --- lib/render.js | 148 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 89 insertions(+), 59 deletions(-) diff --git a/lib/render.js b/lib/render.js index 317f36a..1cf7c6b 100644 --- a/lib/render.js +++ b/lib/render.js @@ -1,4 +1,4 @@ -'use strict'; +"use strict" // Dependencies const DOMPurify = require('dompurify'); @@ -19,10 +19,10 @@ const RENDER_ESCAPE = {'&': '&', '"': '"', '<': '<', '>': '>'}; /* * Render templates - * - * */ -module.exports = function (_options, data, callback) { +module.exports = render; + +function render (template_name, _options, data) { let self = this; // define options @@ -32,74 +32,90 @@ module.exports = function (_options, data, callback) { }); // check if config object for template exists - if (!self.config.templates[options.tmpl]) { - return callback(new Error('Template config "' + options.tmpl + '" not found.')) + if (!self.config.templates[template_name]) { + return Promise.reject(new Error('Template config "' + template_name + '" not found.')); } - // get template - template.call(self, options.tmpl, self.config.templates[options.tmpl], (err, tmpl) => { + return template(self, template_name, self.config.templates[template_name]) + .then(doRender(self, options, data)); +} + +function doRender (self, options, data) { + return (tmpl) => { - if (err) { - return callback(err); + // set default render options + if (tmpl.options) { + for (let prop in tmpl.options) { + options[prop] = tmpl.options[prop]; + } } - doRender.call(self, tmpl, options, data); - callback(null); - }); -}; + // set document title + if (tmpl.title) { + document.title = tmpl.title; + } -function doRender (tmpl, options, data) { - let self = this; + if (tmpl.render) { + return mergeData(tmpl, options, data).then((html) => { - // set document title - if (tmpl.title) { - document.title = tmpl.title; - } + if (!html) { - let html; + // attch events to document + if (tmpl.events) { + events.call(self, tmpl, options, document, data); + } - // create html - // TODO cache rendered html if data is the same? - if (tmpl.render) { - if (data && data instanceof Array) { - html = ''; - data.forEach(function (item) { - html += DOMPurify.sanitize(tmpl.render(item, options, escFn)); - }); - } else { - html = DOMPurify.sanitize(tmpl.render(data, options, escFn)); - } - } + // render html + } else if (typeof tmpl.to === 'object') { - if (!html) { - if (tmpl.events) { - events.call(self, tmpl, options, document, data); - } - return; - } + // clear html before appending + if (options.clearList) { + tmpl.to.innerHTML = ''; + } - // render html - if (typeof tmpl.to === 'object') { + html = DOMPurify.sanitize(html); - // clear html before appending - if (options.clearList) { - tmpl.to.innerHTML = ''; - } + // append dom events + if (!tmpl.events) { + tmpl.to.insertAdjacentHTML(options.position, html); + } else { + let tmpElm = document.createElement(tmpl.to.tagName); + tmpElm.innerHTML = html; - // append dom events - if (!tmpl.events) { - tmpl.to.insertAdjacentHTML(options.position, html); - } else { - let tmpElm = document.createElement(tmpl.to.tagName); - tmpElm.innerHTML = html; + // setup flow event streams + events.call(self, tmpl, options, tmpElm, data); - // setup flow event streams - events.call(self, tmpl, options, tmpElm, data); + Array.from(tmpElm.children).forEach(function (elm) { + tmpl.to.appendChild(elm); + }); + } + } - Array.from(tmpElm.children).forEach(function (elm) { - tmpl.to.appendChild(elm); + return html || ""; }); } + + return Promise.resolve(""); + }; +} + +function mergeData (tmpl, options, data) { + if (tmpl.render) { + if (data && data instanceof Array) { + let jobs = []; + data.forEach(function (item) { + jobs.push(tmpl.render(item, options, escFn)); + }); + return Promise.all(jobs).then((values) => { + let html = ""; + values.forEach((snippet) => { + html += snippet; + }); + return html; + }); + } else { + return tmpl.render(data, options, escFn); + } } } @@ -111,13 +127,27 @@ function doRender (tmpl, options, data) { * @param {object} The data object. * @param {string} The data key. */ -function escFn (data, key, options) { +function escFn (self, data, key, options) { + + let template = false; + + // check if it's a template name + if (key.charAt(0) === "[") { + //console.log(options) + template = true; + key = key.slice(1, -1); + } + // get the string value - var str = key.indexOf('.') > 0 ? getPathValue(key, data) : (data[key] || null); + let str = key.indexOf('.') > 0 ? getPathValue(key, data) : (data[key] || null); // if str is null or undefined str = str === null ? (options.leaveKeys ? '{' + key + '}' : '') : str; + if (template) { + return render.call(self, str, options, data); + } + if (typeof str === 'object') { str = JSON.stringify(str, null, '\t'); } else { @@ -126,12 +156,12 @@ function escFn (data, key, options) { // escape html chars if (!options.dontEscape) { - return str.replace(/[&\"<>]/g, (_char) => { + str = str.replace(/[&\"<>]/g, (_char) => { return RENDER_ESCAPE[_char]; }); } - return str; + return Promise.resolve(str); } /** From ea08ba8f71fd1497ecffdd0b10e37d188d7f5d4d Mon Sep 17 00:00:00 2001 From: Adrian Ottiker Date: Tue, 30 May 2017 21:05:55 +0200 Subject: [PATCH 3/5] use promises instead of callbacks --- lib/template.js | 80 ++++++++++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/lib/template.js b/lib/template.js index c991c85..63d55ca 100644 --- a/lib/template.js +++ b/lib/template.js @@ -3,20 +3,17 @@ const TEMPLATE_ESCAPE = {"\\": "\\\\", "\n": "\\n", "\r": "\\r", "'": "\\'"}; // template factory -module.exports = function (name, config, callback) { - let self = this; +module.exports = function (self, name, config) { // check template cache let tmplCacheKey = self._name + name; if (self.templates[tmplCacheKey]) { // refresh dom target, in case the target is removed from DOM - let err = ensureDomTarget(config.to, self.templates[tmplCacheKey]); - if (err instanceof Error) { - return callback(err); - } - - return callback(null, self.templates[tmplCacheKey]); + return ensureDomTarget(config.to, self.templates[tmplCacheKey]) + .then(() => { + return self.templates[tmplCacheKey]; + }); } // load css @@ -34,12 +31,13 @@ module.exports = function (name, config, callback) { // check if html snippet is already loaded if (!config.html || self.htmls[config.html]) { - createTemplate.call(self, config, callback); + return createTemplate(self, config); + // load html snippet } else { let error; - fetch(config.html, { + return fetch(config.html, { method: 'get', credentials: 'same-origin', mode: 'cors' @@ -51,13 +49,12 @@ module.exports = function (name, config, callback) { }).then(function (snippet) { if (error) { - callback(new Error(snippet)); - } else { - - self.htmls[config.html] = snippet; - createTemplate.call(self, config, callback); + return Promise.reject(new Error(snippet)); } - }).catch(callback); + + self.htmls[config.html] = snippet; + return createTemplate(self, config); + }); } }; @@ -68,21 +65,38 @@ module.exports = function (name, config, callback) { * @private * @param {string} The HTML string. */ -function create (tmpl) { - return new Function("_", "o", "e", "return '" + - (tmpl || '').replace(/[\\\n\r']/g, function(_char) { - return TEMPLATE_ESCAPE[_char]; - }).replace(/{\s*([\w\.]+)\s*}/g, "' + e(_,'$1',o) + '") + "'" - ); +function create (self, tmpl) { + return (data, options, escape) => { + + let res = tmpl.match(/{\s*([\[\]\w\.]+)\s*}/g); + if (res === null) { + return Promise.resolve(tmpl); + } + + let jobs = []; + let html = tmpl; + if (res instanceof Array) { + res.forEach((field) => { + jobs.push( + escape(self, data, field.slice(1, -1), options) + .then((value) => { + html = html.replace(field, value); + }) + ); + }) + } + return Promise.all(jobs).then(() => { + return html; + }); + }; } -function createTemplate (config, callback) { - let self = this; +function createTemplate (self, config) { let template = { events: config.events, streams: {}, - + options: config.options, // reset target with selector if target is removed from the document obs: new MutationObserver(function (event) { @@ -101,20 +115,16 @@ function createTemplate (config, callback) { // create render function if (config.html) { - template.render = create(self.htmls[config.html]); + template.render = create(self, self.htmls[config.html]); } // cache template self.templates[config.name] = template; // get dom target - let err = ensureDomTarget(config.to, template); - if (err instanceof Error) { - return callback(err); - } - - // cache and return - callback(null, template); + return ensureDomTarget(config.to, template).then(() => { + return template; + }); } function ensureDomTarget (selector, template) { @@ -122,10 +132,12 @@ function ensureDomTarget (selector, template) { if (selector) { template.to = document.querySelector(selector); if (!template.to) { - return new Error('View.template: Target "' + selector + '" not found.'); + return Promise.reject(new Error('View.template: Target "' + selector + '" not found.')); } // observe target parent, to know when target is removed from DOM template.obs.observe(template.to.parentNode, {childList: true}); } + + return Promise.resolve(); } From 9ef693c0b29cb69efbb137e6d7f0d2b93327ec5f Mon Sep 17 00:00:00 2001 From: Adrian Ottiker Date: Fri, 2 Jun 2017 19:42:47 +0200 Subject: [PATCH 4/5] testing out ideas for a new type of renderer --- index.js | 74 ++++++++++++++++++++++--------------------------- lib/render.js | 36 ++++++++++++------------ lib/template.js | 4 +-- 3 files changed, 52 insertions(+), 62 deletions(-) diff --git a/index.js b/index.js index 7bdd56b..ce0b2ef 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,4 @@ -'use strict'; - -// Dependencies -const Events = require('events'); -const render = require('./lib/render'); -const state = require('./lib/state'); +"use strict"; /* * Returns a flow-view instance @@ -47,43 +42,40 @@ const state = require('./lib/state'); * } * } */ -module.exports = config => { - let self = new Events(); - - // create the config object - config = config || {}; - self.config = {}; - self.config.states = config.states || {}; - self.config.templates = config.templates || {}; - // caches - self.templates = {}; - self.htmls = {}; +// Dependencies +const Events = require("events"); +const render = require("./lib/render"); +const state = require("./lib/state"); - // flow-view methods - self.state = state; - self.render = render; - self.addStates = states => { - Object.keys(states).forEach(stateName => { - self.config.states[stateName] = states[stateName]; - }); - }; - self.addTemplates = templates => { - Object.keys(templates).forEach(templateName => { - self.config.templates[templateName] = templates[templateName] - }); - }; - self.close = () => { - window.close(); - }; - self.dom = key => { - let path = key.split(':'); +// TODO use a service worker +const View = { + templates: {}, + snippets: {}, + listeners: [], + close: window.close, + dom: (key) => { + key = key.split(":"); + key[0] = document.querySelector(key[0]); + return key[0][key[1]] !== undefined ? key[0][key[1]] : key[0]; + } +}; - // get find an element in the document - path[0] = document.querySelector(path[0]); +// onevent will be called on dom events - return path[0][path[1]] !== undefined ? path[0][path[1]] : path[0]; - } +View.render = render(View); +View.state = state(View); +module.exports = Object.freeze(View); - return self; -}; + /* + self.addStates = states => { + Object.keys(states).forEach(stateName => { + self.config.states[stateName] = states[stateName]; + }); + }; + self.addTemplates = templates => { + Object.keys(templates).forEach(templateName => { + self.config.templates[templateName] = templates[templateName] + }); + }; + */ diff --git a/lib/render.js b/lib/render.js index 1cf7c6b..1a732a1 100644 --- a/lib/render.js +++ b/lib/render.js @@ -20,25 +20,25 @@ const RENDER_ESCAPE = {'&': '&', '"': '"', '<': '<', '>': '>'}; /* * Render templates */ -module.exports = render; - -function render (template_name, _options, data) { - let self = this; - - // define options - let options = {}; - Object.keys(DEFAULT_OPTIONS).forEach(optionName => { - options[optionName] = _options[optionName] || DEFAULT_OPTIONS[optionName]; - }); +module.exports = (context) => { + return (template_name, _options, data) => { + let self = this; + + // define options + let options = {}; + Object.keys(DEFAULT_OPTIONS).forEach(optionName => { + options[optionName] = _options[optionName] || DEFAULT_OPTIONS[optionName]; + }); - // check if config object for template exists - if (!self.config.templates[template_name]) { - return Promise.reject(new Error('Template config "' + template_name + '" not found.')); - } + // check if config object for template exists + if (!context.config.templates[template_name]) { + return Promise.reject(new Error('Template config "' + template_name + '" not found.')); + } - return template(self, template_name, self.config.templates[template_name]) - .then(doRender(self, options, data)); -} + return template(context, template_name, context.config.templates[template_name]) + .then(doRender(context, options, data)); + }; +}; function doRender (self, options, data) { return (tmpl) => { @@ -142,7 +142,7 @@ function escFn (self, data, key, options) { let str = key.indexOf('.') > 0 ? getPathValue(key, data) : (data[key] || null); // if str is null or undefined - str = str === null ? (options.leaveKeys ? '{' + key + '}' : '') : str; + str = str === undefined ? (options.leaveKeys ? '{' + key + '}' : '') : str; if (template) { return render.call(self, str, options, data); diff --git a/lib/template.js b/lib/template.js index 63d55ca..c316d0d 100644 --- a/lib/template.js +++ b/lib/template.js @@ -1,6 +1,4 @@ -'use strict'; - -const TEMPLATE_ESCAPE = {"\\": "\\\\", "\n": "\\n", "\r": "\\r", "'": "\\'"}; +"use strict"; // template factory module.exports = function (self, name, config) { From e797c0e657189cc00c26685283f895e7b5953316 Mon Sep 17 00:00:00 2001 From: Adrian Ottiker Date: Thu, 22 Jun 2017 11:57:09 +0200 Subject: [PATCH 5/5] backup commit WIP --- index.js | 7 +--- lib/render.js | 7 ++-- lib/state.js | 93 ++++++++++++++++++++++++----------------------- lib/template.js | 1 + package-lock.json | 12 ++++++ 5 files changed, 65 insertions(+), 55 deletions(-) create mode 100644 package-lock.json diff --git a/index.js b/index.js index ce0b2ef..fd925d0 100644 --- a/index.js +++ b/index.js @@ -44,7 +44,6 @@ */ // Dependencies -const Events = require("events"); const render = require("./lib/render"); const state = require("./lib/state"); @@ -60,12 +59,10 @@ const View = { return key[0][key[1]] !== undefined ? key[0][key[1]] : key[0]; } }; - -// onevent will be called on dom events - View.render = render(View); View.state = state(View); -module.exports = Object.freeze(View); + +module.exports = View; /* self.addStates = states => { diff --git a/lib/render.js b/lib/render.js index 1a732a1..42f6501 100644 --- a/lib/render.js +++ b/lib/render.js @@ -18,11 +18,10 @@ const DEFAULT_ELEMENT_NAME = 'element'; const RENDER_ESCAPE = {'&': '&', '"': '"', '<': '<', '>': '>'}; /* - * Render templates + * Render templates */ module.exports = (context) => { return (template_name, _options, data) => { - let self = this; // define options let options = {}; @@ -31,11 +30,11 @@ module.exports = (context) => { }); // check if config object for template exists - if (!context.config.templates[template_name]) { + if (!context.templates[template_name]) { return Promise.reject(new Error('Template config "' + template_name + '" not found.')); } - return template(context, template_name, context.config.templates[template_name]) + return template(context, template_name, context.templates[template_name]) .then(doRender(context, options, data)); }; }; diff --git a/lib/state.js b/lib/state.js index 65b5e28..2c91d5b 100644 --- a/lib/state.js +++ b/lib/state.js @@ -10,61 +10,62 @@ const DEFAULT_OPTIONS = { * * @public */ -module.exports = function activateState (_options, callback) { - let self = this; - - // define options - let options = {}; - Object.keys(DEFAULT_OPTIONS).forEach(optionName => { - options[optionName] = _options[optionName] || DEFAULT_OPTIONS[optionName]; - }); - - // state must exist - let state = self.config.states[options.name]; - if (!state) { - return callback(new Error('State "' + options.name + '" not found.')); - } - - // activate state elements - state.forEach(function (config) { - - // call other states - if (config.states) { - let nextFn = function () {}; - config.states.forEach(function (stateName) { - activateState.call(self, { name: stateName }, nextFn); - }); +module.exports = function activateState (self) { + return (_options, callback) => { + + // define options + let options = {}; + Object.keys(DEFAULT_OPTIONS).forEach(optionName => { + options[optionName] = _options[optionName] || DEFAULT_OPTIONS[optionName]; + }); + + // state must exist + let state = self.config.states[options.name]; + if (!state) { + return callback(new Error('State "' + options.name + '" not found.')); } - let selector = config.sel || options.selector; - // handle element config - let element = config.element || options.element; - if (element) { - selector = "[data-element='" + element + "']" + " " + (selector || ""); - } - - // return with error if state has no selector - if (selector) { - selector = typeof selector !== 'string' ? selector : (document).querySelectorAll(selector); + // activate state elements + state.forEach(function (config) { - // manipulate classes - if (config.add || config.toggle || config.rm) { - manipulateClasses(selector, config); + // call other states + if (config.states) { + let nextFn = function () {}; + config.states.forEach(function (stateName) { + activateState.call(self, { name: stateName }, nextFn); + }); } - // manipulate attributes - if (config.attr || config.rmAttr) { - manipulateAttributes(selector, config); + let selector = config.sel || options.selector; + // handle element config + let element = config.element || options.element; + if (element) { + selector = "[data-element='" + element + "']" + " " + (selector || ""); } - // manipulate properties - if (config.prop) { - manipulateProperties(selector, config); + // return with error if state has no selector + if (selector) { + selector = typeof selector !== 'string' ? selector : (document).querySelectorAll(selector); + + // manipulate classes + if (config.add || config.toggle || config.rm) { + manipulateClasses(selector, config); + } + + // manipulate attributes + if (config.attr || config.rmAttr) { + manipulateAttributes(selector, config); + } + + // manipulate properties + if (config.prop) { + manipulateProperties(selector, config); + } } - } - }); + }); - callback(null); + callback(null); + }; }; /** diff --git a/lib/template.js b/lib/template.js index c316d0d..aff7d83 100644 --- a/lib/template.js +++ b/lib/template.js @@ -15,6 +15,7 @@ module.exports = function (self, name, config) { } // load css + // TODO ignore css's already in header if (config.css) { config.css.forEach(function (url) { let link = document.createElement('link'); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..93e5558 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,12 @@ +{ + "name": "flow-view", + "version": "0.1.0", + "lockfileVersion": 1, + "dependencies": { + "dompurify": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-0.7.4.tgz", + "integrity": "sha1-einyl+oBmCgXQ0h7h8qW3zIkjPU=" + } + } +}