-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add route base class for observable models
- Allows you to return an observable from the `model` hook of the route - Pauses the transition until the first value is emitted, just like returning a Promise - Replaces the `model` property of the controller as new values are emitted
- Loading branch information
1 parent
0001781
commit 29f7c7a
Showing
7 changed files
with
186 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export const LATER_VALUE_SUBSCRIPTION = Symbol(); | ||
export const MOST_RECENT_VALUE = Symbol(); | ||
export const RESET = Symbol(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { default as Route } from "./route"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import Route from "@ember/routing/route"; | ||
import { partition, take } from "rxjs/operators"; | ||
import { Promise as RSVPPromise } from "rsvp"; | ||
|
||
import { | ||
LATER_VALUE_SUBSCRIPTION, | ||
MOST_RECENT_VALUE, | ||
RESET | ||
} from "./-private/symbols"; | ||
|
||
export default class ObservableModelRoute extends Route { | ||
deserialize() { | ||
this[RESET](); | ||
|
||
const observable = super.deserialize(...arguments); | ||
|
||
const [first, later] = observable.pipe( | ||
partition((_, index) => index === 0) | ||
); | ||
|
||
this[LATER_VALUE_SUBSCRIPTION] = later.subscribe({ | ||
next: value => { | ||
if (this.controller) { | ||
this.controller.set("model", value); | ||
} else { | ||
this[MOST_RECENT_VALUE] = value; | ||
} | ||
}, | ||
complete: () => { | ||
this[RESET](); | ||
} | ||
}); | ||
|
||
return first.pipe(take(1)).toPromise(RSVPPromise); | ||
} | ||
|
||
/** | ||
* Reset any state that we're hanging off the instance | ||
*/ | ||
[RESET]() { | ||
if (this[LATER_VALUE_SUBSCRIPTION]) { | ||
this[LATER_VALUE_SUBSCRIPTION].unsubscribe(); | ||
} | ||
} | ||
|
||
/** | ||
* If more than one value was emitted before the controller was | ||
* set up, we should take the most recent value as the model, not | ||
* the original one | ||
* | ||
* @param {Ember.Controller} controller | ||
* @param {any} model | ||
*/ | ||
setupController(controller, model) { | ||
if (this[MOST_RECENT_VALUE]) { | ||
model = this[MOST_RECENT_VALUE]; | ||
|
||
this[MOST_RECENT_VALUE] = undefined; | ||
} | ||
|
||
super.setupController(controller, model); | ||
} | ||
|
||
resetController() { | ||
this[RESET](); | ||
|
||
super.resetController && super.resetController(...arguments); | ||
} | ||
|
||
willDestroy() { | ||
this[RESET](); | ||
|
||
super.willDestroy && super.willDestroy(...arguments); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { module, test } from "qunit"; | ||
import { visit } from "@ember/test-helpers"; | ||
import { setupApplicationTest } from "ember-qunit"; | ||
import td from "testdouble"; | ||
import { setupScheduler } from "ember-observable/test-support"; | ||
import { LATER_VALUE_SUBSCRIPTION } from "ember-observable/-private/symbols"; | ||
import { of, merge } from "rxjs"; | ||
import { delay } from "rxjs/operators"; | ||
|
||
module("Acceptance | observable model", function(hooks) { | ||
setupApplicationTest(hooks); | ||
setupScheduler(hooks); | ||
|
||
test("it handles multiple values over time", async function(assert) { | ||
this.owner.lookup("route:observable-model").model = () => { | ||
return merge( | ||
of(1), | ||
of(2).pipe(delay(100, this.scheduler)), | ||
of(3).pipe(delay(200, this.scheduler)) | ||
); | ||
}; | ||
|
||
await visit("/observable-model"); | ||
|
||
assert.dom().hasText("1", "Model hook resolves with the initial value"); | ||
|
||
await this.scheduler.advanceBy(100); | ||
|
||
assert.dom().hasText("2", "Model replaced with the second value"); | ||
|
||
await this.scheduler.advanceBy(100); | ||
|
||
assert.dom().hasText("3", "Model replaced with the third value"); | ||
}); | ||
|
||
test("it takes the latest value if multiple are emitted before resolution", async function(assert) { | ||
this.owner.lookup("route:observable-model").model = () => { | ||
return of(1, 2); | ||
}; | ||
|
||
await visit("/observable-model"); | ||
|
||
assert.dom().hasText("2", "Model used the later value"); | ||
}); | ||
|
||
module("unsubscribing from later values", function(hooks) { | ||
hooks.beforeEach(async function(assert) { | ||
const route = this.owner.lookup("route:observable-model"); | ||
route.model = () => { | ||
return merge(of(1), of(2).pipe(delay(100, this.scheduler))); | ||
}; | ||
|
||
await visit("/observable-model"); | ||
|
||
this.subscription = route[LATER_VALUE_SUBSCRIPTION]; | ||
td.replace(this.subscription, "unsubscribe"); | ||
|
||
assert.verify( | ||
this.subscription.unsubscribe(), | ||
{ times: 0 }, | ||
"Initialled subscribed to the observable" | ||
); | ||
}); | ||
|
||
test("when the observer completes", async function(assert) { | ||
await this.scheduler.advanceBy(100); | ||
|
||
assert.verify( | ||
this.subscription.unsubscribe(), | ||
"Unsubscribed after completion" | ||
); | ||
}); | ||
|
||
test("when navigating away from the route", async function(assert) { | ||
await visit("/"); | ||
|
||
assert.verify( | ||
this.subscription.unsubscribe(), | ||
"Unsubscribed after navigating away from the page" | ||
); | ||
}); | ||
|
||
test("when the model hook is refreshed", async function(assert) { | ||
await visit("/observable-model?page=2"); | ||
|
||
assert.verify( | ||
this.subscription.unsubscribe(), | ||
"Unsubscribed after navigating away from the page" | ||
); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import Route from "ember-observable/route"; | ||
|
||
export default class ObservableModel extends Route { | ||
constructor() { | ||
super(...arguments); | ||
|
||
this.queryParams = { | ||
page: { | ||
refreshModel: true | ||
} | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{{model}} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters