Skip to content

Conversation

cvializ
Copy link
Contributor

@cvializ cvializ commented May 11, 2017

Implement an input-debounced event and example.

Only merge this after #9252 to use the its performant debounce method.

Closes #8824 and #5702

});
} else if (name == 'input-debounced') {
this.root_.addEventListener('input', debounce_(event => {
this.trigger(dev().assertElement(event.target), name, event);
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if block?

Copy link
Contributor Author

@cvializ cvializ May 15, 2017

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?

Copy link
Contributor

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(...*)}
Copy link
Contributor Author

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()}

Copy link
Contributor

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(...*)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}, DEFAULT_DEBOUNCE_WAIT);

this.root_.addEventListener('input', event => {
inputDebounced(event.target, event);
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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];
    }
  }
}

} 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));
Copy link
Contributor

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 */
Copy link
Contributor

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} */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm?

Copy link
Contributor Author

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.

Copy link
Contributor

@aghassemi aghassemi left a 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));
Copy link
Contributor

@aghassemi aghassemi May 16, 2017

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.

Copy link
Contributor Author

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]);
Copy link
Contributor

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.

Copy link
Contributor Author

@cvializ cvializ May 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately Chrome gives CustomEvent instances a path property and when you assign it to a variable, that variable is somehow not equal to the property on the event. using deep solves this. I'm not sure what else to do other than special-casing this property in the test.

screen shot 2017-05-16 at 10 20 32 am

const event = createCustomEvent(window, 'MyEvent', {foo: 'bar'});
const deferredEvent = new DeferredEvent(event);

for (const key in deferredEvent) {
Copy link
Contributor

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.

@aghassemi
Copy link
Contributor

@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?

@cvializ
Copy link
Contributor Author

cvializ commented May 18, 2017

I think that sounds like a safe, low-effort way to minimize risk. I'll add it in

@cvializ cvializ force-pushed the cv-event branch 2 times, most recently from 8567a0a to eac22d4 Compare May 19, 2017 18:08
@cvializ cvializ merged commit dfc7c5a into ampproject:master May 23, 2017
@cvializ cvializ deleted the cv-event branch August 31, 2017 20:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants