Skip to content

Commit

Permalink
Support multiple copies of addon (#17)
Browse files Browse the repository at this point in the history
Closes #16
  • Loading branch information
kevinkucharczyk authored Mar 17, 2024
1 parent 687e444 commit a0adf29
Show file tree
Hide file tree
Showing 6 changed files with 421 additions and 96 deletions.
5 changes: 5 additions & 0 deletions .changeset/chilly-clouds-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ember-provide-consume-context": minor
---

Support multiple copies of addon
34 changes: 20 additions & 14 deletions ember-provide-consume-context/src/-private/decorators.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
import type ContextRegistry from '../context-registry';
import { DECORATED_PROPERTY_CLASSES } from './provide-consume-context-container';
import { EMBER_PROVIDE_CONSUME_CONTEXT_KEY } from './provide-consume-context-container';
import { getContextValue, hasContext } from './utils';

export function provide(contextKey: keyof ContextRegistry) {
return function decorator(target: any, key: string) {
// Track the class as having a decorated property. Later, this will be used
// on instances of this component to register them as context providers.
const currentContexts = DECORATED_PROPERTY_CLASSES.get(target.constructor);
if (currentContexts == null) {
DECORATED_PROPERTY_CLASSES.set(target.constructor, {
[contextKey]: key,
});
} else {
DECORATED_PROPERTY_CLASSES.set(target.constructor, {
...currentContexts,
[contextKey]: key,
});
}
// Define a property on the class, which will later be used on instances of
// the component to register them as context providers.
const currentContexts = Object.getOwnPropertyDescriptor(
target,
EMBER_PROVIDE_CONSUME_CONTEXT_KEY,
);

// A class can have multiple @provide decorated properties, we need to
// merge the definitions
const contextsValue = {
...currentContexts?.value,
[contextKey]: key,
};

Object.defineProperty(target, EMBER_PROVIDE_CONSUME_CONTEXT_KEY, {
value: contextsValue,
writable: true,
configurable: true,
});
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,36 @@ function overrideVM(runtime: any) {
// "program" instance, which is all we need to get the current opcode.
const opcode = this.program.opcode(this.pc);

if (opcode.type === Op.GetComponentSelf) {
// Get the component instance from the VM
// (that's the VM's component instance, not the Glimmer Component one)
// https://github.com/glimmerjs/glimmer-vm/blob/68d371bdccb41bc239b8f70d832e956ce6c349d8/packages/%40glimmer/runtime/lib/compiled/opcodes/component.ts#L579
const instance = this.fetchValue<ComponentInstance>(opcode.op1);

// Add the component to the stack
this.env.provideConsumeContextContainer?.enter(instance);
// When there are updates/rerenders, make sure we add to the stack again
this.updateWith(new ProvideConsumeContextUpdateOpcode(instance));
}
// Getting "type" may fail with "Expected value to be present", coming from
// https://github.com/glimmerjs/glimmer-vm/blob/f03632077d98910de7ae3f7c22ebed98cb4f909a/packages/%40glimmer/program/lib/program.ts#L116
try {
const { type, op1 } = opcode;

if (type === Op.GetComponentSelf) {
// Get the component instance from the VM
// (that's the VM's component instance, not the Glimmer Component one)
// https://github.com/glimmerjs/glimmer-vm/blob/68d371bdccb41bc239b8f70d832e956ce6c349d8/packages/%40glimmer/runtime/lib/compiled/opcodes/component.ts#L579
const instance = this.fetchValue<ComponentInstance>(op1);

// Add the component to the stack
this.env.provideConsumeContextContainer?.enter(instance);
// When there are updates/rerenders, make sure we add to the stack again
this.updateWith(new ProvideConsumeContextUpdateOpcode(instance));
}

if (opcode.type === Op.DidRenderLayout) {
// Get the component instance from the VM
// (that's the VM's component instance, not the Glimmer Component one)
// https://github.com/glimmerjs/glimmer-vm/blob/68d371bdccb41bc239b8f70d832e956ce6c349d8/packages/%40glimmer/runtime/lib/compiled/opcodes/component.ts#L832
const instance = this.fetchValue<ComponentInstance>(opcode.op1);
if (type === Op.DidRenderLayout) {
// Get the component instance from the VM
// (that's the VM's component instance, not the Glimmer Component one)
// https://github.com/glimmerjs/glimmer-vm/blob/68d371bdccb41bc239b8f70d832e956ce6c349d8/packages/%40glimmer/runtime/lib/compiled/opcodes/component.ts#L832
const instance = this.fetchValue<ComponentInstance>(op1);

// After the component has rendered, remove it from the stack
this.env.provideConsumeContextContainer?.exit(instance);
// On updates/rerenders, make sure to remove from the stack again
this.updateWith(new ProvideConsumeContextDidRenderOpcode(instance));
// After the component has rendered, remove it from the stack
this.env.provideConsumeContextContainer?.exit(instance);
// On updates/rerenders, make sure to remove from the stack again
this.updateWith(new ProvideConsumeContextDidRenderOpcode(instance));
}
} catch {
// ignore
}

return originalNext.apply(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,32 @@ import type { ComponentInstance } from '@glimmer/interfaces';
import { Stack } from '@glimmer/util';
import type ContextRegistry from '../context-registry';

// Map of component class that contain @provide decorated properties, with their
// respective context keys and property names
export const DECORATED_PROPERTY_CLASSES = new WeakMap<
any,
Record<keyof ContextRegistry, string>
>();
// Map of instances of the ContextProvider component, or components that contain
// @provide decorated properties
export const PROVIDER_INSTANCES = new WeakMap<any, Record<string, string>>();

export function trackProviderInstanceContexts(
export const EMBER_PROVIDE_CONSUME_CONTEXT_KEY = Symbol.for(
'EMBER_PROVIDE_CONSUME_CONTEXT_KEY',
);

export function setContextMetadataOnContextProviderInstance(
instance: any,
contextDefinitions: [
contextKey: keyof ContextRegistry,
propertyKey: string,
][],
) {
const currentContexts = PROVIDER_INSTANCES.get(instance);
if (currentContexts == null) {
PROVIDER_INSTANCES.set(instance, Object.fromEntries(contextDefinitions));
} else {
PROVIDER_INSTANCES.set(instance, {
...currentContexts,
...Object.fromEntries(contextDefinitions),
});
}
const currentContexts = Object.getOwnPropertyDescriptor(
instance,
EMBER_PROVIDE_CONSUME_CONTEXT_KEY,
);

const contextsValue = {
...currentContexts?.value,
...Object.fromEntries(contextDefinitions),
};

Object.defineProperty(instance, EMBER_PROVIDE_CONSUME_CONTEXT_KEY, {
value: contextsValue,
writable: true,
configurable: true,
});
}

interface Contexts {
Expand Down Expand Up @@ -71,34 +71,11 @@ export class ProvideConsumeContextContainer {
}

enter(instance: ComponentInstance): void {
const componentDefinitionClass = instance.definition.state;
const actualComponentInstance = (instance?.state as any)?.component;

if (actualComponentInstance != null) {
const isDecorated = DECORATED_PROPERTY_CLASSES.has(
componentDefinitionClass,
);

// If this is an instance of a component that contains @provide decorated
// properties, add this instance - and the context keys - to the
// PROVIDE_INSTANCES map, so that the context values can be set in the
// next step
if (isDecorated) {
const contextKeys = DECORATED_PROPERTY_CLASSES.get(
componentDefinitionClass,
);

if (contextKeys != null) {
trackProviderInstanceContexts(
actualComponentInstance,
Object.entries(contextKeys) as [keyof ContextRegistry, string][],
);
}
}

const isProviderInstance = PROVIDER_INSTANCES.has(
actualComponentInstance,
);
const isProviderInstance =
actualComponentInstance[EMBER_PROVIDE_CONSUME_CONTEXT_KEY];

if (isProviderInstance) {
this.registerProvider(actualComponentInstance);
Expand Down Expand Up @@ -131,9 +108,11 @@ export class ProvideConsumeContextContainer {
}
}

const registeredContexts = PROVIDER_INSTANCES.get(provider);
const registeredContexts = provider[EMBER_PROVIDE_CONSUME_CONTEXT_KEY];
if (registeredContexts != null) {
Object.entries(registeredContexts).forEach(([contextKey, key]) => {
Object.entries(
registeredContexts as Record<keyof ContextRegistry, string>,
).forEach(([contextKey, key]) => {
if (key in provider) {
providerContexts[contextKey] = {
instance: provider,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Component from '@glimmer/component';
import { trackProviderInstanceContexts } from '../-private/provide-consume-context-container';
import { setContextMetadataOnContextProviderInstance } from '../-private/provide-consume-context-container';
import type ContextRegistry from '../context-registry';

interface ContextProviderSignature<K extends keyof ContextRegistry> {
Expand All @@ -18,7 +18,7 @@ export default class ContextProvider<
constructor(owner: unknown, args: ContextProviderSignature<K>['Args']) {
super(owner, args);

trackProviderInstanceContexts(this, [[args.key, 'value']]);
setContextMetadataOnContextProviderInstance(this, [[args.key, 'value']]);
}

get value() {
Expand Down
Loading

0 comments on commit a0adf29

Please sign in to comment.