diff --git a/packages/angular/projects/angular-sdk/README.md b/packages/angular/projects/angular-sdk/README.md index 392e626888..0f3e0966a8 100644 --- a/packages/angular/projects/angular-sdk/README.md +++ b/packages/angular/projects/angular-sdk/README.md @@ -129,7 +129,6 @@ If `initializing` and `reconciling` are not given, the feature flag value that i determine what will be rendered. ```html -
This is shown when the feature flag is enabled.
@@ -152,6 +151,9 @@ This parameter is optional, if omitted, the `thenTemplate` will always be render The `domain` parameter is _optional_ and will be used as domain when getting the OpenFeature provider. +The `updateOnConfigurationChanged` and `updateOnContextChanged` parameter are _optional_ and used to disable the +automatic re-rendering on flag value or context change. They are set to `true` by default. + The template referenced in `else` will be rendered if the evaluated feature flag is `false` for the `booleanFeatureFlag` directive and if the `value` does not match evaluated flag value for all other directives. This parameter is _optional_. @@ -163,7 +165,8 @@ This parameter is _optional_, if omitted, the `then` and `else` templates will b ##### Boolean Feature Flag ```html -
+
This is shown when the feature flag is enabled.
@@ -180,7 +183,8 @@ This parameter is _optional_, if omitted, the `then` and `else` templates will b ##### Number Feature Flag ```html -
+
This is shown when the feature flag matches the specified discount rate.
@@ -197,7 +201,8 @@ This parameter is _optional_, if omitted, the `then` and `else` templates will b ##### String Feature Flag ```html -
+
This is shown when the feature flag matches the specified theme color.
@@ -214,7 +219,8 @@ This parameter is _optional_, if omitted, the `then` and `else` templates will b ##### Object Feature Flag ```html -
+
This is shown when the feature flag matches the specified user configuration.
@@ -228,21 +234,35 @@ This parameter is _optional_, if omitted, the `then` and `else` templates will b ``` +##### Opting-out of automatic re-rendering + +By default, the directive re-renders when the flag value changes or the context changes. + +In cases, this is not desired, re-rendering can be disabled for both events: + +```html +
+ This is shown when the feature flag is enabled. +
+``` + ##### Consuming the evaluation details The `evaluation details` can be used when rendering the templates. -The directives [`$implicit`](https://angular.dev/guide/directives/structural-directives#structural-directive-shorthand) value will be bound to the flag value and additionally the value `evaluationDetails` will be +The directives [`$implicit`](https://angular.dev/guide/directives/structural-directives#structural-directive-shorthand) +value will be bound to the flag value and additionally the value `evaluationDetails` will be bound to the whole evaluation details. They can be referenced in all templates. The following example shows `value` being implicitly bound and `details` being bound to the evaluation details. ```html -
+
It was a match! The theme color is {{ value }} because of {{ details.reason }}
- + It was no match! The theme color is {{ value }} because of {{ details.reason }} diff --git a/packages/angular/projects/angular-sdk/src/lib/feature-flag.directive.spec.ts b/packages/angular/projects/angular-sdk/src/lib/feature-flag.directive.spec.ts index 66b91ca49b..509b8c4e6b 100644 --- a/packages/angular/projects/angular-sdk/src/lib/feature-flag.directive.spec.ts +++ b/packages/angular/projects/angular-sdk/src/lib/feature-flag.directive.spec.ts @@ -155,6 +155,23 @@ import { {{ value }}
+
+
+ Flag On +
+ +
Flag Off
+
+
`, }) @@ -380,6 +397,28 @@ describe('FeatureFlagDirective', () => { await expectRenderedText(fixture, 'case-6', 'Flag Off'); }); + it('should opt-out of re-rendering when flag value changes', async () => { + const { fixture, client, provider } = await createTestingModule({ + flagConfiguration: { + 'test-flag': { + variants: { default: true }, + defaultVariant: 'default', + disabled: false, + }, + 'new-test-flag': { + variants: { default: false }, + defaultVariant: 'default', + disabled: false, + }, + }, + }); + await waitForClientReady(client); + await expectRenderedText(fixture, 'case-12', 'Flag On'); + + await updateFlagValue(provider, false); + await expectRenderedText(fixture, 'case-12', 'Flag On'); + }); + it('should evaluate on flag domain change', async () => { const { fixture, client } = await createTestingModule({ flagConfiguration: { diff --git a/packages/angular/projects/angular-sdk/src/lib/feature-flag.directive.ts b/packages/angular/projects/angular-sdk/src/lib/feature-flag.directive.ts index f5fa21617d..d7304adb4e 100644 --- a/packages/angular/projects/angular-sdk/src/lib/feature-flag.directive.ts +++ b/packages/angular/projects/angular-sdk/src/lib/feature-flag.directive.ts @@ -13,6 +13,7 @@ import { ClientProviderEvents, ClientProviderStatus, EvaluationDetails, + EventDetails, EventHandler, FlagValue, JsonValue, @@ -42,7 +43,14 @@ export abstract class FeatureFlagDirective implements OnDes protected _client: Client; protected _lastEvaluationResult: EvaluationDetails; - protected _flagChangeHandler: EventHandler | null = null; + + protected _readyHandler: EventHandler | null = null; + protected _flagChangeHandler: EventHandler | null = null; + protected _contextChangeHandler: EventHandler | null = null; + protected _reconcilingHandler: EventHandler | null = null; + + protected _updateOnContextChanged: boolean = true; + protected _updateOnConfigurationChanged: boolean = true; protected _thenTemplateRef: TemplateRef> | null; protected _thenViewRef: EmbeddedViewRef | null; @@ -85,25 +93,49 @@ export abstract class FeatureFlagDirective implements OnDes if (this._client) { this.disposeClient(this._client); } - this._client = OpenFeature.getClient(this._featureFlagDomain); - this._flagChangeHandler = () => { + + const baseHandler = () => { const result = this.getFlagDetails(this._featureFlagKey, this._featureFlagDefault); this.onFlagValue(result, this._client.providerStatus); }; - this._client.addHandler(ClientProviderEvents.ContextChanged, this._flagChangeHandler); + this._flagChangeHandler = () => { + if (this._updateOnConfigurationChanged) { + baseHandler(); + } + }; + + this._contextChangeHandler = () => { + if (this._updateOnContextChanged) { + baseHandler(); + } + }; + + this._readyHandler = () => baseHandler(); + this._reconcilingHandler = () => baseHandler(); + this._client.addHandler(ClientProviderEvents.ConfigurationChanged, this._flagChangeHandler); - this._client.addHandler(ClientProviderEvents.Ready, this._flagChangeHandler); - this._client.addHandler(ClientProviderEvents.Reconciling, this._flagChangeHandler); + this._client.addHandler(ClientProviderEvents.ContextChanged, this._contextChangeHandler); + this._client.addHandler(ClientProviderEvents.Ready, this._readyHandler); + this._client.addHandler(ClientProviderEvents.Reconciling, this._reconcilingHandler); } private disposeClient(client: Client) { + if (this._contextChangeHandler()) { + client.removeHandler(ClientProviderEvents.ContextChanged, this._contextChangeHandler); + } + if (this._flagChangeHandler) { - client.removeHandler(ClientProviderEvents.ContextChanged, this._flagChangeHandler); client.removeHandler(ClientProviderEvents.ConfigurationChanged, this._flagChangeHandler); - client.removeHandler(ClientProviderEvents.Ready, this._flagChangeHandler); - client.removeHandler(ClientProviderEvents.Reconciling, this._flagChangeHandler); + } + + if (this._readyHandler) { + client.removeHandler(ClientProviderEvents.Ready, this._readyHandler); + } + + if (this._reconcilingHandler) { + client.removeHandler(ClientProviderEvents.Reconciling, this._reconcilingHandler); } } @@ -226,6 +258,28 @@ export class BooleanFeatureFlagDirective extends FeatureFlagDirective i super.featureFlagDomain = domain; } + /** + * Update the component if the provider emits a ConfigurationChanged event. + * Set to false to prevent components from re-rendering when flag value changes + * are received by the associated provider. + * Defaults to true. + */ + @Input({ required: false }) + set booleanFeatureFlagUpdateOnConfigurationChanged(enabled: boolean | undefined) { + this._updateOnConfigurationChanged = enabled ?? true; + } + + /** + * Update the component when the OpenFeature context changes. + * Set to false to prevent components from re-rendering when attributes which + * may be factors in flag evaluation change. + * Defaults to true. + */ + @Input({ required: false }) + set booleanFeatureFlagUpdateOnContextChanged(enabled: boolean | undefined) { + this._updateOnContextChanged = enabled ?? true; + } + /** * Template to be displayed when the feature flag is false. */ @@ -324,6 +378,28 @@ export class NumberFeatureFlagDirective extends FeatureFlagDirective imp super.featureFlagDomain = domain; } + /** + * Update the component if the provider emits a ConfigurationChanged event. + * Set to false to prevent components from re-rendering when flag value changes + * are received by the associated provider. + * Defaults to true. + */ + @Input({ required: false }) + set numberFeatureFlagUpdateOnConfigurationChanged(enabled: boolean | undefined) { + this._updateOnConfigurationChanged = enabled ?? true; + } + + /** + * Update the component when the OpenFeature context changes. + * Set to false to prevent components from re-rendering when attributes which + * may be factors in flag evaluation change. + * Defaults to true. + */ + @Input({ required: false }) + set numberFeatureFlagUpdateOnContextChanged(enabled: boolean | undefined) { + this._updateOnContextChanged = enabled ?? true; + } + /** * Template to be displayed when the feature flag does not match value. */ @@ -422,6 +498,28 @@ export class StringFeatureFlagDirective extends FeatureFlagDirective imp super.featureFlagDomain = domain; } + /** + * Update the component if the provider emits a ConfigurationChanged event. + * Set to false to prevent components from re-rendering when flag value changes + * are received by the associated provider. + * Defaults to true. + */ + @Input({ required: false }) + set stringFeatureFlagUpdateOnConfigurationChanged(enabled: boolean | undefined) { + this._updateOnConfigurationChanged = enabled ?? true; + } + + /** + * Update the component when the OpenFeature context changes. + * Set to false to prevent components from re-rendering when attributes which + * may be factors in flag evaluation change. + * Defaults to true. + */ + @Input({ required: false }) + set stringFeatureFlagUpdateOnContextChanged(enabled: boolean | undefined) { + this._updateOnContextChanged = enabled ?? true; + } + /** * Template to be displayed when the feature flag does not match value. */ @@ -520,6 +618,28 @@ export class ObjectFeatureFlagDirective extends FeatureFlag super.featureFlagDomain = domain; } + /** + * Update the component if the provider emits a ConfigurationChanged event. + * Set to false to prevent components from re-rendering when flag value changes + * are received by the associated provider. + * Defaults to true. + */ + @Input({ required: false }) + set objectFeatureFlagUpdateOnConfigurationChanged(enabled: boolean | undefined) { + this._updateOnConfigurationChanged = enabled ?? true; + } + + /** + * Update the component when the OpenFeature context changes. + * Set to false to prevent components from re-rendering when attributes which + * may be factors in flag evaluation change. + * Defaults to true. + */ + @Input({ required: false }) + set objectFeatureFlagUpdateOnContextChanged(enabled: boolean | undefined) { + this._updateOnContextChanged = enabled ?? true; + } + /** * Template to be displayed when the feature flag does not match value. */