From 4f250df23c38f64eb768fd80daf20d098eea8bf9 Mon Sep 17 00:00:00 2001 From: David-Emmanuel DIVERNOIS Date: Fri, 29 Sep 2023 18:12:23 +0200 Subject: [PATCH] feat: customization of widgets config types --- angular/headless/src/lib/config.ts | 118 +++++++++++++ .../headless/src/lib/slot.directive.spec.ts | 5 +- angular/headless/src/lib/slot.directive.ts | 4 +- .../headless/src/lib/slotDefault.directive.ts | 2 +- angular/headless/src/lib/slotTypes.ts | 32 ++++ angular/headless/src/lib/utils.ts | 156 ++---------------- angular/headless/src/public-api.ts | 8 +- react/headless/Slot.tsx | 2 +- react/headless/WidgetsDefaultConfig.tsx | 63 ------- react/headless/config.tsx | 105 ++++++++++++ react/headless/index.ts | 9 +- react/headless/slotTypes.ts | 27 +++ react/headless/utils.ts | 58 +------ svelte/headless/Slot.svelte | 5 +- svelte/headless/config.ts | 84 ++++++++++ svelte/headless/index.ts | 8 +- svelte/headless/slotTypes.ts | 46 ++++++ svelte/headless/utils.ts | 148 ++--------------- 18 files changed, 474 insertions(+), 406 deletions(-) create mode 100644 angular/headless/src/lib/config.ts create mode 100644 angular/headless/src/lib/slotTypes.ts delete mode 100644 react/headless/WidgetsDefaultConfig.tsx create mode 100644 react/headless/config.tsx create mode 100644 react/headless/slotTypes.ts create mode 100644 svelte/headless/config.ts create mode 100644 svelte/headless/slotTypes.ts diff --git a/angular/headless/src/lib/config.ts b/angular/headless/src/lib/config.ts new file mode 100644 index 0000000000..fc27055bdc --- /dev/null +++ b/angular/headless/src/lib/config.ts @@ -0,0 +1,118 @@ +import type {Partial2Levels, Widget, WidgetFactory, WidgetProps, WidgetsConfigStore} from '@agnos-ui/core'; +import {createWidgetsConfig} from '@agnos-ui/core'; +import type {ReadableSignal} from '@amadeus-it-group/tansu'; +import {computed} from '@amadeus-it-group/tansu'; +import type {FactoryProvider} from '@angular/core'; +import {InjectionToken, Injector, Optional, SkipSelf, inject, runInInjectionContext} from '@angular/core'; +import type {AdaptPropsSlots} from './slotTypes'; +import type {WithPatchSlots} from './utils'; +import {callWidgetFactoryWithConfig} from './utils'; + +export type WidgetsConfig = { + [WidgetName in keyof import('@agnos-ui/core').WidgetsConfig]: AdaptPropsSlots; +}; + +export const widgetsConfigFactory = ( + widgetsConfigInjectionToken = new InjectionToken>('widgetsConfig') +) => { + /** + * Creates a provider of widgets default configuration that inherits from any widgets default configuration already defined at an upper level + * in the Angular dependency injection system. It contains its own set of widgets configuration properties that override the same properties form + * the parent configuration. + * + * @remarks + * The configuration is computed from the parent configuration in two steps: + * - first step: the parent configuration is transformed by the adaptParentConfig function (if specified). + * If adaptParentConfig is not specified, this step is skipped. + * - second step: the configuration from step 1 is merged (2-levels deep) with the own$ store. The own$ store initially contains + * an empty object (i.e. no property from the parent is overridden). It can be changed by calling set on the store returned by + * {@link injectWidgetsConfig}. + * + * @param adaptParentConfig - optional function that receives a 2-levels copy of the widgets default configuration + * defined at an upper level in the Angular dependency injection system (or an empty object if there is none) and returns the widgets + * default configuration to be used. + * It is called only if the configuration is needed, and was not yet computed for the current value of the parent configuration. + * It is called in a tansu reactive context, so it can use any tansu store and will be called again if those stores change. + * It is also called in an Angular injection context, so it can call the Angular inject function to get and use dependencies from the + * Angular dependency injection system. + + * @returns DI provider to be included a list of `providers` (for example at a component level or + * any other level of the Angular dependency injection system) + * + * @example + * ```typescript + * @Component({ + * // ... + * providers: [ + * provideWidgetsConfig((parentConfig) => { + * // first step configuration: transforms the parent configuration + * parentConfig.rating = parentConfig.rating ?? {}; + * parentConfig.rating.className = `${parentConfig.rating.className ?? ''} my-rating-extra-class` + * return parentConfig; + * }) + * ] + * }) + * class MyComponent { + * widgetsConfig = injectWidgetsConfig(); + * constructor() { + * this.widgetsConfig.set({ + * // second step configuration: overrides the parent configuration + * rating: { + * slotStar: MyCustomSlotStar + * } + * }); + * } + * // ... + * } + * ``` + */ + const provideWidgetsConfig = (adaptParentConfig?: (config: Partial2Levels) => Partial2Levels): FactoryProvider => ({ + provide: widgetsConfigInjectionToken, + useFactory: (parent: WidgetsConfigStore | null) => { + if (adaptParentConfig) { + const injector = inject(Injector); + const originalAdaptParentConfig = adaptParentConfig; + adaptParentConfig = (value) => runInInjectionContext(injector, () => originalAdaptParentConfig(value)); + } + return createWidgetsConfig(parent ?? undefined, adaptParentConfig); + }, + deps: [[new SkipSelf(), new Optional(), widgetsConfigInjectionToken]], + }); + + /** + * Returns the widgets default configuration store that was provided in the current injection context. + * Throws if the no widgets default configuration store was provided. + * + * @remarks + * This function must be called from an injection context, such as a constructor, a factory function, a field initializer or + * a function used with {@link https://angular.io/api/core/runInInjectionContext | runInInjectionContext}. + * + * @returns the widgets default configuration store. + */ + const injectWidgetsConfig = () => inject(widgetsConfigInjectionToken); + + const injectWidgetConfig = (widgetName: N): ReadableSignal | undefined> => { + const widgetsConfig = inject(widgetsConfigInjectionToken, {optional: true}); + return computed(() => widgetsConfig?.()[widgetName]); + }; + + const callWidgetFactory = ( + factory: WidgetFactory, + widgetName?: null | keyof Config, + defaultConfig: Partial> | ReadableSignal>> = {} + ): WithPatchSlots => callWidgetFactoryWithConfig(factory, defaultConfig, widgetName ? (injectWidgetConfig(widgetName) as any) : null); + + return { + /** + * Dependency Injection token which can be used to provide or inject the widgets default configuration store. + */ + widgetsConfigInjectionToken, + provideWidgetsConfig, + injectWidgetsConfig, + injectWidgetConfig, + callWidgetFactory, + }; +}; + +export const {widgetsConfigInjectionToken, provideWidgetsConfig, injectWidgetConfig, injectWidgetsConfig, callWidgetFactory} = + widgetsConfigFactory(); diff --git a/angular/headless/src/lib/slot.directive.spec.ts b/angular/headless/src/lib/slot.directive.spec.ts index 7437f56d46..56056b6e5c 100644 --- a/angular/headless/src/lib/slot.directive.spec.ts +++ b/angular/headless/src/lib/slot.directive.spec.ts @@ -4,9 +4,10 @@ import {ChangeDetectionStrategy, Component, Injectable, Input, ViewChild, inject import {toSignal} from '@angular/core/rxjs-interop'; import {TestBed} from '@angular/core/testing'; import {describe, expect, it} from 'vitest'; +import {injectWidgetsConfig, provideWidgetsConfig} from './config'; import {SlotDirective} from './slot.directive'; -import type {SlotContent} from './utils'; -import {ComponentTemplate, injectWidgetsConfig, provideWidgetsConfig} from './utils'; +import type {SlotContent} from './slotTypes'; +import {ComponentTemplate} from './slotTypes'; describe('slot directive', () => { @Component({ diff --git a/angular/headless/src/lib/slot.directive.ts b/angular/headless/src/lib/slot.directive.ts index 5f6c926fff..995f5cd113 100644 --- a/angular/headless/src/lib/slot.directive.ts +++ b/angular/headless/src/lib/slot.directive.ts @@ -1,8 +1,8 @@ import {DOCUMENT} from '@angular/common'; import type {ComponentRef, EmbeddedViewRef, OnChanges, OnDestroy, SimpleChanges, Type} from '@angular/core'; import {Directive, EnvironmentInjector, Input, TemplateRef, ViewContainerRef, createComponent, inject, reflectComponentType} from '@angular/core'; -import type {SlotContent} from './utils'; -import {ComponentTemplate} from './utils'; +import type {SlotContent} from './slotTypes'; +import {ComponentTemplate} from './slotTypes'; abstract class SlotHandler, Slot extends SlotContent = SlotContent> { constructor(public viewContainerRef: ViewContainerRef, public document: Document) {} diff --git a/angular/headless/src/lib/slotDefault.directive.ts b/angular/headless/src/lib/slotDefault.directive.ts index 421f7c0741..9af040496d 100644 --- a/angular/headless/src/lib/slotDefault.directive.ts +++ b/angular/headless/src/lib/slotDefault.directive.ts @@ -1,7 +1,7 @@ import type {WritableSignal} from '@amadeus-it-group/tansu'; import type {OnInit} from '@angular/core'; import {Directive, Input, TemplateRef, inject} from '@angular/core'; -import type {SlotContent} from './utils'; +import type {SlotContent} from './slotTypes'; @Directive({selector: '[auSlotDefault]', standalone: true}) export class SlotDefaultDirective implements OnInit { diff --git a/angular/headless/src/lib/slotTypes.ts b/angular/headless/src/lib/slotTypes.ts new file mode 100644 index 0000000000..3f7ba4776d --- /dev/null +++ b/angular/headless/src/lib/slotTypes.ts @@ -0,0 +1,32 @@ +import type {SlotContent as CoreSlotContent, Widget, WidgetFactory, WidgetProps, WidgetSlotContext, WidgetState} from '@agnos-ui/core'; +import type {TemplateRef, Type} from '@angular/core'; + +export class ComponentTemplate}> { + constructor(public readonly component: Type, public readonly templateProp: K) {} +} + +export type SlotContent = + | CoreSlotContent + | TemplateRef + | Type + | ComponentTemplate; + +export type AdaptSlotContentProps> = Props extends WidgetSlotContext + ? WidgetSlotContext> & AdaptPropsSlots>> + : AdaptPropsSlots; + +export type AdaptPropsSlots = Omit & { + [K in keyof Props & `slot${string}`]: Props[K] extends CoreSlotContent ? SlotContent> : Props[K]; +}; + +export type AdaptWidgetFactories = { + [K in keyof T]: T[K] extends WidgetFactory ? WidgetFactory> : T[K]; +}; + +export type AdaptWidgetSlots = Widget< + AdaptPropsSlots>, + AdaptPropsSlots>, + AdaptWidgetFactories, + W['actions'], + W['directives'] +>; diff --git a/angular/headless/src/lib/utils.ts b/angular/headless/src/lib/utils.ts index b78219ab8a..2315ed4b1b 100644 --- a/angular/headless/src/lib/utils.ts +++ b/angular/headless/src/lib/utils.ts @@ -1,136 +1,9 @@ -import type { - SlotContent as CoreSlotContent, - WidgetsConfig as CoreWidgetsConfig, - Partial2Levels, - Widget, - WidgetFactory, - WidgetProps, - WidgetSlotContext, - WidgetState, - WidgetsConfigStore, -} from '@agnos-ui/core'; -import {createWidgetsConfig} from '@agnos-ui/core'; -import type {ReadableSignal, SubscribableStore} from '@amadeus-it-group/tansu'; -import {computed, readable, writable} from '@amadeus-it-group/tansu'; -import type {FactoryProvider, SimpleChanges, TemplateRef, Type} from '@angular/core'; -import {InjectionToken, Injector, Optional, SkipSelf, inject, runInInjectionContext} from '@angular/core'; - -export class ComponentTemplate}> { - constructor(public readonly component: Type, public readonly templateProp: K) {} -} - -export type SlotContent = - | CoreSlotContent - | TemplateRef - | Type - | ComponentTemplate; - -export type AdaptSlotContentProps> = Props extends WidgetSlotContext - ? WidgetSlotContext> & AdaptPropsSlots>> - : AdaptPropsSlots; - -export type AdaptPropsSlots = Omit & { - [K in keyof Props & `slot${string}`]: Props[K] extends CoreSlotContent ? SlotContent> : Props[K]; -}; - -export type AdaptWidgetFactories = { - [K in keyof T]: T[K] extends WidgetFactory ? WidgetFactory> : T[K]; -}; - -export type AdaptWidgetSlots = Widget< - AdaptPropsSlots>, - AdaptPropsSlots>, - AdaptWidgetFactories, - W['actions'], - W['directives'] ->; - -export type WidgetsConfig = { - [WidgetName in keyof CoreWidgetsConfig]: AdaptPropsSlots; -}; - -/** - * Dependency Injection token which can be used to provide or inject the widgets default configuration store. - */ -export const widgetsConfigInjectionToken = new InjectionToken>('widgetsConfig'); - -/** - * Creates a provider of widgets default configuration that inherits from any widgets default configuration already defined at an upper level - * in the Angular dependency injection system. It contains its own set of widgets configuration properties that override the same properties form - * the parent configuration. - * - * @remarks - * The configuration is computed from the parent configuration in two steps: - * - first step: the parent configuration is transformed by the adaptParentConfig function (if specified). - * If adaptParentConfig is not specified, this step is skipped. - * - second step: the configuration from step 1 is merged (2-levels deep) with the own$ store. The own$ store initially contains - * an empty object (i.e. no property from the parent is overridden). It can be changed by calling set on the store returned by - * {@link injectWidgetsConfig}. - * - * @param adaptParentConfig - optional function that receives a 2-levels copy of the widgets default configuration - * defined at an upper level in the Angular dependency injection system (or an empty object if there is none) and returns the widgets - * default configuration to be used. - * It is called only if the configuration is needed, and was not yet computed for the current value of the parent configuration. - * It is called in a tansu reactive context, so it can use any tansu store and will be called again if those stores change. - * It is also called in an Angular injection context, so it can call the Angular inject function to get and use dependencies from the - * Angular dependency injection system. - - * @returns DI provider to be included a list of `providers` (for example at a component level or - * any other level of the Angular dependency injection system) - * - * @example - * ```typescript - * @Component({ - * // ... - * providers: [ - * provideWidgetsConfig((parentConfig) => { - * // first step configuration: transforms the parent configuration - * parentConfig.rating = parentConfig.rating ?? {}; - * parentConfig.rating.className = `${parentConfig.rating.className ?? ''} my-rating-extra-class` - * return parentConfig; - * }) - * ] - * }) - * class MyComponent { - * widgetsConfig = injectWidgetsConfig(); - * constructor() { - * this.widgetsConfig.set({ - * // second step configuration: overrides the parent configuration - * rating: { - * slotStar: MyCustomSlotStar - * } - * }); - * } - * // ... - * } - * ``` - */ -export const provideWidgetsConfig = ( - adaptParentConfig?: (config: Partial2Levels) => Partial2Levels -): FactoryProvider => ({ - provide: widgetsConfigInjectionToken, - useFactory: (parent: WidgetsConfigStore | null) => { - if (adaptParentConfig) { - const injector = inject(Injector); - const originalAdaptParentConfig = adaptParentConfig; - adaptParentConfig = (value) => runInInjectionContext(injector, () => originalAdaptParentConfig(value)); - } - return createWidgetsConfig(parent ?? undefined, adaptParentConfig); - }, - deps: [[new SkipSelf(), new Optional(), widgetsConfigInjectionToken]], -}); - -/** - * Returns the widgets default configuration store that was provided in the current injection context. - * Throws if the no widgets default configuration store was provided. - * - * @remarks - * This function must be called from an injection context, such as a constructor, a factory function, a field initializer or - * a function used with {@link https://angular.io/api/core/runInInjectionContext | runInInjectionContext}. - * - * @returns the widgets default configuration store. - */ -export const injectWidgetsConfig = () => inject(widgetsConfigInjectionToken); +import type {Widget, WidgetFactory, WidgetProps} from '@agnos-ui/core'; +import {toReadableStore} from '@agnos-ui/core'; +import type {ReadableSignal} from '@amadeus-it-group/tansu'; +import {computed, writable} from '@amadeus-it-group/tansu'; +import type {SimpleChanges, TemplateRef} from '@angular/core'; +import type {SlotContent} from './slotTypes'; const createPatchSlots = (set: (object: Partial) => void) => { let lastValue: Partial = {}; @@ -160,18 +33,17 @@ export type WithPatchSlots = W & { }): void; }; -export const callWidgetFactory = ( +export const callWidgetFactoryWithConfig = ( factory: WidgetFactory, - widgetName: keyof WidgetsConfig | null, - defaultConfig: Partial> | ReadableSignal>> = {} + defaultConfig?: Partial> | ReadableSignal> | undefined>, + widgetConfig?: null | undefined | ReadableSignal> | undefined> ): WithPatchSlots => { - const defaultConfigStore = typeof defaultConfig !== 'function' ? readable(defaultConfig) : defaultConfig; + const defaultConfig$ = toReadableStore(defaultConfig); const slots$ = writable({}); - const widgetsConfig = widgetName ? inject(widgetsConfigInjectionToken, {optional: true}) : undefined; return { - ...(factory({ - config: computed(() => ({...(defaultConfigStore() as any), ...(widgetName ? widgetsConfig?.()[widgetName] : undefined), ...slots$()})), - }) as any), + ...factory({ + config: computed(() => ({...defaultConfig$(), ...widgetConfig?.(), ...slots$()})), + }), patchSlots: createPatchSlots(slots$.set), }; }; @@ -185,5 +57,3 @@ export function patchSimpleChanges(patchFn: (obj: any) => void, changes: SimpleC } patchFn(obj); } - -export type ExtractStoreType = T extends SubscribableStore ? U : never; diff --git a/angular/headless/src/public-api.ts b/angular/headless/src/public-api.ts index 943b085db4..6530bbca4b 100644 --- a/angular/headless/src/public-api.ts +++ b/angular/headless/src/public-api.ts @@ -7,11 +7,15 @@ export * from '@agnos-ui/core'; export * from './lib/slot.directive'; export * from './lib/slotDefault.directive'; export * from './lib/use.directive'; +export * from './lib/slotTypes'; export * from './lib/utils'; +export * from './lib/config'; -export type {SlotContent, WidgetsConfig} from './lib/utils'; import type {PropsConfig, WidgetFactory, WidgetProps, WidgetState} from '@agnos-ui/core'; -import type {AdaptSlotContentProps, AdaptWidgetSlots} from './lib/utils'; +import type {AdaptSlotContentProps, AdaptWidgetSlots} from './lib/slotTypes'; + +export type {SlotContent} from './lib/slotTypes'; +export type {WidgetsConfig} from './lib/config'; export type AccordionWidget = AdaptWidgetSlots; export type AccordionProps = WidgetProps; diff --git a/react/headless/Slot.tsx b/react/headless/Slot.tsx index 79c1d89f73..45e2347e08 100644 --- a/react/headless/Slot.tsx +++ b/react/headless/Slot.tsx @@ -1,5 +1,5 @@ import {Component} from 'react'; -import type {SlotContent} from './utils'; +import type {SlotContent} from './slotTypes'; type SlotFunction> = ((props: Props) => React.ReactNode) | React.FunctionComponent; diff --git a/react/headless/WidgetsDefaultConfig.tsx b/react/headless/WidgetsDefaultConfig.tsx deleted file mode 100644 index 61cc9a1f21..0000000000 --- a/react/headless/WidgetsDefaultConfig.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import type {Partial2Levels} from '@agnos-ui/core'; -import {createWidgetsConfig} from '@agnos-ui/core'; -import type {PropsWithChildren} from 'react'; -import {useContext, useEffect, useMemo} from 'react'; -import type {WidgetsConfig} from './utils'; -import {widgetsConfigContext} from './utils'; - -/** - * React component that provides in the React context (for all AgnosUI descendant widgets) a new widgets default configuration - * store that inherits from any widgets default configuration store already defined at an upper level in the React context hierarchy. - * It contains its own set of widgets configuration properties that override the same properties form the parent configuration. - * - * @remarks - * The configuration is computed from the parent configuration in two steps: - * - first step: the parent configuration is transformed by the adaptParentConfig function (if specified). - * If adaptParentConfig is not specified, this step is skipped. - * - second step: the configuration from step 1 is merged (2-levels deep) with the properties of the component. - * - * @param adaptParentConfig - optional function that receives a 2-levels copy of the widgets default configuration - * defined at an upper level in the Svelte context hierarchy (or an empty object if there is none) and returns the widgets - * default configuration to be used. - * It is called only if the configuration is needed, and was not yet computed for the current value of the parent configuration. - * It is called in a tansu reactive context, so it can use any tansu store and will be called again if those stores change. - * - * @returns the resulting widgets default configuration store, which contains 3 additional properties that are stores: - * parent$, adaptedParent$ (containing the value computed after the first step), and own$ (that contains only overridding properties). - * The resulting store is writable, its set function is actually the set function of the own$ store. - * - * @example - * ```tsx - * { - * parentConfig.rating = parentConfig.rating ?? {}; - * parentConfig.rating.className = `${parentConfig.rating.className ?? ''} my-rating-extra-class` - * return parentConfig; - * }} - * rating={{slotStar: MyCustomSlotStar}} - * /> - * ``` - */ -export const WidgetsDefaultConfig = ({ - children, - adaptParentConfig, - ...props -}: PropsWithChildren> & { - adaptParentConfig?: (config: Partial2Levels) => Partial2Levels; -}) => { - const config$ = useContext(widgetsConfigContext); - let storeRecreated = false; - - const store$ = useMemo(() => { - const store = createWidgetsConfig(config$, adaptParentConfig); - store.set(props); - storeRecreated = true; - return store; - }, [config$, adaptParentConfig]); - useEffect(() => { - if (!storeRecreated) { - store$.set(props); - } - }, [props]); - return {children}; -}; diff --git a/react/headless/config.tsx b/react/headless/config.tsx new file mode 100644 index 0000000000..3da093ad1f --- /dev/null +++ b/react/headless/config.tsx @@ -0,0 +1,105 @@ +import { + createWidgetsConfig, + type Partial2Levels, + type Widget, + type WidgetFactory, + type WidgetProps, + type WidgetState, + type WidgetsConfigStore, +} from '@agnos-ui/core'; +import {computed} from '@amadeus-it-group/tansu'; +import type {ReactNode} from 'react'; +import {createContext, useContext, useEffect, useMemo} from 'react'; +import type {AdaptPropsSlots} from './slotTypes'; +import {usePropsAsStore, useWidget} from './utils'; + +export type WidgetsConfig = { + [WidgetName in keyof import('@agnos-ui/core').WidgetsConfig]: AdaptPropsSlots; +}; + +export const widgetsConfigFactory = ( + widgetsConfigContext = createContext(undefined as undefined | WidgetsConfigStore) +) => { + const useWidgetContext = (widgetName: keyof Config | null, defaultConfig?: Partial) => { + const widgetsConfig = useContext(widgetsConfigContext); + const defaultConfig$ = usePropsAsStore(defaultConfig); + return useMemo(() => computed(() => ({...defaultConfig$(), ...(widgetName ? widgetsConfig?.()[widgetName] : undefined)})), [widgetsConfig]); + }; + + const useWidgetWithConfig = ( + factory: WidgetFactory, + props: Partial> | undefined, + widgetName: keyof Config | null, + defaultProps?: Partial> + ): [WidgetState, W] => useWidget(factory, props, {config: useWidgetContext(widgetName, defaultProps)}); + + /** + * React component that provides in the React context (for all AgnosUI descendant widgets) a new widgets default configuration + * store that inherits from any widgets default configuration store already defined at an upper level in the React context hierarchy. + * It contains its own set of widgets configuration properties that override the same properties form the parent configuration. + * + * @remarks + * The configuration is computed from the parent configuration in two steps: + * - first step: the parent configuration is transformed by the adaptParentConfig function (if specified). + * If adaptParentConfig is not specified, this step is skipped. + * - second step: the configuration from step 1 is merged (2-levels deep) with the properties of the component. + * + * @param adaptParentConfig - optional function that receives a 2-levels copy of the widgets default configuration + * defined at an upper level in the Svelte context hierarchy (or an empty object if there is none) and returns the widgets + * default configuration to be used. + * It is called only if the configuration is needed, and was not yet computed for the current value of the parent configuration. + * It is called in a tansu reactive context, so it can use any tansu store and will be called again if those stores change. + * + * @returns the resulting widgets default configuration store, which contains 3 additional properties that are stores: + * parent$, adaptedParent$ (containing the value computed after the first step), and own$ (that contains only overridding properties). + * The resulting store is writable, its set function is actually the set function of the own$ store. + * + * @example + * ```tsx + * { + * parentConfig.rating = parentConfig.rating ?? {}; + * parentConfig.rating.className = `${parentConfig.rating.className ?? ''} my-rating-extra-class` + * return parentConfig; + * }} + * rating={{slotStar: MyCustomSlotStar}} + * /> + * ``` + */ + const WidgetsDefaultConfig = ({ + children, + adaptParentConfig, + ...props + }: Partial2Levels & { + adaptParentConfig?: (config: Partial2Levels) => Partial2Levels; + children?: ReactNode | undefined; + }) => { + const config$ = useContext(widgetsConfigContext); + let storeRecreated = false; + + const store$ = useMemo(() => { + const store = createWidgetsConfig(config$, adaptParentConfig); + store.set(props as any); + storeRecreated = true; + return store; + }, [config$, adaptParentConfig]); + useEffect(() => { + if (!storeRecreated) { + store$.set(props as any); + } + }, [props]); + return {children}; + }; + + return { + /** + * React context which can be used to provide or consume the widgets default configuration store. + */ + widgetsConfigContext, + useWidgetContext, + useWidgetWithConfig, + WidgetsDefaultConfig, + }; +}; + +export const {widgetsConfigContext, useWidgetContext, useWidgetWithConfig, WidgetsDefaultConfig} = widgetsConfigFactory(); diff --git a/react/headless/index.ts b/react/headless/index.ts index 1a6e63e29c..7cd767b4b6 100644 --- a/react/headless/index.ts +++ b/react/headless/index.ts @@ -2,13 +2,16 @@ export * from '@agnos-ui/core'; export * from './Portal'; export * from './Slot'; -export * from './WidgetsDefaultConfig'; +export * from './slotTypes'; export * from './utils'; +export * from './config'; import type {PropsConfig, WidgetFactory, WidgetProps, WidgetState} from '@agnos-ui/core'; -import type {AdaptSlotContentProps, AdaptWidgetSlots} from './utils'; +import type {AdaptSlotContentProps, AdaptWidgetSlots} from './slotTypes'; + +export type {SlotContent} from './slotTypes'; +export type {WidgetsConfig} from './config'; -export type {SlotContent, WidgetsConfig} from './utils'; export type AccordionWidget = AdaptWidgetSlots; export type AccordionProps = WidgetProps; export type AccordionState = WidgetState; diff --git a/react/headless/slotTypes.ts b/react/headless/slotTypes.ts new file mode 100644 index 0000000000..fcc20ede7b --- /dev/null +++ b/react/headless/slotTypes.ts @@ -0,0 +1,27 @@ +import type {SlotContent as CoreSlotContent, Widget, WidgetFactory, WidgetProps, WidgetSlotContext, WidgetState} from '@agnos-ui/core'; + +export type SlotContent = + | CoreSlotContent + | ((props: Props) => React.ReactNode) + | React.ComponentType + | React.ReactNode; + +export type AdaptSlotContentProps> = Props extends WidgetSlotContext + ? WidgetSlotContext> & AdaptPropsSlots>> + : AdaptPropsSlots; + +export type AdaptPropsSlots = Omit & { + [K in keyof Props & `slot${string}`]: Props[K] extends CoreSlotContent ? SlotContent> : Props[K]; +}; + +export type AdaptWidgetFactories = { + [K in keyof T]: T[K] extends WidgetFactory ? WidgetFactory> : T[K]; +}; + +export type AdaptWidgetSlots = Widget< + AdaptPropsSlots>, + AdaptPropsSlots>, + AdaptWidgetFactories, + W['actions'], + W['directives'] +>; diff --git a/react/headless/utils.ts b/react/headless/utils.ts index 0334d9a07a..202b7a1c6c 100644 --- a/react/headless/utils.ts +++ b/react/headless/utils.ts @@ -1,41 +1,9 @@ -import type {Directive, Widget, WidgetFactory, WidgetProps, WidgetState, WidgetsConfigStore} from '@agnos-ui/core'; +import type {Directive, Widget, WidgetProps, WidgetState} from '@agnos-ui/core'; import {findChangedProperties} from '@agnos-ui/core'; import type {ReadableSignal, WritableSignal} from '@amadeus-it-group/tansu'; -import {asReadable, computed, writable} from '@amadeus-it-group/tansu'; +import {asReadable, writable} from '@amadeus-it-group/tansu'; import type {RefCallback} from 'react'; -import {createContext, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; - -import type {SlotContent as CoreSlotContent, WidgetsConfig as CoreWidgetsConfig, WidgetSlotContext} from '@agnos-ui/core'; - -export type SlotContent = - | CoreSlotContent - | ((props: Props) => React.ReactNode) - | React.ComponentType - | React.ReactNode; - -export type AdaptSlotContentProps> = Props extends WidgetSlotContext - ? WidgetSlotContext> & AdaptPropsSlots>> - : AdaptPropsSlots; - -export type AdaptPropsSlots = Omit & { - [K in keyof Props & `slot${string}`]: Props[K] extends CoreSlotContent ? SlotContent> : Props[K]; -}; - -export type WidgetsConfig = { - [WidgetName in keyof CoreWidgetsConfig]: AdaptPropsSlots; -}; - -export type AdaptWidgetFactories = { - [K in keyof T]: T[K] extends WidgetFactory ? WidgetFactory> : T[K]; -}; - -export type AdaptWidgetSlots = Widget< - AdaptPropsSlots>, - AdaptPropsSlots>, - AdaptWidgetFactories, - W['actions'], - W['directives'] ->; +import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; export function useWidget Widget>( createWidget: Factory, @@ -101,13 +69,8 @@ export function useDirective(directive: Directive, args?: T) { return ref; } -/** - * React context which can be used to provide or consume the widgets default configuration store. - */ -export const widgetsConfigContext = createContext(undefined as undefined | WidgetsConfigStore); - const propsEqual = (a: T, b: T) => !findChangedProperties(a, b); -const usePropsAsStore = (props?: Partial): ReadableSignal> => { +export const usePropsAsStore = (props?: Partial): ReadableSignal> => { const storeRef = useRef>>(); if (!storeRef.current) { storeRef.current = writable({...props}, {equal: propsEqual}); @@ -118,16 +81,3 @@ const usePropsAsStore = (props?: Partial): ReadableSignal

asReadable(storeRef.current!), [storeRef.current!]); }; - -const useWidgetContext = (widgetName: keyof WidgetsConfig | null, defaultConfig?: Partial) => { - const widgetsConfig = useContext(widgetsConfigContext); - const defaultConfig$ = usePropsAsStore(defaultConfig); - return useMemo(() => computed(() => ({...defaultConfig$(), ...(widgetName ? widgetsConfig?.()[widgetName] : undefined)})), [widgetsConfig]); -}; - -export const useWidgetWithConfig = ( - factory: WidgetFactory, - props: Partial> | undefined, - widgetName: keyof WidgetsConfig | null, - defaultProps?: Partial> -): [WidgetState, W] => useWidget(factory, props, {config: useWidgetContext(widgetName, defaultProps)}); diff --git a/svelte/headless/Slot.svelte b/svelte/headless/Slot.svelte index 8c247d08ae..a7ccc2ad09 100644 --- a/svelte/headless/Slot.svelte +++ b/svelte/headless/Slot.svelte @@ -1,6 +1,7 @@