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..0907acaa 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; } }, @@ -198,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') { @@ -223,12 +227,14 @@ } 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') { - $node[key] = val; + // For natively supported HTMLElement.prototype methods such as onclick() + var prop = Phenotype.get(key); + if (prop) prop.set.call($node, val); } }, $type: function(model, namespace) { @@ -349,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]; @@ -358,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); } } }, @@ -382,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); } } }, @@ -414,7 +420,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 +458,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 +530,16 @@ $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 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 }, create: function($context) { diff --git a/test/Phenotype.js b/test/Phenotype.js index 11779e03..244e7b47 100644 --- a/test/Phenotype.js +++ b/test/Phenotype.js @@ -533,7 +533,7 @@ 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() { + it("function (only native HTMLElement methods are supported)", function() { const $parent = document.createElement("div"); const $node = document.createElement("div") $node.Genotype = {} @@ -541,16 +541,27 @@ describe("Phenotype", function() { $parent.appendChild($node) // Before - compare($node.getAttribute("fun"), null) - compare($node.fun, undefined) + compare($node.getAttribute("onclick"), null) - Phenotype.set($node, "fun", function(arg) { + 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); // tries once for HTMLElement and finds onclick so only one time trial. + + + // 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($node.fun("sad"), "fun sad") // Attached as a variable + compare(spy.O.getOwnPropertyDescriptor.callCount, 2); // tries both for HTMLElement and Element, because `fun` doesn't exist. + }) }) }) diff --git a/test/integration.js b/test/integration.js index c8891c72..3805f98a 100644 --- a/test/integration.js +++ b/test/integration.js @@ -6,6 +6,35 @@ 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", + onclick: function(e) { console.log("clicked"); }, + _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 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\"); }"); + }) + +}); describe("Nucleus", function() { var cleanup = require('jsdom-global')() God.plan(window);