Socket
Socket
Sign inDemoInstall

ember-resources

Package Overview
Dependencies
Maintainers
1
Versions
93
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

ember-resources

An implementation of Resources with some helpful utilities


Version published
Weekly downloads
12K
decreased by-6.09%
Maintainers
1
Weekly downloads
 
Created
Source

ember-resources

npm version CI

An implementation of Resources in Ember.JS without decorators.

Compatibility

  • Ember.js v3.25+
  • TypeScript v4.2+

NOTE: if you are also using ember-could-get-used-to-this, @use is not compatible with this 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.

Installation

npm install ember-resources
# or
yarn add ember-resources
# or
ember install ember-resources

Examples

import { useResource, useFunction, useTask } from 'ember-resources';

class MyClass {
  data = useResource(this, DataClass, () => [arg list]);

  data1 = useFunction(this, () => { /* synchronous function */ })

  data2 = useFunction(this, async () => {}),

  data3 = useTask(this.someEmberConcurrencyTask, () => [optional arg list]);
}


Usage

useResource

useResource takes a LifecycleResource and an args thunk.

class MyClass {
  data = useResource(this, SomeResource, () => [arg list]);
}

When any tracked data in the args thunk, the update function on SomeResource will be called.

  • 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.
  • 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 task so 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 "ran" when accessed, and automatically updates when it needs to.

class MyClass {
  myData = useTask(this, this._myTask, () => [args, to, task])

  @task
  *_myTask(args, to, task)  { /* ... */ }
}

Accessing myData will represent the last TaskInstance, so all the expected properties are available: value, isRunning, isFinished, etc

Making your own Resources with

LifecycleResource

This resource base class has 3 lifecycle hooks:

  • setup - called upon first access of the resource
  • update - called when any tracked used during setup changes
  • teardown - called when the containing context is torn down

An example of this might be an object that you want to have perform some complex or async behavior

class MyResource extends LifecycleResource {
  @tracked isRunning;
  @tracked error;

  get status() {
    if (this.isRunning) return 'pending';
    if (this.error) return this.error;

    return 'idle';
  }

  setup() {
    this.doAsyncTask();
  }

  update() {
    this.doAsyncTask();
  }

  async doAsyncTask() {
    let [ids] = this.args.positional;

    this.isRunning = true;
    this.error = undefined;

    try {
      // some long running stuff here
    } catch (e) {
      this.error = e
    }

    this.isRunning = false;
  }
}

Using your custom Resource would look like

class ContainingClass {
  data = useResource(this, MyResource, () => [this.ids])
}
function Resources

While functions can be "stateless", Resources don't provide much value unless you can have state. function Resources solve this by passing the previous invocation's return value as an argument to the next time the function is called.

Example:

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 via this.info.value.characters in the StarWarsInfo class

While this example is a bit contrived, hopefully it demonstrates how the state arg works. During the first invocation, state is falsey, allowing the rest of the function to execute. The next time this.ids changes, the function will be called again, 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 long as the function doesn't interact with this).

In this example, where the function is async, the "value" of info.value is undefined until the function completes.

To help prevent accidental async footguns, even if a function is synchronous, it is still ran asynchronously, therefor, the thunk cannot be avoided.

class MyClass {
  @tracked num = 3;

  info = useResource(this, () => {
    return this.num * 2;
  });
}

this.info.value will be undefined, then 6 and will not change when num changes.

Thunks

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 consuming tracked data within setup / initialization (which is done for you with useFunction).

  • The thunk is "just a function" that allows tracked data to be lazily consumed by the resource.

The args thunk accepts the following data shapes:

() => [an, array]
() => ({ hello: 'there' })
() => ({ named: {...}, positional: [...] })
An array

when an array is passed, inside the Resource, this.args.named will be empty and this.args.positional will contain the result of the thunk.

for function resources, this is the only type of thunk allowed.

An object of named args

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.

An object containing both named args and positional args

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 property
  • this.args.positional will be the value of the result of the thunk's positional property

This is the same shape of args used throughout Ember's Helpers, Modifiers, etc

Public Types

import type { ArgsWrapper, Named, Positional } from 'ember-resources';

where:

ArgsWrapper

interface ArgsWrapper {
  positional?: unknown[];
  named?: Record<string, unknown>;
}

this is a utility interface that represents all of the args used throughout Ember.

Example

class MyResource extends LifecycleResource { // default args type
  constructor(owner: unknown, args: ArgsWrapper) {
    super(owner, args);
  }
}

Shorthand for positional only

export interface Positional<T extends Array<unknown>> {
  positional: T;
}

Example:

class MyResource extends LifecycleResource<Positional<[number]>> {
}

Shorthand for named only

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: or positional: keys that would be required if not using these shorthands...

Testing

If your resources are consumed by components, you'll want to continue to test using rendering tests, as things should "just work" with those style of tests.

Where things get interesting is when you want to unit test your resources.

There are two approaches:

new the resource directly

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 to be called manually, because we aren't using useResource, which wraps the Ember-builtin invokeHelper, which takes care of reactivity for us. As a consequence, any changes to the args wrapper will not cause updates to the resource instance.

Create a wrapper context for reactive consumption

If, instead of creating MyResource directly, like in the example above, it is wrapped in a test class and utilizes useResource:

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 to await settled() after a change to a reactive property so that you allow time for the framework to propagate changes to all the reactive bits.

Example:

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);

Contributing

See the Contributing guide for details.

License

This project is licensed under the MIT License.

Thanks

This library wouldn't be possible without the work of:

So much appreciate for the work both you have put in to Resources <3

Keywords

FAQs

Package last updated on 20 Jul 2021

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc