-
Notifications
You must be signed in to change notification settings - Fork 4k
Implement debounced input action #9277
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
src/service/action-impl.js
Outdated
| }); | ||
| } else if (name == 'input-debounced') { | ||
| this.root_.addEventListener('input', debounce_(event => { | ||
| this.trigger(dev().assertElement(event.target), name, event); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Danger! If this is a native event, the target and other properties of the event may be gone by the time this method is executed. Firefox in particular has issues.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the heads up. Since the if blocks above have the same caveat, and this is only used to trigger amp actions I think it should be safe for this use-case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if block?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ones just a few lines above this one e.g. if (name == 'keydown') { ... }. Does anything make this case different than those?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those event listeners are executed sync, this one is async. The event object doesn't behave properly in async execution.
| * @param {function(...*)} callback | ||
| * @param {number} minInterval the minimum time interval in millisecond | ||
| * @returns {function()} | ||
| * @returns {function(...*)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the right signature for a function that just forwards its arguments? I made this change because check-types showed a type mismatch for the input-debounced debounce callback which is {function(!Event)}, not {function()}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * @param {function(...*)} callback | ||
| * @param {number} minInterval the minimum time interval in millisecond | ||
| * @returns {function()} | ||
| * @returns {function(...*)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/service/action-impl.js
Outdated
| }, DEFAULT_DEBOUNCE_WAIT); | ||
|
|
||
| this.root_.addEventListener('input', event => { | ||
| inputDebounced(event.target, event); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'd need a full clone of the event to avoid the issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have a canonical AMP way to clone a native object?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not yet.
function cloneEvent(event) {
const clone = map();
for (const prop in event) {
const value = event[prop];
// Avoid functions like stopPropagation, since they can't do anything in an async context.
if (typeof value !== 'function') {
clone[prop] = event[prop];
}
}
}
src/service/action-impl.js
Outdated
| } else if (name == 'input-debounced') { | ||
| const inputDebounced = debounce(this.ampdoc.win, event => { | ||
| const target = dev().assertElement(event.target); | ||
| this.trigger(target, name, /** @type {!Event} */ (event)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're gonna hit bugs later on with this typecast. Why not create a DeferredEvent type?
| return clone; | ||
| } | ||
|
|
||
| /** @private */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Having this throw will likely let us find bugs quicker.
| constructor(event) { | ||
| cloneWithoutFunctions(event, this); | ||
|
|
||
| /** @type {?Object} */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An additional details property was being added to the event object by addChangeDetails_ and the compiler states you cannot add properties to dict types (which ES6 classes are) after construction. So I added the property in the constructor.
The amount of code mutating native objects in AMP is too damn high! It almost always adds technical debt to do it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's also wait for #9305 before merging this.
| } else if (name == 'input-debounced') { | ||
| const debouncedInput = debounce(this.ampdoc.win, event => { | ||
| const target = dev().assertElement(event.target); | ||
| this.trigger(target, name, /** @type {!ActionEventDef} */ (event)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
similar tochange event we want to include the value of the input at the time of trigger as details on the event object. Might require some refactoring of addChangeDetails_ to share code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point, I'll add that in.
|
|
||
| for (const key in deferredEvent) { | ||
| if (typeof deferredEvent[key] !== 'function') { | ||
| expect(deferredEvent[key]).to.deep.equal(event[key]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit deep shouldn't be necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| const event = createCustomEvent(window, 'MyEvent', {foo: 'bar'}); | ||
| const deferredEvent = new DeferredEvent(event); | ||
|
|
||
| for (const key in deferredEvent) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please test for stopPropagation and preventDefault specifically.
|
@ericlindley-g @cvializ I like to suggest putting an experiment flag around this event and merging (instead of waiting to figure out the trust-level stuff first). WDYT? |
|
I think that sounds like a safe, low-effort way to minimize risk. I'll add it in |
8567a0a to
eac22d4
Compare

Implement an
input-debouncedevent and example.Only merge this after #9252 to use the its performant debounce method.
Closes #8824 and #5702