Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 72 additions & 7 deletions packages/@ember/-internals/glimmer/lib/create-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
* @module @ember/helper
*/
import { precompileTemplate } from '@ember/template-compilation';
import { lookupRenderContext, provideRenderContext } from '@glimmer/runtime/lib/render-scope';
import {
lookupRenderContext,
lookupRenderContextFor,
provideRenderContext,
} from '@glimmer/runtime/lib/render-scope';
import { valueForRef } from '@glimmer/reference/lib/reference';
import InternalComponent, {
type OpaqueInternalComponentConstructor,
Expand All @@ -11,12 +15,14 @@ import InternalComponent, {

/**
* The shape returned by `createContext`. Use `Provide` in templates to bind a
* value into the render tree and read `value` to get the nearest enclosing
* provided value.
* value into the render tree, read `value` to get the nearest enclosing
* provided value, or call `consume(this)` to read it from a component
* instance's position in the render tree (e.g. in an event handler).
*/
export interface Context<T> {
Provide: OpaqueInternalComponentConstructor;
get value(): T;
consume(consumer?: object): T;
}

/**
Expand Down Expand Up @@ -69,11 +75,44 @@ export interface Context<T> {
* legitimate "fall back to undefined" state. If you want a default, provide
* one at the application root.
*
* The returned object also has a `consume()` method. With no argument it
* behaves exactly like reading `value`. Passed a component instance, it
* resolves the context from that component's position in the render tree
* instead -- the way to read a context from event handlers and other code
* that runs outside of rendering:
*
* ```gjs
* import Component from '@glimmer/component';
* import { createContext } from '@ember/helper';
* import { on } from '@ember/modifier';
*
* const theme = createContext<Theme>();
*
* class ThemedButton extends Component {
* onClick = () => {
* // Outside of rendering, `theme.value` would throw -- but passing the
* // component instance resolves the nearest <theme.Provide> above it.
* console.log(theme.consume(this).color);
* };
*
* <template>
* <button {{on 'click' this.onClick}}>{{yield}}</button>
* </template>
* }
* ```
*
* Any rendered component instance works with every context -- the instance
* identifies a position in the render tree, not any one context's value.
* Resolution happens at `consume` time, so the value read is the provider's
* current `@value`, not a snapshot from render time.
*
* @method createContext
* @static
* @for @ember/helper
* @returns {Object} An object with `Provide` (a component that takes a
* `@value`) and `value` (a getter that reads the nearest provided value).
* `@value`), `value` (a getter that reads the nearest provided value), and
* `consume` (reads ambiently like `value`, or from a component instance's
* position).
* @public
*/
export function createContext<T>(): Context<T> {
Expand Down Expand Up @@ -104,19 +143,45 @@ export function createContext<T>(): Context<T> {
}
}

// The ambient read backing both the `value` getter and a no-argument
// `consume()` call: resolve the nearest provider at the currently-rendering
// position.
function resolveAmbient(): T {
let read = lookupRenderContext(key);
if (read === undefined) {
throw new Error(
'A context `value` was read outside of rendering. The render-tree scope is only available during rendering -- there is nothing to read. To read a context from an event handler or other async callback, pass the component instance: `context.consume(this)`.'
);
}
if (read === null) {
throw new Error(
'No matching `<Provide>` was found in the render tree. Wrap consumers in `<Context.Provide @value={{...}}>...</Context.Provide>`, or provide a default at the application root.'
);
}
return read() as T;
}

return {
Provide: opaquify(Provide, PROVIDE_TEMPLATE),

get value(): T {
let read = lookupRenderContext(key);
return resolveAmbient();
},

consume(consumer?: object): T {
if (consumer === undefined) {
return resolveAmbient();
}

let read = lookupRenderContextFor(consumer, key);
if (read === undefined) {
throw new Error(
'A context `value` was read outside of rendering. The render-tree scope is only available during rendering -- there is nothing to read.'
'`consume(component)` expects a component instance that has been rendered -- the instance is how the context finds its position in the render tree. Pass the component itself (usually `this`), not another object, and only after the component has rendered.'
);
}
if (read === null) {
throw new Error(
'No matching `<Provide>` was found in the render tree. Wrap consumers in `<Context.Provide @value={{...}}>...</Context.Provide>`, or provide a default at the application root.'
'No matching `<Provide>` was found above the component passed to `consume`. Wrap the component in `<Context.Provide @value={{...}}>...</Context.Provide>`, or provide a default at the application root.'
);
}
return read() as T;
Expand Down
Loading
Loading