Skip to content

Commit

Permalink
Merge pull request #4 from alexlafroscia/from-event-modifier
Browse files Browse the repository at this point in the history
`from-event` element modifier
  • Loading branch information
alexlafroscia authored Feb 14, 2019
2 parents 98d09a5 + 1a60ddb commit 744d585
Show file tree
Hide file tree
Showing 8 changed files with 400 additions and 1 deletion.
71 changes: 71 additions & 0 deletions addon/modifiers/from-event.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Ember from "ember";
import { fromEvent } from "rxjs";

export default Ember._setModifierManager(
() => ({
createModifier() {
return {
subscription: undefined,
element: undefined
};
},

_setupSubscription(state, eventName, operatorOrObserver, maybeObserver) {
const { element } = state;
let operator, observer;

if (operatorOrObserver && maybeObserver) {
operator = operatorOrObserver;
observer = maybeObserver;
} else if (operatorOrObserver && !maybeObserver) {
observer = operatorOrObserver;
}

let observable = fromEvent(element, eventName);

if (operator) {
observable = observable.pipe(operator);
}

state.subscription = observable.subscribe(observer);
},

installModifier(
state,
element,
{
positional: [eventName, operatorOrObserver, maybeObserver]
}
) {
state.element = element;

this._setupSubscription(
state,
eventName,
operatorOrObserver,
maybeObserver
);
},

updateModifier(
state,
{
positional: [eventName, operatorOrSubscribe, maybeObserver]
}
) {
state.subscription.unsubscribe();

this._setupSubscription(
state,
eventName,
operatorOrSubscribe,
maybeObserver
);
},

destroyModifier({ subscription }) {
subscription.unsubscribe();
}
}),
class FromEventModifier {}
);
1 change: 1 addition & 0 deletions app/modifiers/from-event.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "ember-rx/modifiers/from-event";
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
},
"dependencies": {
"ember-auto-import": "^1.2.19",
"ember-cli-babel": "^7.1.2",
"ember-cli-babel": "^7.4.2",
"ember-stream-helper": "^1.0.1",
"rxjs": "^6.4.0"
},
Expand Down Expand Up @@ -53,6 +53,7 @@
"ember-export-application-global": "^2.0.0",
"ember-load-initializers": "^1.1.0",
"ember-maybe-import-regenerator": "^0.1.6",
"ember-modifier-manager-polyfill": "^1.0.2",
"ember-qunit": "^3.4.1",
"ember-resolver": "^5.0.1",
"ember-source": "~3.7.0",
Expand Down
90 changes: 90 additions & 0 deletions tests/dummy/app/pods/docs/features/element-modifier/template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Element Modifier

This addon provides an Element Modifier that can be used to capture events from DOM elements using RxJS's [`fromEvent`][rxjs-from-event]. This allows you to subscribe to events using an RxJS observer.

> Note: To use this feature, you'll need either Ember 3.8+ or the [element modifier polyfill][polyfill].
## Basic Usage

You can subscribe to events from the DOM element using the `from-event` element modifier. It is passed an event to listen to and an observer definition.

```js
import Component from "@ember/component";

export default class MyComponent extends Component {
onClickEvent() {
console.log("Click event received");
}
}
```

```hbs
<button {{from-event 'click' this.onClickEvent}}>
Click me!
</button>
```

## Providing an operator

If you want to pipe the events through an operator, you can also provide an additional argument to `from-event`. An operator can be placed between the event name and the observer to apply it to the observable.

```js
import Component from "@ember/component";
import { filter } from "rxjs/operators";

export default class MyComponent extends Component {
everyOtherClick() {
return filter((_event, index) => index % 2 === 0);
}

onClickEvent() {
console.log("Click event received");
}
}
```

```hbs
<button {{from-event 'click' this.everyOtherClick this.onClickEvent}}>
Click me!
</button>
```

Because the events will be piped through the operator, only every other event will be logged.

## Using a Subject

If you want to multi-cast to different observers, you can also pass a [Subject][rxjs-subject] to `frorm-event`. Because Subjects are operators, this behavior "just works"!

