An implementation of Resources in Ember.JS without decorators.
NOTE: if you are also using ember-could-get-used-to-this, @use
is not compatible withthis library's LifecycleResource
, and useResource
does not work with ember-could-get-used-to-this' Resource
.However, both libraries can still be used in the same project.
npm install ember-resources
# or
yarn add ember-resources
# or
ember install ember-resources
import { useFunction } from 'ember-resources';
class MyClass {
data = useFunction(this, async () => {
let response = await fetch('...');
let json = await response.json();
return json;
}),
}
useResource
useResource
takes either a Resource
or LifecycleResource
and an args thunk.
import { useResource } from 'ember-resources';
class MyClass {
data = useResource(this, SomeResource, () => [arg list]);
}
When any tracked data in the args thunk is updated, the Resource will be updated as well
The this
is to keep track of destruction -- so when MyClass
is destroyed, all the resources attached to it can also be destroyed.
The resource will do nothing until it is accessed. Meaning, if you have a template that guardsaccess to the data, like:
the Resource will not be instantiated until isModalShowing
is true.
For more info on Thunks, scroll to the bottom of the README
useTask
This is a utility wrapper like useResource
, but can be passed an ember-concurrency taskso that the ember-concurrency task can reactively be re-called whenever args change.This largely eliminates the need to start concurrency tasks from the constructor, modifiers,getters, etc.
A concurrency task accessed via useTask
is only invoked when accessed, and automatically updateswhen it needs to.
import { useTask } from 'ember-resources';
class MyClass {
myData = useTask(this, this._myTask, () => [args, to, task])
@task
*_myTask(args, to, task) { /* ... */ }
}
Accessing this.myData
will represent the last TaskInstance
, so all the expected properties are available:value
, isRunning
, isFinished
, etc.See: the TaskInstance docs for more info.
NOTE: ember-resources
does not have a dependency on ember-concurrency
Resource
Resources extending this base class have no lifecycle hooks to encourage data-derivation (via getters) andgenerally simpler state-management than you'd otherwise see with the typical lifecycle-hook-aware Resource.
For example, this is how you'd handle initial setup, updates, and teardown with a Resource
import { Resource } from 'ember-resources';
import { registerDestructor } from '@ember/destroyable';
class MyResource extends Resource {
constructor(owner, args, previous) {
super(owner, args, previous);
if (!previous) {
// initial setup
} else {
// update
}
registerDestructor(this, () => {
// teardown function for each instance
});
}
}
This works much like useFunction
, in that the previous instance is passed to the next instance and thereis no overall persisting instance of MyResource
as the args
update. This technique eliminates the needto worry about if your methods, properties, and getters might conflict with the base class's API, which isa common complaint among the anti-class folks.
Many times, however, you may not even need to worry about destruction, which is partially what makes optingin to having a "destructor" so fun -- you get to choose how much lifecycle your Resource
has.
More info: @ember/destroyable
So why even have a class at all?
You may still want to manage state internal to your resource, such as if you were implementing a"bulk selection" resource for use in tabular data. This hypothetical resource may track its ownpartial / all / none selected state. If the args to this resource change, you get to decide if youwant to reset the state, or pass it on to the next instance of the selection resource.
import { Resource } from 'ember-resources';
class Selection extends Resource {
@tracked state = NONE; /* or SOME, ALL, ALL_EXCEPT */
constructor(owner, args, previous) {
super(owner, args, previous);
let { filterQueryString } = args.named;
if (previous && previous.args.named.filterQueryString !== filterQueryString) {
// reset the state when the consumer has changed which records we care about.
this.state = NONE;
}
}
@action selectAll() { this.state = ALL; }
@action deselectAll() { this.state = NONE; }
@action toggleItem(item) { /* ... */ }
// etc
}
usage of this Resource could look like this:
// in either a component or route:
export default class MyComponent extends Component {
@service router;
get filter() {
return this.router.currentRouter.queryParams.filter;
}
// implementation omitted for brevity -- could be passed to EmberTable or similar
records = useResource(this, EmberDataQuery, () => ({ filter: this.filter }));
// the `this.selection.state` property is re-set to NONE when `this.filter` changes
selection = useResource(this, Selection, () => ({ filterQueryString: this.filter }))
}
For library authors, it may be a kindness to consumers of your library to wrap the useResource
call so that they only need to manage one import -- for example:
// addon/index.js
// @private
import { Selection } from './wherever/it/is.js';
// @public
export function useSelection(destroyable, thunk) {
return useResource(destroyable, Selection, thunk);
}
Another example of interacting with previous state may be a "load more" style data loader / pagination:
import { Resource } from 'ember-resources';
import { isDestroyed, isDestroying } from '@ember/destroyable';
class DataLoader extends Resource {
constructor(owner, args, previous) {
super(owner, args, previous);
this.results = previous?.results;
let { url, offset } = this.args.named;
fetch(`${url}?offset=${offset}`)
.then(response => response.json())
.then(results => {
if (isDestroyed(this) || isDestroying(this)) return;
this.results = this.results.concat(result);
});
}
}
consumption of the above resource:
import { useResource } from 'ember-resources';
class MyComponent extends Component {
@tracked offset = 0;
data = useResource(this, DataLoader, () => ({ url: '...', offset: this.offset }));
// when a button is clicked, load the next 50 records
@action loadMore() { this.offset += 50; }
}
LifecycleResource
When possible, you'll want to favor Resource
over LifecycleResource
as Resource
is simpler.
They key differences are that the LifecycleResource
base class has 3 lifecycle hooks
setup
- called upon first access of the resourceupdate
- called when any tracked
used during setup
changesteardown
- called when the containing context is torn downThe main advantage to the LifecycleResource
is that the teardown hook is for "last teardown",whereas with Resource
, if a destructor is registered in the destructor, there is no way to knowif that destruction is the final destruction.
An example of when you'd want to reach for the LifecycleResource
is when you're managing external long-livedstate that needs a final destruction call, such as with XState, which requires that the "State machine interpreter"is stopped when you are discarding the parent context (such as a component).
An example
import { LifecycleResource } from 'ember-resources';
import { createMachine, interpret } from 'xstate';
const machine = createMachine(/* ... see XState docs for this function this ... */);
class MyResource extends LifecycleResource {
@tracked state;
setup() {
this.interpreter = interpret(machine).onTransition(state => this.state = state);
}
update() {
this.interpreter.send('ARGS_UPDATED', this.args);
}
teardown() {
this.interpreter.stop();
}
}
Using this Resource is the exact same as Resource
import { useResource } from 'ember-resources';
class ContainingClass {
state = useResource(this, MyResource, () => [...])
}
There is however a semi-unintuitive technique you could use to continue to use Resource
for the final
teardown:
import { Resource } from 'ember-resources';
import { registerDestructor, unregisterDestructior } from '@ember/destroyable';
class MyResource extends Resource {
constructor(owner, args, previous) {
super(owner, args, previous);
registerDestructor(this, this.myFinalCleanup);
if (previous) {
// prevent destruction
unregisterDestructor(prev, prev.myFinalCleanup);
} else {
// setup
}
}
@action myFinalCleanup() { /* ... */ }
}
function
ResourcesWhile functions can be "stateless", Resources don't provide much value unlessyou can have state. function
Resources solve this by passing the previousinvocation's return value as an argument to the next time the function is called.
Example:
import { useFunction } from 'ember-resources';
class StarWarsInfo {
// access result on info.value
info = useFunction(this, async (state, ...args) => {
if (state) {
let { characters } = state;
return { characters };
}
let [ids] = args;
let response = await fetch(`/characters/${ids}`) ;
let characters = await response.json();
return { characters };
}, () => [this.ids /* defined somewhere */])
}
characters
would be accessed viathis.info.value.characters
in theStarWarsInfo
class
While this example is a bit contrived, hopefully it demonstrates how the state
argworks. During the first invocation, state
is falsey, allowing the rest of thefunction to execute. The next time this.ids
changes, the function will be calledagain, except state
will be the { characters }
value during the first invocation,and the function will return the initial data.
This particular technique could be used to run any async function safely (as longas the function doesn't interact with this
).
In this example, where the function is async
, the "value" of info.value
is undefined
until thefunction completes.
To help prevent accidental async footguns, even if a function is synchronous, it is still ranasynchronously, therefor, the thunk cannot be avoided.
import { useFunction } from 'ember-resources';
class MyClass {
@tracked num = 3;
info = useFunction(this, () => {
return this.num * 2;
});
}
this.info.value
will be undefined
, then 6
and will not change when num
changes.
With the exception of the useResource
+ class
combination, all Thunks are optional.The main caveat is that if your resources will not update without a thunk -- or consumingtracked data within setup / initialization (which is done for you with useFunction
).
The args thunk accepts the following data shapes:
() => [an, array]
() => ({ hello: 'there' })
() => ({ named: {...}, positional: [...] })
when an array is passed, inside the Resource, this.args.named
will be emptyand this.args.positional
will contain the result of the thunk.
for function resources, this is the only type of thunk allowed.
when an object is passed where the key named
is not present,this.args.named
will contain the result of the thunk and this.args.positional
will be empty.
when an object is passed containing either keys: named
or positional
:
this.args.named
will be the value of the result of the thunk's named
propertythis.args.positional
will be the value of the result of the thunk's positional
propertyThis is the same shape of args used throughout Ember's Helpers, Modifiers, etc
These patterns are primarily unexplored so if you run in to any issues,please open a bug report / issue.
Composing class-based resources is expected to "just work", as classes maintain their own state.
import Component from '@glimmer/component';
import { useFunction } from 'ember-resources';
class MyComponent extends Component {
rand = useFunction(this, () => {
return useFunction(this, () => Math.random());
});
}
Accessing the result of Math.random()
would be done via:
Something to note about composing resources is that if arguments passed to theouter resource change, the inner resources are discarded entirely.
For example, you'll need to manage the inner resource's cache invalidation yourself if you wantthe inner resource's behavior to be reactive based on outer arguments:
import Component from '@glimmer/component';
import { useFunction } from 'ember-resources';
class MyComponent extends Component {
@tracked id = 1;
@tracked storeName = 'blogs';
records = useFunction(this, (state, storeName) => {
let result: Array<string | undefined> = [];
if (state?.previous?.storeName === storeName) {
return state.previous.innerFunction;
}
let innerFunction = useFunction(this, (prev, id) => {
// pretend we fetched a record using the store service
let newValue = `record:${storeName}-${id}`;
result = [...(prev || []), newValue];
return result;
},
() => [this.id]
);
return new Proxy(innerFunction, {
get(target, key, receiver) {
if (key === 'previous') {
return {
innerFunction,
storeName,
};
}
return Reflect.get(target, key, receiver);
},
});
},
() => [this.storeName]
);
}
import type { ArgsWrapper, Named, Positional } from 'ember-resources';
where:
interface ArgsWrapper {
positional?: unknown[];
named?: Record<string, unknown>;
}
this is a utility interface that represents all of the args used throughoutEmber.
Example
class MyResource extends LifecycleResource { // default args type
constructor(owner: unknown, args: ArgsWrapper) {
super(owner, args);
}
}
export interface Positional<T extends Array<unknown>> {
positional: T;
}
Example:
class MyResource extends LifecycleResource<Positional<[number]>> {
}
export interface Named<T extends Record<string, unknown>> {
named: T;
}
Example:
class MyResource extends LifecycleResource<Named<{ bananas: number }>> {
}
These shorthands are 3 characters sharter than using the named:
orpositional:
keys that would be required if not using these shorthands...
If your resources are consumed by components, you'll want to continue totest using rendering tests, as things should "just work" with those style oftests.
Where things get interesting is when you want to unit test your resources.
There are two approaches:
new
the resource directlyimport { LifecycleResource } from 'ember-resources';
test('my test', function(assert) {
class MyResource extends LifecycleResource {
// ...
}
let instance = new MyResource(this.owner, { /* args wrapper */ });
// assertions with instance
})
The caveat here is that the setup
and update
functions will have tobe called manually, because we aren't using useResource
, which wraps theEmber-builtin invokeHelper
, which takes care of reactivity for us. As aconsequence, any changes to the args wrapper will not cause updates tothe resource instance.
For the Resource
base class, there is a static helper method which helps simulatethe update
behavior.
import { Resource } from 'ember-resources';
test ('my test', function (assert) {
class MyResource extends Resource {
// ...
}
let instance = new MyResource(this.owner, { /* args wrapper */ });
let nextInstance = MyResource.next(instance, { /* args wrapper */ });
});
Resource.next
, however, does not destroy the instance. For that, you'll want to usedestroy
from @ember/destroyable
.
import { destroy } from '@ember/destroyable';
// ...
destroy(instance);
If, instead of creating MyResource
directly, like in the example above,it is wrapped in a test class and utilizes useResource
:
import { useResource } from 'ember-resources';
class TestContext {
data = useResource(this, MyResource, () => { ... })
}
changes to args will trigger calls to setup
and update
.
NOTE: like with all reactivity testing in JS, it's important toawait settled()
after a change to a reactive property so that you allowtime for the framework to propagate changes to all the reactive bits.
Example:
import { LifecycleResource, useResource } from 'ember-resources';
test('my test', async function (assert) {
class Doubler extends LifecycleResource<{ positional: [number] }> {
get num() {
return this.args.positional[0] * 2;
}
}
class Test {
@tracked count = 0;
data = useResource(this, Doubler, () => [this.count]);
}
let foo = new Test();
assert.equal(foo.data.num, 0);
foo.count = 3;
await settled();
assert.equal(foo.data.num, 6);
List of addons that use and wrap ember-resources
to provide more specific functionality:
See the Contributing guide for details.
This project is licensed under the MIT License.
This library wouldn't be possible without the work of:
So much appreciate for the work both you have put in to Resources <3
官方文档地址:http://emberjs.com/guides/routing/ 由于是从word考过来的,格式不是太好,大家可以直接去下载我的完整word。里面除了翻译还有原创内容。 http://download.csdn.net/detail/kevinwon1985/5230326 -----------------------------------------------------
Ember.js于2022年11月29号发布v4.9.0版本。 截止2022年11月30号,emberjs/ember.js目前拥有22.3k个Star,4.3k个Fork,license为MIT,Issues问题数量共有365个待解决。 通过微信小程序:MyGit。搜索Ember.js并关注,可及时掌握了解Ember.js最近更新信息,可收到微信提醒通知。 Ember.js - A JavaSc
Ember检查器是一个浏览器插件,用于调试Ember应用程序。 灰烬检查员包括以下主题 - S.No. 灰烬检查员方式和描述 1 安装Inspector 您可以安装Ember检查器来调试您的应用程序。 2 Object Inspector Ember检查器允许与Ember对象进行交互。 3 The View Tree 视图树提供应用程序的当前状态。 4 检查路由,数据选项卡和库信息 您可以看到检查
英文原文: http://emberjs.com/guides/getting-ember/index/ Ember构建 Ember的发布管理团队针对Ember和Ember Data维护了不同的发布方法。 频道 最新的Ember和Ember Data的 Release,Beta 和 Canary 构建可以在这里找到。每一个频道都提供了一个开发版、最小化版和生产版。更多关于不同频道的信息可以查看博客
ember-emojione ember-emojione is your emoji solution for Ember, based on the EmojiOne project. EmojiOne version 2 is used, which is free to use for everyone (CC BY-SA 4.0), you're only required to giv
Ember 3D Ember 3D is an Ember addon for using Three.js - an easy to use, lightweight, javascript 3D library. It is designed to: Prescribe a solid file structure to Three.js code using ES6 modules. Ena
Ember Table An addon to support large data set and a number of features around table. Ember Table canhandle over 100,000 rows without any rendering or performance issues. Ember Table 3.x supports: Emb
vscode-ember This is the VSCode extension to use the Ember Language Server. Features All features currently only work in Ember-CLI apps that use classic structure and are a rough first draft with a lo