summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWilson Page <wilsonpage@me.com>2015-02-13 19:49:50 +0000
committerWilson Page <wilsonpage@me.com>2016-01-04 12:21:44 +0000
commit4ade1a6b6b14fcef9686ab9eb03e6e4951b948fc (patch)
tree1cf3226ef4b51cbd069bc01ca15343f081646657
parent6c4958941d2c86cdfa6dc17a8b286399f3f71729 (diff)
downloadfastdom-4ade1a6b6b14fcef9686ab9eb03e6e4951b948fc.zip
fastdom-4ade1a6b6b14fcef9686ab9eb03e6e4951b948fc.tar.gz
fastdom-4ade1a6b6b14fcef9686ab9eb03e6e4951b948fc.tar.bz2
-rw-r--r--.gitignore2
-rw-r--r--.jshintignore2
-rw-r--r--.jshintrc45
-rw-r--r--.npmignore9
-rw-r--r--.travis.yml21
-rw-r--r--History.md46
-rw-r--r--README.md157
-rw-r--r--bower.json5
-rw-r--r--component.json15
-rw-r--r--examples/animation.html16
-rw-r--r--examples/aspect-ratio.html64
-rw-r--r--extensions/fastdom-promised.js77
-rw-r--r--extensions/fastdom-sandbox.js147
-rw-r--r--fastdom-strict.js960
-rw-r--r--fastdom.js232
-rw-r--r--fastdom.min.js1
-rw-r--r--index.js416
-rw-r--r--package.json35
-rw-r--r--src/fastdom-strict.js49
-rw-r--r--test/fastdom-promised-test.js48
-rw-r--r--test/fastdom-sandbox-test.js87
-rw-r--r--test/fastdom-strict-test.js78
-rw-r--r--test/fastdom-test.js389
-rw-r--r--test/index.html29
-rw-r--r--test/karma.conf.js56
-rw-r--r--test/setup.js18
-rw-r--r--test/test.clear.js110
-rw-r--r--test/test.defer.js176
-rw-r--r--test/test.set.js319
-rw-r--r--webpack.config.js14
30 files changed, 2388 insertions, 1235 deletions
diff --git a/.gitignore b/.gitignore
index fd4f2b0..fd3af96 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
node_modules
.DS_Store
+npm-debug.log
+test/coverage
diff --git a/.jshintignore b/.jshintignore
index 3c3629e..0412688 100644
--- a/.jshintignore
+++ b/.jshintignore
@@ -1 +1,3 @@
node_modules
+fastdom.js
+strict.js
diff --git a/.jshintrc b/.jshintrc
index ebec543..e1f2b74 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -1,11 +1,38 @@
{
- // Enforcing options
- "camelcase": true,
- "curly": false,
- "quotmark": "single",
-
- // Relaxing options
- "boss": true,
- "sub": true,
- "laxbreak": true
+ "camelcase": false,
+ "forin": false,
+ "latedef": "nofunc",
+ "newcap": false,
+ "noarg": true,
+ "node": true,
+ "nonew": true,
+ "quotmark": "single",
+ "undef": true,
+ "unused": "vars",
+ "trailing": true,
+ "maxlen": 80,
+ "laxbreak": true,
+ "sub": true,
+
+ "eqnull": true,
+ "expr": true,
+
+ "maxerr": 1000,
+ "regexdash": true,
+ "laxcomma": true,
+ "proto": true,
+ "boss": true,
+
+ "esnext": true,
+
+ "browser": true,
+ "devel": true,
+ "nonstandard": true,
+ "worker": true,
+
+ "-W078": true,
+
+ "predef": [
+ "define"
+ ]
}
diff --git a/.npmignore b/.npmignore
index cf940ae..2461b10 100644
--- a/.npmignore
+++ b/.npmignore
@@ -1,3 +1,6 @@
-/node_modules/
-/examples/
-/test/
+**/*.
+bower.json
+webpack.config.js
+node_modules
+examples
+test
diff --git a/.travis.yml b/.travis.yml
index f5eb376..8ed9373 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,5 +1,20 @@
language: node_js
+
node_js:
- - "0.10"
-script:
- - "npm test"
+ - 5.1.0
+
+addons:
+ firefox: latest
+ apt:
+ sources:
+ - google-chrome
+ packages:
+ - google-chrome-stable
+
+before_script:
+ - export CHROME_BIN=$(which google-chrome-stable)
+ - export DISPLAY=:99.0
+ - sh -e /etc/init.d/xvfb start
+
+after_script:
+ - npm run coveralls
diff --git a/History.md b/History.md
deleted file mode 100644
index c83c992..0000000
--- a/History.md
+++ /dev/null
@@ -1,46 +0,0 @@
-
-0.8.5 / 2014-12-26
-==================
-
- * fix - Invalid JSDoc
-
-0.8.4 / 2013-10-25
-==================
-
- * fix - ensure that exceptions thrown inside a read/write job don't prevent the rest of the jobs being flushed.
-
-0.8.3 / 2013-10-25
-==================
-
- * fix - ensure rAF loop can continue when error is thrown inside job.
-
-0.8.2 / 2013-10-15
-==================
-
- * fix - prevent unnecessary frame being scheduled when write requested inside read callback, inside write callback.
-
-0.8.1 / 2013-10-15
-==================
-
- * change - if `fastdom.onError` handler is registered, errors are caught and handler is called.
-
-0.8.0 / 2013-10-14
-==================
-
- * change - to a rAF loop technique of emptying frame queue to prevent frame conflicts
- * add - ability to call `FastDom#defer` with no frame argument to schedule job for next free frame
- * change - errors not caught by default
-
-0.7.1 / 2013-10-05
-==================
-
- * fix - memory leaks with undeleted refs
- * fix - context not being passed to `.defer` jobs
-
-0.7.0 / 2013-10-05
-==================
-
- * add - `FastDom#clear` clears read, write and defer jobs by id
- * remove - `FastDom#clearRead`
- * remove - `FastDom#clearWrite`
- * change - directory structure by removing `/lib`
diff --git a/README.md b/README.md
index 36a2ab2..0cb6517 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,21 @@
-# fastdom [![Build Status](https://travis-ci.org/wilsonpage/fastdom.svg?branch=master)](https://travis-ci.org/wilsonpage/fastdom)
+# fastdom [![Build Status](https://travis-ci.org/wilsonpage/fastdom.svg?branch=master)](https://travis-ci.org/wilsonpage/fastdom) [![Coverage Status](https://coveralls.io/repos/wilsonpage/fastdom/badge.svg?branch=v1-beta&service=github)](https://coveralls.io/github/wilsonpage/fastdom?branch=v1-beta)
-Eliminates layout thrashing by batching DOM read/write operations (~750 bytes gzipped).
+Eliminates layout thrashing by batching DOM read/write operations (580 bytes gzipped compressed).
```js
-fastdom.read(function() {
+fastdom.measure(function() {
console.log('read');
});
-fastdom.write(function() {
+fastdom.mutate(function() {
console.log('write');
});
-fastdom.read(function() {
+fastdom.measure(function() {
console.log('read');
});
-fastdom.write(function() {
+fastdom.mutate(function() {
console.log('write');
});
```
@@ -38,22 +38,20 @@ write
FastDom is CommonJS and AMD compatible, you can install it in one of the following ways:
-``` sh
+```sh
$ npm install fastdom
```
-``` sh
+```sh
$ bower install fastdom
```
-``` sh
-$ component install wilsonpage/fastdom
-```
-or [download](http://github.com/wilsonpage/fastdom/raw/master/index.js).
+
+or [download](http://github.com/wilsonpage/fastdom/raw/master/fastdom.js).
## How it works
-FastDom works as a regulatory layer between your app/library and the DOM. By batching DOM access we **avoid unnecessary document reflows and speed up layout perfomance dramatically**.
+FastDom works as a regulatory layer between your app/library and the DOM. By batching DOM access we **avoid unnecessary document reflows** and dramatically **speed up layout perfomance**.
-Each read/write job is added to a corresponding read/write queue. The queues are emptied (reads, then writes) at the turn of the next frame using [`window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame).
+Each measure/mutate job is added to a corresponding measure/mutate queue. The queues are emptied (reads, then writes) at the turn of the next frame using [`window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame).
FastDom aims to behave like a singleton across *all* modules in your app. When any module requires `'fastdom'` they get the same instance back, meaning FastDom can harmonize DOM access app-wide.
@@ -61,87 +59,142 @@ Potentially a third-party library could depend on FastDom, and better integrate
## API
-### FastDom#read(callback[, context])
+### FastDom#measure(callback[, context])
-Schedules a job for the 'read' queue. Returns a unique ID that can be used to clear the scheduled job.
+Schedules a job for the 'measure' queue. Returns a unique ID that can be used to clear the scheduled job.
```js
-fastdom.read(function() {
+fastdom.measure(function() {
var width = element.clientWidth;
});
```
-### FastDom#write(callback[, context])
+### FastDom#mutate(callback[, context])
-Schedules a job for the 'write' queue. Returns a unique ID that can be used to clear the scheduled job.
+Schedules a job for the 'mutate' queue. Returns a unique ID that can be used to clear the scheduled job.
```js
-fastdom.write(function() {
+fastdom.mutate(function() {
element.style.width = width + 'px';
});
```
-### FastDom#defer([frames,] callback[, context])
+### FastDom#clear(id)
-Defers a job for the number of frames specified. This is useful if you have a particualrly expensive piece of work to do, and don't want it to be done with all the other work.
+Clears **any** scheduled job.
-For example; you are using third party library that doesn't expose an API that allows you split DOM read/write work, `fastdom.defer()` will push this work futher into the future and prevent it from disrupting other carefully batched work.
+```js
+var read = fastdom.measure(function(){});
+var write = fastdom.mutate(function(){});
+
+fastdom.clear(read);
+fastdom.clear(write);
+```
+
+## Strict mode
+
+It's very important that all DOM mutations or measurements go through `fastdom` to ensure good performance; to help you with this we wrote `fastdom-strict`. When `fastdom-strict.js` is loaded, it will throw errors when sensitive DOM APIs are called at the wrong time.
+
+This is useful when working with a large team who might not all be aware of `fastdom` or its benefits. It can also prove useful for catching 'un-fastdom-ed' code when migrating an app to `fastdom`.
+
+```html
+<script src="fastdom.js"></script>
+<script src="fastdom-strict.js"></script>
+```
+
+```js
+element.clientWidth; // throws
+fastdom.mutate(function() { element.clientWidth; }); // throws
+fastdom.measure(function() { element.clientWidth; }); // does not throw
+```
```js
-fastdom.defer(3, expensiveStuff);
+"Error: Can only get .clientWidth during 'measure' phase"
```
-`FastDom#defer` can also be called without the `frames` argument to push work onto next available frame.
+> `fastdom-strict` will not throw if nodes are not attached to the document.
+
+You should use `fastdom-strict` in develelopment to catch rendering performance issues before they hit production. It is not advisable to use `fastdom-strict` in production.
+
+## Exceptions
+
+FastDom is async, this can therefore mean that when a job comes around to being executed, the node you were working with may no longer be there. These errors are usually not critical, but they can cripple your app.
+
+FastDom allows you to register an `catch` handler. If `fastdom.catch` has been registered, FastDom will catch any errors that occur in your jobs, and run the handler instead.
```js
-// Runs in frame 1
-fastdom.defer(expensiveStuff1);
+fastdom.catch = function(error) {
+ // Do something if you want
+};
+
+```
+
+## Extensions
+
+The core fastdom library is designed to be as light as possible. Additional functionality can be bolted on in the form of 'extensions'. It's worth noting that Fastdom is a 'singleton' by design, so all tasks (even those scheduled by extensions) will reach the same global task queue.
+
+**Fastdom ships with some extensions:**
-// Runs in frame 2
-fastdom.defer(expensiveStuff2);
+- [`fastdom-promised`](extensions/fastdom-promised.js) - Adds Promise based API
+- [`fastdom-sandbox`](extensions/fastdom-sandbox.js) - Adds task grouping concepts
-// Runs in frame 3
-fastdom.defer(expensiveStuff3);
+### Using an extension
+
+Use the `.extend()` method to extend the current `fastdom` to create a new object.
+
+```html
+<script src="fastdom.js"></script>
+<script src="extensions/fastdom-promised.js"></script>
```
-### FastDom#clear(id)
+```js
+var myFastdom = fastdom.extend(fastdomPromised);
-Clears **any** scheduled job.
+myFastdom.mutate(...).then(...);
+```
+
+Extensions can be chained to construct a fully customised `fastdom`.
```js
-var read = fastdom.read(function(){});
-var write = fastdom.write(function(){});
-var defer = fastdom.defer(4, function(){});
+var myFastdom = fastdom
+ .extend(fastdomPromised)
+ .extend(fastdomSandbox);
+```
-fastdom.clear(read);
-fastdom.clear(write);
-fastdom.clear(defer);
+### Writing an extension
+
+```js
+var myFastdom = fastdom.extend({
+ measure: function(fn, ctx) {
+ // do custom stuff ...
+
+ // then call the parent method
+ return this.fastdom.measure(fn, ctx);
+ },
+
+ mutate: ...
+});
```
-## Exceptions
+You'll notice `this.fastdom` references the parent `fastdom`. If you're extending a core API and aren't calling the parent method, you're doing something wrong.
-FastDom is async, this can therefore mean that when a job comes around to being executed, the node you were working with may no longer be there. These errors are usually not critical, but they can cripple your app. FastDom allows you to register an `onError` handler. If `fastdom.onError` has been registered, FastDom will catch any errors that occur in your jobs, and run the handler instead.
+When distributing an extension only export a plain object to allow users to compose their own `fastdom`.
```js
-fastdom.onError = function(error) {
- // Do something if you want
+module.exports = {
+ measure: ...,
+ mutate: ...,
+ clear: ...
};
-
```
## Tests
-#### With PhantomJS
-
-``` sh
+```sh
$ npm install
$ npm test
```
-#### Without PhantomJS
-
-Open `test/index.html` in your browser.
-
## Author
- **Wilson Page** - [@wilsonpage](http://twitter.com/wilsonpage)
@@ -149,6 +202,8 @@ Open `test/index.html` in your browser.
## Contributors
- **Wilson Page** - [@wilsonpage](http://twitter.com/wilsonpage)
+- **Paul Irish** - [@paul_irish](http://github.com/paul_irish)
+- **Kornel Lesinski** - [@pornel](http://github.com/pornel)
- **George Crawford** - [@georgecrawford](http://github.com/georgecrawford)
## License
diff --git a/bower.json b/bower.json
index 4cb247b..0ab8906 100644
--- a/bower.json
+++ b/bower.json
@@ -1,9 +1,10 @@
{
"name": "fastdom",
"description": "Eliminates layout thrashing by batching DOM read/write operations",
- "main": "index.js",
+ "version": "0.8.6",
+ "main": "fastdom.js",
"scripts": [
- "index.js"
+ "fastdom.js"
],
"ignore": [
"examples/",
diff --git a/component.json b/component.json
deleted file mode 100644
index 78877a0..0000000
--- a/component.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "name": "fastdom",
- "description": "Eliminates layout thrashing by batching DOM read/write operations",
- "version": "0.8.6",
- "main": "index.js",
- "scripts": [
- "index.js"
- ],
- "ignore": [
- "examples/",
- "test/",
- "README.md"
- ],
- "license": "MIT"
-}
diff --git a/examples/animation.html b/examples/animation.html
index b8dfeb7..2dbefd6 100644
--- a/examples/animation.html
+++ b/examples/animation.html
@@ -2,7 +2,7 @@
<html>
<head>
<title>FastDom: Animation Example</title>
- <script type="text/javascript" src="../index.js"></script>
+ <script type="text/javascript" src="../fastdom.min.js"></script>
</head>
<body>
@@ -53,21 +53,25 @@
},
async: function(m) {
- // Use fastdom to batch the reads and writes with exactly the same code as the 'sync' routine
- fastdom.read(function() {
+ // Use fastdom to batch the reads
+ // and writes with exactly the same
+ // code as the 'sync' routine
+ fastdom.measure(function() {
var top = movers[m].offsetTop;
- fastdom.write(function() {
+ fastdom.mutate(function() {
mover.setLeft(movers[m], top);
});
});
},
noread: function(m) {
- // Simply use the array index as the top value, so no DOM read is required
+ // Simply use the array index
+ // as the top value, so no DOM
+ // read is required
mover.setLeft(movers[m], m);
},
setLeft: function(mover, top) {
- mover.style.left = ((Math.sin(top + timestamp/1000) + 1) * 500) + 'px';
+ mover.style.transform = 'translateX( ' +((Math.sin(top + timestamp/1000) + 1) * 500) + 'px)';
}
};
diff --git a/examples/aspect-ratio.html b/examples/aspect-ratio.html
index 9832393..2856757 100644
--- a/examples/aspect-ratio.html
+++ b/examples/aspect-ratio.html
@@ -6,7 +6,7 @@
<style>
* {
- box-sizing: border-box;
+ box-sizing: border-box;
}
div {
@@ -25,28 +25,37 @@
<button id="resetbtn">reset</button>
<section id="perf"></section>
<section id="container"></section>
- <script type="text/javascript" src="../index.js"></script>
+ <script src="../fastdom.js"></script>
<script>
var n;
var start;
var divs;
// Setup
- function reset() {
+ function reset(done) {
n = input.value;
divs = [];
- container.innerHTML = '';
- for (var i = 0; i < n; i++) {
- var div = document.createElement('div');
- div.style.width = Math.round(Math.random() * window.innerWidth) + 'px';
- container.appendChild(div);
- divs.push(div);
- }
+ fastdom.measure(function() {
+ var winWidth = window.innerWidth;
+
+ fastdom.mutate(function() {
+ container.innerHTML = '';
+
+ for (var i = 0; i < n; i++) {
+ var div = document.createElement('div');
+ div.style.width = Math.round(Math.random() * winWidth) + 'px';
+ container.appendChild(div);
+ divs.push(div);
+ }
+
+ if (done) done();
+ });
+ });
}
function setAspect(div, i) {
- var aspect = 9/16;
+ var aspect = 9 / 16;
var isLast = i === (n - 1);
var h = div.clientWidth * aspect;
@@ -58,7 +67,7 @@
}
function setAspectRequestAnimationFrame(div, i) {
- var aspect = 9/16;
+ var aspect = 9 / 16;
var isLast = i === (n - 1);
// READ
@@ -77,15 +86,15 @@
}
function setAspectFastDom(div, i) {
- var aspect = 9/16;
+ var aspect = 9 / 16;
var isLast = i === (n - 1);
// READ
- fastdom.read(function() {
+ fastdom.measure(function() {
var h = div.clientWidth * aspect;
// WRITE
- fastdom.write(function() {
+ fastdom.mutate(function() {
div.style.height = h + 'px';
if (isLast) {
@@ -100,24 +109,29 @@
}
withoutFastDom.onclick = function() {
- reset();
- start = performance.now();
- divs.forEach(setAspect);
+ reset(function() {
+ start = performance.now();
+ divs.forEach(setAspect);
+ });
};
withFastDom.onclick = function() {
- reset();
- start = performance.now();
- divs.forEach(setAspectFastDom);
+ reset(function() {
+ start = performance.now();
+ divs.forEach(setAspectFastDom);
+ });
};
withRequestAnimationFrame.onclick = function() {
- reset();
- start = performance.now();
- divs.forEach(setAspectRequestAnimationFrame);
+ reset(function() {
+ start = performance.now();
+ divs.forEach(setAspectRequestAnimationFrame);
+ });
};
- resetbtn.onclick = reset;
+ resetbtn.onclick = function() {
+ reset();
+ };
</script>
</body>
</html>
diff --git a/extensions/fastdom-promised.js b/extensions/fastdom-promised.js
new file mode 100644
index 0000000..e002072
--- /dev/null
+++ b/extensions/fastdom-promised.js
@@ -0,0 +1,77 @@
+!(function() {
+
+/**
+ * Wraps fastdom in a Promise API
+ * for improved control-flow.
+ *
+ * @example
+ *
+ * // returning a result
+ * fastdom.measure(() => el.clientWidth)
+ * .then(result => ...);
+ *
+ * // returning promises from tasks
+ * fastdom.measure(() => {
+ * var w = el1.clientWidth;
+ * return fastdom.mutate(() => el2.style.width = w + 'px');
+ * }).then(() => console.log('all done'));
+ *
+ * // clearing pending tasks
+ * var promise = fastdom.measure(...)
+ * fastdom.clear(promise);
+ *
+ * @type {Object}
+ */
+var exports = {
+ initialize: function() {
+ this._tasks = new Map();
+ },
+
+ mutate: function(fn, ctx) {
+ return create(this, 'mutate', fn, ctx);
+ },
+
+ measure: function(fn, ctx) {
+ return create(this, 'measure', fn, ctx);
+ },
+
+ clear: function(promise) {
+ var tasks = this._tasks;
+ var task = tasks.get(promise);
+ this.fastdom.clear(task);
+ tasks.delete(task);
+ }
+};
+
+/**
+ * Create a fastdom task wrapped in
+ * a 'cancellable' Promise.
+ *
+ * @param {FastDom} fastdom
+ * @param {String} type - 'measure'|'muatate'
+ * @param {Function} fn
+ * @return {Promise}
+ */
+function create(promised, type, fn, ctx) {
+ var tasks = promised._tasks;
+ var fastdom = promised.fastdom;
+ var task;
+
+ var promise = new Promise(function(resolve, reject) {
+ task = fastdom[type](function() {
+ tasks.delete(promise);
+ try { resolve(fn()); }
+ catch (e) { reject(e); }
+ }, ctx);
+ });
+
+ tasks.set(promise, task);
+ return promise;
+}
+
+// Expose to CJS, AMD or global
+if ((typeof define)[0] == 'f') define(function() { return exports; });
+else if ((typeof module)[0] == 'o') module.exports = exports;
+else window.fastdomPromised = exports;
+
+})(); \ No newline at end of file
diff --git a/extensions/fastdom-sandbox.js b/extensions/fastdom-sandbox.js
new file mode 100644
index 0000000..39afc28
--- /dev/null
+++ b/extensions/fastdom-sandbox.js
@@ -0,0 +1,147 @@
+(function(exports) {
+
+/**
+ * Mini logger
+ *
+ * @return {Function}
+ */
+var debug = 0 ? console.log.bind(console, '[fastdom-sandbox]') : function() {};
+
+/**
+ * Exports
+ */
+
+/**
+ * Create a new `Sandbox`.
+ *
+ * Scheduling tasks via a sandbox is
+ * useful because you can clear all
+ * sandboxed tasks in one go.
+ *
+ * This is handy when working with view
+ * components. You can create one sandbox
+ * per component and call `.clear()` when
+ * tearing down.
+ *
+ * @example
+ *
+ * var sandbox = fastdom.sandbox();
+ *
+ * sandbox.measure(function() { console.log(1); });
+ * sandbox.measure(function() { console.log(2); });
+ *
+ * fastdom.measure(function() { console.log(3); });
+ * fastdom.measure(function() { console.log(4); });
+ *
+ * sandbox.clear();
+ *
+ * // => 3
+ * // => 4
+ *
+ * @return {Sandbox}
+ * @public
+ */
+exports.sandbox = function() {
+ return new Sandbox(this.fastdom);
+};
+
+/**
+ * Initialize a new `Sandbox`
+ *
+ * @param {FastDom} fastdom
+ */
+
+function Sandbox(fastdom) {
+ this.fastdom = fastdom;
+ this.tasks = [];
+ debug('initialized');
+}
+
+/**
+ * Schedule a 'measure' task.
+ *
+ * @param {Function} fn
+ * @param {Object} ctx
+ * @return {Object} can be passed to .clear()
+ */
+Sandbox.prototype.measure = function(fn, ctx) {
+ var tasks = this.tasks;
+ var task = this.fastdom.measure(function() {
+ tasks.splice(tasks.indexOf(task));
+ fn.call(ctx);
+ });
+
+ tasks.push(task);
+ return task;
+};
+
+/**
+ * Schedule a 'mutate' task.
+ *
+ * @param {Function} fn
+ * @param {Object} ctx
+ * @return {Object} can be passed to .clear()
+ */
+Sandbox.prototype.mutate = function(fn, ctx) {
+ var tasks = this.tasks;
+ var task = this.fastdom.mutate(function() {
+ tasks.splice(tasks.indexOf(task));
+ fn.call(ctx);
+ });
+
+ this.tasks.push(task);
+ return task;
+};
+
+/**
+ * Clear a single task or is no task is
+ * passsed, all tasks in the `Sandbox`.
+ *
+ * @param {Object} task (optional)
+ */
+
+Sandbox.prototype.clear = function(task) {
+ if (!arguments.length) clearAll(this.fastdom, this.tasks);
+ remove(this.tasks, task);
+ return this.fastdom.clear(task);
+};
+
+/**
+ * Clears all the given tasks from
+ * the given `FastDom`.
+ *
+ * @param {FastDom} fastdom
+ * @param {Array} tasks
+ * @private
+ */
+
+function clearAll(fastdom, tasks) {
+ debug('clear all', fastdom, tasks);
+ var i = tasks.length;
+ while (i--) {
+ fastdom.clear(tasks[i]);
+ tasks.splice(i, 1);
+ }
+}
+
+/**
+ * Remove an item from an Array.
+ *
+ * @param {Array} array
+ * @param {*} item
+ * @return {Boolean}
+ */
+function remove(array, item) {
+ var index = array.indexOf(item);
+ return !!~index && !!array.splice(index, 1);
+}
+
+/**
+ * Expose
+ */
+
+if ((typeof define)[0] == 'f') define(function() { return exports; });
+else if ((typeof module)[0] == 'o') module.exports = exports;
+else window.fastdomSandbox = exports;
+
+})({});
diff --git a/fastdom-strict.js b/fastdom-strict.js
new file mode 100644
index 0000000..007521d
--- /dev/null
+++ b/fastdom-strict.js
@@ -0,0 +1,960 @@
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory(require("fastdom"));
+ else if(typeof define === 'function' && define.amd)
+ define(["fastdom"], factory);
+ else if(typeof exports === 'object')
+ exports["fastdom"] = factory(require("fastdom"));
+ else
+ root["fastdom"] = factory(root["fastdom"]);
+})(this, function(__WEBPACK_EXTERNAL_MODULE_2__) {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ exports: {},
+/******/ id: moduleId,
+/******/ loaded: false
+/******/ };
+
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+
+
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ function(module, exports, __webpack_require__) {
+
+ 'use strict';
+
+ var strictdom = __webpack_require__(1);
+ var fastdom = __webpack_require__(2);
+
+ /**
+ * Mini logger
+ *
+ * @return {Function}
+ */
+ var debug = 0 ? console.log.bind(console, '[fastdom-strict]') : function() {};
+
+ /**
+ * Enabled state
+ *
+ * @type {Boolean}
+ */
+ var enabled = false;
+
+ window.fastdom = module.exports = fastdom.extend({
+ measure: function(task, ctx) {
+ debug('measure');
+ return this.fastdom.measure(function() {
+ if (!enabled) return task();
+ return strictdom.phase('measure', task);
+ }, ctx);
+ },
+
+ mutate: function(task, ctx) {
+ debug('mutate');
+ return this.fastdom.mutate(function() {
+ if (!enabled) return task();
+ return strictdom.phase('mutate', task);
+ }, ctx);
+ },
+
+ strict: function(value) {
+ if (value) {
+ enabled = true;
+ strictdom.enable();
+ } else {
+ enabled = false;
+ strictdom.disable();
+ }
+ }
+ });
+
+ // turn on strict-mode
+ window.fastdom.strict(true);
+
+
+/***/ },
+/* 1 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;!(function() {
+ 'use strict';
+
+ var debug = 0 ? console.log.bind(console, '[strictdom]') : function() {};
+
+ /**
+ * Crude webkit test.
+ *
+ * @type {Boolean}
+ */
+ var isWebkit = !!window.webkitURL;
+
+ /**
+ * List of properties observed.
+ *
+ * @type {Object}
+ */
+ var properties = {
+ prototype: {
+ Document: {
+ execCommand: Mutate,
+ elementFromPoint: Measure,
+ elementsFromPoint: Measure,
+ scrollingElement: Measure
+ },
+
+ Node: {
+ appendChild: {
+ type: Mutate,
+ test: function(dom, parent, args) {
+ var attached = isAttached(parent) || isAttached(args[0]);
+ if (attached && dom.not('mutate')) throw error(3, this.name);
+ }
+ },
+
+ insertBefore: {
+ type: Mutate,
+ test: function(dom, parent, args) {
+ var attached = isAttached(parent) || isAttached(args[0]);
+ if (attached && dom.not('mutate')) throw error(3, this.name);
+ }
+ },
+
+ removeChild: {
+ type: Mutate,
+ test: function(dom, parent, args) {
+ var attached = isAttached(parent) || isAttached(args[0]);
+ if (attached && dom.not('mutate')) throw error(3, this.name);
+ }
+ },
+
+ textContent: Mutate
+ },
+
+ Element: {
+ scrollIntoView: Mutate,
+ scrollBy: Mutate,
+ scrollTo: Mutate,
+ getClientRects: Measure,
+ getBoundingClientRect: Measure,
+ clientLeft: Measure,
+ clientWidth: Measure,
+ clientHeight: Measure,
+ scrollLeft: Accessor,
+ scrollTop: Accessor,
+ scrollWidth: Measure,
+ scrollHeight: Measure,
+ innerHTML: Mutate,
+ outerHTML: Mutate,
+ insertAdjacentHTML: Mutate,
+ remove: Mutate,
+ setAttribute: Mutate,
+ removeAttribute: Mutate,
+ className: Mutate,
+ classList: ClassList
+ },
+
+ HTMLElement: {
+ offsetLeft: Measure,
+ offsetTop: Measure,
+ offsetWidth: Measure,
+ offsetHeight: Measure,
+ offsetParent: Measure,
+ innerText: Accessor,
+ outerText: Accessor,
+ focus: Measure,
+ blur: Measure,
+ style: Style,
+
+ // `element.dataset` is hard to wrap.
+ // We could use `Proxy` but it's not
+ // supported in Chrome yet. Not too
+ // concerned as `data-` attributes are
+ // not often associated with render.
+ // dataset: DATASET
+ },
+
+ CharacterData: {
+ remove: Mutate,
+ data: Mutate
+ },
+
+ Range: {
+ getClientRects: Measure,
+ getBoundingClientRect: Measure
+ },
+
+ MouseEvent: {
+ layerX: Measure,
+ layerY: Measure,
+ offsetX: Measure,
+ offsetY: Measure
+ },
+
+ HTMLButtonElement: {
+ reportValidity: Measure
+ },
+
+ HTMLDialogElement: {
+ showModal: Mutate
+ },
+
+ HTMLFieldSetElement: {
+ reportValidity: Measure
+ },
+
+ HTMLImageElement: {
+ width: Accessor,
+ height: Accessor,
+ x: Measure,
+ y: Measure
+ },
+
+ HTMLInputElement: {
+ reportValidity: Measure
+ },
+
+ HTMLKeygenElement: {
+ reportValidity: Measure
+ },
+
+ SVGSVGElement: {
+ currentScale: Accessor
+ }
+ },
+
+ instance: {
+ window: {
+ getComputedStyle: {
+ type: Measure,
+
+ /**
+ * Throws when the Element is in attached
+ * and strictdom is not in the 'measure' phase.
+ *
+ * @param {StrictDom} strictdom
+ * @param {Window} win
+ * @param {Object} args
+ */
+ test: function(strictdom, win, args) {
+ if (isAttached(args[0]) && strictdom.not('measure')) {
+ throw error(2, 'getComputedStyle');
+ }
+ }
+ },
+
+ innerWidth: {
+ type: isWebkit ? Value : Measure,
+
+ /**
+ * Throws when the window is nested (in <iframe>)
+ * and StrictDom is not in the 'measure' phase.
+ *
+ * @param {StrictDom} strictdom
+ */
+ test: function(strictdom) {
+ var inIframe = window !== window.top;
+ if (inIframe && strictdom.not('measure')) {
+ throw error(2, '`.innerWidth` (in iframe)');
+ }
+ }
+ },
+
+ innerHeight: {
+ type: isWebkit ? Value : Measure,
+
+ /**
+ * Throws when the window is nested (in <iframe>)
+ * and StrictDom is not in the 'measure' phase.
+ *
+ * @param {StrictDom} strictdom
+ */
+ test: function(strictdom) {
+ var inIframe = window !== window.top;
+ if (inIframe && strictdom.not('measure')) {
+ throw error(2, '`.innerHeight` (in iframe)');
+ }
+ }
+ },
+
+ scrollX: isWebkit ? Value : Measure,
+ scrollY: isWebkit ? Value : Measure,
+ scrollBy: Mutate,
+ scrollTo: Mutate,
+ scroll: Mutate,
+ }
+ }
+ };
+
+ /**
+ * The master controller for all properties.
+ *
+ * @param {Window} win
+ */
+ function StrictDom(win) {
+ this.properties = [];
+ this._phase = null;
+ this.win = win;
+
+ this.createPrototypeProperties();
+ this.createInstanceProperties();
+ }
+
+ StrictDom.prototype = {
+
+ /**
+ * Set the current phase.
+ * @param {[type]} value [description]
+ * @return {[type]} [description]
+ */
+ phase: function(type, task) {
+ if (!arguments.length) return this._phase;
+ if (!this.knownPhase(type)) throw error(4, type);
+
+ var previous = this._phase;
+ this._phase = type;
+
+ if (typeof task != 'function') return;
+
+ var result = task();
+ this._phase = previous;
+ return result;
+ },
+
+ knownPhase: function(value) {
+ return !!~['measure', 'mutate', null].indexOf(value);
+ },
+
+ is: function(value) {
+ return this._phase === value;
+ },
+
+ not: function(value) {
+ return !this.is(value);
+ },
+
+ /**
+ * Enable strict mode.
+ *
+ * @public
+ */
+ enable: function() {
+ if (this.enabled) return;
+ debug('enable');
+ var i = this.properties.length;
+ while (i--) this.properties[i].enable();
+ this.enabled = true;
+ },
+
+ /**
+ * Disable strict mode.
+ *
+ * @public
+ */
+ disable: function() {
+ if (!this.enabled) return;
+ debug('disable');
+ var i = this.properties.length;
+ while (i--) this.properties[i].disable();
+ this.enabled = false;
+ this.phase(null);
+ },
+
+ /**
+ * Create wrappers for each of
+ * of the prototype properties.
+ *
+ * @private
+ */
+ createPrototypeProperties: function() {
+ debug('create prototype properties');
+ var props = properties.prototype;
+ for (var key in props) {
+ for (var name in props[key]) {
+ var object = this.win[key] && this.win[key].prototype;
+ if (!object || !object.hasOwnProperty(name)) continue;
+ this.properties.push(this.create(object, name, props[key][name]));
+ }
+ }
+ },
+
+ /**
+ * Create wrappers for each of
+ * of the instance properties.
+ *
+ * @private
+ */
+ createInstanceProperties: function() {
+ debug('create instance properties');
+ var props = properties.instance;
+ for (var key in props) {
+ for (var name in props[key]) {
+ var object = this.win[key];
+ if (!object || !object.hasOwnProperty(name)) continue;
+ this.properties.push(this.create(object, name, props[key][name]));
+ }
+ }
+ },
+
+ /**
+ * Create a wrapped `Property` that
+ * can be individually enabled/disabled.
+ *
+ * @param {Object} object - the parent object (eg. Node.prototype)
+ * @param {String} name - the property name (eg. 'appendChild')
+ * @param {(constructor|Object)} config - from the above property definition
+ * @return {Property}
+ */
+ create: function(object, name, config) {
+ debug('create', name);
+ var Constructor = config.type || config;
+ return new Constructor(object, name, config, this);
+ }
+ };
+
+ /**
+ * Create a new `Property`.
+ *
+ * A wrapper around a property that observes
+ * usage, throwing errors when used in the
+ * incorrect phase.
+ *
+ * @param {Object} object - the parent object (eg. Node.prototype)
+ * @param {[type]} name - the property name (eg. 'appendChild')
+ * @param {(constructor|Object)} config - from the above definition
+ * @param {StrictDom} strictdom - injected as a dependency
+ */
+ function Property(object, name, config, strictdom) {
+ debug('Property', name, config);
+
+ this.strictdom = strictdom;
+ this.object = object;
+ this.name = name;
+
+ var descriptor = this.getDescriptor();
+
+ // defaults can be overriden from config
+ if (typeof config == 'object') Object.assign(this, config);
+
+ this.descriptors = {
+ unwrapped: descriptor,
+ wrapped: this.wrap(descriptor)
+ };
+ }
+
+ Property.prototype = {
+
+ /**
+ * Get the property's descriptor.
+ *
+ * @return {Object}
+ * @private
+ */
+ getDescriptor: function() {
+ debug('get descriptor', this.name);
+ return Object.getOwnPropertyDescriptor(this.object, this.name);
+ },
+
+ /**
+ * Enable observation by replacing the
+ * current descriptor with the wrapped one.
+ *
+ * @private
+ */
+ enable: function() {
+ debug('enable', this.name);
+ Object.defineProperty(this.object, this.name, this.descriptors.wrapped);
+ },
+
+ /**
+ * Disable observation by replacing the
+ * current descriptor with the original one.
+ *
+ * @private
+ */
+ disable: function() {
+ debug('disable', this.name);
+ Object.defineProperty(this.object, this.name, this.descriptors.unwrapped);
+ },
+
+ // to be overwritten by subclass
+ wrap: function() {}
+ };
+
+ /**
+ * A wrapper for properties that measure
+ * geometry data from the DOM.
+ *
+ * Once a `Measure` property is enabled
+ * it can only be used when StrictDom
+ * is in the 'measure' phase, else it
+ * will throw.
+ *
+ * @constructor
+ * @extends Property
+ */
+ function Measure() {
+ Property.apply(this, arguments);
+ }
+
+ Measure.prototype = extend(Property, {
+
+ /**
+ * Return a wrapped descriptor.
+ *
+ * @param {Object} descriptor
+ * @return {Object}
+ */
+ wrap: function(descriptor) {
+ debug('wrap measure', this.name);
+
+ var clone = Object.assign({}, descriptor);
+ var value = descriptor.value;
+ var get = descriptor.get;
+ var self = this;
+
+ if (typeof value == 'function') {
+ clone.value = function() {
+ debug('measure', self.name);
+ self.test(self.strictdom, this, arguments);
+ return value.apply(this, arguments);
+ };
+ } else if (get) {
+ clone.get = function() {
+ debug('measure', self.name);
+ self.test(self.strictdom, this, arguments);
+ return get.apply(this, arguments);
+ };
+ }
+
+ return clone;
+ },
+
+ /**
+ * Throws an Error if the element is attached
+ * and StrictDOM is not in the 'measure' phase.
+ *
+ * If methods/properties are used without
+ * a context (eg. `getComputedStyle()` instead
+ * of `window.getComputedStyle()`) we infer
+ * a `window` context.
+ *
+ * @param {StrictDom} strictdom
+ * @param {Node} ctx
+ */
+ test: function(strictdom, ctx) {
+ if (isAttached(ctx || window) && strictdom.not('measure')) {
+ throw error(2, this.name);
+ }
+ }
+ });
+
+ /**
+ * A wrapper for properties that mutate
+ * to the DOM, triggering style/reflow
+ * operations.
+ *
+ * Once a `Mutate` property is enabled
+ * it can only be used when StrictDom
+ * is in the 'measure' phase, else it
+ * will throw.
+ *
+ * @constructor
+ * @extends Property
+ */
+ function Mutate() {
+ Property.apply(this, arguments);
+ }
+
+ Mutate.prototype = extend(Property, {
+
+ /**
+ * Return a wrapped descriptor.
+ *
+ * @param {Object} descriptor
+ * @return {Object}
+ */
+ wrap: function(descriptor) {
+ debug('wrap mutate', this.name);
+
+ var clone = Object.assign({}, descriptor);
+ var value = descriptor.value;
+ var self = this;
+
+ if (typeof value == 'function') {
+ clone.value = function() {
+ self.test(self.strictdom, this, arguments);
+ return value.apply(this, arguments);
+ };
+ } else if (descriptor.set) {
+ clone.set = function() {
+ self.test(self.strictdom, this, arguments);
+ return descriptor.set.apply(this, arguments);
+ };
+ }
+
+ return clone;
+ },
+
+ /**
+ * Throws an Error if the element is attached
+ * and StrictDOM is not in the 'mutate' phase.
+ *
+ * If methods/properties are used without
+ * a context (eg. `getComputedStyle()` instead
+ * of `window.getComputedStyle()`) we infer
+ * a `window` context.
+ *
+ * @param {StrictDom} strictdom
+ * @param {Node} ctx
+ */
+ test: function(strictdom, ctx) {
+ if (isAttached(ctx || window) && strictdom.not('mutate')) {
+ throw error(3, this.name);
+ }
+ }
+ });
+
+ /**
+ * A wrapper for 'accessor' (get/set) properties.
+ *
+ * An `Accessor` should be used to wrap
+ * properties that can both measure and mutate
+ * the DOM (eg. `element.scrollTop`).
+ *
+ * @constructor
+ * @extends Property
+ */
+ function Accessor() {
+ Property.apply(this, arguments);
+ }
+
+ Accessor.prototype = extend(Property, {
+
+ /**
+ * Return a wrapped descriptor.
+ *
+ * @param {Object} descriptor
+ * @return {Object}
+ */
+ wrap: function(descriptor) {
+ debug('wrap accessor', this.name);
+
+ var clone = Object.assign({}, descriptor);
+ var get = descriptor.get;
+ var set = descriptor.set;
+ var self = this;
+
+ if (get) {
+ clone.get = function() {
+ self.testRead(self.strictdom, this, arguments);
+ return get.apply(this, arguments);
+ };
+ }
+
+ if (descriptor.set) {
+ clone.set = function() {
+ self.testWrite(self.strictdom, this, arguments);
+ return set.apply(this, arguments);
+ };
+ }
+
+ return clone;
+ },
+
+ testRead: Measure.prototype.test,
+ testWrite: Mutate.prototype.test
+ });
+
+ /**
+ * A wrapper for 'value' properties.
+ *
+ * A `Value` should be used to wrap special
+ * values that like `window.innerWidth`, which
+ * in Chrome (not Gecko) are not normal 'getter'
+ * functions, but magical flat getters.
+ *
+ * Value wrappers are a for very special cases.
+ *
+ * @constructor
+ * @extends Property
+ */
+ function Value() {
+ Property.apply(this, arguments);
+ }
+
+ Value.prototype = extend(Property, {
+
+ /**
+ * Calling `Object.getOwnDescriptor()` can
+ * trigger a reflow as it returns the `value`
+ * of the property. So here we just
+ * return an empty object instead.
+ *
+ * @return {Object}
+ * @private
+ */
+ getDescriptor: function() {
+ return {};
+ },
+
+ /**
+ * Value wrappers are disabled by simply
+ * deleting them from the instance,
+ * revealing the original descriptor.
+ *
+ * @private
+ */
+ disable: function() {
+ delete this.object[this.name];
+ },
+
+ /**
+ * Return a wrapped descriptor.
+ *
+ * `Value` properties are actually on the
+ * instance of objects. To wrap them we need
+ * to replace them with a getter which
+ * deletes itself on access, call into the v8
+ * interceptor, and then add themselves back.
+ *
+ * This won't be fast, but these are rarely
+ * accessed so it should be fine.
+ *
+ * @param {Object} descriptor
+ * @return {Object}
+ */
+ wrap: function(descriptor) {
+ debug('wrap value');
+ var name = this.name;
+ var self = this;
+
+ descriptor.get = function() {
+ debug('get value', name);
+ self.test(self.strictdom, this, arguments);
+ self.disable();
+ var result = this[name];
+ self.enable();
+ return result;
+ };
+
+ return descriptor;
+ },
+
+ test: Measure.prototype.test
+ });
+
+ function Style() {
+ Property.apply(this, arguments);
+ }
+
+ Style.prototype = extend(Property, {
+ wrap: function(descriptor) {
+ debug('wrap style');
+ var strictdom = this.strictdom;
+ var clone = Object.assign({}, descriptor);
+ clone.get = function() { return new StrictStyle(this, strictdom); };
+ return clone;
+ }
+ });
+
+ function ClassList() {
+ Property.apply(this, arguments);
+ }
+
+ ClassList.prototype = extend(Property, {
+ wrap: function(descriptor) {
+ debug('wrap style');
+ var strictdom = this.strictdom;
+ var clone = Object.assign({}, descriptor);
+ clone.get = function() { return new StrictClassList(this, strictdom); };
+ return clone;
+ }
+ });
+
+ function StrictStyle(el, strictdom) {
+ this.strictdom = strictdom;
+ this.el = el;
+ }
+
+ StrictStyle.prototype = {
+ _getter: getDescriptor(HTMLElement.prototype, 'style').get,
+ _get: function() {
+ return this._getter.call(this.el);
+ },
+
+ setProperty: function(key, value) {
+ var illegal = isAttached(this.el) && this.strictdom.not('mutate');
+ if (illegal) throw error(1, 'style.' + key);
+ return this._get()[key] = value;
+ },
+
+ removeProperty: function(key) {
+ var illegal = isAttached(this.el) && this.strictdom.not('mutate');
+ if (illegal) throw error(1, 'style.' + key);
+ return this._get().removeProperty(key);
+ }
+ };
+
+ // dynamically construct prototype
+ // from real element.style
+ (function() {
+ var styles = document.createElement('div').style;
+ var proto = {};
+
+ for (var key in styles) {
+ if (styles[key] === '') {
+ Object.defineProperty(StrictStyle.prototype, key, {
+ get: getter(key),
+ set: setter(key)
+ });
+ }
+ }
+
+ [
+ 'item',
+ 'getPropertyValue',
+ 'getPropertyCSSValue',
+ 'getPropertyPriority'
+ ].forEach(function(method) {
+ StrictStyle.prototype[method] = caller(method);
+ });
+
+ function getter(key) {
+ return function() {
+ return this._get()[key];
+ };
+ }
+
+ function setter(key) {
+ return function(value) {
+ var illegal = isAttached(this.el) && this.strictdom.not('mutate');
+ if (illegal) throw error(1, 'style.' + key);
+ return this.setProperty(key, value);
+ };
+ }
+
+ function caller(key) {
+ return function() {
+ var style = this._get();
+ return style[key].apply(style, arguments);
+ };
+ }
+
+ return proto;
+ })();
+
+ function StrictClassList(el, strictdom) {
+ this.strictdom = strictdom;
+ this.el = el;
+ }
+
+ StrictClassList.prototype = {
+ _getter: getDescriptor(Element.prototype, 'classList').get,
+ _get: function() { return this._getter.call(this.el); },
+
+ add: function(className) {
+ var illegal = isAttached(this.el) && this.strictdom.not('mutate');
+ if (illegal) throw error(1, 'class names');
+ this._get().add(className);
+ },
+
+ contains: function(className) {
+ return this._get().contains(className);
+ },
+
+ remove: function(className) {
+ var illegal = isAttached(this.el) && this.strictdom.not('mutate');
+ if (illegal) throw error(1, 'class names');
+ this._get().remove(className);
+ },
+
+ toggle: function() {
+ var illegal = isAttached(this.el) && this.strictdom.not('mutate');
+ if (illegal) throw error(1, 'class names');
+ var classList = this._get();
+ return classList.toggle.apply(classList, arguments);
+ }
+ };
+
+ /**
+ * Utils
+ */
+
+ function error(type) {
+ return new Error({
+ 1: 'Can only set ' + arguments[1] + ' during \'mutate\' phase',
+ 2: 'Can only get ' + arguments[1] + ' during \'measure\' phase',
+ 3: 'Can only call `.' + arguments[1] + '()` during \'mutate\' phase',
+ 4: 'Invalid phase: ' + arguments[1]
+ }[type]);
+ }
+
+ function getDescriptor(object, prop) {
+ return Object.getOwnPropertyDescriptor(object, prop);
+ }
+
+ function extend(parent, props) {
+ return Object.assign(Object.create(parent.prototype), props);
+ }
+
+ function isAttached(el) {
+ return el === window || document.contains(el);
+ }
+
+ /**
+ * Exports
+ */
+
+ // Only ever allow one `StrictDom` per document
+ var exports = window['strictdom'] = (window['strictdom'] || new StrictDom(window)); // jshint ignore:line
+
+ // CJS & AMD support
+ if (("function")[0] == 'f') !(__WEBPACK_AMD_DEFINE_RESULT__ = function() { return exports; }.call(exports, __webpack_require__, exports, module), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+ else if ((typeof module)[0] == 'o') module.exports = exports;
+
+ })();
+
+
+/***/ },
+/* 2 */
+/***/ function(module, exports) {
+
+ module.exports = __WEBPACK_EXTERNAL_MODULE_2__;
+
+/***/ }
+/******/ ])
+});
+; \ No newline at end of file
diff --git a/fastdom.js b/fastdom.js
new file mode 100644
index 0000000..919ac4f
--- /dev/null
+++ b/fastdom.js
@@ -0,0 +1,232 @@
+!(function(win) {
+
+/**
+ * FastDom
+ *
+ * Eliminates layout thrashing
+ * by batching DOM read/write
+ * interactions.
+ *
+ * @author Wilson Page <wilsonpage@me.com>
+ * @author Kornel Lesinski <kornel.lesinski@ft.com>
+ */
+
+'use strict';
+
+/**
+ * Mini logger
+ *
+ * @return {Function}
+ */
+var debug = 0 ? console.log.bind(console, '[fastdom]') : function() {};
+
+/**
+ * Normalized rAF
+ *
+ * @type {Function}
+ */
+var raf = win.requestAnimationFrame
+ || win.webkitRequestAnimationFrame
+ || win.mozRequestAnimationFrame
+ || win.msRequestAnimationFrame
+ || function(cb) { return setTimeout(cb, 16); };
+
+/**
+ * Initialize a `FastDom`.
+ *
+ * @constructor
+ */
+function FastDom() {
+ var self = this;
+ self.reads = [];
+ self.writes = [];
+ self.raf = raf.bind(win); // test hook
+ debug('initialized', self);
+}
+
+FastDom.prototype = {
+ constructor: FastDom,
+
+ /**
+ * Adds a job to the read batch and
+ * schedules a new frame if need be.
+ *
+ * @param {Function} fn
+ * @public
+ */
+ measure: function(fn, ctx) {
+ debug('measure');
+ var task = { fn: fn, ctx: ctx };
+ this.reads.push(task);
+ scheduleFlush(this);
+ return task;
+ },
+
+ /**
+ * Adds a job to the
+ * write batch and schedules
+ * a new frame if need be.
+ *
+ * @param {Function} fn
+ * @public
+ */
+ mutate: function(fn, ctx) {
+ debug('mutate');
+ var task = { fn: fn, ctx: ctx };
+ this.writes.push(task);
+ scheduleFlush(this);
+ return task;
+ },
+
+ /**
+ * Clears a scheduled 'read' or 'write' task.
+ *
+ * @param {Object} task
+ * @return {Boolean} success
+ * @public
+ */
+ clear: function(task) {
+ debug('clear', task);
+ return remove(this.reads, task) || remove(this.writes, task);
+ },
+
+ /**
+ * Extend this FastDom with some
+ * custom functionality.
+ *
+ * Because fastdom must *always* be a
+ * singleton, we're actually extending
+ * the fastdom instance. This means tasks
+ * scheduled by an extension still enter
+ * fastdom's global task queue.
+ *
+ * The 'super' instance can be accessed
+ * from `this.fastdom`.
+ *
+ * @example
+ *
+ * var myFastdom = fastdom.extend({
+ * // called on creation
+ * initialize: function() {
+ *
+ * },
+ *
+ * // override a method
+ * measure: function(fn) {
+ * // do extra stuff ...
+ *
+ * // then call the original
+ * return this.fastdom.measure(fn);
+ * },
+ *
+ * ...
+ * });
+ *
+ * @param {Object} props properties to mixin
+ * @return {FastDom}
+ */
+ extend: function(props) {
+ debug('extend', props);
+ if (typeof props != 'object') throw new Error('expected object');
+
+ var child = Object.create(this);
+ Object.assign(child, props);
+ child.fastdom = this;
+
+ // run optional creation hook
+ if (child.initialize) child.initialize();
+
+ return child;
+ },
+
+ // override this with a function
+ // to prevent Errors in console
+ // when tasks throw
+ catch: null
+};
+
+/**
+ * Schedules a new read/write
+ * batch if one isn't pending.
+ *
+ * @private
+ */
+
+function scheduleFlush(fastdom) {
+ if (!fastdom.scheduled) {
+ fastdom.scheduled = true;
+ fastdom.raf(flush.bind(null, fastdom));
+ debug('flush scheduled');
+ }
+}
+
+/**
+ * Runs queued `read` and `write` tasks.
+ *
+ * Errors are caught and thrown by default.
+ * If a `.catch` function has been defined
+ * it is called instead.
+ *
+ * @private
+ */
+function flush(fastdom) {
+ debug('flush');
+
+ var writes = fastdom.writes;
+ var reads = fastdom.reads;
+ var error;
+
+ try {
+ debug('flushing reads', reads.length);
+ runTasks(reads);
+ debug('flushing writes', writes.length);
+ runTasks(writes);
+ } catch (e) { error = e; }
+
+ fastdom.scheduled = false;
+
+ // If the batch errored we may still have tasks queued
+ if (reads.length || writes.length) scheduleFlush(fastdom);
+
+ if (error) {
+ debug('task errored', error.message);
+ if (fastdom.catch) fastdom.catch(error);
+ else throw error;
+ }
+}
+
+/**
+ * We run this inside a try catch
+ * so that if any jobs error, we
+ * are able to recover and continue
+ * to flush the batch until it's empty.
+ *
+ * @private
+ */
+
+function runTasks(tasks) {
+ debug('run tasks');
+ var task; while (task = tasks.shift()) task.fn.call(task.ctx);
+}
+
+/**
+ * Remove an item from an Array.
+ *
+ * @param {Array} array
+ * @param {*} item
+ * @return {Boolean}
+ */
+function remove(array, item) {
+ var index = array.indexOf(item);
+ return !!~index && !!array.splice(index, 1);
+}
+
+// There should never be more than
+// one instance of `FastDom` in an app
+var exports = win.fastdom = (win.fastdom || new FastDom()); // jshint ignore:line
+
+// Expose to CJS & AMD
+if ((typeof define)[0] == 'f') define(function() { return exports; });
+else if ((typeof module)[0] == 'o') module.exports = exports;
+
+})(window);
diff --git a/fastdom.min.js b/fastdom.min.js
new file mode 100644
index 0000000..66bd2f1
--- /dev/null
+++ b/fastdom.min.js
@@ -0,0 +1 @@
+!function(t){"use strict";function e(){var e=this;e.reads=[],e.writes=[],e.raf=o.bind(t)}function n(t){t.scheduled||(t.scheduled=!0,t.raf(i.bind(null,t)))}function i(t){var e,i=t.writes,s=t.reads;try{r(s),r(i)}catch(o){e=o}if(t.scheduled=!1,(s.length||i.length)&&n(t),e){if(!t["catch"])throw e;t["catch"](e)}}function r(t){for(var e;e=t.shift();)e.fn.call(e.ctx)}function s(t,e){var n=t.indexOf(e);return!!~n&&!!t.splice(n,1)}var o=t.requestAnimationFrame||t.webkitRequestAnimationFrame||t.mozRequestAnimationFrame||t.msRequestAnimationFrame||function(t){return setTimeout(t,16)};e.prototype={constructor:e,measure:function(t,e){var i={fn:t,ctx:e};return this.reads.push(i),n(this),i},mutate:function(t,e){var i={fn:t,ctx:e};return this.writes.push(i),n(this),i},clear:function(t){return s(this.reads,t)||s(this.writes,t)},extend:function(t){if("object"!=typeof t)throw new Error("expected object");var e=Object.create(this);return Object.assign(e,t),e.fastdom=this,e.initialize&&e.initialize(),e},"catch":null};var exports=t.fastdom=t.fastdom||new e;"f"==(typeof define)[0]?define(function(){return exports}):"o"==(typeof module)[0]&&(module.exports=exports)}(window);
diff --git a/index.js b/index.js
deleted file mode 100644
index e91cc9c..0000000
--- a/index.js
+++ /dev/null
@@ -1,416 +0,0 @@
-/**
- * FastDom
- *
- * Eliminates layout thrashing
- * by batching DOM read/write
- * interactions.
- *
- * @author Wilson Page <wilsonpage@me.com>
- */
-
-;(function(fastdom){
-
- 'use strict';
-
- // Normalize rAF
- var raf = window.requestAnimationFrame
- || window.webkitRequestAnimationFrame
- || window.mozRequestAnimationFrame
- || window.msRequestAnimationFrame
- || function(cb) { return window.setTimeout(cb, 1000 / 60); };
-
- /**
- * Creates a fresh
- * FastDom instance.
- *
- * @constructor
- */
- function FastDom() {
- this.frames = [];
- this.lastId = 0;
-
- // Placing the rAF method
- // on the instance allows
- // us to replace it with
- // a stub for testing.
- this.raf = raf;
-
- this.batch = {
- hash: {},
- read: [],
- write: [],
- mode: null
- };
- }
-
- /**
- * Adds a job to the
- * read batch and schedules
- * a new frame if need be.
- *
- * @param {Function} fn
- * @public
- */
- FastDom.prototype.read = function(fn, ctx) {
- var job = this.add('read', fn, ctx);
- var id = job.id;
-
- // Add this job to the read queue
- this.batch.read.push(job.id);
-
- // We should *not* schedule a new frame if:
- // 1. We're 'reading'
- // 2. A frame is already scheduled
- var doesntNeedFrame = this.batch.mode === 'reading'
- || this.batch.scheduled;
-
- // If a frame isn't needed, return
- if (doesntNeedFrame) return id;
-
- // Schedule a new
- // frame, then return
- this.scheduleBatch();
- return id;
- };
-
- /**
- * Adds a job to the
- * write batch and schedules
- * a new frame if need be.
- *
- * @param {Function} fn
- * @public
- */
- FastDom.prototype.write = function(fn, ctx) {
- var job = this.add('write', fn, ctx);
- var mode = this.batch.mode;
- var id = job.id;
-
- // Push the job id into the queue
- this.batch.write.push(job.id);
-
- // We should *not* schedule a new frame if:
- // 1. We are 'writing'
- // 2. We are 'reading'
- // 3. A frame is already scheduled.
- var doesntNeedFrame = mode === 'writing'
- || mode === 'reading'
- || this.batch.scheduled;
-
- // If a frame isn't needed, return
- if (doesntNeedFrame) return id;
-
- // Schedule a new
- // frame, then return
- this.scheduleBatch();
- return id;
- };
-
- /**
- * Defers the given job
- * by the number of frames
- * specified.
- *
- * If no frames are given
- * then the job is run in
- * the next free frame.
- *
- * @param {Number} frame
- * @param {Function} fn
- * @public
- */
- FastDom.prototype.defer = function(frame, fn, ctx) {
-
- // Accepts two arguments
- if (typeof frame === 'function') {
- ctx = fn;
- fn = frame;
- frame = 1;
- }
-
- var self = this;
- var index = frame - 1;
-
- return this.schedule(index, function() {
- self.run({
- fn: fn,
- ctx: ctx
- });
- });
- };
-
- /**
- * Clears a scheduled 'read',
- * 'write' or 'defer' job.
- *
- * @param {Number|String} id
- * @public
- */
- FastDom.prototype.clear = function(id) {
-
- // Defer jobs are cleared differently
- if (typeof id === 'function') {
- return this.clearFrame(id);
- }
-
- // Allow ids to be passed as strings
- id = Number(id);
-
- var job = this.batch.hash[id];
- if (!job) return;
-
- var list = this.batch[job.type];
- var index = list.indexOf(id);
-
- // Clear references
- delete this.batch.hash[id];
- if (~index) list.splice(index, 1);
- };
-
- /**
- * Clears a scheduled frame.
- *
- * @param {Function} frame
- * @private
- */
- FastDom.prototype.clearFrame = function(frame) {
- var index = this.frames.indexOf(frame);
- if (~index) this.frames.splice(index, 1);
- };
-
- /**
- * Schedules a new read/write
- * batch if one isn't pending.
- *
- * @private
- */
- FastDom.prototype.scheduleBatch = function() {
- var self = this;
-
- // Schedule batch for next frame
- this.schedule(0, function() {
- self.batch.scheduled = false;
- self.runBatch();
- });
-
- // Set flag to indicate
- // a frame has been scheduled
- this.batch.scheduled = true;
- };
-
- /**
- * Generates a unique
- * id for a job.
- *
- * @return {Number}
- * @private
- */
- FastDom.prototype.uniqueId = function() {
- return ++this.lastId;
- };
-
- /**
- * Calls each job in
- * the list passed.
- *
- * If a context has been
- * stored on the function
- * then it is used, else the
- * current `this` is used.
- *
- * @param {Array} list
- * @private
- */
- FastDom.prototype.flush = function(list) {
- var id;
-
- while (id = list.shift()) {
- this.run(this.batch.hash[id]);
- }
- };
-
- /**
- * Runs any 'read' jobs followed
- * by any 'write' jobs.
- *
- * We run this inside a try catch
- * so that if any jobs error, we
- * are able to recover and continue
- * to flush the batch until it's empty.
- *
- * @private
- */
- FastDom.prototype.runBatch = function() {
- try {
-
- // Set the mode to 'reading',
- // then empty all read jobs
- this.batch.mode = 'reading';
- this.flush(this.batch.read);
-
- // Set the mode to 'writing'
- // then empty all write jobs
- this.batch.mode = 'writing';
- this.flush(this.batch.write);
-
- this.batch.mode = null;
-
- } catch (e) {
- this.runBatch();
- throw e;
- }
- };
-
- /**
- * Adds a new job to
- * the given batch.
- *
- * @param {Array} list
- * @param {Function} fn
- * @param {Object} ctx
- * @returns {Number} id
- * @private
- */
- FastDom.prototype.add = function(type, fn, ctx) {
- var id = this.uniqueId();
- return this.batch.hash[id] = {
- id: id,
- fn: fn,
- ctx: ctx,
- type: type
- };
- };
-
- /**
- * Runs a given job.
- *
- * Applications using FastDom
- * have the options of setting
- * `fastdom.onError`.
- *
- * This will catch any
- * errors that may throw
- * inside callbacks, which
- * is useful as often DOM
- * nodes have been removed
- * since a job was scheduled.
- *
- * Example:
- *
- * fastdom.onError = function(e) {
- * // Runs when jobs error
- * };
- *
- * @param {Object} job
- * @private
- */
- FastDom.prototype.run = function(job){
- var ctx = job.ctx || this;
- var fn = job.fn;
-
- // Clear reference to the job
- delete this.batch.hash[job.id];
-
- // If no `onError` handler
- // has been registered, just
- // run the job normally.
- if (!this.onError) {
- return fn.call(ctx);
- }
-
- // If an `onError` handler
- // has been registered, catch
- // errors that throw inside
- // callbacks, and run the
- // handler instead.
- try { fn.call(ctx); } catch (e) {
- this.onError(e);
- }
- };
-
- /**
- * Starts a rAF loop
- * to empty the frame queue.
- *
- * @private
- */
- FastDom.prototype.loop = function() {
- var self = this;
- var raf = this.raf;
-
- // Don't start more than one loop
- if (this.looping) return;
-
- raf(function frame() {
- var fn = self.frames.shift();
-
- // If no more frames,
- // stop looping
- if (!self.frames.length) {
- self.looping = false;
-
- // Otherwise, schedule the
- // next frame
- } else {
- raf(frame);
- }
-
- // Run the frame. Note that
- // this may throw an error
- // in user code, but all
- // fastdom tasks are dealt
- // with already so the code
- // will continue to iterate
- if (fn) fn();
- });
-
- this.looping = true;
- };
-
- /**
- * Adds a function to
- * a specified index
- * of the frame queue.
- *
- * @param {Number} index
- * @param {Function} fn
- * @return {Function}
- * @private
- */
- FastDom.prototype.schedule = function(index, fn) {
-
- // Make sure this slot
- // hasn't already been
- // taken. If it has, try
- // re-scheduling for the next slot
- if (this.frames[index]) {
- return this.schedule(index + 1, fn);
- }
-
- // Start the rAF
- // loop to empty
- // the frame queue
- this.loop();
-
- // Insert this function into
- // the frames queue and return
- return this.frames[index] = fn;
- };
-
- // We only ever want there to be
- // one instance of FastDom in an app
- fastdom = fastdom || new FastDom();
-
- /**
- * Expose 'fastdom'
- */
-
- if (typeof module !== 'undefined' && module.exports) {
- module.exports = fastdom;
- } else if (typeof define === 'function' && define.amd) {
- define(function(){ return fastdom; });
- } else {
- window['fastdom'] = fastdom;
- }
-
-})(window.fastdom);
diff --git a/package.json b/package.json
index cbbda26..55389b9 100644
--- a/package.json
+++ b/package.json
@@ -2,9 +2,16 @@
"name": "fastdom",
"description": "Eliminates layout thrashing by batching DOM read/write operations",
"version": "0.8.6",
- "main": "index.js",
+ "main": "fastdom.js",
"scripts": {
- "test": "jshint . && mocha-phantomjs test/index.html"
+ "lint": "jshint src/*",
+ "unit": "karma start test/karma.conf.js --single-run",
+ "test": "npm run -s unit && npm run -s lint",
+ "test-dev": "karma start test/karma.conf.js --browsers Firefox",
+ "coveralls": "cat test/coverage/lcov.info | coveralls",
+ "compress": "uglifyjs fastdom.js --compress='drop_console,sequences,dead_code,booleans,conditionals,unused,if_return,join_vars,pure_funcs=\"debug\"' --mangle --reserved='require,define,module,exports' > fastdom.min.js",
+ "build": "webpack && npm run -s compress",
+ "watch": "webpack -w"
},
"homepage": "https://github.com/wilsonpage/fastdom",
"author": {
@@ -17,10 +24,24 @@
},
"license": "MIT",
"devDependencies": {
- "mocha": "~1.12.0",
- "jshint": "~2.1.0",
- "sinon": "~1.7.3",
- "chai": "~1.7.2",
- "mocha-phantomjs": "~3.1.2"
+ "browserify": "^10.2.1",
+ "chai": "^3.4.1",
+ "coveralls": "^2.11.6",
+ "jshint": "^2.8.0",
+ "karma": "^0.13.15",
+ "karma-chai-sinon": "^0.1.5",
+ "karma-chrome-launcher": "^0.2.2",
+ "karma-coverage": "^0.5.3",
+ "karma-firefox-launcher": "^0.1.3",
+ "karma-mocha": "^0.2.1",
+ "karma-mocha-reporter": "^1.1.3",
+ "mocha": "^2.3.4",
+ "sinon": "^1.17.2",
+ "sinon-chai": "^2.8.0",
+ "uglify-js": "^2.4.23",
+ "webpack": "^1.12.9"
+ },
+ "dependencies": {
+ "strictdom": "^1.0.0"
}
}
diff --git a/src/fastdom-strict.js b/src/fastdom-strict.js
new file mode 100644
index 0000000..86d3f4e
--- /dev/null
+++ b/src/fastdom-strict.js
@@ -0,0 +1,49 @@
+'use strict';
+
+var strictdom = require('strictdom');
+var fastdom = require('../fastdom');
+
+/**
+ * Mini logger
+ *
+ * @return {Function}
+ */
+var debug = 0 ? console.log.bind(console, '[fastdom-strict]') : function() {};
+
+/**
+ * Enabled state
+ *
+ * @type {Boolean}
+ */
+var enabled = false;
+
+window.fastdom = module.exports = fastdom.extend({
+ measure: function(task, ctx) {
+ debug('measure');
+ return this.fastdom.measure(function() {
+ if (!enabled) return task();
+ return strictdom.phase('measure', task);
+ }, ctx);
+ },
+
+ mutate: function(task, ctx) {
+ debug('mutate');
+ return this.fastdom.mutate(function() {
+ if (!enabled) return task();
+ return strictdom.phase('mutate', task);
+ }, ctx);
+ },
+
+ strict: function(value) {
+ if (value) {
+ enabled = true;
+ strictdom.enable();
+ } else {
+ enabled = false;
+ strictdom.disable();
+ }
+ }
+});
+
+// turn on strict-mode
+window.fastdom.strict(true);
diff --git a/test/fastdom-promised-test.js b/test/fastdom-promised-test.js
new file mode 100644
index 0000000..959800e
--- /dev/null
+++ b/test/fastdom-promised-test.js
@@ -0,0 +1,48 @@
+/*global suite, setup, test, assert, sinon, fastdomPromised*/
+/*jshint maxlen:false*/
+
+suite('fastdom-promised', function() {
+ var fastdom;
+
+ setup(function() {
+ fastdom = window.fastdom.extend(fastdomPromised);
+ });
+
+ test('it returns a Promise that resolves after the task is run', function(done) {
+ var spy = sinon.spy();
+
+ fastdom.measure(spy)
+ .then(function() {
+ sinon.assert.calledOnce(spy);
+ done();
+ });
+ });
+
+ test('promises can be returned from tasks', function() {
+ var spy1 = sinon.spy();
+ var spy2 = sinon.spy();
+
+ return fastdom.measure(function() {
+ spy1();
+ return fastdom.mutate(spy2);
+ })
+
+ .then(function() {
+ sinon.assert.calledOnce(spy1);
+ sinon.assert.calledOnce(spy2);
+ assert.isTrue(spy1.calledBefore(spy2));
+ });
+ });
+
+ test('calling `fastdom.clear(promise)` works', function(done) {
+ var spy = sinon.spy();
+ var task = fastdom.measure(spy);
+
+ fastdom.clear(task);
+
+ requestAnimationFrame(function() {
+ sinon.assert.notCalled(spy);
+ done();
+ });
+ });
+});
diff --git a/test/fastdom-sandbox-test.js b/test/fastdom-sandbox-test.js
new file mode 100644
index 0000000..43b6626
--- /dev/null
+++ b/test/fastdom-sandbox-test.js
@@ -0,0 +1,87 @@
+/*global suite, setup, test, assert, sinon, fastdomSandbox, fastdomPromised*/
+/* jshint maxlen:false */
+
+suite('fastdom-sandbox', function() {
+ var raf = window.requestAnimationFrame;
+ var fastdom;
+
+ setup(function() {
+ fastdom = new window.fastdom.constructor();
+ fastdom = fastdom.extend(window.fastdomSandbox);
+ });
+
+ test('It works as normal', function(done) {
+ var sandbox = fastdom.sandbox();
+
+ sandbox.measure(function() {
+ sandbox.mutate(function() {
+ done();
+ });
+ });
+ });
+
+ test('Its possible to clear all sandbox jobs', function(done) {
+ var sandbox = fastdom.sandbox();
+ var spy = sinon.spy();
+
+ sandbox.measure(spy);
+ sandbox.mutate(spy);
+
+ fastdom.measure(function() {
+ fastdom.mutate(function() {
+ assert.isTrue(spy.notCalled);
+ done();
+ });
+ });
+
+ sandbox.clear();
+ });
+
+ test('It clears individual tasks', function(done) {
+ var sandbox = fastdom.sandbox();
+ var spy = sinon.spy();
+
+ var task = sandbox.measure(spy);
+ sandbox.clear(task);
+ sandbox.measure(function() {
+ assert.isTrue(spy.notCalled);
+ done();
+ });
+ });
+
+ test('it works with fastdom-promised', function(done) {
+ var myFastdom = fastdom
+ .extend(fastdomPromised)
+ .extend(fastdomSandbox);
+
+ var sandbox = myFastdom.sandbox();
+ var spy1 = sinon.spy();
+ var spy2 = sinon.spy();
+
+ sandbox.measure(spy1)
+ .then(function() {
+ return sandbox.mutate(spy2);
+ })
+
+ .then(function() {
+ sinon.assert.calledOnce(spy1);
+ sinon.assert.calledOnce(spy2);
+
+ spy1.reset();
+ spy2.reset();
+
+ sandbox.measure(spy1);
+ sandbox.measure(spy2);
+ sandbox.clear();
+
+ raf(function() {
+ console.log(3);
+ sinon.assert.notCalled(spy1);
+ sinon.assert.notCalled(spy2);
+ done();
+ });
+ })
+
+ .catch(done);
+ });
+});
diff --git a/test/fastdom-strict-test.js b/test/fastdom-strict-test.js
new file mode 100644
index 0000000..688e8b2
--- /dev/null
+++ b/test/fastdom-strict-test.js
@@ -0,0 +1,78 @@
+/*global suite, setup, suiteSetup, suiteTeardown, teardown, test, assert, fastdomPromised*/
+/*jshint maxlen:false*/
+
+suite('fastdom-strict', function() {
+ var fastdom;
+ var el;
+
+ suiteSetup(function(done) {
+ var script = document.createElement('script');
+ script.src = '/base/fastdom-strict.js';
+ document.head.appendChild(script);
+ script.onload = function() {
+ fastdom = window.fastdom.extend(fastdomPromised);
+ done();
+ };
+ });
+
+ suiteTeardown(function() {
+ fastdom.strict(false);
+ });
+
+ setup(function() {
+ return fastdom.mutate(function() {
+ el = document.createElement('div');
+ el.style.height = '100px';
+ el.style.width = '100px';
+ document.body.appendChild(el);
+ });
+ });
+
+ teardown(function() {
+ return fastdom.mutate(function() {
+ el.remove();
+ });
+ });
+
+ test('measuring throws outside of fastdom', function() {
+ assert.throws(function() {
+ return el.clientWidth;
+ });
+ });
+
+ test('measuring does not throws inside `fastdom.measure()`', function() {
+ return fastdom.measure(function() {
+ return el.clientWidth;
+ });
+ });
+
+ test('mutating throws outside of fastdom', function() {
+ assert.throws(function() {
+ el.innerHTML = 'foo';
+ });
+ });
+
+ test('mutating does not throws inside `fastdom.mutate()`', function() {
+ return fastdom.mutate(function() {
+ el.innerHTML = 'foo';
+ });
+ });
+
+ test('it can be disabled and enabled', function(done) {
+ fastdom.strict(false);
+
+ assert.doesNotThrow(function() {
+ return el.clientWidth;
+ });
+
+ fastdom.strict(true);
+
+ assert.throws(function() {
+ return el.clientWidth;
+ });
+
+ fastdom.measure(function() {
+ el.clientWidth;
+ }).then(done);
+ });
+});
diff --git a/test/fastdom-test.js b/test/fastdom-test.js
new file mode 100644
index 0000000..f28dff6
--- /dev/null
+++ b/test/fastdom-test.js
@@ -0,0 +1,389 @@
+/*jshint maxlen:false*/
+/*global suite, setup, teardown, test, assert, sinon, fastdomSandbox, fastdomPromised*/
+
+suite('fastdom', function() {
+ var raf = window.requestAnimationFrame;
+ var fastdom;
+
+ setup(function() {
+ fastdom = new window.fastdom.constructor();
+ });
+
+ test('it runs reads before writes', function(done) {
+ var read = sinon.spy(function() {
+ assert(!write.called);
+ });
+
+ var write = sinon.spy(function() {
+ assert(read.called);
+ done();
+ });
+
+ fastdom.measure(read);
+ fastdom.mutate(write);
+ });
+
+ test('it calls all reads together, followed by all writes', function(done) {
+ var read1 = sinon.spy();
+ var read2 = sinon.spy();
+ var write1 = sinon.spy();
+ var write2 = sinon.spy();
+
+ // Assign unsorted
+ fastdom.measure(read1);
+ fastdom.mutate(write1);
+ fastdom.measure(read2);
+ fastdom.mutate(write2);
+
+ // After the queue has been emptied
+ // check the callbacks were called
+ // in the correct order.
+ raf(function() {
+ assert(read1.calledBefore(read2));
+ assert(read2.calledBefore(write1));
+ assert(write1.calledBefore(write2));
+ done();
+ });
+ });
+
+ test('it calls a read in the same frame if scheduled inside a read callback', function(done) {
+ var cb = sinon.spy();
+
+ fastdom.measure(function() {
+
+ // Schedule a callback for *next* frame
+ raf(cb);
+
+ // Schedule a read callback
+ // that should be run in the
+ // current frame checking that
+ // the RAF callback has not
+ // yet been fired.
+ fastdom.measure(function() {
+ assert(!cb.called);
+ done();
+ }, this);
+ }, this);
+ });
+
+ test('it calls a write in the same frame if scheduled inside a read callback', function(done) {
+ var cb = sinon.spy();
+
+ fastdom.measure(function() {
+
+ // Schedule a callback for *next* frame
+ raf(cb);
+
+ // Schedule a read callback
+ // that should be run in the
+ // current frame checking that
+ // the RAF callback has not
+ // yet been fired.
+ fastdom.mutate(function() {
+ assert(!cb.called);
+ done();
+ }, this);
+ }, this);
+ });
+
+ test('it calls a read in the *next* frame if scheduled inside a write callback', function(done) {
+ var cb = sinon.spy();
+
+ fastdom.mutate(function() {
+
+ // Schedule a callback for *next* frame
+ raf(cb);
+
+ // Schedule a read that should be
+ // called in the next frame, meaning
+ // the test callback should have already
+ // been called.
+ fastdom.measure(function() {
+ assert(cb.called);
+ done();
+ }, this);
+ }, this);
+ });
+
+ test('it does not request a new frame when a write is requested inside a nested read', function(done) {
+ var callback = sinon.spy();
+
+ fastdom.mutate(function() {
+ fastdom.measure(function() {
+
+ // Schedule a callback for *next* frame
+ raf(callback);
+
+ // Schedule a read callback
+ // that should be run in the
+ // current frame checking that
+ // the RAF callback has not
+ // yet been fired.
+ fastdom.mutate(function() {
+ assert(!callback.called);
+ done();
+ });
+ });
+ });
+ });
+
+ test('it schedules a new frame when a read is requested in a nested write', function(done) {
+ fastdom.raf = sinon.spy(fastdom, 'raf');
+
+ fastdom.measure(function() {
+ fastdom.mutate(function() {
+ fastdom.measure(function(){
+
+ // Should have scheduled a new frame
+ assert(fastdom.raf.calledTwice);
+ done();
+ });
+ });
+ });
+ });
+
+ test('it runs nested reads in the same frame', function(done) {
+ sinon.spy(fastdom, 'raf');
+
+ fastdom.measure(function() {
+ fastdom.measure(function() {
+ fastdom.measure(function() {
+ fastdom.measure(function() {
+
+ // Should not have scheduled a new frame
+ sinon.assert.calledOnce(fastdom.raf);
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ test('it runs nested writes in the same frame', function(done) {
+ fastdom.raf = sinon.spy(fastdom, 'raf');
+
+ fastdom.mutate(function() {
+ fastdom.mutate(function() {
+ fastdom.mutate(function() {
+ fastdom.mutate(function() {
+
+ // Should not have scheduled a new frame
+ sinon.assert.calledOnce(fastdom.raf);
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ test('it calls a "read" callback with the given context', function(done) {
+ fastdom.measure(function() {
+ assert.equal(this.foo, 'bar');
+ done();
+ }, { foo: 'bar' });
+ });
+
+ test('it calls a "write" callback with the given context', function(done) {
+ fastdom.mutate(function() {
+ assert.equal(this.foo, 'bar');
+ done();
+ }, { foo: 'bar' });
+ });
+
+ test('it has an empty job hash when batch complete', function(done) {
+ var ran = 0;
+
+ fastdom.measure(function(){ ran += 1; });
+ fastdom.measure(function(){ ran += 2; });
+ fastdom.mutate(function(){ ran += 4; });
+ fastdom.mutate(function(){ ran += 8; });
+
+ // Check there are four jobs stored
+ assert.equal(ran, 0);
+
+ raf(function() {
+ assert.equal(ran, 15);
+ done();
+ });
+ });
+
+ test('it maintains correct context if single method is registered twice', function(done) {
+ var ctx1 = { foo: 'bar' };
+ var ctx2 = { bar: 'baz' };
+
+ function shared() {}
+
+ var spy1 = sinon.spy(shared);
+ var spy2 = sinon.spy(shared);
+
+ fastdom.measure(spy1, ctx1);
+ fastdom.measure(spy2, ctx2);
+
+ raf(function() {
+ assert(spy1.calledOn(ctx1));
+ assert(spy2.calledOn(ctx2));
+ done();
+ });
+ });
+
+ test('it runs .catch() handler on error if one has been registered', function(done) {
+ fastdom.catch = sinon.spy();
+
+ fastdom.measure(function() { throw 'err1'; });
+ fastdom.mutate(function() { throw 'err2'; });
+
+ raf(function() {
+ raf(function() {
+ assert(fastdom.catch.calledTwice, 'twice');
+ assert(fastdom.catch.getCall(0).calledWith('err1'), 'bla');
+ assert(fastdom.catch.getCall(1).calledWith('err2'), 'bl2');
+ done();
+ });
+ });
+ });
+
+ suite('exceptions', function() {
+
+ // temporarily disable mocha error detection
+ setup(function() {
+ this.onerror = window.onerror;
+ window.onerror = null;
+ });
+
+ // re-enable mocha error detection
+ teardown(function() {
+ window.onerror = this.onerror;
+ });
+
+ test('it flushes remaining tasks in next frame if prior task throws', function(done) {
+ var spy = sinon.spy();
+
+ fastdom.measure(function() { throw new Error('error'); });
+ fastdom.measure(spy);
+
+ raf(function() {
+ sinon.assert.notCalled(spy);
+ raf(function() {
+ sinon.assert.calledOnce(spy);
+ done();
+ });
+ });
+ });
+ });
+
+ test('it stops rAF loop once frame queue is empty', function(done) {
+ var callback = sinon.spy();
+
+ sinon.spy(fastdom, 'raf');
+ fastdom.measure(callback);
+
+ raf(function() {
+ assert(callback.called);
+ assert(fastdom.raf.calledOnce);
+ done();
+ });
+ });
+
+ suite('clear', function() {
+ test('it does not run "read" job if cleared (sync)', function(done) {
+ var read = sinon.spy();
+ var id = fastdom.measure(read);
+ fastdom.clear(id);
+
+ raf(function() {
+ raf(function() {
+ assert(!read.called);
+ done();
+ });
+ });
+ });
+
+ test('it fails silently if job not found in queue', function(done) {
+ var read = sinon.spy();
+ var read2 = sinon.spy();
+
+ var id = fastdom.measure(read);
+ fastdom.clear(id);
+
+ raf(function() {
+ assert(!read2.called);
+ done();
+ });
+ });
+
+ test('it does not run "write" job if cleared (async)', function(done) {
+ var read = sinon.spy();
+ var write = sinon.spy();
+
+ var id = fastdom.mutate(write);
+ fastdom.measure(function() {
+ fastdom.clear(id);
+
+ raf(function() {
+ assert(!read.called);
+ done();
+ });
+ });
+ });
+
+ test('it does not run "write" job if cleared', function(done) {
+ var write = sinon.spy();
+ var id = fastdom.mutate(write);
+
+ fastdom.clear(id);
+
+ raf(function() {
+ assert(!write.called);
+ done();
+ });
+ });
+
+ test('it removes reference to the job if cleared', function(done) {
+ var write = sinon.spy();
+ var id = fastdom.mutate(2, write);
+
+ fastdom.clear(id);
+
+ raf(function() {
+ raf(function() {
+ raf(function() {
+ assert(!write.called);
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ suite('FastDom#extend()', function() {
+ test('it has the properties of given object', function() {
+ var fastdom2 = fastdom.extend({ prop: 'foo' });
+ assert.equal(fastdom2.prop, 'foo');
+ });
+
+ test('it can extend an extension', function() {
+ var fastdom2 = fastdom.extend({ prop: 'foo' });
+ var fastdom3 = fastdom2.extend({ prop: 'bar' });
+
+ assert.equal(fastdom2.prop, 'foo');
+ assert.equal(fastdom3.prop, 'bar');
+
+ assert.equal(fastdom2.fastdom, fastdom);
+ assert.equal(fastdom3.fastdom, fastdom2);
+ });
+
+ test('it throws if argument is not object', function() {
+ assert.throws(function() {
+ fastdom.extend();
+ });
+
+ assert.throws(function() {
+ fastdom.extend('oopsie');
+ });
+
+ assert.throws(function() {
+ fastdom.extend(999);
+ });
+ });
+ });
+});
diff --git a/test/index.html b/test/index.html
deleted file mode 100644
index f757410..0000000
--- a/test/index.html
+++ /dev/null
@@ -1,29 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
- <meta charset="utf-8">
- <title>Mocha Tests</title>
- <link rel="stylesheet" href="../node_modules/mocha/mocha.css" />
-</head>
-<body>
- <div id="mocha"></div>
- <script src="../node_modules/mocha/mocha.js"></script>
- <script src="../node_modules/chai/chai.js"></script>
- <script src="../node_modules/sinon/lib/sinon.js"></script>
- <script src="../node_modules/sinon/lib/sinon/match.js"></script>
- <script src="../node_modules/sinon/lib/sinon/spy.js"></script>
- <script src="../node_modules/sinon/lib/sinon/stub.js"></script>
- <script>mocha.setup('tdd')</script>
- <script src="../index.js"></script>
- <script src="setup.js"></script>
- <script src="test.set.js"></script>
- <script src="test.clear.js"></script>
- <script src="test.defer.js"></script>
- <script>
- mocha.checkLeaks();
-
- if (window.mochaPhantomJS) mochaPhantomJS.run();
- else mocha.run();
- </script>
-</body>
-</html> \ No newline at end of file
diff --git a/test/karma.conf.js b/test/karma.conf.js
new file mode 100644
index 0000000..d639b36
--- /dev/null
+++ b/test/karma.conf.js
@@ -0,0 +1,56 @@
+'use strict';
+
+module.exports = function(config) {
+ config.set({
+ basePath: '..',
+
+ browsers: [
+ 'chrome',
+ 'Firefox'
+ ],
+
+ frameworks: [
+ 'mocha',
+ 'chai-sinon'
+ ],
+
+ reporters: [
+ 'mocha',
+ 'coverage'
+ ],
+
+ coverageReporter: {
+ type : 'lcov',
+ dir : 'test/',
+ subdir: 'coverage'
+ },
+
+ preprocessors: {
+ 'fastdom.js': ['coverage'],
+ 'extensions/*.js': ['coverage']
+ },
+
+ client: {
+ captureConsole: true,
+ mocha: { ui: 'tdd' }
+ },
+
+ customLaunchers: {
+ chrome: {
+ base: 'Chrome',
+ flags: ['--no-sandbox']
+ }
+ },
+
+ files: [
+ 'fastdom.js',
+ 'extensions/fastdom-promised.js',
+ 'extensions/fastdom-sandbox.js',
+ 'test/fastdom-sandbox-test.js',
+ 'test/fastdom-promised-test.js',
+ 'test/fastdom-strict-test.js',
+ 'test/fastdom-test.js',
+ { pattern: 'fastdom-strict.js', included: false }
+ ]
+ });
+};
diff --git a/test/setup.js b/test/setup.js
deleted file mode 100644
index 0844695..0000000
--- a/test/setup.js
+++ /dev/null
@@ -1,18 +0,0 @@
-
-// RequestAnimationFrame Polyfill
-var raf = window.requestAnimationFrame
- || window.webkitRequestAnimationFrame
- || window.mozRequestAnimationFrame
- || function(cb) { window.setTimeout(cb, 1000 / 60); };
-
-// Make constructor
-var FastDom = fastdom.constructor;
-
-// Alias chai.assert
-var assert = chai.assert;
-
-function objectLength(object) {
- var l = 0;
- for (var key in object) l++;
- return l;
-} \ No newline at end of file
diff --git a/test/test.clear.js b/test/test.clear.js
deleted file mode 100644
index 1230b16..0000000
--- a/test/test.clear.js
+++ /dev/null
@@ -1,110 +0,0 @@
-
-suite('clear', function(){
-
- test('Should not run "read" job if cleared (sync)', function(done) {
- var fastdom = new FastDom();
- var read = sinon.spy();
-
- var id = fastdom.read(read);
- fastdom.clear(id);
-
- raf(function() {
- assert(!read.called);
- done();
- });
- });
-
- test('Should fail silently if job not found in queue', function(done) {
- var fastdom = new FastDom();
- var read = sinon.spy();
- var read2 = sinon.spy();
-
- var id = fastdom.read(read);
- fastdom.clear(id);
-
- raf(function() {
- assert(!read2.called);
- done();
- });
- });
-
- test('Should not run "write" job if cleared (async)', function(done) {
- var fastdom = new FastDom();
- var read = sinon.spy();
- var write = sinon.spy();
-
- var id = fastdom.write(write);
- fastdom.read(function() {
- fastdom.clear(id);
-
- raf(function() {
- assert(!read.called);
- done();
- });
- });
- });
-
- test('Should not run "write" job if cleared', function(done) {
- var fastdom = new FastDom();
- var write = sinon.spy();
- var id = fastdom.write(write);
-
- fastdom.clear(id);
-
- raf(function() {
- assert(!write.called);
- done();
- });
- });
-
- test('Should not run "defer" job if cleared', function(done) {
- var fastdom = new FastDom();
- var callback = sinon.spy();
- var id = fastdom.defer(3, callback);
-
- fastdom.clear(id);
-
- raf(function() {
- raf(function() {
- raf(function() {
- raf(function() {
- assert(!callback.called);
- done();
- });
- });
- });
- });
- });
-
- test('Should remove reference to the job if cleared', function(done) {
- var fastdom = new FastDom();
- var write = sinon.spy();
- var id = fastdom.write(2, write);
-
- fastdom.clear(id);
-
- raf(function() {
- raf(function() {
- raf(function() {
- assert(!write.called);
- assert(!fastdom.batch.hash[id]);
- done();
- });
- });
- });
- });
-
- test('Should accept String ids', function(done) {
- var fastdom = new FastDom();
- var read = sinon.spy();
-
- var id = fastdom.read(read);
-
- fastdom.clear(id.toString());
-
- raf(function() {
- assert(!read.called);
- done();
- });
- });
-}); \ No newline at end of file
diff --git a/test/test.defer.js b/test/test.defer.js
deleted file mode 100644
index 89b5729..0000000
--- a/test/test.defer.js
+++ /dev/null
@@ -1,176 +0,0 @@
-
-suite('defer', function(){
-
- test('Should run the job after the specified number of frames', function(done) {
- var fastdom = new FastDom();
- var job = sinon.spy();
-
- fastdom.defer(3, job);
-
- raf(function() {
- assert(!job.called);
- raf(function() {
- assert(!job.called);
- raf(function() {
- assert(job.called);
- done();
- });
- });
- });
- });
-
- test('Should call a deferred callback with the given context', function(done) {
- var fastdom = new FastDom();
- var cb = sinon.spy();
- var ctx = { foo: 'bar' };
-
- fastdom.defer(2, function() {
- assert.equal(this.foo, 'bar');
- done();
- }, ctx);
- });
-
- test('Should run work at next frame if frames argument not supplied.', function(done) {
- var fastdom = new FastDom();
- var callback1 = sinon.spy();
- var callback2 = sinon.spy();
-
- fastdom.defer(callback1);
-
- raf(function() {
- assert(callback1.called);
- done();
- });
- });
-
- test('Should run each job on a different frame.', function(done) {
- var fastdom = new FastDom();
- var callback1 = sinon.spy();
- var callback2 = sinon.spy();
- var callback3 = sinon.spy();
-
- fastdom.defer(callback1);
- fastdom.defer(callback2);
- fastdom.defer(callback3);
-
- raf(function() {
- assert(callback1.called);
- assert(!callback2.called);
- assert(!callback3.called);
- raf(function() {
- assert(callback2.called);
- assert(!callback3.called);
- raf(function() {
- assert(callback3.called);
- done();
- });
- });
- });
- });
-
- test('Should run fill empty frames before later work is run.', function(done) {
- var fastdom = new FastDom();
- var callback1 = sinon.spy();
- var callback2 = sinon.spy();
- var callback3 = sinon.spy();
- var callback4 = sinon.spy();
-
- // Frame 3
- fastdom.defer(3, callback3);
-
- // Frame 1
- fastdom.defer(callback1);
-
- // Frame 2
- fastdom.defer(callback2);
-
- // Frame 4
- fastdom.defer(callback4);
-
- raf(function() {
- assert(callback1.called);
- assert(!callback2.called);
- assert(!callback3.called);
- assert(!callback4.called);
- raf(function() {
- assert(callback2.called);
- assert(!callback3.called);
- assert(!callback4.called);
- raf(function() {
- assert(callback3.called);
- assert(!callback4.called);
- raf(function() {
- assert(callback4.called);
- done();
- });
- });
- });
- });
- });
-
- test('Should run the next frame even if frame before it errors', function(done) {
- var fastdom = new FastDom();
- var rafOld = fastdom.raf;
- var error = sinon.stub().throws();
- var callback = sinon.spy();
-
- // Wrap requestAnimationFrame method
- // so that we can catch any errors
- // that may be thrown in the callback
- sinon.stub(fastdom, 'raf', function(fn) {
- var wrapped = function() {
- try { fn(); } catch (e) {}
- };
-
- rafOld(wrapped);
- });
-
- fastdom.defer(error);
- fastdom.defer(callback);
-
- raf(function() {
- raf(function() {
- assert(callback.called, 'The second job was run');
- done();
- });
- });
- });
-
- test('Should continue to run future jobs when the last frame errors', function(done) {
- var fastdom = new FastDom();
- var rafOld = fastdom.raf;
- var error = sinon.stub().throws();
- var callback1 = sinon.spy();
- var callback2 = sinon.spy();
-
- // Wrap requestAnimationFrame method
- // so that we can catch any errors
- // that may be thrown in the callback
- sinon.stub(fastdom, 'raf', function(fn) {
- var wrapped = function() {
- try { fn(); } catch (e) {}
- };
-
- rafOld(wrapped);
- });
-
- fastdom.defer(callback1);
- fastdom.defer(error);
-
- setTimeout(function() {
- fastdom.defer(callback2);
- }, 40);
-
- raf(function() {
- assert(callback1.called, 'the first job was run');
- raf(function() {
- setTimeout(function(){
- raf(function() {
- assert(callback2.called, 'the third job was run');
- done();
- });
- }, 40);
- });
- });
- });
-});
diff --git a/test/test.set.js b/test/test.set.js
deleted file mode 100644
index 811c489..0000000
--- a/test/test.set.js
+++ /dev/null
@@ -1,319 +0,0 @@
-
-suite('set', function() {
-
- test('Should run reads before writes', function(done) {
- var fastdom = new FastDom();
-
- var read = sinon.spy(function() {
- assert(!write.called);
- });
-
- var write = sinon.spy(function() {
- assert(read.called);
- done();
- });
-
- fastdom.read(read);
- fastdom.write(write);
- });
-
- test('Should call all reads together, followed by all writes', function(done) {
- var fastdom = new FastDom();
- var read1 = sinon.spy();
- var read2 = sinon.spy();
- var write1 = sinon.spy();
- var write2 = sinon.spy();
-
- // Assign unsorted
- fastdom.read(read1);
- fastdom.write(write1);
- fastdom.read(read2);
- fastdom.write(write2);
-
- // After the queue has been emptied
- // check the callbacks were called
- // in the correct order.
- raf(function() {
- assert(read1.calledBefore(read2));
- assert(read2.calledBefore(write1));
- assert(write1.calledBefore(write2));
- done();
- });
- });
-
- test('Should call a read in the same frame if scheduled inside a read callback', function(done) {
- var fastdom = new FastDom();
- var cb = sinon.spy();
-
- fastdom.read(function() {
-
- // Schedule a callback for *next* frame
- raf(cb);
-
- // Schedule a read callback
- // that should be run in the
- // current frame checking that
- // the RAF callback has not
- // yet been fired.
- fastdom.read(function() {
- assert(!cb.called);
- done();
- });
-
- // Should not have scheduled a new frame
- assert(fastdom.frames.length === 0);
- });
- });
-
- test('Should call a write in the same frame if scheduled inside a read callback', function(done) {
- var fastdom = new FastDom();
- var cb = sinon.spy();
-
- fastdom.read(function() {
-
- // Schedule a callback for *next* frame
- raf(cb);
-
- // Schedule a read callback
- // that should be run in the
- // current frame checking that
- // the RAF callback has not
- // yet been fired.
- fastdom.write(function() {
- assert(!cb.called);
- done();
- });
-
- // Should not have scheduled a new frame
- assert(fastdom.frames.length === 0);
- });
- });
-
- test('Should call a read in the *next* frame if scheduled inside a write callback', function(done) {
- var fastdom = new FastDom();
- var cb = sinon.spy();
-
- fastdom.write(function() {
-
- // Schedule a callback for *next* frame
- raf(cb);
-
- // Schedule a read that should be
- // called in the next frame, meaning
- // the test callback should have already
- // been called.
- fastdom.read(function() {
- assert(cb.called);
- done();
- });
-
- // Should have scheduled a new frame
- assert(fastdom.frames.length === 1, 'the is one pending frame');
- });
- });
-
- test('Should not request a new frame when a write is requested inside a nested read', function(done) {
- var fastdom = new FastDom();
- var callback = sinon.spy();
-
- fastdom.write(function() {
- fastdom.read(function() {
-
- // Schedule a callback for *next* frame
- raf(callback);
-
- // Schedule a read callback
- // that should be run in the
- // current frame checking that
- // the RAF callback has not
- // yet been fired.
- fastdom.write(function() {
- assert(!callback.called);
- done();
- });
-
- // Should not have scheduled a new frame
- assert(fastdom.frames.length === 0);
- });
- });
- });
-
- test('Should schedule a new frame when a read is requested in a nested write', function(done) {
- var fastdom = new FastDom();
-
- fastdom.read(function() {
- fastdom.write(function() {
- fastdom.read(function(){});
-
- // Should have scheduled a new frame
- assert(fastdom.frames.length === 1);
- done();
- });
- });
- });
-
- test('Should run nested reads in the same frame', function(done) {
- var fastdom = new FastDom();
- var callback = sinon.spy();
-
- fastdom.read(function() {
- fastdom.read(function() {
- fastdom.read(function() {
- fastdom.read(function() {
-
- // Should not have scheduled a new frame
- assert(fastdom.frames.length === 0);
- done();
- });
- });
- });
- });
- });
-
- test('Should run nested writes in the same frame', function(done) {
- var fastdom = new FastDom();
- var callback = sinon.spy();
-
- fastdom.write(function() {
- fastdom.write(function() {
- fastdom.write(function() {
- fastdom.write(function() {
-
- // Should not have scheduled a new frame
- assert(fastdom.frames.length === 0);
- done();
- });
- });
- });
- });
- });
-
- test('Should call a "read" callback with the given context', function(done) {
- var fastdom = new FastDom();
- var cb = sinon.spy();
- var ctx = { foo: 'bar' };
-
- fastdom.read(function() {
- assert.equal(this.foo, 'bar');
- done();
- }, ctx);
- });
-
- test('Should call a "write" callback with the given context', function(done) {
- var fastdom = new FastDom();
- var cb = sinon.spy();
- var ctx = { foo: 'bar' };
-
- fastdom.write(function() {
- assert.equal(this.foo, 'bar');
- done();
- }, ctx);
- });
-
- test('Should have empty job hash when batch complete', function(done) {
- var fastdom = new FastDom();
-
- fastdom.read(function(){});
- fastdom.read(function(){});
- fastdom.write(function(){});
- fastdom.write(function(){});
-
- // Check there are four jobs stored
- assert.equal(objectLength(fastdom.batch.hash), 4);
-
- raf(function() {
- assert.equal(objectLength(fastdom.batch.hash), 0);
- done();
- });
- });
-
- test('Should maintain correct context if single method is registered twice', function(done) {
- var fastdom = new FastDom();
- var ctx1 = { foo: 'bar' };
- var ctx2 = { bar: 'baz' };
-
- function shared(){}
-
- var spy1 = sinon.spy(shared);
- var spy2 = sinon.spy(shared);
-
- fastdom.read(spy1, ctx1);
- fastdom.read(spy2, ctx2);
-
- raf(function() {
- assert(spy1.calledOn(ctx1));
- assert(spy2.calledOn(ctx2));
- done();
- });
- });
-
- test('Should run onError handler if one has been registered', function(done) {
- var fastdom = new FastDom();
- var err1 = { some: 'error1' };
- var err2 = { some: 'error2' };
-
- fastdom.onError = sinon.spy();
-
- fastdom.read(function() {
- throw err1;
- });
-
- fastdom.write(function() {
- throw err2;
- });
-
- raf(function() {
- assert(fastdom.onError.calledTwice);
- assert(fastdom.onError.getCall(0).calledWith(err1));
- assert(fastdom.onError.getCall(1).calledWith(err2));
- done();
- });
- });
-
- test('Should stop rAF loop once frame queue is empty', function(done) {
- var fastdom = new FastDom();
- var callback = sinon.spy();
-
- fastdom.read(callback);
-
- raf(function() {
- assert(callback.called);
- assert(fastdom.looping === false);
- done();
- });
- });
-
-
- test('Should continue to flush the queue until empty even if a job errors', function(done) {
- var fastdom = new FastDom();
- var read = sinon.spy();
- var write = sinon.spy();
- var flush = fastdom.runBatch;
- var error = sinon.stub().throws();
- var errorsThrown = false;
-
- sinon.stub(fastdom, 'runBatch', function() {
- try {
- flush.apply(fastdom, arguments);
- } catch (e) {
- errorsThrown = true;
- }
- });
-
- fastdom.read(read);
- fastdom.write(write);
- fastdom.read(error);
- fastdom.read(read);
- fastdom.write(error);
- fastdom.write(write);
-
- raf(function() {
- assert(read.calledTwice, 'the callback was called both times');
- assert(write.calledTwice, 'the callback was called both times');
- assert(fastdom.batch.read.length === 0, 'the queue is empty');
- assert(fastdom.batch.write.length === 0, 'the queue is empty');
- assert(errorsThrown, 'real errors were thrown');
- done();
- });
- });
-});
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000..c115fd9
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,14 @@
+module.exports = [
+ {
+ entry: './src/fastdom-strict.js',
+ output: {
+ filename: 'fastdom-strict.js',
+ library: 'fastdom',
+ libraryTarget: 'umd'
+ },
+
+ externals: {
+ '../fastdom': 'fastdom'
+ }
+ }
+];