Skip to content

Commit

Permalink
Add debounce (formerly known as job), tests, docs.
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinpschaaf committed Jan 20, 2015
1 parent 1bc61f3 commit 5188704
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 14 deletions.
109 changes: 100 additions & 9 deletions PRIMER.md
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,7 @@ Example:
<a name="key-listeners"></a>
## Key listener setup

Polymer will automatically listen for `keypress` events and call handlers specified in the `keyPresses` object, which maps key codes to handler functions. The key may either be specified as a keyboard code or one of several convenience strings supported:
Polymer will automatically listen for `keydown` events and call handlers specified in the `keyPresses` object, which maps key codes to handler functions. The key may either be specified as a keyboard code or one of several convenience strings supported:

* ESC_KEY
* ENTER_KEY
Expand Down Expand Up @@ -1171,24 +1171,115 @@ Flexbox children:
# Migration Notes
### Styling
This section covers how to deal with yet-unimplemented and/or de-scoped features in Polymer 0.8 as compared to 0.5. Many of these are simply un-implemented; that is, we will likely have a final "solution" that addresses the need, we just haven't tackled that feature yet as we address items in priority order. Other solutions in 0.8 may be lower-level as compared to 0.5, and will be explained here.
### Self / Child Configuration
As the final 0.8 API solidifies, this section will be updated accordingly. As such, this section should be considered answers "how do I solve problem xyz <em>TODAY</em>", rather than a representation of the final Polymer 0.8 API.

### Binding limitations
## Styling

TODO: explain shadow/shady DOM styling considerations.

## Self / Child Configuration

Lifecycle callback timing and best practices are in high flux at the moment.

Currently, at `created` time, children are not stamped yet. As such, configuring any properties that may have side-effects involving children will error. As such, it is not reccomended to use the `created` callback for self-configuration.

There is a work-in-progress `configure` callback that is called top-down after children have been stamped & `created` , but is not ready for use yet.


## Binding limitations

Current limitations that are on the backlog for evaluation/improvement:

* no sub-textContent binding
* Use `<span>`'s to break up textContent into discrete elements
* no attribute binding
* no good class/style
* Call `this.toggleAttribute` / `this.attributeFollows` from change handlers
* no good class/style binding support
* Call `this.toggleClass`, `this.style.prop = ...` from change handlers

## Compound property effects

Polymer 0.8 currently has no built-in support for compound observation or compound expressions. This problem space is on the backlog to be tackled in the near future. This section will discuss lower-level tools that are available in 0.8 that can be used instead.

Assume an element has a boolean property that should be set when either of two conditions are true: e.g. when `<my-parent>.isManager == true` OR `<my-parent>.mode == 2`, you want to set `<my-child>.disabled = true`.

The most naive way to achieve this in 0.8 is with separate change handlers for the dependent properties that set a `shouldDisable` property bound to the `my-child`.

Example:

```html
<template>
<my-child disabled="{{shouldDisable}}"></my-child>
</template>
<script>
Polymer({
is: 'my-parent',
bind: {
isManager: 'computeShouldDisable',
mode: 'computeShouldDisable',
},
// Warning: Called once for every change to dependent properties!
computeShouldDisable: function() {
this.shouldDisable = this.isManager || (this.mode == 2);
}
});
</script>
```

