diff --git a/.eslintignore b/.eslintignore index 72df3730..701947ed 100644 --- a/.eslintignore +++ b/.eslintignore @@ -13,6 +13,8 @@ # misc /coverage/ !.* +.*/ +.eslintcache # ember-try /.node_modules.ember-try/ diff --git a/.eslintrc.js b/.eslintrc.js index 610f5936..23b0222d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,55 +10,59 @@ module.exports = { ecmaVersion: 2018, sourceType: 'module', ecmaFeatures: { - legacyDecorators: true - } + legacyDecorators: true, + }, }, - plugins: [ - 'ember' - ], + plugins: ['ember'], extends: [ 'eslint:recommended', - 'plugin:ember/recommended' + 'plugin:ember/recommended', + 'plugin:prettier/recommended', ], env: { - browser: true + browser: true, }, rules: { 'ember/no-jquery': 'off', + 'ember/no-mixins': 'off', 'ember/no-new-mixins': 'off', - 'no-useless-escape': 'off' + 'no-useless-escape': 'off', }, overrides: [ // node files { files: [ - '.eslintrc.js', - '.template-lintrc.js', - 'ember-cli-build.js', - 'index.js', - 'testem.js', - 'blueprints/*/index.js', - 'config/**/*.js', - 'server/**/*.js', - 'tests/dummy/config/**/*.js' - ], - excludedFiles: [ - 'addon/**', - 'addon-test-support/**', - 'app/**', - 'tests/dummy/app/**' + './.eslintrc.js', + './.prettierrc.js', + './.template-lintrc.js', + './ember-cli-build.js', + './index.js', + './testem.js', + './blueprints/*/index.js', + './config/**/*.js', + './server/**/*.js', + './tests/dummy/config/**/*.js', ], parserOptions: { - sourceType: 'script' + sourceType: 'script', }, env: { browser: false, - node: true + node: true, }, plugins: ['node'], - rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, { - 'node/no-unpublished-require': 'off' - }) - } - ] + extends: ['plugin:node/recommended'], + rules: { + 'node/no-unpublished-require': 'off', + }, + }, + { + // Test files: + files: ['tests/**/*-test.{js,ts}'], + extends: ['plugin:qunit/recommended'], + rules: { + 'qunit/require-expect': 'off', + }, + }, + ], }; diff --git a/.gitignore b/.gitignore index c40a1b2a..7e0f7ddc 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ /.env* /.pnp* /.sass-cache +/.eslintcache /connect.lock /coverage/ /libpeerconnection.log diff --git a/.npmignore b/.npmignore index bd09adff..7fa8adb4 100644 --- a/.npmignore +++ b/.npmignore @@ -10,10 +10,13 @@ /.editorconfig /.ember-cli /.env* +/.eslintcache /.eslintignore /.eslintrc.js /.git/ /.gitignore +/.prettierignore +/.prettierrc.js /.template-lintrc.js /.travis.yml /.watchmanconfig @@ -23,6 +26,7 @@ /ember-cli-build.js /testem.js /tests/ +/yarn-error.log /yarn.lock .gitkeep @@ -30,3 +34,4 @@ /.node_modules.ember-try/ /bower.json.ember-try /package.json.ember-try +/config/addon-docs.js \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..92216555 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,21 @@ +# unconventional js +/blueprints/*/files/ +/vendor/ + +# compiled output +/dist/ +/tmp/ + +# dependencies +/bower_components/ +/node_modules/ + +# misc +/coverage/ +!.* +.eslintcache + +# ember-try +/.node_modules.ember-try/ +/bower.json.ember-try +/package.json.ember-try diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 00000000..534e6d35 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = { + singleQuote: true, +}; diff --git a/.template-lintrc.js b/.template-lintrc.js index f3873700..1e2b8578 100644 --- a/.template-lintrc.js +++ b/.template-lintrc.js @@ -1,5 +1,8 @@ 'use strict'; module.exports = { - extends: 'octane' + extends: 'recommended', + rules: { + 'no-inline-styles': false, + }, }; diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2c0d33f4..1fb80a98 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,9 +8,8 @@ ## Linting -* `yarn lint:hbs` -* `yarn lint:js` -* `yarn lint:js --fix` +* `yarn lint` +* `yarn lint:fix` ## Running tests diff --git a/addon/-private/cache.js b/addon/-private/cache.js index 5bbf03d8..0a36052d 100644 --- a/addon/-private/cache.js +++ b/addon/-private/cache.js @@ -1,10 +1,9 @@ -import { queryCacheKey, cacheKey } from './utils/get-key'; +import { queryCacheKey, cacheKey } from './utils/get-key'; /* A cache for queries. */ export default class Cache { - constructor() { this.store = {}; } @@ -21,7 +20,6 @@ export default class Cache { } all() { - return Object.keys(this.store).map(key => this.store[key]); + return Object.keys(this.store).map((key) => this.store[key]); } - } diff --git a/addon/-private/coordinator.js b/addon/-private/coordinator.js index a33823af..94cb7191 100644 --- a/addon/-private/coordinator.js +++ b/addon/-private/coordinator.js @@ -5,19 +5,18 @@ import { get } from '@ember/object'; // cleans options so that the resulting object only contains // data we want to send to the server as query params. -let _cleanParams = function(options) { +let _cleanParams = function (options) { let clean = { ...{}, ...options }; delete clean.reload; delete clean.backgroundReload; return clean; -} +}; /* I know how to retrieve queries from the cache, and also assemble queries that are not in the cache but can be derived from them. */ export default class Coordinator { - constructor(store) { this.store = store; this.recordCache = new Cache(); @@ -52,9 +51,9 @@ export default class Coordinator { } queryFor(...args) { - return args.length === 3 ? - this.recordQueryFor(...args) : - this.recordArrayQueryFor(...args); + return args.length === 3 + ? this.recordQueryFor(...args) + : this.recordArrayQueryFor(...args); } dump() { @@ -65,7 +64,9 @@ export default class Coordinator { } recordHasIncludes(type, id, includesString) { - let query = this._assembleRecordQuery(type, id, { include: includesString }); + let query = this._assembleRecordQuery(type, id, { + include: includesString, + }); let nonLoadedIncludes = this._nonLoadedIncludesForQuery(query); return nonLoadedIncludes.length === 0; @@ -99,16 +100,17 @@ export default class Coordinator { } _nonLoadedIncludesForQuery(query) { - let loadedIncludes = get(this, `loadedIncludes.${query.type}.${query.id}`) || []; + let loadedIncludes = + get(this, `loadedIncludes.${query.type}.${query.id}`) || []; let includesString = query.params.include || ''; return includesString .split(',') - .filter(include => !!include) - .filter(include => { - return !loadedIncludes.find(loadedInclude => { + .filter((include) => !!include) + .filter((include) => { + return !loadedIncludes.find((loadedInclude) => { return loadedInclude.indexOf(include) === 0; - }) + }); }); } @@ -123,7 +125,8 @@ export default class Coordinator { _updateLoadedIncludesWithQuery(query) { this.loadedIncludes[query.type] = this.loadedIncludes[query.type] || {}; - this.loadedIncludes[query.type][query.id] = this.loadedIncludes[query.type][query.id] || []; + this.loadedIncludes[query.type][query.id] = + this.loadedIncludes[query.type][query.id] || []; let currentIncludes = this.loadedIncludes[query.type][query.id]; let nonLoadedIncludes = this._nonLoadedIncludesForQuery(query); @@ -131,5 +134,4 @@ export default class Coordinator { this.loadedIncludes[query.type][query.id] = newLoadedIncludes; } - } diff --git a/addon/-private/record-array-query.js b/addon/-private/record-array-query.js index fab407ad..ab53d03b 100644 --- a/addon/-private/record-array-query.js +++ b/addon/-private/record-array-query.js @@ -1,5 +1,4 @@ export default class RecordArrayQuery { - constructor(store, type, params = {}) { this.store = store; this.type = type; @@ -13,14 +12,12 @@ export default class RecordArrayQuery { if (this.value) { promise = this.value.update(); - } else { - promise = this.store.query(this.type, this.params) - .then(records => { - this.value = records; + promise = this.store.query(this.type, this.params).then((records) => { + this.value = records; - return records; - }); + return records; + }); } return promise; @@ -30,13 +27,12 @@ export default class RecordArrayQuery { let includes = this.params && this.params.include; let models = this.value; - if (includes && models) { + if (includes && models) { models - .filter(model => model.trackLoadedIncludes) + .filter((model) => model.trackLoadedIncludes) .forEach((model) => { model.trackLoadedIncludes(includes); }); } } - } diff --git a/addon/-private/record-query.js b/addon/-private/record-query.js index 39e5618e..b7c050cb 100644 --- a/addon/-private/record-query.js +++ b/addon/-private/record-query.js @@ -1,5 +1,4 @@ export default class RecordQuery { - constructor(store, type, id, params = {}) { this.store = store; this.type = type; @@ -8,9 +7,10 @@ export default class RecordQuery { // if we have no params, we can use the model from // the store if it exists, nice lil shortcut here. - this.value = Object.keys(this.params).length === 0 ? - this.store.peekRecord(type, id) : - null; + this.value = + Object.keys(this.params).length === 0 + ? this.store.peekRecord(type, id) + : null; } run() { @@ -18,12 +18,10 @@ export default class RecordQuery { // a blocking promise, so we force reload true. let options = { ...{ reload: true }, ...this.params }; - return this.store.findRecord(this.type, this.id, options) - .then(record => { - this.value = record; + return this.store.findRecord(this.type, this.id, options).then((record) => { + this.value = record; - return record; - }); + return record; + }); } - } diff --git a/addon/-private/utils/get-key.js b/addon/-private/utils/get-key.js index 5b7ea27c..03ce489a 100644 --- a/addon/-private/utils/get-key.js +++ b/addon/-private/utils/get-key.js @@ -1,13 +1,13 @@ -let _serializeParams = function(params={}, prefix) { +let _serializeParams = function (params = {}, prefix) { const query = Object.keys(params) .sort() .map((key) => { - const value = params[key]; + const value = params[key]; if (Array.isArray(params)) { key = `${prefix}[]`; } else if (params === Object(params)) { - key = (prefix ? `${prefix}[${key}]` : key); + key = prefix ? `${prefix}[${key}]` : key; } if (typeof value === 'object' && value !== null) { @@ -20,28 +20,23 @@ let _serializeParams = function(params={}, prefix) { return [].concat.apply([], query).join('&'); }; -let serializeObject = function(params) { +let serializeObject = function (params) { return _serializeParams(params); }; -let queryCacheKey = function(query) { +let queryCacheKey = function (query) { return cacheKey([query.type, query.id, query.params]); }; -let cacheKey = function(args) { +let cacheKey = function (args) { return args - .map(part => typeof part === "object" ? serializeObject(part) : part) - .filter(part => !!part) + .map((part) => (typeof part === 'object' ? serializeObject(part) : part)) + .filter((part) => !!part) .join('::'); -} +}; -let shoeboxize = function(key) { +let shoeboxize = function (key) { return key.replace(/&/g, '--'); // IDGAF -} - -export { - serializeObject, - queryCacheKey, - cacheKey, - shoeboxize -} +}; + +export { serializeObject, queryCacheKey, cacheKey, shoeboxize }; diff --git a/addon/adapters/application.js b/addon/adapters/application.js index 6ba18378..09a6c6ca 100644 --- a/addon/adapters/application.js +++ b/addon/adapters/application.js @@ -1,5 +1,3 @@ import JSONAPIAdapter from '@ember-data/adapter/json-api'; -export default JSONAPIAdapter.extend({ - -}); +export default class ApplicationAdapter extends JSONAPIAdapter {} diff --git a/addon/components/assert-must-preload/component.js b/addon/components/assert-must-preload/component.js index 2c9527cf..50be8c26 100644 --- a/addon/components/assert-must-preload/component.js +++ b/addon/components/assert-must-preload/component.js @@ -1,21 +1,22 @@ +import Component from '@glimmer/component'; import { assert } from '@ember/debug'; -import Component from '@ember/component'; -/** +export default class AssertMustPreloadComponent extends Component { + /** _This component relies on JSON:API, and assumes that your server supports JSON:API includes._ - _{{assert-must-preload}} only works on models that have included the LoadableModel mixin._ + _ only works on models that have included the LoadableModel mixin._ Use this when authoring a component that requires a model to be passed in with certain relationships already loaded. - For example, if you wanted to ensure the following template was never rendered without `post.comments` already loaded, you could add the call to `{{assert-must-preload}}`: + For example, if you wanted to ensure the following template was never rendered without `post.comments` already loaded, you could add the call to ``: ```hbs - {{assert-must-preload post 'comments.author'}} + {{!-- the rest of your template --}} - {{#each post.comments as |comment|}} + {{#each this.post.comments as |comment|}} This comment was written by {{comment.author.name}} {{/each}} ``` @@ -25,17 +26,18 @@ import Component from '@ember/component'; @class AssertMustPreload @public */ -export default Component.extend({ - tagName: '', + constructor() { + super(...arguments); - didReceiveAttrs() { - let [ model, ...includes ] = this.get('args'); + const { model, includes } = this.args; let parentComponent = this.parentView; - let parentName = parentComponent ? parentComponent._debugContainerKey : 'template'; + let parentName = parentComponent + ? parentComponent._debugContainerKey + : 'template'; let includesString = includes.join(','); assert( - `You passed a ${model.constructor.modelName} model into an {{assert-must-preload}}, but that model is not using the Loadable mixin. [ember-data-storefront]`, + `You passed a ${model.constructor.modelName} model into an , but that model is not using the Loadable mixin. [ember-data-storefront]`, model.hasLoaded ); @@ -43,12 +45,5 @@ export default Component.extend({ `You tried to render a ${parentName} that accesses relationships off of a ${model.constructor.modelName}, but that model didn't have all of its required relationships preloaded ('${includesString}'). Please make sure to preload the association. [ember-data-storefront]`, model.hasLoaded(includesString) ); - - return this._super(...arguments); } - -}).reopenClass({ - - positionalParams: 'args' - -}); +} diff --git a/addon/instance-initializers/inject-storefront.js b/addon/instance-initializers/inject-storefront.js index 6ed9a1bc..baaa22a2 100644 --- a/addon/instance-initializers/inject-storefront.js +++ b/addon/instance-initializers/inject-storefront.js @@ -6,5 +6,5 @@ export function initialize(appInstance) { export default { name: 'inject-storefront', after: 'mixin-storefront', - initialize + initialize, }; diff --git a/addon/instance-initializers/mixin-storefront.js b/addon/instance-initializers/mixin-storefront.js index e4e82411..bb050271 100644 --- a/addon/instance-initializers/mixin-storefront.js +++ b/addon/instance-initializers/mixin-storefront.js @@ -9,5 +9,5 @@ export function initialize(appInstance) { export default { name: 'mixin-storefront', after: 'ember-data', - initialize + initialize, }; diff --git a/addon/mixins/fastboot-adapter.js b/addon/mixins/fastboot-adapter.js index ad3cb59d..b63afc88 100644 --- a/addon/mixins/fastboot-adapter.js +++ b/addon/mixins/fastboot-adapter.js @@ -3,7 +3,10 @@ import Mixin from '@ember/object/mixin'; import { inject as service } from '@ember/service'; import { resolve } from 'rsvp'; -import { cacheKey, shoeboxize } from 'ember-data-storefront/-private/utils/get-key'; +import { + cacheKey, + shoeboxize, +} from 'ember-data-storefront/-private/utils/get-key'; import { getOwner } from '@ember/application'; /** This mixin adds fastboot support to your data adapter. It provides no @@ -39,41 +42,58 @@ export default Mixin.create({ ajax(url, type, options = {}) { let cachedPayload = this._getStorefrontBoxedQuery(type, url, options.data); - let maybeAddToShoebox = this._makeStorefrontQueryBoxer(type, url, options.data); - - return cachedPayload ? - resolve(JSON.parse(cachedPayload)) : - this._super(...arguments).then(maybeAddToShoebox); + let maybeAddToShoebox = this._makeStorefrontQueryBoxer( + type, + url, + options.data + ); + + return cachedPayload + ? resolve(JSON.parse(cachedPayload)) + : this._super(...arguments).then(maybeAddToShoebox); }, _makeStorefrontQueryBoxer(type, url, params) { - let fastboot = this.get('fastboot'); - let isFastboot = fastboot && fastboot.get('isFastBoot'); - let cache = this.get('storefront.fastbootDataRequests'); + const { fastboot } = this; + let isFastboot = fastboot && fastboot.isFastBoot; + let cache = this.storefront.fastbootDataRequests; - return function(response) { + return function (response) { if (isFastboot) { - let key = shoeboxize(cacheKey([type, url.replace(/^.*\/\/[^\/]+/, ''), params])); + let key = shoeboxize( + cacheKey([type, url.replace(/^.*\/\/[^\/]+/, ''), params]) + ); cache[key] = JSON.stringify(response); } return response; - } + }; }, _getStorefrontBoxedQuery(type, url, params) { let payload; - let fastboot = this.get('fastboot'); - let isFastboot = fastboot && fastboot.get('isFastBoot'); - let shoebox = fastboot && fastboot.get('shoebox'); + const { fastboot } = this; + let isFastboot = fastboot && fastboot.isFastBoot; + let shoebox = fastboot && fastboot.shoebox; let box = shoebox && shoebox.retrieve('ember-data-storefront'); const config = getOwner(this).resolveRegistration('config:environment'); - const maxAgeMinutes = config.storefront ? config.storefront.maxAge : undefined; - - if (!isFastboot && box && box.queries && Object.keys(box.queries).length > 0) { - const shouldUseShoebox = maxAgeMinutes === undefined || this.isDateValid(box.created, maxAgeMinutes); - let key = shoeboxize(cacheKey([type, url.replace(/^.*\/\/[^\/]+/, ''), params])); + const maxAgeMinutes = config.storefront + ? config.storefront.maxAge + : undefined; + + if ( + !isFastboot && + box && + box.queries && + Object.keys(box.queries).length > 0 + ) { + const shouldUseShoebox = + maxAgeMinutes === undefined || + this.isDateValid(box.created, maxAgeMinutes); + let key = shoeboxize( + cacheKey([type, url.replace(/^.*\/\/[^\/]+/, ''), params]) + ); if (shouldUseShoebox) { payload = box.queries[key]; @@ -86,5 +106,5 @@ export default Mixin.create({ isDateValid(createdString, maxAgeMinutes) { return (new Date() - new Date(createdString)) / 1000 / 60 < maxAgeMinutes; - } -}) + }, +}); diff --git a/addon/mixins/loadable-model.js b/addon/mixins/loadable-model.js index a1b8b7d6..1c37ffe1 100644 --- a/addon/mixins/loadable-model.js +++ b/addon/mixins/loadable-model.js @@ -1,6 +1,5 @@ import Mixin from '@ember/object/mixin'; -import { deprecate } from '@ember/debug' -import { assert } from '@ember/debug'; +import { deprecate, assert } from '@ember/debug'; import { resolve } from 'rsvp'; import { isArray } from '@ember/array'; import { get } from '@ember/object'; @@ -33,7 +32,6 @@ import { camelize } from '@ember/string'; @public */ export default Mixin.create({ - init() { this._super(...arguments); this.set('_loadedReferences', {}); @@ -49,7 +47,7 @@ export default Mixin.create({ return this.sideload(...args); }, - /** + /** `sideload` gives you an explicit way to asynchronously sideload related data. ```js @@ -101,16 +99,16 @@ export default Mixin.create({ if (typeof possibleOptions === 'string') { options = { - include: args.join(',') + include: args.join(','), }; } else { options = { ...possibleOptions, - ...{ include: args.slice(0,-1).join(',') } + ...{ include: args.slice(0, -1).join(',') }, }; } - return this.get('store').loadRecord(modelName, this.get('id'), options); + return this.store.loadRecord(modelName, this.id, options); }, /** @@ -168,7 +166,7 @@ export default Mixin.create({ } } - return promise.then(data => { + 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; @@ -193,7 +191,9 @@ export default Mixin.create({ @private */ _getRelationshipInfo(name) { - let relationshipInfo = get(this.constructor, `relationshipsByName`).get(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]`, @@ -251,7 +251,7 @@ export default Mixin.create({ if (info.kind === 'hasMany') { models = reference.value() || []; } else if (info.kind === 'belongsTo') { - models = reference.value() ? [ reference.value() ] : []; + models = reference.value() ? [reference.value()] : []; } return models; @@ -267,7 +267,7 @@ export default Mixin.create({ @private */ trackLoadedIncludes(includes) { - includes.split(",").forEach(path => this._trackLoadedIncludePath(path)); + includes.split(',').forEach((path) => this._trackLoadedIncludePath(path)); }, /** @@ -283,7 +283,7 @@ export default Mixin.create({ @private */ _trackLoadedIncludePath(path) { - let [firstInclude, ...rest] = path.split("."); + let [firstInclude, ...rest] = path.split('.'); let relationship = camelize(firstInclude); if (this._hasNamedRelationship(relationship)) { @@ -291,8 +291,8 @@ export default Mixin.create({ if (rest.length) { this._getRelationshipModels(relationship) - .filter(model => model.trackLoadedIncludes) - .forEach(model => model.trackLoadedIncludes(rest.join('.'))); + .filter((model) => model.trackLoadedIncludes) + .forEach((model) => model.trackLoadedIncludes(rest.join('.'))); } } }, @@ -308,9 +308,7 @@ export default Mixin.create({ @private */ _graphHasLoaded(includes) { - return includes - .split(",") - .every(path => this._graphHasLoadedPath(path)); + return includes.split(',').every((path) => this._graphHasLoadedPath(path)); }, /** @@ -322,19 +320,20 @@ export default Mixin.create({ @private */ _graphHasLoadedPath(includePath) { - let [firstInclude, ...rest] = includePath.split("."); + let [firstInclude, ...rest] = includePath.split('.'); let relationship = camelize(firstInclude); let reference = this._getReference(relationship); let hasLoaded = reference && this._hasLoadedReference(relationship); if (rest.length === 0) { return hasLoaded; - } else { let models = this._getRelationshipModels(relationship); - let childrenHaveLoaded = models.every(model => { - return model.trackLoadedIncludes && model._graphHasLoaded(rest.join(".")); + let childrenHaveLoaded = models.every((model) => { + return ( + model.trackLoadedIncludes && model._graphHasLoaded(rest.join('.')) + ); }); return hasLoaded && childrenHaveLoaded; @@ -363,8 +362,12 @@ export default Mixin.create({ */ hasLoaded(includesString) { let modelName = this.constructor.modelName; - return this.get('store').hasLoadedIncludesForRecord(modelName, this.get('id'), includesString) || - this._graphHasLoaded(includesString); - } - + return ( + this.store.hasLoadedIncludesForRecord( + modelName, + this.id, + includesString + ) || this._graphHasLoaded(includesString) + ); + }, }); diff --git a/addon/mixins/loadable-store.js b/addon/mixins/loadable-store.js index 14b0dc40..5dbf519e 100644 --- a/addon/mixins/loadable-store.js +++ b/addon/mixins/loadable-store.js @@ -1,5 +1,5 @@ import Mixin from '@ember/object/mixin'; -import { deprecate } from '@ember/debug' +import { deprecate } from '@ember/debug'; import { resolve } from 'rsvp'; import Coordinator from 'ember-data-storefront/-private/coordinator'; @@ -12,7 +12,6 @@ import Coordinator from 'ember-data-storefront/-private/coordinator'; @public */ export default Mixin.create({ - init() { this._super(...arguments); @@ -23,7 +22,7 @@ export default Mixin.create({ `loadRecords` can be used in place of `store.query` to fetch a collection of records for the given type and options. ```diff - this.get('store') + this.store - .query('post', { filter: { popular: true } }) + .loadRecords('post', { filter: { popular: true } }) .then(models => models); @@ -53,17 +52,17 @@ export default Mixin.create({ @return {Promise} a promise resolving with the record array @public */ - loadRecords(type, options={}) { + loadRecords(type, options = {}) { let query = this.coordinator.recordArrayQueryFor(type, options); let shouldBlock = options.reload || !query.value; - let shouldBackgroundReload = (options.backgroundReload !== undefined) ? options.backgroundReload : true; + let shouldBackgroundReload = + options.backgroundReload !== undefined ? options.backgroundReload : true; let promise; let fetcher; if (shouldBlock) { promise = query.run(); fetcher = promise; - } else { promise = resolve(query.value); @@ -89,7 +88,7 @@ export default Mixin.create({ `loadRecord` can be used in place of `store.findRecord` to fetch a single record for the given type, id and options. ```diff - this.get('store') + this.store - .findRecord('post', 1, { include: 'comments' }) + .loadRecord('post', 1, { include: 'comments' }) .then(post => post); @@ -99,10 +98,10 @@ export default Mixin.create({ ```js // simple fetch - this.get('store').loadRecord('post', 1); + this.store.loadRecord('post', 1); // includes - this.get('store').loadRecord('post', 1, { include: 'comments' }); + this.store.loadRecord('post', 1, { include: 'comments' }); ``` This solves many common bugs where `findRecord` would return immediately, even if important `includes` had never been loaded. @@ -123,15 +122,15 @@ export default Mixin.create({ @return {Promise} a promise resolving with the record array @public */ - loadRecord(type, id, options={}) { + loadRecord(type, id, options = {}) { let query = this.coordinator.recordQueryFor(type, id, options); let shouldBlock = options.reload || !query.value; - let shouldBackgroundReload = (options.backgroundReload !== undefined) ? options.backgroundReload : true; + let shouldBackgroundReload = + options.backgroundReload !== undefined ? options.backgroundReload : true; let promise; if (shouldBlock) { promise = query.run(); - } else { promise = resolve(query.value); @@ -149,7 +148,7 @@ export default Mixin.create({ Lets you check whether you've ever loaded related data for a model. ```js - this.get('store').hasLoadedIncludesForRecord('post', '1', 'comments.author'); + this.store.hasLoadedIncludesForRecord('post', '1', 'comments.author'); ``` @method hasLoadedIncludesForRecord @@ -169,6 +168,5 @@ export default Mixin.create({ */ resetCache() { this.coordinator = new Coordinator(this); - } - + }, }); diff --git a/addon/mixins/loadable.js b/addon/mixins/loadable.js index 4f73ac1f..a7cc0749 100644 --- a/addon/mixins/loadable.js +++ b/addon/mixins/loadable.js @@ -1,16 +1,14 @@ import Mixin from '@ember/object/mixin'; -import { deprecate } from '@ember/debug' +import { deprecate } from '@ember/debug'; import { on } from '@ember/object/evented'; import LoadableModel from './loadable-model'; export default Mixin.create(LoadableModel, { - - showDeprecations: on('init', function() { + showDeprecations: on('init', function () { deprecate( 'The Loadable mixin has been renamed to LoadableMixin. Please change all instances of Loadable in your app to LoadableMixin. Loadable will be removed in 1.0.', false, { id: 'ember-data-storefront.loadable', until: '1.0.0' } ); - }) - + }), }); diff --git a/addon/mixins/snapshottable.js b/addon/mixins/snapshottable.js index 2bd473d2..76272d12 100644 --- a/addon/mixins/snapshottable.js +++ b/addon/mixins/snapshottable.js @@ -3,7 +3,6 @@ import { isArray } from '@ember/array'; import Mixin from '@ember/object/mixin'; export default Mixin.create({ - /* Graph for a post looks like @@ -32,32 +31,45 @@ export default Mixin.create({ } } */ - takeSnapshot(graph={}) { + takeSnapshot(graph = {}) { let snapshot = { model: this, relationships: {} }; - Object.keys(graph).forEach(key => { + Object.keys(graph).forEach((key) => { let node = graph[key]; let relationship = this.get(key); if (isArray(relationship)) { - snapshot.relationships[key] = relationship.map(model => ({ model, relationships: {} })); + snapshot.relationships[key] = relationship.map((model) => ({ + model, + relationships: {}, + })); } else { - snapshot.relationships[key] = { model: relationship, relationships: {} }; + snapshot.relationships[key] = { + model: relationship, + relationships: {}, + }; } // call all this recursively instead if (typeof node === 'object') { - Object.keys(node).forEach(subkey => { + Object.keys(node).forEach((subkey) => { let namedRelationshipMeta = snapshot.relationships[key]; if (namedRelationshipMeta) { if (isArray(namedRelationshipMeta)) { - namedRelationshipMeta.forEach(relationshipSnapshot => { + namedRelationshipMeta.forEach((relationshipSnapshot) => { let nestedRelationship = relationshipSnapshot.model.get(subkey); if (isArray(nestedRelationship)) { - relationshipSnapshot.relationships[subkey] = nestedRelationship.map(model => ({ model, relationships: {} })); + relationshipSnapshot.relationships[subkey] = + nestedRelationship.map((model) => ({ + model, + relationships: {}, + })); } else { - relationshipSnapshot.relationships[subkey] = { model: nestedRelationship, relationships: {} }; + relationshipSnapshot.relationships[subkey] = { + model: nestedRelationship, + relationships: {}, + }; } // check the node (would be handled by recursive call) @@ -67,9 +79,16 @@ export default Mixin.create({ let nestedRelationship = namedRelationshipMeta.model.get(subkey); if (isArray(nestedRelationship)) { - namedRelationshipMeta.relationships[subkey] = nestedRelationship.map(model => ({ model, relationships: {} })); + namedRelationshipMeta.relationships[subkey] = + nestedRelationship.map((model) => ({ + model, + relationships: {}, + })); } else { - namedRelationshipMeta.relationships[subkey] = { model: nestedRelationship, relationships: {} }; + namedRelationshipMeta.relationships[subkey] = { + model: nestedRelationship, + relationships: {}, + }; } } } @@ -106,15 +125,21 @@ export default Mixin.create({ restoreSnapshot(snapshot) { snapshot.model && snapshot.model.rollbackAttributes(); - Object.keys(snapshot.relationships).forEach(key => { + Object.keys(snapshot.relationships).forEach((key) => { let relationshipSnapshot = snapshot.relationships[key]; if (isArray(relationshipSnapshot)) { - this.set(key, relationshipSnapshot.map(meta => meta.model)); - relationshipSnapshot.forEach(rSnapshot => { + this.set( + key, + relationshipSnapshot.map((meta) => meta.model) + ); + relationshipSnapshot.forEach((rSnapshot) => { let model = rSnapshot.model; model.rollbackAttributes(); if (Object.keys(rSnapshot.relationships).length) { - assert(`You're trying to restore a snapshot on a ${model._debugContainerKey} but that model isn't snapshottable. Be sure to include the Snapshottable mixin.`, model.restoreSnapshot !== undefined); + assert( + `You're trying to restore a snapshot on a ${model._debugContainerKey} but that model isn't snapshottable. Be sure to include the Snapshottable mixin.`, + model.restoreSnapshot !== undefined + ); model.restoreSnapshot(rSnapshot); } }); @@ -128,11 +153,13 @@ export default Mixin.create({ } if (Object.keys(relationshipSnapshot.relationships).length) { - assert(`You're trying to restore a snapshot on a ${model._debugContainerKey} but that model isn't snapshottable. Be sure to include the Snapshottable mixin.`, model.restoreSnapshot !== undefined); + assert( + `You're trying to restore a snapshot on a ${model._debugContainerKey} but that model isn't snapshottable. Be sure to include the Snapshottable mixin.`, + model.restoreSnapshot !== undefined + ); model.restoreSnapshot(relationshipSnapshot); } } }); - } - + }, }); diff --git a/addon/services/store.js b/addon/services/store.js new file mode 100644 index 00000000..9891c5e5 --- /dev/null +++ b/addon/services/store.js @@ -0,0 +1,172 @@ +import Store from '@ember-data/store'; +import { deprecate } from '@ember/debug'; +import { resolve } from 'rsvp'; +import Coordinator from 'ember-data-storefront/-private/coordinator'; + +/** + This service adds new data-loading methods to Ember Data's store. + + It is automatically mixed into your application's store when you install the addon. + + @class LoadableStore + @public +*/ +export default class LoadableStore extends Store { + constructor() { + super(...arguments); + + this.resetCache(); + } + + /** + `loadRecords` can be used in place of `store.query` to fetch a collection of records for the given type and options. + + ```diff + this.store + - .query('post', { filter: { popular: true } }) + + .loadRecords('post', { filter: { popular: true } }) + .then(models => models); + ``` + + `loadRecords` caches based on the query you provide, so each of the following examples would return a blocking promise the first time they are called, and instantly resolve from the cache thereafter. + + ```js + // filters + store.loadRecords('post', { filter: { popular: true }}); + + // pagination + store.loadRecords('post', { page: { limit: 10, offset: 0 }}); + + // includes + store.loadRecords('post', { include: 'comments' }); + + // force an already loaded set to reload (blocking promise) + store.loadRecords('post', { reload: true }); + ``` + + In most cases, `loadRecords` should be a drop-in replacement for `query` that eliminates bugs and improves your app's caching. + + @method loadRecords + @param {String} type type of model to load + @param {Object} options (optional) a hash of options + @return {Promise} a promise resolving with the record array + @public + */ + loadRecords(type, options = {}) { + let query = this.coordinator.recordArrayQueryFor(type, options); + let shouldBlock = options.reload || !query.value; + let shouldBackgroundReload = + options.backgroundReload !== undefined ? options.backgroundReload : true; + let promise; + let fetcher; + + if (shouldBlock) { + promise = query.run(); + fetcher = promise; + } else { + promise = resolve(query.value); + + fetcher = shouldBackgroundReload ? query.run() : resolve(); + } + + fetcher.then(() => query.trackIncludes()); + + return promise; + } + + loadAll(...args) { + deprecate( + 'loadAll has been renamed to loadRecords. Please change all instances of loadAll in your app to loadRecords. loadAll will be removed in 1.0.', + false, + { id: 'ember-data-storefront.loadAll', until: '1.0.0' } + ); + + return this.loadRecords(...args); + } + + /** + `loadRecord` can be used in place of `store.findRecord` to fetch a single record for the given type, id and options. + + ```diff + this.store + - .findRecord('post', 1, { include: 'comments' }) + + .loadRecord('post', 1, { include: 'comments' }) + .then(post => post); + ``` + + `loadRecord` caches based on the query you provide, so each of the following examples would return a blocking promise the first time they are called, and synchronously resolve from the cache thereafter. + + ```js + // simple fetch + this.store.loadRecord('post', 1); + + // includes + this.store.loadRecord('post', 1, { include: 'comments' }); + ``` + + This solves many common bugs where `findRecord` would return immediately, even if important `includes` had never been loaded. + + Similar to `store.findRecord`, you can force a query to reload using `reload: true`: + + ``` + // force an already loaded set to reload (blocking promise) + store.loadRecord('post', 1, { reload: true }); + ``` + + In most cases, `loadRecord` should be a drop-in replacement for `findRecord` that eliminates bugs and improves your app's caching. + + @method loadRecord + @param {String} type type of model to load + @param {Number} id id of model to load + @param {Object} options (optional) a hash of options + @return {Promise} a promise resolving with the record array + @public + */ + loadRecord(type, id, options = {}) { + let query = this.coordinator.recordQueryFor(type, id, options); + let shouldBlock = options.reload || !query.value; + let shouldBackgroundReload = + options.backgroundReload !== undefined ? options.backgroundReload : true; + let promise; + + if (shouldBlock) { + promise = query.run(); + } else { + promise = resolve(query.value); + + if (shouldBackgroundReload) { + query.run(); + } + } + + return promise; + } + + /** + _This method relies on JSON:API, and assumes that your server supports JSON:API includes._ + + Lets you check whether you've ever loaded related data for a model. + + ```js + this.store.hasLoadedIncludesForRecord('post', '1', 'comments.author'); + ``` + + @method hasLoadedIncludesForRecord + @param {String} type type of model to check + @param {Number} id id of model to check + @param {String} includesString a JSON:API includes string representing the relationships to check + @return {Boolean} whether the includesString has been loaded + @public + */ + hasLoadedIncludesForRecord(type, id, includesString) { + return this.coordinator.recordHasIncludes(type, id, includesString); + } + + /** + @method resetCache + @private + */ + resetCache() { + this.coordinator = new Coordinator(this); + } +} diff --git a/addon/services/storefront.js b/addon/services/storefront.js index 1b7bb563..b6917006 100644 --- a/addon/services/storefront.js +++ b/addon/services/storefront.js @@ -1,17 +1,18 @@ import Service, { inject as service } from '@ember/service'; -import { deprecate } from '@ember/debug' +import { deprecate } from '@ember/debug'; +import { tracked } from '@glimmer/tracking'; // do not delete this service! it's being used to communicte cached payloads // between the client and the browser -export default Service.extend({ - store: service(), +export default class StorefrontService extends Service { + @service store; - fastbootDataRequests: null, + @tracked fastbootDataRequests = null; - init() { - this._super(...arguments); - this.set('fastbootDataRequests', {}); - }, + constructor() { + super(...arguments); + this.fastbootDataRequests = {}; + } findAll() { deprecate( @@ -20,8 +21,8 @@ export default Service.extend({ { id: 'ember-data-storefront.storefront-find-all', until: '1.0.0' } ); - return this.get('store').loadAll(...arguments); - }, + return this.store.loadAll(...arguments); + } loadAll() { deprecate( @@ -30,8 +31,8 @@ export default Service.extend({ { id: 'ember-data-storefront.storefront-load-all', until: '1.0.0' } ); - return this.get('store').loadAll(...arguments); - }, + return this.store.loadAll(...arguments); + } findRecord() { deprecate( @@ -40,8 +41,8 @@ export default Service.extend({ { id: 'ember-data-storefront.storefront-find-record', until: '1.0.0' } ); - return this.get('store').findRecord(...arguments); - }, + return this.store.findRecord(...arguments); + } loadRecord() { deprecate( @@ -50,18 +51,21 @@ export default Service.extend({ { id: 'ember-data-storefront.storefront-load-record', until: '1.0.0' } ); - return this.get('store').findRecord(...arguments); - }, + return this.store.findRecord(...arguments); + } hasLoadedIncludesForRecord() { deprecate( 'The storefront service has been deprecated, please use store.hasLoadedIncludesForRecord instead. Will be removed in 1.0.', false, - { id: 'ember-data-storefront.storefront-has-loaded-includes-for-record', until: '1.0.0' } + { + id: 'ember-data-storefront.storefront-has-loaded-includes-for-record', + until: '1.0.0', + } ); - return this.get('store').hasLoadedIncludesForRecord(...arguments); - }, + return this.store.hasLoadedIncludesForRecord(...arguments); + } resetCache() { deprecate( @@ -70,7 +74,6 @@ export default Service.extend({ { id: 'ember-data-storefront.storefront-reset-cache', until: '1.0.0' } ); - return this.get('store').resetCache(...arguments); + return this.store.resetCache(...arguments); } - -}); +} diff --git a/app/instance-initializers/inject-storefront.js b/app/instance-initializers/inject-storefront.js index cc377974..dae8639b 100644 --- a/app/instance-initializers/inject-storefront.js +++ b/app/instance-initializers/inject-storefront.js @@ -1 +1,4 @@ -export { default, initialize } from 'ember-data-storefront/instance-initializers/inject-storefront'; +export { + default, + initialize, +} from 'ember-data-storefront/instance-initializers/inject-storefront'; diff --git a/app/instance-initializers/mixin-storefront.js b/app/instance-initializers/mixin-storefront.js index d8cc4ac1..08f65bec 100644 --- a/app/instance-initializers/mixin-storefront.js +++ b/app/instance-initializers/mixin-storefront.js @@ -1 +1,4 @@ -export { default, initialize } from 'ember-data-storefront/instance-initializers/mixin-storefront'; +export { + default, + initialize, +} from 'ember-data-storefront/instance-initializers/mixin-storefront'; diff --git a/app/services/store.js b/app/services/store.js new file mode 100644 index 00000000..8297e808 --- /dev/null +++ b/app/services/store.js @@ -0,0 +1 @@ +export { default } from 'ember-data-storefront/services/store'; diff --git a/app/transitions.js b/app/transitions.js index f1300faf..9e8fa5de 100644 --- a/app/transitions.js +++ b/app/transitions.js @@ -1,4 +1,4 @@ -export default function(){ +export default function () { // Add your transitions here, like: // this.transition( // this.fromRoute('people.index'), diff --git a/config/addon-docs.js b/config/addon-docs.js index 6feb2fd9..28590a13 100644 --- a/config/addon-docs.js +++ b/config/addon-docs.js @@ -4,69 +4,6 @@ const AddonDocsConfig = require('ember-cli-addon-docs/lib/config'); module.exports = class extends AddonDocsConfig { - /* - Return a boolean indicating whether or not the current deploy should - actually run. The `info` parameter contains details about the most recent - git commit. Note that you can also access any configured environment - variables via `process.ENV`. - - info.branch => the current branch or `null` if not on a branch - info.sha => the current sha - info.abbreviatedSha => the first 10 chars of the current sha - info.tag => the tag for the current sha or `null` if none exists - info.committer => the committer for the current sha - info.committerDate => the commit date for the current sha - info.author => the author for the current sha - info.authorDate => the authored date for the current sha - info.commitMessage => the commit message for the current sha - - Note that CI providers typically check out a specific commit hash rather - than a named branch, so `info.branch` may not be available in CI builds. - */ - shouldDeploy(info) { - /* - For example, you might configure your CI builds to execute `ember deploy` - at the end of each successful run, but you may only want to actually - deploy builds on the `master` branch with the default ember-try scenario. - To accomplish that on Travis, you could write: - - return process.env.TRAVIS_BRANCH === 'master' - && process.env.EMBER_TRY_SCENARIO == 'ember-default'; - */ - return super.shouldDeploy(info); - } - - /* - Return a string indicating a subdirectory in the gh-pages branch you want - to deploy to, or nothing to deploy to the root. This hook receives the same - info object as `shouldDeploy` above. - */ - deployDirectory(info) { - /* - For example, to deploy a permalink-able copy of your docs site any time - you tag a release, you could write: - - return info.tag ? `tags/${info.tag}` : 'master'; - */ - return super.deployDirectory(info); - } - - /* - By default, the folder returned by `deployDirectory()` above will be - emptied out before a new revision of the docs application is written there. - - To retain certain files across deploys, return an array of file paths or - globs, relative to the deploy directory, indicating files/directories that - should not be removed before deploying. - */ - preservedPaths(info) { - /* - For example, if you had static JSON in your gh-pages branch powering - something like a blog UI that you want to manage separately from your - app deploys, you might write: - - return ['blog-posts/*.json', ...super.preservedPaths(info)]; - */ - return super.preservedPaths(info); - } + // See https://ember-learn.github.io/ember-cli-addon-docs/docs/deploying + // for details on configuration you can override here. }; diff --git a/config/deploy.js b/config/deploy.js index c3119ab8..9ff76f5a 100644 --- a/config/deploy.js +++ b/config/deploy.js @@ -1,12 +1,12 @@ /* eslint-env node */ 'use strict'; -module.exports = function(deployTarget) { +module.exports = function (deployTarget) { let ENV = { build: {}, git: { - repo: 'git@github.com:embermap/ember-data-storefront.git' - } + repo: 'git@github.com:embermap/ember-data-storefront.git', + }, }; if (deployTarget === 'development') { diff --git a/config/ember-try.js b/config/ember-try.js index e6c09bf9..c897e844 100644 --- a/config/ember-try.js +++ b/config/ember-try.js @@ -1,74 +1,74 @@ -"use strict"; +'use strict'; -const getChannelURL = require("ember-source-channel-url"); +const getChannelURL = require('ember-source-channel-url'); -module.exports = function() { +module.exports = function () { return Promise.all([ - getChannelURL("release"), - getChannelURL("beta"), - getChannelURL("canary") - ]).then(urls => { + getChannelURL('release'), + getChannelURL('beta'), + getChannelURL('canary'), + ]).then((urls) => { return { useYarn: true, scenarios: [ { - name: "ember-lts-3.12", + name: 'ember-lts-3.12', npm: { devDependencies: { - "ember-source": "~3.12.0", - "ember-data": "~3.12.0" + 'ember-source': '~3.12.0', + 'ember-data': '~3.12.0', }, resolutions: { - "ember-data": "~3.12.0" - } - } + 'ember-data': '~3.12.0', + }, + }, }, { - name: "ember-lts-3.16", + name: 'ember-lts-3.16', npm: { devDependencies: { - "ember-source": "~3.16.0", - "ember-data": "~3.16.0" + 'ember-source': '~3.16.0', + 'ember-data': '~3.16.0', }, resolutions: { - "ember-data": "~3.16.0" - } - } + 'ember-data': '~3.16.0', + }, + }, }, { - name: "ember-release", + name: 'ember-release', npm: { devDependencies: { - "ember-source": urls[0], - "ember-data": "latest" - } - } + 'ember-source': urls[0], + 'ember-data': 'latest', + }, + }, }, { - name: "ember-beta", + name: 'ember-beta', npm: { devDependencies: { - "ember-source": urls[1], - "ember-data": "beta" - } - } + 'ember-source': urls[1], + 'ember-data': 'beta', + }, + }, }, { - name: "ember-canary", + name: 'ember-canary', npm: { devDependencies: { - "ember-source": urls[2], - "ember-data": "canary" - } - } + 'ember-source': urls[2], + 'ember-data': 'canary', + }, + }, }, { - name: "ember-default", + name: 'ember-default', npm: { - devDependencies: {} - } - } - ] + devDependencies: {}, + }, + }, + ], }; }); }; diff --git a/config/environment.js b/config/environment.js index 0dfaed47..331ab30d 100644 --- a/config/environment.js +++ b/config/environment.js @@ -1,5 +1,5 @@ 'use strict'; -module.exports = function(/* environment, appConfig */) { - return { }; +module.exports = function (/* environment, appConfig */) { + return {}; }; diff --git a/ember-cli-build.js b/ember-cli-build.js index eeaed7d1..47e9fe9a 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -2,14 +2,11 @@ const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); -module.exports = function(defaults) { +module.exports = function (defaults) { let app = new EmberAddon(defaults, { svgJar: { - sourceDirs: [ - 'public', - 'tests/dummy/public' - ] - } + sourceDirs: ['public', 'tests/dummy/public'], + }, }); /* @@ -19,5 +16,12 @@ module.exports = function(defaults) { behave. You most likely want to be modifying `./index.js` or app's build file */ - return app.toTree(); + const { maybeEmbroider } = require('@embroider/test-setup'); + return maybeEmbroider(app, { + skipBabel: [ + { + package: 'qunit', + }, + ], + }); }; diff --git a/fastboot-tests/adapter-test.js b/fastboot-tests/adapter-test.js index 4b152c51..e79efdaf 100644 --- a/fastboot-tests/adapter-test.js +++ b/fastboot-tests/adapter-test.js @@ -3,7 +3,7 @@ const FastBoot = require('fastboot'); const { execFileSync } = require('child_process'); const { module: Qmodule, test } = require('qunit'); -const jsdom = require("jsdom"); +const jsdom = require('jsdom'); const { JSDOM } = jsdom; const postsRouter = require('../server/mocks/posts'); const express = require('express'); @@ -12,17 +12,17 @@ const express = require('express'); execFileSync('node', ['./node_modules/.bin/ember', 'build']); let visitOptions = { - request: { headers: { host: 'localhost:4201' } } + request: { headers: { host: 'localhost:4201' } }, }; -Qmodule('Fastboot', function(hooks) { +Qmodule('Fastboot', function (hooks) { let fastboot; let server; - hooks.before(async function() { + hooks.before(async function () { fastboot = new FastBoot({ distPath: 'dist', - resilient: false + resilient: false, }); let app = express(); @@ -30,27 +30,35 @@ Qmodule('Fastboot', function(hooks) { server = app.listen(4201); }); - hooks.after(async function() { + hooks.after(async function () { server.close(); }); - test('A fastboot rendered app should display loadRecords data fetched by the server', async function(assert) { - let page = await fastboot.visit('/fastboot-tests/load-all-posts', visitOptions); + test('A fastboot rendered app should display loadRecords data fetched by the server', async function (assert) { + let page = await fastboot.visit( + '/fastboot-tests/load-all-posts', + visitOptions + ); let html = await page.html(); let dom = new JSDOM(html); - let post1 = dom.window.document.querySelector('[data-test-id=post-title-1]'); + let post1 = dom.window.document.querySelector( + '[data-test-id=post-title-1]' + ); assert.equal(post1.textContent.trim(), 'Hello from Ember CLI HTTP Mocks'); }); - test('A fastboot rendered app should put storefront loadRecords queries in the shoebox', async function(assert) { - let page = await fastboot.visit('/fastboot-tests/load-all-posts', visitOptions); + test('A fastboot rendered app should put storefront loadRecords queries in the shoebox', async function (assert) { + let page = await fastboot.visit( + '/fastboot-tests/load-all-posts', + visitOptions + ); let html = await page.html(); let dom = new JSDOM(html); - let shoebox = dom.window.document - .querySelector('#shoebox-ember-data-storefront') - .textContent; + let shoebox = dom.window.document.querySelector( + '#shoebox-ember-data-storefront' + ).textContent; let cache = JSON.parse(shoebox); let keys = Object.keys(cache.queries); @@ -59,8 +67,11 @@ Qmodule('Fastboot', function(hooks) { assert.ok(cache.queries['GET::/posts::filter[popular]=true']); }); - test('A fastboot rendered app should display loadRecord data fetched by the server', async function(assert) { - let page = await fastboot.visit('/fastboot-tests/load-record-post/1', visitOptions); + test('A fastboot rendered app should display loadRecord data fetched by the server', async function (assert) { + let page = await fastboot.visit( + '/fastboot-tests/load-record-post/1', + visitOptions + ); let html = await page.html(); let dom = new JSDOM(html); let post1 = dom.window.document.querySelector('[data-test-id=post-title]'); @@ -68,14 +79,17 @@ Qmodule('Fastboot', function(hooks) { assert.equal(post1.textContent.trim(), 'Hello from Ember CLI HTTP Mocks'); }); - test('A fastboot rendered app should put storefront loadRecords queries in the shoebox', async function(assert) { - let page = await fastboot.visit('/fastboot-tests/load-record-post/1', visitOptions); + test('A fastboot rendered app should put storefront loadRecords queries in the shoebox', async function (assert) { + let page = await fastboot.visit( + '/fastboot-tests/load-record-post/1', + visitOptions + ); let html = await page.html(); let dom = new JSDOM(html); - let shoebox = dom.window.document - .querySelector('#shoebox-ember-data-storefront') - .textContent; + let shoebox = dom.window.document.querySelector( + '#shoebox-ember-data-storefront' + ).textContent; let cache = JSON.parse(shoebox); let keys = Object.keys(cache.queries); @@ -84,23 +98,31 @@ Qmodule('Fastboot', function(hooks) { assert.ok(cache.queries['GET::/posts/1']); }); - test('A fastboot rendered app should display findAll data fetched by the server', async function(assert) { - let page = await fastboot.visit('/fastboot-tests/find-all-posts', visitOptions); + test('A fastboot rendered app should display findAll data fetched by the server', async function (assert) { + let page = await fastboot.visit( + '/fastboot-tests/find-all-posts', + visitOptions + ); let html = await page.html(); let dom = new JSDOM(html); - let post1 = dom.window.document.querySelector('[data-test-id=post-title-1]'); + let post1 = dom.window.document.querySelector( + '[data-test-id=post-title-1]' + ); assert.equal(post1.textContent.trim(), 'Hello from Ember CLI HTTP Mocks'); }); - test('A fastboot rendered app should put findAll queries in the shoebox', async function(assert) { - let page = await fastboot.visit('/fastboot-tests/find-all-posts', visitOptions); + test('A fastboot rendered app should put findAll queries in the shoebox', async function (assert) { + let page = await fastboot.visit( + '/fastboot-tests/find-all-posts', + visitOptions + ); let html = await page.html(); let dom = new JSDOM(html); - let shoebox = dom.window.document - .querySelector('#shoebox-ember-data-storefront') - .textContent; + let shoebox = dom.window.document.querySelector( + '#shoebox-ember-data-storefront' + ).textContent; let cache = JSON.parse(shoebox); let keys = Object.keys(cache.queries); @@ -108,5 +130,4 @@ Qmodule('Fastboot', function(hooks) { assert.equal(keys.length, 1); assert.ok(cache.queries['GET::/posts::include=comments']); }); - }); diff --git a/fastboot/instance-initializers/ember-data-storefront.js b/fastboot/instance-initializers/ember-data-storefront.js index 7c22d102..547ff988 100644 --- a/fastboot/instance-initializers/ember-data-storefront.js +++ b/fastboot/instance-initializers/ember-data-storefront.js @@ -1,7 +1,5 @@ export function initialize(applicationInstance) { - let shoebox = applicationInstance - .lookup('service:fastboot') - .get('shoebox'); + let shoebox = applicationInstance.lookup('service:fastboot').get('shoebox'); let storefront = applicationInstance.lookup('service:storefront'); @@ -11,11 +9,11 @@ export function initialize(applicationInstance) { }, get queries() { return storefront.get('fastbootDataRequests'); - } + }, }); } export default { name: 'ember-data-storefront', - initialize + initialize, }; diff --git a/index.js b/index.js index e0e3d0f6..bf6468bc 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ 'use strict'; module.exports = { - name: 'ember-data-storefront', + name: require('./package').name, isDevelopingAddon() { return false; @@ -24,7 +24,7 @@ module.exports = { } this.app = app; - this.addonConfig = this.app.project.config(app.env)['ember-data-storefront'] || {}; - } - + this.addonConfig = + this.app.project.config(app.env)['ember-data-storefront'] || {}; + }, }; diff --git a/package.json b/package.json index d9df52f6..0f83b3fd 100644 --- a/package.json +++ b/package.json @@ -5,89 +5,106 @@ "keywords": [ "ember-addon" ], + "homepage": "https://embermap.github.io/ember-data-storefront", "repository": "https://github.com/embermap/ember-data-storefront", "license": "MIT", "author": "", - "directories": { - "doc": "doc", - "test": "tests" - }, "contributors": [ "Sam Selikoff (https://embermap.com)", "Ryan Toronto (https://embermap.com)" ], + "directories": { + "doc": "doc", + "test": "tests" + }, "scripts": { - "build": "ember build", + "build": "ember build --environment=production", + "lint": "npm-run-all --aggregate-output --continue-on-error --parallel \"lint:!(fix)\"", + "lint:fix": "npm-run-all --aggregate-output --continue-on-error --parallel lint:*:fix", "lint:hbs": "ember-template-lint .", - "lint:js": "eslint .", + "lint:hbs:fix": "ember-template-lint . --fix", + "lint:js": "eslint . --cache", + "lint:js:fix": "eslint . --fix", "start": "ember serve", - "test": "ember test", - "test:all": "ember try:each", + "test": "npm-run-all lint test:*", + "test:ember": "ember test", + "test:ember-compatibility": "ember try:each", "test:node": "qunit node-tests/**/*-test.js", "test:fastboot": "qunit fastboot-tests" }, "dependencies": { - "ember-cli-babel": "^7.5.0", - "global": "^4.3.2" + "ember-cli-babel": "^7.26.6", + "ember-cli-htmlbars": "^4.0.1" }, "devDependencies": { "@ember/jquery": "^1.1.0", - "@ember/optional-features": "^1.0.0", + "@ember/optional-features": "^2.0.0", + "@ember/test-helpers": "^2.4.2", + "@embroider/test-setup": "^0.43.5", "@fortawesome/ember-fontawesome": "^0.2.3", "@fortawesome/free-solid-svg-icons": "^5.15.2", + "@glimmer/component": "^1.0.4", + "@glimmer/tracking": "^1.0.4", "babel-eslint": "^10.0.3", "broccoli-asset-rev": "^3.0.0", - "ember-ajax": "^5.0.0", "ember-auto-import": "^1.5.3", - "ember-cli": "~3.16.0", - "ember-cli-addon-docs": "0.10.0", - "ember-cli-addon-docs-yuidoc": "^0.2.3", - "ember-cli-dependency-checker": "^3.0.0", + "ember-cli": "~3.28.0", + "ember-cli-addon-docs": "^3.0.0", + "ember-cli-addon-docs-yuidoc": "^1.0.0", + "ember-cli-dependency-checker": "^3.2.0", "ember-cli-deploy": "^1.0.2", "ember-cli-deploy-build": "^2.0.0", - "ember-cli-deploy-git": "^1.3.0", + "ember-cli-deploy-git": "^1.3.4", "ember-cli-deploy-git-ci": "^1.0.1", - "ember-cli-eslint": "^5.1.0", "ember-cli-fastboot": "^2.0.4", - "ember-cli-htmlbars": "^4.0.1", - "ember-cli-inject-live-reload": "^1.8.2", - "ember-cli-mirage": "1.1.6", - "ember-cli-qunit": "^4.3.2", + "ember-cli-inject-live-reload": "^2.1.0", + "ember-cli-mirage": "^2.2.0", "ember-cli-sass": "^10.0.0", "ember-cli-sri": "^2.1.1", "ember-cli-tachyons-shim": "^4.9.1", - "ember-cli-uglify": "^3.0.0", + "ember-cli-terser": "^4.0.2", "ember-component-css": "^0.7.4", - "ember-concurrency": "^1.0.0", - "ember-data": "~3.16.0", + "ember-concurrency": "^2.1.2", + "ember-data": "~3.28.0", "ember-disable-prototype-extensions": "^1.1.3", "ember-export-application-global": "^2.0.0", "ember-fetch": "^8.0.0", - "ember-load-initializers": "^2.0.0", + "ember-load-initializers": "^2.1.2", "ember-maybe-import-regenerator": "^0.1.6", - "ember-qunit-assert-helpers": "^0.2.1", - "ember-resolver": "^7.0.0", - "ember-router-scroll": "~1.3.2", - "ember-source": "~3.16.0", - "ember-source-channel-url": "^2.0.1", - "ember-test-selectors": "3.0.0", + "ember-page-title": "^6.2.2", + "ember-qunit": "^5.1.4", + "ember-resolver": "^8.0.2", + "ember-router-scroll": "^4.1.1", + "ember-source": "~3.28.0", + "ember-source-channel-url": "^3.0.0", + "ember-template-lint": "^3.7.0", + "ember-test-selectors": "^6.0.0", "ember-try": "^1.4.0", - "eslint-plugin-ember": "^7.1.0", - "eslint-plugin-node": "^11.0.0", + "eslint": "^7.32.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-ember": "^10.5.4", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-qunit": "^6.2.0", "express": "^4.16.4", "glob": "^7.1.4", "jsdom": "^16.0.0", "liquid-fire": "^0.31.0", "loader.js": "^4.7.0", "morgan": "^1.9.1", + "npm-run-all": "^4.1.5", + "prettier": "^2.3.2", + "qunit": "^2.17.0", "qunit-dom": "^1.0.0", "sass": "^1.17.2", - "strip-indent": "^3.0.0", "tachyons": "4.11.1" }, "engines": { "node": "10.* || >= 12.*" }, + "ember": { + "edition": "octane" + }, "ember-addon": { "configPath": "tests/dummy/config" } diff --git a/server/.eslintrc.js b/server/.eslintrc.js index 1147d299..1a4431d8 100644 --- a/server/.eslintrc.js +++ b/server/.eslintrc.js @@ -1,5 +1,5 @@ module.exports = { env: { - node: true - } + node: true, + }, }; diff --git a/server/index.js b/server/index.js index b4b418c6..fd379004 100644 --- a/server/index.js +++ b/server/index.js @@ -10,13 +10,13 @@ // }); // }; -module.exports = function(app) { - const globSync = require('glob').sync; - const mocks = globSync('./mocks/**/*.js', { cwd: __dirname }).map(require); +module.exports = function (app) { + const globSync = require('glob').sync; + const mocks = globSync('./mocks/**/*.js', { cwd: __dirname }).map(require); // Log proxy requests const morgan = require('morgan'); app.use(morgan('dev')); - mocks.forEach(route => route(app)); + mocks.forEach((route) => route(app)); }; diff --git a/server/mocks/posts.js b/server/mocks/posts.js index 20e75650..0b5c4909 100644 --- a/server/mocks/posts.js +++ b/server/mocks/posts.js @@ -1,33 +1,33 @@ /* eslint-env node */ 'use strict'; -module.exports = function(app) { +module.exports = function (app) { const express = require('express'); let postsRouter = express.Router(); - postsRouter.get('/', function(req, res) { + postsRouter.get('/', function (req, res) { res.send({ data: [ { type: 'posts', id: 1, attributes: { - title: 'Hello from Ember CLI HTTP Mocks' - } - } - ] + title: 'Hello from Ember CLI HTTP Mocks', + }, + }, + ], }); }); - postsRouter.get('/1', function(req, res) { + postsRouter.get('/1', function (req, res) { res.send({ data: { type: 'posts', id: 1, attributes: { - title: 'Hello from Ember CLI HTTP Mocks' - } - } + title: 'Hello from Ember CLI HTTP Mocks', + }, + }, }); }); diff --git a/testem.js b/testem.js index 49f53fee..ed2f3712 100644 --- a/testem.js +++ b/testem.js @@ -3,12 +3,8 @@ module.exports = { test_page: 'tests/index.html?hidepassed', disable_watching: true, - launch_in_ci: [ - 'Chrome' - ], - launch_in_dev: [ - 'Chrome' - ], + launch_in_ci: ['Chrome'], + launch_in_dev: ['Chrome'], browser_start_timeout: 120, browser_args: { Chrome: { @@ -20,8 +16,8 @@ module.exports = { '--disable-software-rasterizer', '--mute-audio', '--remote-debugging-port=0', - '--window-size=1440,900' - ].filter(Boolean) - } - } + '--window-size=1440,900', + ].filter(Boolean), + }, + }, }; diff --git a/tests/acceptance/load-all-test.js b/tests/acceptance/load-all-test.js index f4758f5d..ae9902e7 100644 --- a/tests/acceptance/load-all-test.js +++ b/tests/acceptance/load-all-test.js @@ -1,12 +1,10 @@ import { module, test } from 'qunit'; -import { visit, click, find, waitUntil } from "@ember/test-helpers"; +import { visit, click, find, waitUntil } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import { startMirage } from 'dummy/initializers/ember-cli-mirage'; function t(...args) { - return args - .map(arg => `[data-test-id="${arg}"]`) - .join(' '); + return args.map((arg) => `[data-test-id="${arg}"]`).join(' '); } async function domHasChanged(selector) { @@ -15,23 +13,23 @@ async function domHasChanged(selector) { let currentUi = find(selector).textContent; return currentUi !== previousUi; - }) + }); } -module('Acceptance | data fetching docs', function(hooks) { +module('Acceptance | data fetching docs', function (hooks) { let server; setupApplicationTest(hooks); - hooks.beforeEach(function() { + hooks.beforeEach(function () { server = startMirage(); }); - hooks.afterEach(function() { + hooks.afterEach(function () { server.shutdown(); }); - test('data fetching guide', async function(assert) { + test('data fetching guide', async function (assert) { // need our data fetching to be slow for these tests. server.timing = 1000; diff --git a/tests/acceptance/load-relationship-test.js b/tests/acceptance/load-relationship-test.js index c6143a06..7ca5163b 100644 --- a/tests/acceptance/load-relationship-test.js +++ b/tests/acceptance/load-relationship-test.js @@ -3,23 +3,27 @@ import { visit, click } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import startMirage from 'dummy/tests/helpers/start-mirage'; -module('Acceptance | load relationship', function(hooks) { +module('Acceptance | load relationship', function (hooks) { setupApplicationTest(hooks); startMirage(hooks); - test('the load demo works', async function(assert) { + test('the load demo works', async function (assert) { await visit('/docs/guides/working-with-relationships'); await click('[data-test-id=load-comments]'); - assert.dom('[data-test-id=load-comments-count]').hasText('The post has 3 comments.'); + assert + .dom('[data-test-id=load-comments-count]') + .hasText('The post has 3 comments.'); }); - test('the sideload demo works', async function(assert) { + test('the sideload demo works', async function (assert) { await visit('/docs/guides/working-with-relationships'); await click('[data-test-id=sideload-comments]'); - assert.dom('[data-test-id=sideload-comments-count]').hasText('The post has 5 comments.'); + assert + .dom('[data-test-id=sideload-comments-count]') + .hasText('The post has 5 comments.'); }); }); diff --git a/tests/dummy/app/adapters/post.js b/tests/dummy/app/adapters/post.js index 87cf3b3b..fec94e45 100644 --- a/tests/dummy/app/adapters/post.js +++ b/tests/dummy/app/adapters/post.js @@ -1,9 +1,8 @@ import JSONAPIAdapter from '@ember-data/adapter/json-api'; import FastbootAdapter from 'ember-data-storefront/mixins/fastboot-adapter'; -export default JSONAPIAdapter.extend( - FastbootAdapter, { - +export default class PostAdapter extends JSONAPIAdapter.extend( + FastbootAdapter +) { // namespace: 'foo' - -}); +} diff --git a/tests/dummy/app/app.js b/tests/dummy/app/app.js index bb464aaf..4e004cdd 100644 --- a/tests/dummy/app/app.js +++ b/tests/dummy/app/app.js @@ -8,21 +8,18 @@ import { registerWarnHandler } from '@ember/debug'; Model.reopen(LoadableModel); -const App = Application.extend({ - modulePrefix: config.modulePrefix, - podModulePrefix: config.podModulePrefix, - Resolver -}); +export default class App extends Application { + modulePrefix = config.modulePrefix; + podModulePrefix = config.podModulePrefix; + Resolver = 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) { +registerWarnHandler(function (message, { id }, next) { if (id !== 'ember-test-selectors.empty-tag-name') { next(...arguments); } }); loadInitializers(App, config.modulePrefix); - - -export default App; diff --git a/tests/dummy/app/mixins/itemizable.js b/tests/dummy/app/mixins/itemizable.js deleted file mode 100644 index d6ecd31f..00000000 --- a/tests/dummy/app/mixins/itemizable.js +++ /dev/null @@ -1,4 +0,0 @@ -import Mixin from '@ember/object/mixin'; - -// eslint-disable-next-line ember/no-new-mixins -export default Mixin.create({}); diff --git a/tests/dummy/app/models/author.js b/tests/dummy/app/models/author.js index 8cdb72aa..43361375 100644 --- a/tests/dummy/app/models/author.js +++ b/tests/dummy/app/models/author.js @@ -5,5 +5,4 @@ export default class AuthorModel extends Model { @hasMany() comments; @belongsTo() post; - } diff --git a/tests/dummy/app/models/comment.js b/tests/dummy/app/models/comment.js index 3c7f491b..bcbb5e97 100644 --- a/tests/dummy/app/models/comment.js +++ b/tests/dummy/app/models/comment.js @@ -1,10 +1,7 @@ -import Model, { attr, belongsTo } from '@ember-data/model'; - -export default class CommentModel extends Model { - - @attr('string') text; +import ItemizableModel from './itemizable'; +import { belongsTo } from '@ember-data/model'; +export default class CommentModel extends ItemizableModel { @belongsTo() post; @belongsTo() author; - } diff --git a/tests/dummy/app/models/homepage-item.js b/tests/dummy/app/models/homepage-item.js index 0070f79b..f296211f 100644 --- a/tests/dummy/app/models/homepage-item.js +++ b/tests/dummy/app/models/homepage-item.js @@ -1,7 +1,5 @@ import Model, { belongsTo } from '@ember-data/model'; export default class HomepageItemModel extends Model { - @belongsTo({ polymorphic: true }) itemizable; - } diff --git a/tests/dummy/app/models/itemizable.js b/tests/dummy/app/models/itemizable.js new file mode 100644 index 00000000..669befe2 --- /dev/null +++ b/tests/dummy/app/models/itemizable.js @@ -0,0 +1,7 @@ +import Model, { attr, hasMany } from '@ember-data/model'; + +export default class ItemizableModel extends Model { + @attr('string') text; + + @hasMany() tags; +} diff --git a/tests/dummy/app/models/post.js b/tests/dummy/app/models/post.js index 752d47af..1dad4c0c 100644 --- a/tests/dummy/app/models/post.js +++ b/tests/dummy/app/models/post.js @@ -1,12 +1,9 @@ -import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; - -export default class PostModel extends Model { +import ItemizableModel from './itemizable'; +import { attr, belongsTo, hasMany } from '@ember-data/model'; +export default class PostModel extends ItemizableModel { @attr('string') title; - @attr('string') text; @belongsTo() author; @hasMany() comments; - @hasMany() tags; - } diff --git a/tests/dummy/app/models/tag.js b/tests/dummy/app/models/tag.js index 882a3696..e0bc99ac 100644 --- a/tests/dummy/app/models/tag.js +++ b/tests/dummy/app/models/tag.js @@ -1,7 +1,5 @@ import Model, { hasMany } from '@ember-data/model'; export default class TagModel extends Model { - @hasMany() posts; - } diff --git a/tests/dummy/app/pods/application/template.hbs b/tests/dummy/app/pods/application/template.hbs index a18d6d78..d7792ac2 100644 --- a/tests/dummy/app/pods/application/template.hbs +++ b/tests/dummy/app/pods/application/template.hbs @@ -1,7 +1,7 @@
- {{docs-header}} + {{outlet}} - {{docs-keyboard-shortcuts}} +
\ No newline at end of file diff --git a/tests/dummy/app/pods/components/ui-button/component.js b/tests/dummy/app/pods/components/ui-button/component.js deleted file mode 100644 index 51606842..00000000 --- a/tests/dummy/app/pods/components/ui-button/component.js +++ /dev/null @@ -1,8 +0,0 @@ -import Component from '@ember/component'; - -export default Component.extend({ - tagName: '', - supportsDataTestProperties: true, - - onClick() {}, -}); diff --git a/tests/dummy/app/pods/components/ui-button/template.hbs b/tests/dummy/app/pods/components/ui-button/template.hbs index 02131040..e538b925 100644 --- a/tests/dummy/app/pods/components/ui-button/template.hbs +++ b/tests/dummy/app/pods/components/ui-button/template.hbs @@ -1,13 +1,15 @@ diff --git a/tests/dummy/app/pods/docs/guides/avoiding-errors/template.md b/tests/dummy/app/pods/docs/guides/avoiding-errors/template.md index 86beff24..200066fa 100644 --- a/tests/dummy/app/pods/docs/guides/avoiding-errors/template.md +++ b/tests/dummy/app/pods/docs/guides/avoiding-errors/template.md @@ -5,15 +5,15 @@ These patterns minimize the states in which your application can exist, helping ## Ensuring data is loaded within templates -You can use the `{{assert-must-preload}}` component to throw a dev-time warning if a template is rendered without all of its requisite data. This can help you avoid FOUC and the `n+1` query bug. +You can use the `` component to throw a dev-time warning if a template is rendered without all of its requisite data. This can help you avoid FOUC and the `n+1` query bug. ```hbs -{{assert-must-preload post 'comments.author'}} + -{{#each post.comments as |comment|}} +{{#each this.post.comments as |comment|}} This comment was from {{comment.author.name}} {{/each}} ``` @@ -33,21 +33,22 @@ Async relationships can also lead to surprises in actions by adding unnecessary You can use the `model#hasLoaded` method to throw a dev-time warning if a relationship is not yet loaded. This can help you avoid calling functions on undefined objects. ```js -Component.extend({ - post: null, // passed in post - currentUser: service(), - - actions: { - followAuthor() { - Ember.assert( - "The author isn't loaded", - this.get('post').hasLoaded('author') - ); - - this.get('currentUser').follow(this.get('post.author')); - } +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { assert } from '@ember/debug'; +import { action } from '@ember/object'; + +export default class extends Component { + @service currentUser; + + @action followAuthor() { + const { post } = this.args; // passed in post + + assert('The author is not loaded', post.hasLoaded('author')); + + this.currentUser.follow(post.author); } -}); +} ``` If the `followAuthor` action is called without the post's author being loaded, the developer will see a dev-time error. diff --git a/tests/dummy/app/pods/docs/guides/common-data-issues/template.md b/tests/dummy/app/pods/docs/guides/common-data-issues/template.md index 4f44f5cf..7214f339 100644 --- a/tests/dummy/app/pods/docs/guides/common-data-issues/template.md +++ b/tests/dummy/app/pods/docs/guides/common-data-issues/template.md @@ -4,15 +4,17 @@ General information you might find helpful. ## Linking by model -A common data issue in Ember occurs when using the `{{link-to}}` helper and passing a model. +A common data issue in Ember occurs when using the `` helper and passing a model. ```hbs -{{link-to "View project" "projects.show" project}} + + View project + ``` If a user clicks on this link, the Ember app will transition to the `projects.show` route, but that route's `model` hook will not run. This is by design - Ember treats the passed-in model as the resolved value of its `model()` hook, and skips that hook completely to avoid making any unnecessary network calls. -This behavior is a common source of bugs, primarily because developers can pass models into `{{link-to}}` that don't match what would have been returned by a route's `model()` hook. A good example of this is if a model had been loaded on an index page, and was passed into a detail page, but that detail page was coded to expect a model that had been loaded with additional relationships. +This behavior is a common source of bugs, primarily because developers can pass models into `` that don't match what would have been returned by a route's `model()` hook. A good example of this is if a model had been loaded on an index page, and was passed into a detail page, but that detail page was coded to expect a model that had been loaded with additional relationships. ```js // routes/projects/show.js @@ -21,14 +23,16 @@ model() { } ``` -If the model was loaded without the expected relationships, but then it was passed into `{{link-to}}`, that page would now render in an awkward or broken state. +If the model was loaded without the expected relationships, but then it was passed into ``, that page would now render in an awkward or broken state. Our take on this problem is that _routes should be the only source of truth regarding what data is needed for routes to load_ (that is, what data should block a route from rendering), and they should declare those data needs in their `model()` hooks. -Once routes declare those needs, all transitions into that route should go through those `model()` hooks, as this will guarantee that a route's data needs have been met. To do this, we recommend always passing an `id` to `{{link-to}}`, to force Ember routes to re-run their model hooks every time: +Once routes declare those needs, all transitions into that route should go through those `model()` hooks, as this will guarantee that a route's data needs have been met. To do this, we recommend always passing an `id` to ``, to force Ember routes to re-run their model hooks every time: ```hbs -{{link-to "View project" "projects.show" project.id}} + + View project + ``` diff --git a/tests/dummy/app/pods/docs/guides/data-fetching/demo-1/component.js b/tests/dummy/app/pods/docs/guides/data-fetching/demo-1/component.js index a75d91f5..90ad462b 100644 --- a/tests/dummy/app/pods/docs/guides/data-fetching/demo-1/component.js +++ b/tests/dummy/app/pods/docs/guides/data-fetching/demo-1/component.js @@ -1,77 +1,82 @@ -import Component from '@ember/component'; -import { computed } from '@ember/object'; +import Component from '@glimmer/component'; import { task } from 'ember-concurrency'; import { inject as service } from '@ember/service'; -import { readOnly } from '@ember/object/computed'; import { A } from '@ember/array'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import podNames from 'ember-component-css/pod-names'; -export default Component.extend({ +export default class Demo1Component extends Component { + @service store; - store: service(), + @tracked visitedRoutes; + @tracked isExpanded; get serverPosts() { return window.server.db.dump().posts; - }, + } - clientPosts: computed(function() { - return this.get('store').peekAll('post'); - }), + get clientPosts() { + return this.store.peekAll('post'); + } - model: readOnly('visit.last.value'), - activeRoute: readOnly('visitedRoutes.lastObject'), + get model() { + return this.visit.last.value; + } + + get activeRoute() { + return this.visitedRoutes.lastObject; + } - routes: computed(function() { + get routes() { return { '/posts': { // BEGIN-SNIPPET demo1-posts-route.js // route model() { - return this.get('store').findAll('post'); - } + return this.store.findAll('post'); + }, // END-SNIPPET }, '/posts/1': { // BEGIN-SNIPPET demo1-posts1-route.js // route model() { - return this.get('store').findRecord('post', 1); - } + return this.store.findRecord('post', 1); + }, // END-SNIPPET - } + }, }; - }), - - didInsertElement() { - this._super(...arguments); - this.reset(); - }, - - visit: task(function * (routeName) { - this.get('visitedRoutes').pushObject(routeName); + } - return yield this.get(`routes.${routeName}.model`).call(this); - }), + get styleNamespace() { + return podNames['docs/guides/data-fetching/demo-1']; + } - reset() { - this.get('store').unloadAll('post'); - this.get('store').resetCache(); - this.set('visitedRoutes', A([ '/' ])); - }, + constructor() { + super(...arguments); + this.reset(); + } - actions: { - visitRoute(routeName) { - if (routeName !== this.get('activeRoute')) { - this.get('visit').perform(routeName); - } - }, + @task *visit(routeName) { + this.visitedRoutes.pushObject(routeName); - toggleExpand() { - this.toggleProperty('isExpanded'); - }, + return yield this.routes[routeName].model.call(this); + } - reset() { - this.reset(); + @action visitRoute(routeName) { + if (routeName !== this.activeRoute) { + this.visit.perform(routeName); } } -}); + @action toggleExpand() { + this.isExpanded = !this.isExpanded; + } + + @action reset() { + this.store.unloadAll('post'); + this.store.resetCache(); + this.visitedRoutes = A(['/']); + } +} diff --git a/tests/dummy/app/pods/docs/guides/data-fetching/demo-1/template.hbs b/tests/dummy/app/pods/docs/guides/data-fetching/demo-1/template.hbs index e9b8c340..2e5e827c 100644 --- a/tests/dummy/app/pods/docs/guides/data-fetching/demo-1/template.hbs +++ b/tests/dummy/app/pods/docs/guides/data-fetching/demo-1/template.hbs @@ -1,42 +1,43 @@ +

