From 41fce74e55cddf87875d4308b046845bd4f5e96b Mon Sep 17 00:00:00 2001 From: gliechtenstein Date: Sat, 8 Jul 2017 20:54:07 -0400 Subject: [PATCH 1/7] HTMLElement.prototype.$snapshot() --- .eslintrc.json | 1 - cell.js | 15 ++++++++++++++- test/integration.js | 24 ++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 9e4028e3..cbf21fdc 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -87,7 +87,6 @@ "computed-property-spacing": "error", "eol-last": "error", "func-name-matching": "error", - "func-style": ["error", "declaration", { "allowArrowFunctions": true }], "indent": ["error", 2, { "SwitchCase": 1 }], "key-spacing": "error", "keyword-spacing": ["error"], diff --git a/cell.js b/cell.js index 3d49e0be..97d5ebe6 100644 --- a/cell.js +++ b/cell.js @@ -414,7 +414,7 @@ // 1. No difference if the attribute is just a regular variable // 2. If the attribute is a function, we create a wrapper function that first executes the original function, and then triggers a phenotype update depending on the queue condition if (typeof v === 'function') { - return function() { + var fun = function() { // In the following code, everything inside Nucleus.tick.call is executed AFTER the last line--v.apply($node, arguments)--because it gets added to the event loop and waits until the next render cycle. // 1. Schedule phenotype update by wrapping them in a single tick (requestAnimationFrame) @@ -452,6 +452,8 @@ // 2. Run the actual function, which will modify the queue return v.apply($node, arguments); }; + fun.snapshot = v; + return fun; } else { return v; } @@ -522,6 +524,17 @@ $context.DocumentFragment.prototype.$cell = $context.Element.prototype.$cell = function(gene, options) { return this.$build(gene, [], null, (options && options.namespace) || null, true); }; + $context.DocumentFragment.prototype.$snapshot = $context.Element.prototype.$snapshot = function() { + var snapshot = {}; + for (var key in this.Genotype) { + if (this.Genotype[key].snapshot) { + snapshot[key] = this.Genotype[key].snapshot; + } else { + snapshot[key] = this.Genotype[key]; + } + } + return snapshot; + }; if ($root.NodeList && !$root.NodeList.prototype.forEach) $root.NodeList.prototype.forEach = Array.prototype.forEach; // NodeList.forEach override polyfill }, create: function($context) { diff --git a/test/integration.js b/test/integration.js index c8891c72..585e537a 100644 --- a/test/integration.js +++ b/test/integration.js @@ -6,6 +6,30 @@ const spy = require("./spy.js") const compare = function(actual, expected) { assert.equal(stringify(actual), stringify(expected)); } +describe("DOM prototype overrides", function() { + var cleanup = require('jsdom-global')() + God.plan(window); + it("$snapshot", function() { + cleanup(); + cleanup = require('jsdom-global')() + + document.body.innerHTML = ""; + window.c = { + $cell: true, + _model: [], + id: "el", + _fun: function(message) { return "Fun " + message; } + } + compare(document.body.outerHTML, ""); + God.create(window); + var fun = document.body.querySelector("#el")._fun; + compare(fun.snapshot.toString(), "function (message) { return \"Fun \" + message; }"); + + var snapshot = document.body.querySelector("#el").$snapshot(); + compare(snapshot._fun.toString(), "function (message) { return \"Fun \" + message; }"); + }) + +}); describe("Nucleus", function() { var cleanup = require('jsdom-global')() God.plan(window); From 3831a6e485841aea0cfc31cd1868211fbbf498fa Mon Sep 17 00:00:00 2001 From: gliechtenstein Date: Sat, 8 Jul 2017 22:13:56 -0400 Subject: [PATCH 2/7] functions should not be tied to phenotype. Nucleus handles them --- cell.js | 3 +-- test/Phenotype.js | 19 ------------------- test/integration.js | 5 +++++ 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/cell.js b/cell.js index 97d5ebe6..fd961371 100644 --- a/cell.js +++ b/cell.js @@ -226,9 +226,8 @@ var CSSStyleDeclaration = Object.getOwnPropertyDescriptor($root.HTMLElement.prototype, key).get.call($node); for (var attr in val) { CSSStyleDeclaration[attr] = val[attr]; } } else if (typeof val === 'number' || typeof val === 'string' || typeof val === 'boolean') { + // only set non-function attributes. Functions are taken care of on a nucleus level if ($node.setAttribute) $node.setAttribute(key, val); - } else if (typeof val === 'function') { - $node[key] = val; } }, $type: function(model, namespace) { diff --git a/test/Phenotype.js b/test/Phenotype.js index 11779e03..8158bfb6 100644 --- a/test/Phenotype.js +++ b/test/Phenotype.js @@ -533,25 +533,6 @@ describe("Phenotype", function() { compare($node.getAttribute("data-done"), "true") // only set to the DOM attribute (as string) compare($node["data-done"], undefined) // the property should be undefined }) - it("function", function() { - const $parent = document.createElement("div"); - const $node = document.createElement("div") - $node.Genotype = {} - $node.Meta = {} - $parent.appendChild($node) - - // Before - compare($node.getAttribute("fun"), null) - compare($node.fun, undefined) - - Phenotype.set($node, "fun", function(arg) { - return "fun " + arg; - }) - - // After - compare($node.getAttribute("fun"), null) // Doesn't exist as a DOM attribute - compare($node.fun("sad"), "fun sad") // Attached as a variable - }) }) }) }) diff --git a/test/integration.js b/test/integration.js index 585e537a..3805f98a 100644 --- a/test/integration.js +++ b/test/integration.js @@ -18,6 +18,7 @@ describe("DOM prototype overrides", function() { $cell: true, _model: [], id: "el", + onclick: function(e) { console.log("clicked"); }, _fun: function(message) { return "Fun " + message; } } compare(document.body.outerHTML, ""); @@ -25,8 +26,12 @@ describe("DOM prototype overrides", function() { var fun = document.body.querySelector("#el")._fun; compare(fun.snapshot.toString(), "function (message) { return \"Fun \" + message; }"); + var onclick = document.body.querySelector("#el").Genotype.onclick; + compare(onclick.snapshot.toString(), "function (e) { console.log(\"clicked\"); }"); + var snapshot = document.body.querySelector("#el").$snapshot(); compare(snapshot._fun.toString(), "function (message) { return \"Fun \" + message; }"); + compare(snapshot.onclick.toString(), "function (e) { console.log(\"clicked\"); }"); }) }); From 73bd406b66178bf4190bcf849487dd70cef613e8 Mon Sep 17 00:00:00 2001 From: gliechtenstein Date: Sat, 8 Jul 2017 22:35:24 -0400 Subject: [PATCH 3/7] Proper HTMLElement.prototype event handler handling --- cell.js | 5 ++++- test/Phenotype.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/cell.js b/cell.js index fd961371..95e9dc9d 100644 --- a/cell.js +++ b/cell.js @@ -226,8 +226,11 @@ var CSSStyleDeclaration = Object.getOwnPropertyDescriptor($root.HTMLElement.prototype, key).get.call($node); for (var attr in val) { CSSStyleDeclaration[attr] = val[attr]; } } else if (typeof val === 'number' || typeof val === 'string' || typeof val === 'boolean') { - // only set non-function attributes. Functions are taken care of on a nucleus level if ($node.setAttribute) $node.setAttribute(key, val); + } else if (typeof val === 'function') { + // For natively supported HTMLElement.prototype methods such as onclick() + var prop = Object.getOwnPropertyDescriptor($root.HTMLElement.prototype, key); + if(prop) prop.set.call($node, val); } }, $type: function(model, namespace) { diff --git a/test/Phenotype.js b/test/Phenotype.js index 8158bfb6..47bb1d79 100644 --- a/test/Phenotype.js +++ b/test/Phenotype.js @@ -533,6 +533,36 @@ describe("Phenotype", function() { compare($node.getAttribute("data-done"), "true") // only set to the DOM attribute (as string) compare($node["data-done"], undefined) // the property should be undefined }) + it("function (only native HTMLElement methods are supported)", function() { + const $parent = document.createElement("div"); + const $node = document.createElement("div") + $node.Genotype = {} + $node.Meta = {} + $parent.appendChild($node) + + // Before + compare($node.getAttribute("onclick"), null) + + spy.O.getOwnPropertyDescriptor.reset(); + + Phenotype.set($node, "onclick", function(arg) { + return "fun " + arg; + }) + + // After + compare($node.getAttribute("onclick"), null) // Doesn't exist as a DOM attribute + compare(spy.O.getOwnPropertyDescriptor.callCount, 1); + + + // NON HTMLElement method set + spy.O.getOwnPropertyDescriptor.reset(); + Phenotype.set($node, "fun", function(arg) { + return "fun " + arg; + }) + compare($node.getAttribute("fun"), null) // Doesn't exist as a DOM attribute + compare(spy.O.getOwnPropertyDescriptor.callCount, 1); + + }) }) }) }) From 44c987cd67f198772b4939d444e7043387b7b0ea Mon Sep 17 00:00:00 2001 From: gliechtenstein Date: Sat, 8 Jul 2017 22:54:31 -0400 Subject: [PATCH 4/7] Lint fix --- cell.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cell.js b/cell.js index 95e9dc9d..3d28c302 100644 --- a/cell.js +++ b/cell.js @@ -230,7 +230,7 @@ } else if (typeof val === 'function') { // For natively supported HTMLElement.prototype methods such as onclick() var prop = Object.getOwnPropertyDescriptor($root.HTMLElement.prototype, key); - if(prop) prop.set.call($node, val); + if (prop) prop.set.call($node, val); } }, $type: function(model, namespace) { From 657a75403d961d8c1e671f13641fa54113be54ac Mon Sep 17 00:00:00 2001 From: gliechtenstein Date: Sun, 9 Jul 2017 10:24:11 -0400 Subject: [PATCH 5/7] handle snapshot for $init --- cell.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cell.js b/cell.js index 3d28c302..29345420 100644 --- a/cell.js +++ b/cell.js @@ -101,6 +101,7 @@ if (['$init'].indexOf(key) === -1) { $node.Genotype[key] = Nucleus.bind($node, val); } else { + val.snapshot = val; // snapshot of $init $node.Genotype[key] = val; } }, @@ -527,15 +528,14 @@ return this.$build(gene, [], null, (options && options.namespace) || null, true); }; $context.DocumentFragment.prototype.$snapshot = $context.Element.prototype.$snapshot = function() { - var snapshot = {}; - for (var key in this.Genotype) { - if (this.Genotype[key].snapshot) { - snapshot[key] = this.Genotype[key].snapshot; - } else { - snapshot[key] = this.Genotype[key]; - } - } - return snapshot; + var json = JSON.stringify(this.Genotype, function(k,v) { + if (typeof v === 'function' && v.snapshot) { return "(" + v.snapshot.toString() + ")"; } + return v; + }); + return JSON.parse(json, function(k, v) { + if (typeof v === 'string' && v.indexOf('function') >= 0) { return eval(v); } + return v; + }) }; if ($root.NodeList && !$root.NodeList.prototype.forEach) $root.NodeList.prototype.forEach = Array.prototype.forEach; // NodeList.forEach override polyfill }, From 746e39d2f5a7fd33e1a6d7628a901058fbc0269c Mon Sep 17 00:00:00 2001 From: gliechtenstein Date: Sun, 9 Jul 2017 10:39:07 -0400 Subject: [PATCH 6/7] Lint fix --- cell.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cell.js b/cell.js index 29345420..13b41ca4 100644 --- a/cell.js +++ b/cell.js @@ -528,14 +528,14 @@ return this.$build(gene, [], null, (options && options.namespace) || null, true); }; $context.DocumentFragment.prototype.$snapshot = $context.Element.prototype.$snapshot = function() { - var json = JSON.stringify(this.Genotype, function(k,v) { - if (typeof v === 'function' && v.snapshot) { return "(" + v.snapshot.toString() + ")"; } + var json = JSON.stringify(this.Genotype, function(k, v) { + if (typeof v === 'function' && v.snapshot) { return '(' + v.snapshot.toString() + ')'; } return v; }); return JSON.parse(json, function(k, v) { if (typeof v === 'string' && v.indexOf('function') >= 0) { return eval(v); } return v; - }) + }); }; if ($root.NodeList && !$root.NodeList.prototype.forEach) $root.NodeList.prototype.forEach = Array.prototype.forEach; // NodeList.forEach override polyfill }, From dcbc5abd2bb563233ec3171735b8b7a3766c9788 Mon Sep 17 00:00:00 2001 From: gliechtenstein Date: Sun, 9 Jul 2017 12:11:33 -0400 Subject: [PATCH 7/7] =?UTF-8?q?Safari=20bug=20-=20Object.hasOwnProperty(HT?= =?UTF-8?q?MLElement.prototype,=20=E2=80=9Cstyle=E2=80=9D)=20is=20false?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cell.js | 15 +++++++++------ test/Phenotype.js | 4 ++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/cell.js b/cell.js index 13b41ca4..0907acaa 100644 --- a/cell.js +++ b/cell.js @@ -199,6 +199,9 @@ Phenotype.$init($node); }, multiline: function(fn) { return /\/\*!?(?:@preserve)?[ \t]*(?:\r\n|\n)([\s\S]*?)(?:\r\n|\n)[ \t]*\*\//.exec(fn.toString())[1]; }, + get: function(key) { + return Object.getOwnPropertyDescriptor($root.HTMLElement.prototype, key) || Object.getOwnPropertyDescriptor($root.Element.prototype, key); + }, set: function($node, key, val) { if (key[0] === '$') { if (key === '$type') { @@ -224,13 +227,13 @@ } else if (key === 'value') { $node[key] = val; } else if (key === 'style' && typeof val === 'object') { - var CSSStyleDeclaration = Object.getOwnPropertyDescriptor($root.HTMLElement.prototype, key).get.call($node); + var CSSStyleDeclaration = Phenotype.get(key).get.call($node); for (var attr in val) { CSSStyleDeclaration[attr] = val[attr]; } } else if (typeof val === 'number' || typeof val === 'string' || typeof val === 'boolean') { if ($node.setAttribute) $node.setAttribute(key, val); } else if (typeof val === 'function') { // For natively supported HTMLElement.prototype methods such as onclick() - var prop = Object.getOwnPropertyDescriptor($root.HTMLElement.prototype, key); + var prop = Phenotype.get(key); if (prop) prop.set.call($node, val); } }, @@ -352,7 +355,7 @@ // The "value" attribute needs a special treatment. return Object.getOwnPropertyDescriptor(Object.getPrototypeOf($node), key).get.call($node); } else if (key === 'style') { - return Object.getOwnPropertyDescriptor($root.HTMLElement.prototype, key).get.call($node); + return Phenotype.get(key).get.call($node); } else if (key in $node.Genotype) { // Otherwise utilize Genotype return $node.Genotype[key]; @@ -361,7 +364,7 @@ // For example, there are many DOM attributes such as "tagName" that come with the node by default. // These are not something we directly define on a gene object, but we still need to be able to access them.. // In this case we just use the native HTMLElement.prototype accessor - return Object.getOwnPropertyDescriptor($root.HTMLElement.prototype, key).get.call($node); + return Phenotype.get(key).get.call($node); } } }, @@ -385,11 +388,11 @@ if (key === 'value') { return Object.getOwnPropertyDescriptor(Object.getPrototypeOf($node), key).set.call($node, val); } else if (key === 'style' && typeof val === 'object') { - Object.getOwnPropertyDescriptor($root.HTMLElement.prototype, key).set.call($node, val); + Phenotype.get(key).set.call($node, val); } else if (typeof val === 'number' || typeof val === 'string' || typeof val === 'boolean') { $node.setAttribute(key, val); } else if (typeof val === 'function') { - Object.getOwnPropertyDescriptor($root.HTMLElement.prototype, key).set.call($node, val); + Phenotype.get(key).set.call($node, val); } } }, diff --git a/test/Phenotype.js b/test/Phenotype.js index 47bb1d79..244e7b47 100644 --- a/test/Phenotype.js +++ b/test/Phenotype.js @@ -551,7 +551,7 @@ describe("Phenotype", function() { // After compare($node.getAttribute("onclick"), null) // Doesn't exist as a DOM attribute - compare(spy.O.getOwnPropertyDescriptor.callCount, 1); + compare(spy.O.getOwnPropertyDescriptor.callCount, 1); // tries once for HTMLElement and finds onclick so only one time trial. // NON HTMLElement method set @@ -560,7 +560,7 @@ describe("Phenotype", function() { return "fun " + arg; }) compare($node.getAttribute("fun"), null) // Doesn't exist as a DOM attribute - compare(spy.O.getOwnPropertyDescriptor.callCount, 1); + compare(spy.O.getOwnPropertyDescriptor.callCount, 2); // tries both for HTMLElement and Element, because `fun` doesn't exist. }) })