```js
import Component from "@ember/component";
import { Subject } from "rxjs";
import { filter } from "rxjs/operators";

export default class MyComponent extends Component {
constructor() {
super(...arguments);

this.clickSubject = new Subject();

this.clickSubject.subscribe(() => {
console.log("Logged on every click");
});

this.clickSubject
.pipe(filter((_event, index) => index % 2 === 0))
.subscribe(() => {
console.log("Logged on every other click");
});
}
}
```

```hbs
<button {{from-event 'click' this.clickSubject}}>
Click me!
</button>
```

[rxjs-from-event]: https://rxjs.dev/api/index/function/fromEvent
[rxjs-subject]: https://rxjs.dev/guide/subject
[polyfill]: https://github.com/rwjblue/ember-modifier-manager-polyfill
1 change: 1 addition & 0 deletions tests/dummy/app/pods/docs/template.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
{{nav.item 'Introduction' 'docs.index'}}

{{nav.section 'Features'}}
{{nav.item 'Element Modifier' 'docs.features.element-modifier'}}
{{nav.item 'Run Loop Scheduler' 'docs.features.scheduler'}}

{{nav.section 'Cookbook'}}
Expand Down
1 change: 1 addition & 0 deletions tests/dummy/app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Router.map(function() {
});

this.route("features", function() {
this.route("element-modifier");
this.route("scheduler");
});
});
Expand Down
134 changes: 134 additions & 0 deletions tests/integration/modifiers/from-event-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { module, test } from "qunit";
import { setupRenderingTest } from "ember-qunit";
import { setupScheduler } from "ember-rx/test-support";
import { render, click } from "@ember/test-helpers";
import { Subject } from "rxjs";
import { filter, scan } from "rxjs/operators";
import hbs from "htmlbars-inline-precompile";
import td from "testdouble";

module("Integration | Modifier | from-event", function(hooks) {
setupRenderingTest(hooks);
setupScheduler(hooks);

test("it can subscribe to events", async function(assert) {
this.observer = td.function();

await render(hbs`
<button {{from-event 'click' this.observer}}>
My Button
</button>
`);

await click("button");

assert.verify(
this.observer(td.matchers.isA(MouseEvent)),
"The observer was called with the event"
);
});

test("it can pipe the observable through an operator", async function(assert) {
this.operator = filter((_event, index) => index % 2 === 0);
this.observer = td.function();

await render(hbs`
<button {{from-event 'click' this.operator this.observer}}>
My Button
</button>
`);

await click("button");
await click("button");

assert.verify(
this.observer(td.matchers.isA(MouseEvent)),
{ times: 1 },
"The observer was called one time"
);
});

test("it handles the arguments changing", async function(assert) {
const originalObserver = td.function("original observer");
const newObserver = td.function("new observer");

this.observer = originalObserver;

await render(hbs`
<button {{from-event 'click' this.observer}}>
My Button
</button>
`);

this.set("observer", newObserver);

await click("button");

assert.verify(
originalObserver(td.matchers.isA(MouseEvent)),
{ times: 0 },
"The original observer is never called"
);

assert.verify(
newObserver(td.matchers.isA(MouseEvent)),
{ times: 1 },
"The new observer is called"
);
});

test("it can receive a `Subject` to surface the observable", async function(assert) {
this.subject = new Subject();
const observer = td.function("Original observer");

this.subject.subscribe(observer);

await render(hbs`
<button {{from-event 'click' this.subject}}>
My Button
</button>
`);

await click("button");

assert.verify(
observer(td.matchers.isA(MouseEvent)),
"The observer is called through the Subject"
);
});

module("cookbook", function() {
test("adding to a list with each click", async function(assert) {
this.subject = new Subject();
this.accumulate = scan((acc, event) => [...acc, event], []);

await render(hbs`
<button {{from-event 'click' this.accumulate this.subject}}>
My Button
</button>
<ul>
{{#each (subscribe this.subject) as |item index|}}
<li data-test-index={{index}}>{{index}}</li>
{{/each}}
</ul>
`);

assert
.dom("[data-test-index]")
.exists({ count: 0 }, "Renders no elements");

await click("button");

assert
.dom("[data-test-index]")
.exists({ count: 1 }, "Renders one element");

await click("button");

assert
.dom("[data-test-index]")
.exists({ count: 2 }, "Renders two elements");
});
});
});
Loading

0 comments on commit 744d585

Please sign in to comment.