Due to the synchronous nature of bindings in 0.8, code such as the following will result in `<my-child>.disabled` being set twice (and any side-effects of that property changing to potentially occur twice:

```js
myParent.isManager = false;
myParent.mode = 5;
```

Thus, in order to ensure side effects for any dependent properties only occur once for any number of changes to them during a turn, manually introducing asynchronicity is required. The `debounce` API on the Polymer Base prototype can be used to achieve this. The `debounce` API takes a signal name (String), callback, and optional wait time, and only calls the callback once for any number `debounce` calls with the same `signalName` started within the wait period.

Example:

```html
<template>
<my-child disabled="{{shouldDisable}}"></my-child>
</template>
<script>
Polymer({
is: 'my-parent',
bind: {
isManager: 'computeShouldDisableDebounced',
mode: 'computeShouldDisableDebounced',
},
computeShouldDisableDebounced: function() {
this.debounce('computeShouldDisable', this.computeShouldDisable);
},
// Better: called once for multiple changes
computeShouldDisable: function() {
this.shouldDisable = this.isManager || (this.mode == 2);
}
});
</script>
```
Thus, for the short term we expect users will need to consider compound effects and apply use of the `job` function to ensure efficient

## Structured data and path notification

### Compound property effects
TODO

### Structured data and path notification
## Array notification

### Array notification
TODO

<a name="todo-inheritance"></a>
### Mixins / Inheritance
## Mixins / Inheritance

TODO
73 changes: 73 additions & 0 deletions src/features/more/debounce.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<!--
@license
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<script>

modulate('Debounce', function() {

// usage

// invoke cb.call(this) in 100ms, unless the job is re-registered,
// which resets the timer
//
// this.myJob = this.job(this.myJob, cb, 100)
//
// returns a job handle which can be used to re-register a job

var Debouncer = function(inContext) {
this.context = inContext;
this.boundComplete = this.complete.bind(this)
};
Debouncer.prototype = {
go: function(callback, wait) {
this.callback = callback;
var h;
if (!wait) {
h = requestAnimationFrame(this.boundComplete);
this.handle = function() {
cancelAnimationFrame(h);
};
} else {
h = setTimeout(this.boundComplete, wait);
this.handle = function() {
clearTimeout(h);
};
}
},
stop: function() {
if (this.handle) {
this.handle();
this.handle = null;
}
},
complete: function() {
if (this.handle) {
this.stop();
this.callback.call(this.context);
}
}
};

function debounce(debouncer, callback, wait) {
if (debouncer) {
debouncer.stop();
} else {
debouncer = new Debouncer(this);
}
debouncer.go(callback, wait);
return debouncer;
}

// exports

return debounce;

});

</script>
29 changes: 28 additions & 1 deletion src/features/standard/utils.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->

<link rel="import" href="../more/debounce.html">

<script>

using('Base', function(Base) {
using(['Base', 'Debounce'], function(Base, Debounce) {

Base.addFeature({

Expand Down Expand Up @@ -98,6 +101,30 @@
}
document.head.appendChild(l);
return l;
},

/**
* Debounce signals.
*
* Call `debounce` to defer a named signal, and all subsequent matching signals,
* until a wait time has elapsed with no new signal.
*
* debouncedClickAction: function(e) {
* // processClick only when it's been 100ms since the last click
* this.debounce('click', function() {
* this.processClick;
* }, 100);
* }
*
* @method debounce
* @param String {String} signalName A string identifier to debounce.
* @param Function {Function} callback A function that is called (with `this` context) when the wait time elapses.
* @param Number {Number} wait Time in milliseconds (ms) after the last signal that must elapse before invoking `callback`
* @type Handle
*/
debounce: function(signalName, callback, wait) {
var n = '___' + signalName;
this[n] = Debounce.call(this, this[n], callback, wait);
}

});
Expand Down
46 changes: 42 additions & 4 deletions test/unit/utils.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@

suite('CSS utilities', function() {

test('$$:', function() {
// TODO
});

test('toggleClass', function() {

window.el1.toggleClass('foo-class', true);
Expand All @@ -59,6 +55,48 @@

});

suite('debounce', function() {

test('debounce (no-wait)', function(done) {

var called = 0;
var cb = function() {
called++;
};

window.el1.debounce('foo', cb);
window.el1.debounce('foo', cb);
window.el1.debounce('foo', cb);

setTimeout(function() {
assert.equal(called, 1, 'debounce should be called exactly once');
done();
}, 50);

});

test('debounce (wait)', function(done) {

var called = 0;
var now = Date.now();
var cb = function() {
called++;
};

window.el1.debounce('foo', cb);
window.el1.debounce('foo', cb, 100);
window.el1.debounce('foo', cb, 100);

setTimeout(function() {
assert.equal(called, 1, 'debounce should be called exactly once');
assert(Date.now() - now > 100, 'debounce should be called after at least 100ms');
done();
}, 200);

});

});

</script>
</body>
</html>

0 comments on commit 5188704

Please sign in to comment.