Skip to content

Commit

Permalink
Merge pull request #47 from embermap/load-v2
Browse files Browse the repository at this point in the history
New load and sideload methods to align with ember-data APIs
  • Loading branch information
ryanto authored Sep 12, 2018
2 parents 7704ba8 + fe7c29f commit 76f9ddc
Show file tree
Hide file tree
Showing 31 changed files with 4,525 additions and 2,847 deletions.
5 changes: 3 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,16 @@ module.exports = {
// node files
{
files: [
'ember-cli-build.js',
'index.js',
'testem.js',
'ember-cli-build.js',
'config/**/*.js',
'tests/dummy/config/**/*.js'
],
excludedFiles: [
'app/**',
'addon/**',
'addon-test-support/**',
'app/**',
'tests/dummy/app/**'
],
parserOptions: {
Expand Down
135 changes: 126 additions & 9 deletions addon/mixins/loadable-model.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import Mixin from '@ember/object/mixin';
import { assert } from '@ember/debug';
import { resolve } from 'rsvp';
import { isArray } from '@ember/array';
import { get } from '@ember/object';
import { camelize } from '@ember/string';

/**
_This mixin relies on JSON:API, and assumes that your server supports JSON:API includes._
Expand Down Expand Up @@ -30,44 +35,154 @@ import Mixin from '@ember/object/mixin';
*/
export default Mixin.create({

init() {
this._super(...arguments);
this.set('_loadedReferences', {});
},

/**
`load` gives you an explicit way to asynchronously load related data.
`reloadWith` gives you an explicit way to asynchronously reloadWith related data.
```js
post.load('comments');
post.reloadWith('comments');
```
The above uses Storefront's `loadRecord` method to query your backend for the post along with its comments.
You can also use JSON:API's dot notation to load additional related relationships.
```js
post.load('comments.author');
post.reloadWith('comments.author');
```
Every call to `load()` will return a promise.
Every call to `reloadWith()` will return a promise.
```js
post.load('comments').then(() => console.log('loaded comments!'));
post.reloadWith('comments').then(() => console.log('loaded comments!'));
```
If a relationship has never been loaded, the promise will block until the data is loaded. However, if a relationship has already been loaded (even from calls to `loadRecord` elsewhere in your application), the promise will resolve synchronously with the data from Storefront's cache. This means you don't have to worry about overcalling `load()`.
If a relationship has never been loaded, the promise will block until the data is loaded. However, if a relationship has already been loaded (even from calls to `loadRecord` elsewhere in your application), the promise will resolve synchronously with the data from Storefront's cache. This means you don't have to worry about overcalling `reloadWith()`.
This feature works best when used on relationships that are defined with `{ async: false }` because it allows `load()` to load the data, and `get()` to access the data that has already been loaded.
@method load
@method reloadWith
@param {String} includesString a JSON:API includes string representing the relationships to check
@return {Promise} a promise resolving with the record
@public
*/
load(...includes) {
reloadWith(...includes) {
let modelName = this.constructor.modelName;

return this.get('store').loadRecord(modelName, this.get('id'), {
include: includes.join(',')
});
},

/**
`load` gives you an explicit way to asynchronously load related data.
```js
post.load('comments');
```
The above uses Ember data's references API to load a post's comments from your backend.
Every call to `load()` will return a promise.
```js
post.load('comments').then((comments) => console.log('loaded comments as', comments));
```
If a relationship has never been loaded, the promise will block until the data is loaded. However, if a relationship has already been loaded, the promise will resolve synchronously with the data from the cache. This means you don't have to worry about overcalling `load()`.
@method load
@param {String} name the name of the relationship to load
@return {Promise} a promise resolving with the related data
@public
*/
load(name, options = {}) {
assert(
`The #load method only works with a single relationship, if you need to load multiple relationships in one request please use the #reloadWith method [ember-data-storefront]`,
!isArray(name) && !name.includes(',') && !name.includes('.')
);

let reference = this._getReference(name);
let value = reference.value();
let shouldBlock = !(value || this.hasLoaded(name)) || options.reload;
let promise;

if (shouldBlock) {
let loadMethod = this._getLoadMethod(name, options);
promise = reference[loadMethod].call(reference);
} else {
promise = resolve(value);
reference.reload();
}

return promise.then(data => {
// need to track that we loaded this relationship, since relying on the reference's
// value existing is not enough
this._loadedReferences[name] = true;
return data;
});
},

/**
@method _getRelationshipInfo
@private
*/
_getRelationshipInfo(name) {
let relationshipInfo = get(this.constructor, `relationshipsByName`).get(name);

assert(
`You tried to load the relationship ${name} for a ${this.constructor.modelName}, but that relationship does not exist [ember-data-storefront]`,
relationshipInfo
);

return relationshipInfo;
},

/**
@method _getReference
@private
*/
_getReference(name) {
let relationshipInfo = this._getRelationshipInfo(name);
let referenceMethod = relationshipInfo.kind;
return this[referenceMethod](name);
},

/**
Given a relationship name this method will return the best way to load
that relationship.
@method _getLoadMethod
@private
*/
_getLoadMethod(name, options) {
let relationshipInfo = this._getRelationshipInfo(name);
let reference = this._getReference(name);
let hasLoaded = this._hasLoadedReference(name);
let forceReload = options.reload;
let isAsync;

if (relationshipInfo.kind === 'hasMany') {
isAsync = reference.hasManyRelationship.isAsync;
} else if (relationshipInfo.kind === 'belongsTo') {
isAsync = reference.belongsToRelationship.isAsync;
}

return !forceReload && isAsync && !hasLoaded ? 'load' : 'reload';
},

/**
@method _hasLoadedReference
@private
*/
_hasLoadedReference(name) {
return this._loadedReferences[name];
},

/**
This method returns true if the provided includes string has been loaded and false if not.
Expand All @@ -78,8 +193,10 @@ export default Mixin.create({
*/
hasLoaded(includesString) {
let modelName = this.constructor.modelName;
let hasSideloaded = this.get('store').hasLoadedIncludesForRecord(modelName, this.get('id'), includesString);
let hasLoaded = this._hasLoadedReference(camelize(includesString));

return this.get('store').hasLoadedIncludesForRecord(modelName, this.get('id'), includesString);
return hasLoaded || hasSideloaded;
}

});
3 changes: 2 additions & 1 deletion addon/mixins/loadable-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ export default Mixin.create({
return this.coordinator.recordHasIncludes(type, id, includesString);
},

/**
/**
@method resetCache
@private
*/
resetCache() {
Expand Down
23 changes: 21 additions & 2 deletions config/ember-try.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,30 @@ module.exports = function() {
}
}
},
{
name: 'ember-3.1',
npm: {
devDependencies: {
'ember-source': '~3.1.0',
'ember-data': "~3.1.0"
}
}
},
{
name: 'ember-3.4',
npm: {
devDependencies: {
'ember-source': '~3.4.0',
'ember-data': "~3.4.0"
}
}
},
{
name: 'ember-release',
npm: {
devDependencies: {
'ember-source': urls[0]
'ember-source': urls[0],
'ember-data': 'emberjs/data#release'
}
}
},
Expand All @@ -77,7 +96,7 @@ module.exports = function() {
npm: {
devDependencies: {
'ember-source': urls[2],
'ember-data': 'canary'
'ember-data': 'emberjs/data'
}
}
},
Expand Down
13 changes: 6 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"devDependencies": {
"broccoli-asset-rev": "^2.4.5",
"ember-ajax": "^3.0.0",
"ember-cli": "~3.0.0",
"ember-cli-addon-docs": "^0.5.0",
"ember-cli": "~3.1.0",
"ember-cli-addon-docs": "ryanto/ember-cli-addon-docs#97dcf3bc8478e45c217a38d15e2117df2e18c32e",
"ember-cli-addon-docs-yuidoc": "^0.1.1",
"ember-cli-dependency-checker": "^2.0.0",
"ember-cli-deploy": "^1.0.2",
Expand All @@ -54,7 +54,7 @@
"ember-cli-uglify": "^2.0.0",
"ember-component-css": "^0.5.0",
"ember-concurrency": "^0.8.12",
"ember-data": "~3.0.0",
"ember-data": "~3.1.0",
"ember-disable-prototype-extensions": "^1.1.2",
"ember-export-application-global": "^2.0.0",
"ember-fetch": "^5.0.0",
Expand All @@ -63,14 +63,13 @@
"ember-maybe-import-regenerator": "^0.1.6",
"ember-qunit-assert-helpers": "^0.2.1",
"ember-resolver": "^4.0.0",
"ember-router-scroll": "ryanto/ember-router-scroll#983cb421f5295f6d88119be16aec4dcf662a948f",
"ember-source": "~3.0.0",
"ember-router-scroll": "~1.0.0",
"ember-source": "~3.1.0",
"ember-source-channel-url": "^1.0.1",
"ember-test-selectors": "^0.3.8",
"ember-try": "^0.2.23",
"ember-wait-for-test-helper": "^2.1.1",
"eslint-plugin-ember": "^5.0.0",
"eslint-plugin-node": "^5.2.1",
"eslint-plugin-node": "^6.0.1",
"express": "^4.8.5",
"glob": "^4.0.5",
"jsdom": "^11.6.2",
Expand Down
8 changes: 5 additions & 3 deletions tests/acceptance/load-all-test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { module, test } from 'qunit';
import { visit, click, find } from "@ember/test-helpers";
import { visit, click, find, waitUntil } from "@ember/test-helpers";
import { setupApplicationTest } from 'ember-qunit';
import { waitFor } from 'ember-wait-for-test-helper/wait-for';
import { startMirage } from 'dummy/initializers/ember-cli-mirage';

function t(...args) {
Expand All @@ -12,7 +11,7 @@ function t(...args) {

async function domHasChanged(selector) {
let previousUi = find(selector).textContent;
return await waitFor(() => {
return await waitUntil(() => {
let currentUi = find(selector).textContent;

return currentUi !== previousUi;
Expand All @@ -33,6 +32,9 @@ module('Acceptance | load all', function(hooks) {
});

test('visiting /load-all', async function(assert) {
// need our data fetching to be slow for these tests.
server.timing = 1000;

server.create('post', { id: '1', title: 'Post 1 title' });
server.create('post');

Expand Down
29 changes: 14 additions & 15 deletions tests/acceptance/load-relationship-test.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,31 @@
import { module, test } from 'qunit';
import { visit, click, find } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import { startMirage } from 'dummy/initializers/ember-cli-mirage';
import startMirage from 'dummy/tests/helpers/start-mirage';

module('Acceptance | load relationship', function(hooks) {
let server;

setupApplicationTest(hooks);
startMirage(hooks);

hooks.beforeEach(function() {
server = startMirage();
});
test('the load demo works', async function(assert) {
await visit('/docs/guides/working-with-relationships');

hooks.afterEach(function() {
server.shutdown();
});
await click('[data-test-id=load-comments]');

test('visiting /load-relationship', async function(assert) {
let post = server.create('post', { id: '1', title: 'Post 1 title' });
server.createList('comment', 3, { post });
assert.equal(
find('[data-test-id=load-comments-count]').textContent.trim(),
"The post has 3 comments."
);
});

test('the reloadWith demo works', async function(assert) {
await visit('/docs/guides/working-with-relationships');

await click('[data-test-id=load-comments]');
await click('[data-test-id=reload-with-comments]');

assert.equal(
find('[data-test-id=comments-count]').textContent.trim(),
"The post has 3 comments."
find('[data-test-id=reload-with-comments-count]').textContent.trim(),
"The post has 5 comments."
);
});
});
10 changes: 10 additions & 0 deletions tests/dummy/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import loadInitializers from 'ember-load-initializers';
import config from './config/environment';
import DS from 'ember-data';
import LoadableModel from 'ember-data-storefront/mixins/loadable-model';
import { registerWarnHandler } from '@ember/debug';

DS.Model.reopen(LoadableModel);

Expand All @@ -13,6 +14,15 @@ const App = Application.extend({
Resolver
});

// We'll ignore the empty tag name warning for test selectors since we have
// empty tag names for pass through components.
registerWarnHandler(function(message, { id }, next) {
if (id !== 'ember-test-selectors.empty-tag-name') {
next(...arguments);
}
});

loadInitializers(App, config.modulePrefix);


export default App;
2 changes: 1 addition & 1 deletion tests/dummy/app/pods/application/template.hbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class='font-sans text-black leading-normal'>
<div class='docs-font-sans docs-text-black docs-leading-normal'>
{{docs-header logo='ember-data' name='Storefront'}}

{{outlet}}
Expand Down
Loading

0 comments on commit 76f9ddc

Please sign in to comment.