diff --git a/.changeset/strange-dolls-refuse.md b/.changeset/strange-dolls-refuse.md new file mode 100644 index 0000000..e2ce7f9 --- /dev/null +++ b/.changeset/strange-dolls-refuse.md @@ -0,0 +1,5 @@ +--- +"ember-provide-consume-context": patch +--- + +Avoid infinite loops when reading context values at construct time diff --git a/ember-provide-consume-context/src/-private/provide-consume-context-container.ts b/ember-provide-consume-context/src/-private/provide-consume-context-container.ts index b92663e..5b8c373 100644 --- a/ember-provide-consume-context/src/-private/provide-consume-context-container.ts +++ b/ember-provide-consume-context/src/-private/provide-consume-context-container.ts @@ -52,7 +52,13 @@ export class ProvideConsumeContextContainer { // not the VM ones). // The values are objects that map a string ID (provider ID) to the provider // component instance. - contexts = new WeakMap(); + // "parentContexts" contain references to contexts coming from "above", and + // are used to read values from (which allows a component to provide and consume the same key) + parentContexts = new WeakMap(); + // "nextContexts" are context maps used to propagate context values down + // into the component tree, which includes the merged providers from the + // current component (if any) + nextContexts = new WeakMap(); // Global contexts are registered by test-support helpers to allow easily // providing context values in tests. @@ -135,9 +141,11 @@ export class ProvideConsumeContextContainer { } private registerProvider(provider: any) { - const providerContexts: Contexts = this.currentContexts(); + const parentContexts: Contexts = this.currentContexts(); + const mergedContexts: Contexts = { ...parentContexts }; const registeredContexts = provider[EMBER_PROVIDE_CONSUME_CONTEXT_KEY]; + // If the provider has registered contexts, store references // to them on the current contexts object if (registeredContexts != null) { @@ -145,7 +153,7 @@ export class ProvideConsumeContextContainer { registeredContexts as Record, ).forEach(([contextKey, key]) => { if (key in provider) { - providerContexts[contextKey] = { + mergedContexts[contextKey] = { instance: provider, key, }; @@ -153,16 +161,15 @@ export class ProvideConsumeContextContainer { }); } - this.contexts.set(provider, providerContexts); + this.parentContexts.set(provider, parentContexts); + this.nextContexts.set(provider, mergedContexts); } private registerComponent(component: any) { const currentContexts = this.currentContexts(); - // If a current context reference or global contexts exist, register them to the component - if (Object.keys(currentContexts).length > 0) { - this.contexts.set(component, currentContexts); - } + this.parentContexts.set(component, currentContexts); + this.nextContexts.set(component, currentContexts); } currentContexts() { @@ -170,8 +177,11 @@ export class ProvideConsumeContextContainer { const globalContexts = this.#globalContexts ?? {}; - if (this.contexts.has(current) || Object.keys(globalContexts).length > 0) { - const context = this.contexts.get(current); + if ( + this.nextContexts.has(current) || + Object.keys(globalContexts).length > 0 + ) { + const context = this.nextContexts.get(current); return { ...globalContexts, ...context }; } @@ -179,8 +189,8 @@ export class ProvideConsumeContextContainer { } contextsFor(component: any) { - if (this.contexts.has(component)) { - return this.contexts.get(component); + if (this.parentContexts.has(component)) { + return this.parentContexts.get(component); } // If a context for this component is not yet registered, but diff --git a/test-app/tests/integration/components/decorators-test.ts b/test-app/tests/integration/components/decorators-test.ts index 5747d28..754d454 100644 --- a/test-app/tests/integration/components/decorators-test.ts +++ b/test-app/tests/integration/components/decorators-test.ts @@ -972,4 +972,117 @@ module('Integration | Decorators', function (hooks) { assert.dom('#content-1').hasText('1'); assert.dom('#content-2').hasText('2'); }); + + test('provider that references its own context and reading context value at construct time', async function (assert) { + class TestProviderComponent extends Component<{ + Element: HTMLDivElement; + Blocks: { + default: []; + }; + }> { + @consume('my-test-context') parentContextValue!: number; + + get nextContextValue() { + return (this.parentContextValue ?? 0) + 1; + } + } + + setComponentTemplate( + // @ts-ignore + hbs`{{! @glint-ignore }} + {{yield}} + `, + TestProviderComponent, + ); + + class TestConsumerComponent extends Component<{ + Element: HTMLDivElement; + }> { + @consume('my-test-context') contextValue!: number; + + readValue = this.contextValue; + } + + setComponentTemplate( + // @ts-ignore + hbs`{{! @glint-ignore }} +
{{this.readValue}}
+ `, + TestConsumerComponent, + ); + + interface TestContext { + TestProviderComponent: typeof TestProviderComponent; + TestConsumerComponent: typeof TestConsumerComponent; + } + (this as unknown as TestContext).TestProviderComponent = + TestProviderComponent; + (this as unknown as TestContext).TestConsumerComponent = + TestConsumerComponent; + + await render(hbs` + + + + `); + + assert.dom('#content').hasText('1'); + }); + + test('ContextProvider that references its components context and reading context value at construct time', async function (assert) { + class TestProviderComponent extends Component<{ + Element: HTMLDivElement; + Blocks: { + default: []; + }; + }> { + @consume('my-test-context') parentContextValue!: number; + + @provide('my-test-context') + get nextContextValue() { + return (this.parentContextValue ?? 0) + 1; + } + } + + setComponentTemplate( + // @ts-ignore + hbs`{{! @glint-ignore }} + {{yield}} + `, + TestProviderComponent, + ); + + class TestConsumerComponent extends Component<{ + Element: HTMLDivElement; + }> { + @consume('my-test-context') contextValue!: number; + + readValue = this.contextValue; + } + + setComponentTemplate( + // @ts-ignore + hbs`{{! @glint-ignore }} +
{{this.readValue}}
+ `, + TestConsumerComponent, + ); + + interface TestContext { + TestProviderComponent: typeof TestProviderComponent; + TestConsumerComponent: typeof TestConsumerComponent; + } + (this as unknown as TestContext).TestProviderComponent = + TestProviderComponent; + (this as unknown as TestContext).TestConsumerComponent = + TestConsumerComponent; + + await render(hbs` + + + + `); + + assert.dom('#content').hasText('1'); + }); });