ember-provide-consume-context
v0.6.1
Published
A context API implementation for Ember.js
Downloads
16,156
Readme
ember-provide-consume-context
This addon provides a way to share data through to nested components without having to pass arguments at each level (i.e. no prop drilling).
Compatibility
- Ember.js v4.8 or above
- Embroider or ember-auto-import v2
Installation
ember install ember-provide-consume-context
Usage
Context providers
Data can be provided to all of a component's descendants in one of two ways:
- The
provide
decorator - The
ContextProvider
component
@provide
This library exports a decorator, which can be used on any getter or property to expose it as context to all descendants of the component. The decorator requires a string argument that is the name of the context, which will be used by descendants to look up the value.
import Component from '@glimmer/component';
import { provide } from 'ember-provide-consume-context';
export default class MyComponent extends Component {
@provide('my-context-name')
get someState() {
return 'some value';
}
}
It is possible to expose tracked properties in context, by adding a
@provide
-decorated getter, like so:
import Component from '@glimmer/component';
import { provide } from 'ember-provide-consume-context';
export default class MyComponent extends Component {
@tracked myTrackedValue = 'some value';
@provide('my-context-name')
get someState() {
return this.myTrackedValue;
// or expressions like return this.args.someArgument;
}
}
Any descendants retrieving this context will automatically recompute when the tracked property changes.
Note: For the purposes of the decorator, it doesn't matter what the getter is named. The value of the getter will be exposed under the context name.
ContextProvider
Alternatively, this addon also provides a ContextProvider
component:
<ContextProvider @key="my-context-name" @value={{this.someValue}}>
{{! descendant components can now look up "my-context-name" }}
</ContextProvider>
Context consumers
There are also two ways to retrieve a context value:
- Using the
consume
decorator - Using the
ContextConsumer
component
@consume
The consume
decorator allows us to retrieve a context value in a way that's
similar to working with an Ember service:
import Component from '@glimmer/component';
import { consume } from 'ember-provide-consume-context';
export default class MyChildComponent extends Component {
@consume('my-context-name') myContextValue!: string;
}
<p>The context value is: {{this.myContextValue}}</p>
ContextConsumer
Alternatively, a context value can be retrieved with the ContextConsumer
component. It accepts a @key
string argument, which is the name of the
context, and it yields the value of the context:
<ContextConsumer @key="my-context-name" as |value|>
{{value}}
</ContextConsumer>
Important note: Currently, the @provide
and @consume
decorators only
work in components. Providing or consuming context state from Routes,
Controllers, Helpers or Services does not work.
In Route templates, the ContextProvider
and ContextConsumer
components can
be used for working with contexts.
TypeScript
This addon ships with TypeScript and Glint support.
To take advantage of Glint types (for the ContextProvider
and
ContextConsumer
components), you'll need to import the template registry
interface, as described in the Glint docs:
// types/global.d.ts
import '@glint/environment-ember-loose';
import type EmberContextTemplateRegistry from 'ember-provide-consume-context/template-registry';
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry extends EmberContextTemplateRegistry, /* other addon registries */ {
// local entries
}
}
Additionally, this addon exposes a type registry to associate string context keys with the type of value.
First, you'll need to add
import 'ember-provide-consume-context/context-registry';
somewhere in your source files or type declaration files. This will force TypeScript to merge type declarations for the registry.
Next, you'll need to declare the value type for your context keys, like this:
declare module 'ember-provide-consume-context/context-registry' {
export default interface ContextRegistry {
'my-context-name': string;
'AuthContext': AuthInterface;
// ...
}
}
You can keep a global types/context.d.ts
file and declare all your contexts
there, or you can declare the context types in the same files where you provide
them, like this:
import Component from '@glimmer/component';
import { provide } from 'ember-provide-consume-context';
export default class MyComponent extends Component {
@provide('my-context-name')
get someState() {
return 'some value';
}
}
declare module 'ember-provide-consume-context/context-registry' {
export default interface ContextRegistry {
'my-context-name': string;
}
}
With ContextRegistry
types defined and Glint enabled, the ContextProvider
and ContextConsumer
components will correctly infer the value types based on
the string context key arguments.
When consuming a context with the @consume
decorator, the type can be
retrieved using the registry like this:
import Component from '@glimmer/component';
import { consume } from 'ember-provide-consume-context';
import type ContextRegistry from 'ember-provide-consume-context/context-registry';
export default class MyChildComponent extends Component {
@consume('my-context-name') myContextValue!: ContextRegistry['my-context-name'];
}
myContextValue
will then correctly infer the type of value - provided you have
defined the type elsewhere, of course. The explicit type assignment above is
necessary, because returning a type through decorators automatically isn't
supported in TypeScript.
At the moment, context names must be strings. We have tried symbol keys, but Ember Inspector started throwing errors when parsing component trees where symbols were used as arguments, so we dropped that idea (for now, at least).
Finally, to avoid typos when referencing context keys, and generally making it easier to maintain context definitions, we recommend declaring and exporting your context keys as constants:
import Component from '@glimmer/component';
import { provide } from 'ember-provide-consume-context';
export const MyContext = 'my-context-name' as const;
export default class MyComponent extends Component {
@provide(MyContext)
get someState() {
return 'some value';
}
}
declare module 'ember-provide-consume-context/context-registry' {
export default interface ContextRegistry {
[MyContext]: string;
}
}
import Component from '@glimmer/component';
import { consume } from 'ember-provide-consume-context';
import { MyContext } from 'my-app/components/my-component';
import type ContextRegistry from 'ember-provide-consume-context/context-registry';
export default class MyChildComponent extends Component {
@consume(MyContext) myContextValue!: ContextRegistry[typeof MyContext];
}
Testing
The addon includes two test helpers for setting up context providers in integration tests:
provide
setupRenderWrapper
provide
The provide
helper can be used to set up a global context provider for a test, by calling
it with a context key and its value, for example:
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { provide } from 'ember-provide-consume-context/test-support';
module('component tests', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function (this) {
provide('my-test-context', {
count: 1,
});
});
test('can read context', async function (assert) {
await render(hbs`
<ContextConsumer @key="my-test-context" as |data|>
<div id="content">{{data.count}}</div>
</ContextConsumer>
`);
assert.dom('#content').hasText('1');
});
});
setupRenderWrapper
setupRenderWrapper
can be used to define a template to wrap contents rendered via render
in a test.
This is useful, for example, when it's necessary to wrap content in a context provider exposed by and addon, or an internal context provider from the application. For example:
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupRenderWrapper } from 'ember-provide-consume-context/test-support';
module('component tests', function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function (this) {
setupRenderWrapper(hbs`
<ThemeProvider @mode="dark">
{{outlet}}
</ThemeProvider>
`);
});
test('can read context', async function (assert) {
await render(hbs`
<DesignSystemButton>Button</DesignSystemButton>
`);
// renders in dark mode
});
});
[!IMPORTANT]
The render wrapper must use{{outlet}}
rather than{{yield}}
to render the wrapped content.Internally,
setupRenderWrapper
overrrides the outlet template defined by@ember/test-helpers
: https://github.com/emberjs/ember-test-helpers/blob/9cec68dc6aa9c0a7a449eb89797eb81299fa727f/addon/addon-test-support/%40ember/test-helpers/setup-rendering-context.ts#L68-L69
Inspiration
The idea was to create an API similar to the Context API in React
React Context API
: The original inspiration for this project is, of course, the React ContextSvelte Context API
: Svelte exposes contexts via simplesetContext
andgetContext
functionsVue provide/inject
: Vue allows working with context viaprovide
andinject
functionsember-context
: Another Ember addon that also implements a similar Context API. However, that addon's implementation relies has providers racing on a provider key, while our addon uses the Ember component tree
Contributing
See the Contributing guide for details.
License
This project is licensed under the MIT License.