App

- +
- + + - {{activeRoute}} + {{this.activeRoute}}
- {{#if visit.isRunning}} -

{{fa-icon 'spinner' spin=true}} Loading {{activeRoute}}...

+ {{#if this.visit.isRunning}} +

Loading {{this.activeRoute}}...

{{else}} - {{#if (eq activeRoute '/posts/1')}} + {{#if (eq this.activeRoute '/posts/1')}}

{{!-- BEGIN-SNIPPET demo1-posts1-template.hbs --}} {{! template }} - {{model.title}} + {{this.model.title}} {{!-- END-SNIPPET --}}

- {{else if (eq activeRoute '/posts')}} + {{else if (eq this.activeRoute '/posts')}}
    {{!-- BEGIN-SNIPPET demo1-posts-template.hbs --}} {{! template }} - {{#each model as |post|}} + {{#each this.model as |post|}}
  • {{post.title}}
  • {{/each}} {{!-- END-SNIPPET --}} @@ -49,18 +50,18 @@
- {{#if isExpanded}} + {{#if this.isExpanded}}

Server

@@ -75,7 +76,7 @@ - {{#each serverPosts as |post|}} + {{#each this.serverPosts as |post|}} {{post.id}} {{post.title}} @@ -94,7 +95,7 @@

Store

- POSTS ({{clientPosts.length}}) + POSTS ({{this.clientPosts.length}})

@@ -104,7 +105,7 @@ - {{#each clientPosts as |post|}} + {{#each this.clientPosts as |post|}} @@ -118,7 +119,7 @@

History

    - {{#each visitedRoutes as |route|}} + {{#each this.visitedRoutes as |route|}}
  • {{route}}
  • {{/each}}
@@ -130,14 +131,14 @@

Routes

/posts

- {{docs-snippet name="demo1-posts-route.js"}} - {{docs-snippet name="demo1-posts-template.hbs"}} + +

/posts/1

- {{docs-snippet name="demo1-posts1-route.js"}} - {{docs-snippet name="demo1-posts1-template.hbs"}} + +
@@ -145,3 +146,4 @@ {{/if}} + diff --git a/tests/dummy/app/pods/docs/guides/data-fetching/demo-2/component.js b/tests/dummy/app/pods/docs/guides/data-fetching/demo-2/component.js index 78f5e009..7ceb6222 100644 --- a/tests/dummy/app/pods/docs/guides/data-fetching/demo-2/component.js +++ b/tests/dummy/app/pods/docs/guides/data-fetching/demo-2/component.js @@ -1,77 +1,82 @@ -import Component from '@ember/component'; -import { computed } from '@ember/object'; +import Component from '@glimmer/component'; import { task } from 'ember-concurrency'; import { inject as service } from '@ember/service'; -import { readOnly } from '@ember/object/computed'; import { A } from '@ember/array'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import podNames from 'ember-component-css/pod-names'; -export default Component.extend({ +export default class Demo2Component extends Component { + @service store; - store: service(), + @tracked visitedRoutes; + @tracked isExpanded; get serverPosts() { return window.server.db.dump().posts; - }, + } - clientPosts: computed(function() { - return this.get('store').peekAll('post'); - }), + get clientPosts() { + return this.store.peekAll('post'); + } - model: readOnly('visit.last.value'), - activeRoute: readOnly('visitedRoutes.lastObject'), + get model() { + return this.visit.last.value; + } + + get activeRoute() { + return this.visitedRoutes.lastObject; + } - routes: computed(function() { + get routes() { return { '/posts': { // BEGIN-SNIPPET demo2-posts-route.js // route model() { - return this.get('store').loadRecords('post'); - } + return this.store.loadRecords('post'); + }, // END-SNIPPET }, '/posts/1': { // BEGIN-SNIPPET demo2-posts1-route.js // route model() { - return this.get('store').loadRecord('post', 1); - } + return this.store.loadRecord('post', 1); + }, // END-SNIPPET - } + }, }; - }), - - didInsertElement() { - this._super(...arguments); - this.reset(); - }, - - visit: task(function * (routeName) { - this.get('visitedRoutes').pushObject(routeName); + } - return yield this.get(`routes.${routeName}.model`).call(this); - }), + get styleNamespace() { + return podNames['docs/guides/data-fetching/demo-2']; + } - reset() { - this.get('store').unloadAll('post'); - this.get('store').resetCache(); - this.set('visitedRoutes', A([ '/' ])); - }, + constructor() { + super(...arguments); + this.reset(); + } - actions: { - visitRoute(routeName) { - if (routeName !== this.get('activeRoute')) { - this.get('visit').perform(routeName); - } - }, + @task *visit(routeName) { + this.visitedRoutes.pushObject(routeName); - toggleExpand() { - this.toggleProperty('isExpanded'); - }, + return yield this.routes[routeName].model.call(this); + } - reset() { - this.reset(); + @action visitRoute(routeName) { + if (routeName !== this.activeRoute) { + this.visit.perform(routeName); } } -}); + @action toggleExpand() { + this.isExpanded = !this.isExpanded; + } + + @action reset() { + this.store.unloadAll('post'); + this.store.resetCache(); + this.visitedRoutes = A(['/']); + } +} diff --git a/tests/dummy/app/pods/docs/guides/data-fetching/demo-2/template.hbs b/tests/dummy/app/pods/docs/guides/data-fetching/demo-2/template.hbs index 88a29e92..403bbbbf 100644 --- a/tests/dummy/app/pods/docs/guides/data-fetching/demo-2/template.hbs +++ b/tests/dummy/app/pods/docs/guides/data-fetching/demo-2/template.hbs @@ -1,45 +1,46 @@ +

App

- +
- /posts - - + - {{activeRoute}} + {{this.activeRoute}}
- {{#if visit.isRunning}} -

{{fa-icon 'spinner' spin=true}} Loading {{activeRoute}}...

+ {{#if this.visit.isRunning}} +

Loading {{this.activeRoute}}...

{{else}} - {{#if (eq activeRoute '/posts/1')}} + {{#if (eq this.activeRoute '/posts/1')}}

{{!-- BEGIN-SNIPPET demo2-posts1-template.hbs --}} {{! template }} - {{model.title}} + {{this.model.title}} {{!-- END-SNIPPET --}}

- {{else if (eq activeRoute '/posts')}} + {{else if (eq this.activeRoute '/posts')}}
    {{!-- BEGIN-SNIPPET demo2-posts-template.hbs --}} {{! template }} - {{#each model as |post|}} + {{#each this.model as |post|}}
  • {{post.title}}
  • {{/each}} {{!-- END-SNIPPET --}} @@ -53,18 +54,18 @@
- {{#if isExpanded}} + {{#if this.isExpanded}}

Server

@@ -79,7 +80,7 @@
- {{#each serverPosts as |post|}} + {{#each this.serverPosts as |post|}} @@ -98,7 +99,7 @@

Store

- POSTS ({{clientPosts.length}}) + POSTS ({{this.clientPosts.length}})

{{post.id}} {{post.title}}
{{post.id}} {{post.title}}
@@ -108,7 +109,7 @@ - {{#each clientPosts as |post|}} + {{#each this.clientPosts as |post|}} @@ -122,7 +123,7 @@

History

    - {{#each visitedRoutes as |route|}} + {{#each this.visitedRoutes as |route|}}
  • {{route}}
  • {{/each}}
@@ -134,14 +135,14 @@

Routes

/posts

- {{docs-snippet name="demo2-posts-route.js"}} - {{docs-snippet name="demo2-posts-template.hbs"}} + +

/posts/1

- {{docs-snippet name="demo2-posts1-route.js"}} - {{docs-snippet name="demo2-posts1-template.hbs"}} + +
@@ -149,3 +150,4 @@ {{/if}} + \ No newline at end of file diff --git a/tests/dummy/app/pods/docs/guides/data-fetching/template.md b/tests/dummy/app/pods/docs/guides/data-fetching/template.md index b06ab714..4435e05a 100644 --- a/tests/dummy/app/pods/docs/guides/data-fetching/template.md +++ b/tests/dummy/app/pods/docs/guides/data-fetching/template.md @@ -20,7 +20,7 @@ In the following example, compare how the app behaves when you visit the routes If you followed the steps above, what you'll notice is that under the second scenario, the `/posts` route was actually rendered in two states. First, it showed `post:1` in the list, and then after about a second the app re-rendered and the other two posts appeared. -This is because the `/posts/1` route had already loaded the `post:1` record into Ember Data's store. By the time you visited the `/posts` index route, the promise from the store's `findAll` method resolved immediately with that `post:1` record, and then triggered a background reload of the entire `posts` collection. (Click the {{fa-icon 'angle-down'}} above to expand the demo for more details about what's happening as you navigate through the app.) +This is because the `/posts/1` route had already loaded the `post:1` record into Ember Data's store. By the time you visited the `/posts` index route, the promise from the store's `findAll` method resolved immediately with that `post:1` record, and then triggered a background reload of the entire `posts` collection. (Click the above to expand the demo for more details about what's happening as you navigate through the app.) Ember Data's `findAll` method accepts a `reload` option that we can use to force the promise to block, but then we'd lose the benefits of caching. (Note how after visiting `/posts` for the first time, it's fast on all subsequent visits.) @@ -28,13 +28,13 @@ Storefront's `loadRecords` was designed to avoid re-rendering problems like this ```diff model() { -- return this.get('store').findAll('post'); -+ return this.get('store').loadRecords('post'); +- return this.store.findAll('post'); ++ return this.store.loadRecords('post'); } model() { -- return this.get('store').findRecord('post', 1); -+ return this.get('store').loadRecord('post', 1); +- return this.store.findRecord('post', 1); ++ return this.store.loadRecord('post', 1); } ``` @@ -51,9 +51,9 @@ To correctly replace all calls to `findAll` with `loadRecords` you'll need to al ```diff async model() { -- return this.get('store').findAll('post'); -+ await this.get('store').loadRecords('post'); -+ return this.get('store').peekAll('post'); +- return this.store.findAll('post'); ++ await this.store.loadRecords('post'); ++ return this.store.peekAll('post'); } ``` diff --git a/tests/dummy/app/pods/docs/guides/working-with-relationships/demo-1/component.js b/tests/dummy/app/pods/docs/guides/working-with-relationships/demo-1/component.js index 0b38b1a3..672a0dad 100644 --- a/tests/dummy/app/pods/docs/guides/working-with-relationships/demo-1/component.js +++ b/tests/dummy/app/pods/docs/guides/working-with-relationships/demo-1/component.js @@ -1,46 +1,43 @@ -import Component from '@ember/component'; +import Component from '@glimmer/component'; import { task } from 'ember-concurrency'; import { inject as service } from '@ember/service'; -import { readOnly } from '@ember/object/computed'; -import { defineProperty } from '@ember/object'; +import { defineProperty, notifyPropertyChange } from '@ember/object'; +import { action } from '@ember/object'; -export default Component.extend({ +export default class DocsDemo1Component extends Component { + @service store; - store: service(), + constructor() { + super(...arguments); - didInsertElement() { - this._super(...arguments); - - this.get('loadPost').perform(); + this.loadPost.perform(); this.setup(); - }, + } - loadPost: task(function*() { - return yield this.get('store').findRecord('post', 1); - }), + @task *loadPost() { + return yield this.store.findRecord('post', 1); + } - post: readOnly('loadPost.lastSuccessful.value'), + get post() { + return this.loadPost.lastSuccessful?.value; + } setup() { let tasks = { // BEGIN-SNIPPET working-with-relationships-demo-1.js - loadComments: task(function*() { - yield this.get('post').load('comments'); - }) + loadComments: task(function* () { + yield this.post.load('comments'); + }), // END-SNIPPET }; - this.get('store').resetCache(); + this.store.resetCache(); // We do this to reset loadComments state defineProperty(this, 'loadComments', tasks.loadComments); - this.notifyPropertyChange('loadComments'); - }, - - actions: { - reset() { - this.setup(); - } + notifyPropertyChange(this, 'loadComments'); } - -}); + @action reset() { + this.setup(); + } +} diff --git a/tests/dummy/app/pods/docs/guides/working-with-relationships/demo-1/template.hbs b/tests/dummy/app/pods/docs/guides/working-with-relationships/demo-1/template.hbs index 21dcdbac..720e9b57 100644 --- a/tests/dummy/app/pods/docs/guides/working-with-relationships/demo-1/template.hbs +++ b/tests/dummy/app/pods/docs/guides/working-with-relationships/demo-1/template.hbs @@ -1,36 +1,36 @@ -{{#if post}} +{{#if this.post}} - {{#docs-demo as |demo|}} - {{#demo.example name='working-with-relationships-demo-1.hbs'}} + +
- {{#ui-button style='small' onClick=(action 'reset')}} + Reset - {{/ui-button}} +
- {{#liquid-if loadComments.lastSuccessful}} + {{#liquid-if this.loadComments.lastSuccessful}}

- The post has {{post.comments.length}} comments. + The post has {{this.post.comments.length}} comments.

{{else}} - {{#ui-button - onClick=(perform loadComments) - disabled=loadComments.isRunning - data-test-id='load-comments'}} - {{if loadComments.isIdle 'Load comments' 'Loading...'}} - {{/ui-button}} + + {{if this.loadComments.isIdle 'Load comments' 'Loading...'}} + {{/liquid-if}} - {{/demo.example}} +
{{demo.snippet 'working-with-relationships-demo-1.js'}} {{demo.snippet 'working-with-relationships-demo-1.hbs'}} - {{/docs-demo}} +
{{else}} diff --git a/tests/dummy/app/pods/docs/guides/working-with-relationships/demo-2/component.js b/tests/dummy/app/pods/docs/guides/working-with-relationships/demo-2/component.js index 1366eeec..7e72948b 100644 --- a/tests/dummy/app/pods/docs/guides/working-with-relationships/demo-2/component.js +++ b/tests/dummy/app/pods/docs/guides/working-with-relationships/demo-2/component.js @@ -1,46 +1,43 @@ -import Component from '@ember/component'; +import Component from '@glimmer/component'; import { task } from 'ember-concurrency'; import { inject as service } from '@ember/service'; -import { readOnly } from '@ember/object/computed'; -import { defineProperty } from '@ember/object'; +import { defineProperty, notifyPropertyChange } from '@ember/object'; +import { action } from '@ember/object'; -export default Component.extend({ +export default class DocsDemo2Component extends Component { + @service store; - store: service(), + constructor() { + super(...arguments); - didInsertElement() { - this._super(...arguments); - - this.get('loadPost').perform(); + this.loadPost.perform(); this.setup(); - }, + } - loadPost: task(function*() { - return yield this.get('store').findRecord('post', 2); - }), + @task *loadPost() { + return yield this.store.findRecord('post', 2); + } - post: readOnly('loadPost.lastSuccessful.value'), + get post() { + return this.loadPost.lastSuccessful?.value; + } setup() { let tasks = { // BEGIN-SNIPPET working-with-relationships-demo-2.js - sideloadComments: task(function*() { - yield this.get('post').sideload('comments'); - }) + sideloadComments: task(function* () { + yield this.post.sideload('comments'); + }), // END-SNIPPET }; - this.get('store').resetCache(); + this.store.resetCache(); // We do this to reset loadComments state defineProperty(this, 'sideloadComments', tasks.sideloadComments); - this.notifyPropertyChange('sideloadComments'); - }, - - actions: { - reset() { - this.setup(); - } + notifyPropertyChange(this, 'sideloadComments'); } - -}); + @action reset() { + this.setup(); + } +} diff --git a/tests/dummy/app/pods/docs/guides/working-with-relationships/demo-2/template.hbs b/tests/dummy/app/pods/docs/guides/working-with-relationships/demo-2/template.hbs index e635383b..4cd5ace1 100644 --- a/tests/dummy/app/pods/docs/guides/working-with-relationships/demo-2/template.hbs +++ b/tests/dummy/app/pods/docs/guides/working-with-relationships/demo-2/template.hbs @@ -1,36 +1,36 @@ -{{#if post}} +{{#if this.post}} - {{#docs-demo as |demo|}} - {{#demo.example name='working-with-relationships-demo-2.hbs'}} + +
- {{#ui-button style='small' onClick=(action 'reset')}} + Reset - {{/ui-button}} +
- {{#liquid-if sideloadComments.lastSuccessful}} + {{#liquid-if this.sideloadComments.lastSuccessful}}

- The post has {{post.comments.length}} comments. + The post has {{this.post.comments.length}} comments.

{{else}} - {{#ui-button - onClick=(perform sideloadComments) - disabled=sideloadComments.isRunning - data-test-id='sideload-comments'}} - {{if sideloadComments.isIdle 'Reload with comments' 'Loading...'}} - {{/ui-button}} + + {{if this.sideloadComments.isIdle 'Reload with comments' 'Loading...'}} + {{/liquid-if}} - {{/demo.example}} +
{{demo.snippet 'working-with-relationships-demo-2.js'}} {{demo.snippet 'working-with-relationships-demo-2.hbs'}} - {{/docs-demo}} +
{{else}} diff --git a/tests/dummy/app/pods/docs/template.hbs b/tests/dummy/app/pods/docs/template.hbs index 87cdda13..0b13a587 100644 --- a/tests/dummy/app/pods/docs/template.hbs +++ b/tests/dummy/app/pods/docs/template.hbs @@ -1,6 +1,6 @@ -{{#docs-viewer as |viewer|}} + - {{#viewer.nav as |nav|}} + {{nav.section 'Introduction'}} {{nav.item 'Overview' 'docs.index'}} @@ -10,10 +10,10 @@ {{nav.item 'Avoiding errors' 'docs.guides.avoiding-errors'}} {{nav.item 'Fastboot support' 'docs.guides.fastboot'}} {{nav.item 'Common data issues' 'docs.guides.common-data-issues'}} - {{/viewer.nav}} + - {{#viewer.main}} + {{outlet}} - {{/viewer.main}} + -{{/docs-viewer}} + diff --git a/tests/dummy/app/pods/fastboot-tests/find-all-posts/route.js b/tests/dummy/app/pods/fastboot-tests/find-all-posts/route.js index 58e9aa4d..a62d3389 100644 --- a/tests/dummy/app/pods/fastboot-tests/find-all-posts/route.js +++ b/tests/dummy/app/pods/fastboot-tests/find-all-posts/route.js @@ -1,7 +1,7 @@ import Route from '@ember/routing/route'; -export default Route.extend({ - model: function() { +export default class FindAllPostsRoute extends Route { + model() { return this.store.findAll('post', { include: 'comments' }); - }, -}); + } +} diff --git a/tests/dummy/app/pods/fastboot-tests/find-all-posts/template.hbs b/tests/dummy/app/pods/fastboot-tests/find-all-posts/template.hbs index 103d2b74..0244db28 100644 --- a/tests/dummy/app/pods/fastboot-tests/find-all-posts/template.hbs +++ b/tests/dummy/app/pods/fastboot-tests/find-all-posts/template.hbs @@ -6,7 +6,7 @@ shoebox.

-{{#each model as |post|}} +{{#each this.model as |post|}}
{{post.title}}
diff --git a/tests/dummy/app/pods/fastboot-tests/load-all-posts/route.js b/tests/dummy/app/pods/fastboot-tests/load-all-posts/route.js index 95db93ba..67213aa4 100644 --- a/tests/dummy/app/pods/fastboot-tests/load-all-posts/route.js +++ b/tests/dummy/app/pods/fastboot-tests/load-all-posts/route.js @@ -1,7 +1,7 @@ import Route from '@ember/routing/route'; -export default Route.extend({ - model: function() { - return this.store.loadRecords('post', { filter: { popular: true }}); - }, -}); +export default class LoadAllPostsRoute extends Route { + model() { + return this.store.loadRecords('post', { filter: { popular: true } }); + } +} diff --git a/tests/dummy/app/pods/fastboot-tests/load-all-posts/template.hbs b/tests/dummy/app/pods/fastboot-tests/load-all-posts/template.hbs index 103d2b74..0244db28 100644 --- a/tests/dummy/app/pods/fastboot-tests/load-all-posts/template.hbs +++ b/tests/dummy/app/pods/fastboot-tests/load-all-posts/template.hbs @@ -6,7 +6,7 @@ shoebox.

-{{#each model as |post|}} +{{#each this.model as |post|}}
{{post.title}}
diff --git a/tests/dummy/app/pods/fastboot-tests/load-record-post/route.js b/tests/dummy/app/pods/fastboot-tests/load-record-post/route.js index 90924e73..adf21fe0 100644 --- a/tests/dummy/app/pods/fastboot-tests/load-record-post/route.js +++ b/tests/dummy/app/pods/fastboot-tests/load-record-post/route.js @@ -1,7 +1,7 @@ import Route from '@ember/routing/route'; -export default Route.extend({ - model: function() { +export default class LoadRecordPostRoute extends Route { + model() { return this.store.loadRecord('post', 1); - }, -}); + } +} diff --git a/tests/dummy/app/pods/fastboot-tests/load-record-post/template.hbs b/tests/dummy/app/pods/fastboot-tests/load-record-post/template.hbs index 9bff88e1..9d222b92 100644 --- a/tests/dummy/app/pods/fastboot-tests/load-record-post/template.hbs +++ b/tests/dummy/app/pods/fastboot-tests/load-record-post/template.hbs @@ -7,5 +7,5 @@

- {{model.title}} + {{this.model.title}}
diff --git a/tests/dummy/app/pods/index/template.hbs b/tests/dummy/app/pods/index/template.hbs index e5741e20..f80117b2 100644 --- a/tests/dummy/app/pods/index/template.hbs +++ b/tests/dummy/app/pods/index/template.hbs @@ -1,10 +1,11 @@ -{{docs-hero}} +
{{post.id}} {{post.title}}