From e30295305e133ba240e5dc691eb80ab04199c12e Mon Sep 17 00:00:00 2001 From: Vittly Date: Tue, 22 Jun 2021 12:49:22 +0500 Subject: [PATCH] feat(di): make di able to keep anything BREAKING CHANGE: `HOC` and `IRegistryComponents` aren't exported from di, and generic-param for `Registry.set` has different meaning --- packages/di/README.md | 40 +++++++++-- packages/di/di.tsx | 124 +++++++++++++++++++---------------- packages/di/test/di.test.tsx | 116 ++++++++++++++++---------------- 3 files changed, 157 insertions(+), 123 deletions(-) diff --git a/packages/di/README.md b/packages/di/README.md index 18f92e20..c4e8644b 100644 --- a/packages/di/README.md +++ b/packages/di/README.md @@ -6,6 +6,7 @@ DI package helps to solve similar tasks with minimum effort: - decouple _desktop_ and _mobile_ versions of a component - implement an _experimental_ version of a component alongside the common one +- store components and their auxiliaries (like settings and functions) in a single place ## Install @@ -160,19 +161,19 @@ export const App = () => ( ) ``` -with `useComponentRegistry` (_require react version 16.8.0+_) +with `useRegistry` (_require react version 16.8.0+_) ```tsx import React from 'react' import { cn } from '@bem-react/classname' -import { useComponentRegistry } from '@bem-react/di' +import { useRegistry } from '@bem-react/di' // No Header or Footer imports const cnApp = cn('App') export const App = () => { - const { Header, Footer } = useComponentRegistry(cnApp()) + const { Header, Footer } = useRegistry(cnApp()) return ( <> @@ -228,9 +229,9 @@ import { AppDesktop, registryId } from './App@desktop' const expRegistry = new Registry({ id: registryId }) // extends original Header -expRegistry.extends('Header', BaseHeader => props => ( +expRegistry.extends('Header', (BaseHeader) => (props) => (
- +
)) @@ -239,3 +240,32 @@ export const AppDesktopExperimental = withRegistry(expRegistry)(AppDesktop) ``` _DI_ merges nested registries composing and ordinary components for you. So you always can get a reference to previous component's implementation. + +## Storing other + +_DI_ registry may keep not only components but also their settings and any other auxiliaries (like functions). + +```tsx +import { useRegistry } from '@bem-react/di' + +const cnHeader = cn('Header') + +export const Header = (props) => { + const { theme, showNotification, prepareProps } = useRegistry(cnApp()) + + // one function is used to fulfill props + const { title, username } = prepareProps(props) + + useEffect(() => { + // another function is used inside hook + showNotification() + }) + + return ( +
+

{title}

+

Greetings ${username}

+
+ ) +} +``` diff --git a/packages/di/di.tsx b/packages/di/di.tsx index 72205cb7..9956d96e 100644 --- a/packages/di/di.tsx +++ b/packages/di/di.tsx @@ -90,47 +90,49 @@ export const useRegistries = () => { return useContext(registryContext) } -export const useComponentRegistry = (id: string) => { +export const useRegistry = (id: string) => { const registries = useRegistries() return registries[id].snapshot() } +/** + * @deprecated consider using 'useRegistry' instead + */ +export const useComponentRegistry = useRegistry + export interface IRegistryOptions { id: string overridable?: boolean } -const registryHocMark = 'RegistryHoc' -export type HOC = (WrappedComponent: ComponentType) => ComponentType +const registryOverloadMark = 'RegistryOverloadHMark' -type IRegistryEntity = ComponentType | IRegistryHOC -export type IRegistryComponents = Record +type SimpleOverload = (Base: T) => T -interface IRegistryHOC extends React.FC { - $symbol: typeof registryHocMark - hoc: HOC +interface IRegistryEntityOverload { + $symbol: typeof registryOverloadMark + overload: SimpleOverload } -function withBase(hoc: HOC): IRegistryHOC { - const fakeComponent: IRegistryHOC = () => { - throw new Error(`Not found base component for enhance HOC: ${hoc.toString()}`) - } - - fakeComponent.$symbol = registryHocMark as typeof registryHocMark - fakeComponent.hoc = hoc +type IRegistryEntity = T | IRegistryEntityOverload +export type IRegistryEntities = Record - return fakeComponent +function withOverload(overload: SimpleOverload): IRegistryEntityOverload { + return { + $symbol: registryOverloadMark, + overload, + } } -function isHoc(component: IRegistryEntity): component is IRegistryHOC { - return (component as IRegistryHOC).$symbol === registryHocMark +function isOverload(entity: IRegistryEntity): entity is IRegistryEntityOverload { + return (entity as IRegistryEntityOverload).$symbol === registryOverloadMark } export class Registry { id: string overridable: boolean - private components: IRegistryComponents = {} + private entities: IRegistryEntities = {} constructor({ id, overridable = true }: IRegistryOptions) { this.id = id @@ -138,84 +140,85 @@ export class Registry { } /** - * Set react component in registry by id. + * Set registry entry by id. * - * @param id component id - * @param component valid react component + * @param id entry id + * @param entity valid registry entity */ - set(id: string, component: ComponentType) { - this.components[id] = component + set(id: string, entity: T) { + this.entities[id] = entity return this } /** - * Set hoc for extends component in registry by id + * Set extender for registry entry by id. * - * @param id component id - * @param hoc hoc for extends component + * @param id entry id + * @param overload valid registry entity extender */ - extends(id: string, hoc: HOC) { - this.components[id] = withBase(hoc) + extends(id: string, overload: SimpleOverload) { + this.entities[id] = withOverload(overload) return this } /** - * Set react components in registry via object literal. + * Set react entities in registry via object literal. * - * @param componentsSet set of valid react components + * @param entitiesSet set of valid registry entities */ - fill(componentsSet: IRegistryComponents) { - for (const key in componentsSet) { - this.components[key] = componentsSet[key] + fill(entitiesSet: IRegistryEntities) { + for (const key in entitiesSet) { + this.entities[key] = entitiesSet[key] } return this } /** - * Get react component from registry by id. + * Get entry from registry by id. * - * @param id component id + * @param id entry id */ get(id: string): IRegistryEntity { if (__DEV__) { - if (!this.components[id]) { - throw new Error(`Component with id '${id}' not found.`) + if (!this.entities[id]) { + throw new Error(`Entry with id '${id}' not found.`) } } - return this.components[id] + return this.entities[id] } /** - * Returns list of components from registry. + * Returns raw entities from registry. */ snapshot(): RT { - return this.components as any + return this.entities as any } /** - * Override components by external registry. + * Override entities by external registry. * @internal * * @param otherRegistry external registry */ merge(otherRegistry?: Registry) { const clone = new Registry({ id: this.id, overridable: this.overridable }) - clone.fill(this.components) + clone.fill(this.entities) if (!otherRegistry) return clone - const otherRegistryComponents = otherRegistry.snapshot() + const otherRegistryEntities = otherRegistry.snapshot() - for (const componentName in otherRegistryComponents) { - if (!otherRegistryComponents.hasOwnProperty(componentName)) continue + for (const entityName in otherRegistryEntities) { + if (!otherRegistryEntities.hasOwnProperty(entityName)) continue - clone.components[componentName] = this.mergeComponents( - clone.components[componentName], - otherRegistryComponents[componentName], + clone.entities[entityName] = this.mergeEntities( + this.id, + clone.entities[entityName], + otherRegistryEntities[entityName], ) } @@ -223,21 +226,28 @@ export class Registry { } /** - * Returns extended or replacing for base impleme + * Returns extended or replaced entity * + * @param id entity entry id * @param base base implementation * @param overrides overridden implementation */ - private mergeComponents(base: IRegistryEntity, overrides: IRegistryEntity): IRegistryEntity { - if (isHoc(overrides)) { - if (!base) return overrides + private mergeEntities( + id: string, + base: IRegistryEntity, + overrides: IRegistryEntity, + ): IRegistryEntity { + if (isOverload(overrides)) { + if (!base && __DEV__) { + throw new Error(`Overload has no base in Registry '${id}'.`) + } - if (isHoc(base)) { - // If both components are hocs, then create compose-hoc - return withBase((Base) => overrides.hoc(base.hoc(Base))) + if (isOverload(base)) { + // If both entities are hocs, then create compose-hoc + return withOverload((Base) => overrides.overload(base.overload(Base))) } - return overrides.hoc(base) + return overrides.overload(base) } return overrides diff --git a/packages/di/test/di.test.tsx b/packages/di/test/di.test.tsx index 6c63ea27..f81097fc 100644 --- a/packages/di/test/di.test.tsx +++ b/packages/di/test/di.test.tsx @@ -7,7 +7,7 @@ import { RegistryConsumer, ComponentRegistryConsumer, useRegistries, - useComponentRegistry, + useRegistry, } from '../di' import { compose } from '../../core/core' @@ -17,7 +17,7 @@ interface ICommonProps { describe('@bem-react/di', () => { describe('Registry', () => { - test('should set and components by id', () => { + test('should set components by id', () => { const registry = new Registry({ id: 'registry' }) const Component1 = () => null const Component2 = () => @@ -60,6 +60,19 @@ describe('@bem-react/di', () => { expect(registry.snapshot()).toEqual(snapshot) }) + test('should add/retrieve other values', () => { + const registry = new Registry({ id: 'registry' }) + const functor = () => 'value' + + registry.set('id-1', 1).fill({ 'id-2': '2-string', 'id-3': functor }) + + expect(registry.snapshot()).toEqual({ + 'id-1': 1, + 'id-2': '2-string', + 'id-3': functor, + }) + }) + test('should merge registries', () => { const registry = new Registry({ id: 'registry' }) const Component1 = () => null @@ -119,7 +132,7 @@ describe('@bem-react/di', () => { test("should throw error when component doesn't exist", () => { const registry = new Registry({ id: 'registry' }) - expect(() => registry.get('id')).toThrow("Component with id 'id' not found.") + expect(() => registry.get('id')).toThrow("Entry with id 'id' not found.") }) }) @@ -131,7 +144,7 @@ describe('@bem-react/di', () => { registry.fill({ Element }) const View: React.FC = ({ children }) => { - const { Element } = useComponentRegistry('uniq-1') + const { Element } = useRegistry('uniq-1') return {children} } @@ -280,7 +293,7 @@ describe('@bem-react/di', () => { expect(render().text()).toEqual('contentextra') }) - test('should extend components is registry', () => { + test('should extend components in registry', () => { interface ICompositorRegistry { Element1: React.ComponentType Element2: React.ComponentType @@ -294,7 +307,7 @@ describe('@bem-react/di', () => { compositorRegistry.set('Element2', Element2) const overridedCompositorRegistry = new Registry({ id: 'Compositor' }) - overridedCompositorRegistry.extends('Element1', (Base) => { + overridedCompositorRegistry.extends>('Element1', (Base) => { return () => (
extended @@ -303,7 +316,7 @@ describe('@bem-react/di', () => { }) const superOverridedCompositorRegistry = new Registry({ id: 'Compositor' }) - superOverridedCompositorRegistry.extends('Element1', (Base) => { + superOverridedCompositorRegistry.extends>('Element1', (Base) => { return () => (
super @@ -333,77 +346,61 @@ describe('@bem-react/di', () => { expect(render().text()).toEqual('contentextra') }) - test('should throw error when try render hoc without base implementation', () => { - const Element2: React.FC = () => extra - - const compositorRegistry = new Registry({ id: 'Compositor' }) - compositorRegistry.set('Element2', Element2) + test('should extend other values in registry', () => { + interface ICompositorRegistry { + prop: String + functionProp: () => String + } - const overridedCompositorRegistry = new Registry({ id: 'Compositor' }) - overridedCompositorRegistry.extends('Element1', (Base) => { - return () => ( -
- extended -
- ) + const compositorRegistry = new Registry({ id: 'Compositor' }).fill({ + prop: 'foo', + functionProp: () => 'bar', }) - const otherCompositorRegistry = new Registry({ id: 'Compositor' }) - otherCompositorRegistry.extends('Element1', (Base) => () => ) - otherCompositorRegistry.set('Element2', Element2) - - interface ICompositorRegistry { - Element1: React.ComponentType - Element2: React.ComponentType - } + const overridedCompositorRegistry = new Registry({ id: 'Compositor' }) + overridedCompositorRegistry.extends('prop', (Base) => `extended ${Base}`) + overridedCompositorRegistry.extends<() => String>('functionProp', (Base) => () => + `extended ${Base()}`, + ) const CompositorPresenter: React.FC = () => ( - {({ Element1, Element2 }: ICompositorRegistry) => ( - <> - - - + {({ prop, functionProp }: ICompositorRegistry) => ( +
+ {prop} / {functionProp()} +
)}
) const Compositor = withRegistry(compositorRegistry)(CompositorPresenter) - const OverridenCompositor = withRegistry(overridedCompositorRegistry)(Compositor) - const OtherCompositor = withRegistry(otherCompositorRegistry)(CompositorPresenter) + const OverridedCompositor = withRegistry(overridedCompositorRegistry)(Compositor) - expect(() => render()).toThrow( - 'Not found base component for enhance HOC', - ) - expect(() => render()).toThrow( - 'Not found base component for enhance HOC', - ) + expect(render().text()).toEqual('foo / bar') + expect(render().text()).toEqual('extended foo / extended bar') }) - test('should pass down hoc if at the current level there was no basic implementation', () => { - interface ICompositorRegistry { - Element1: React.ComponentType - Element2: React.ComponentType - } - - const Element1: React.FC = () => content + test('should throw error when try render hoc without base implementation', () => { const Element2: React.FC = () => extra const compositorRegistry = new Registry({ id: 'Compositor' }) - compositorRegistry.set('Element1', Element1) + // There is no Element1 here compositorRegistry.set('Element2', Element2) const overridedCompositorRegistry = new Registry({ id: 'Compositor' }) - - const superOverridedCompositorRegistry = new Registry({ id: 'Compositor' }) - superOverridedCompositorRegistry.extends('Element1', (Base) => { + overridedCompositorRegistry.extends>('Element1', (Base) => { return () => (
- super + extended
) }) + interface ICompositorRegistry { + Element1: React.ComponentType + Element2: React.ComponentType + } + const CompositorPresenter: React.FC = () => ( {({ Element1, Element2 }: ICompositorRegistry) => ( @@ -416,14 +413,11 @@ describe('@bem-react/di', () => { ) const Compositor = withRegistry(compositorRegistry)(CompositorPresenter) - const OverridedCompositor = withRegistry(overridedCompositorRegistry)(Compositor) - const SuperOverridedCompositor = withRegistry(superOverridedCompositorRegistry)( - OverridedCompositor, - ) + const OverridenCompositor = withRegistry(overridedCompositorRegistry)(Compositor) - expect(render().text()).toEqual('super contentextra') - expect(render().text()).toEqual('contentextra') - expect(render().text()).toEqual('contentextra') + expect(() => render()).toThrow( + "Overload has no base in Registry 'Compositor'", + ) }) test('should allow to use any registry in context', () => { @@ -548,7 +542,7 @@ describe('@bem-react/di', () => { expect(render().text()).toEqual('content') }) - test('should provide assign registry with useComponentRegistry', () => { + test('should provide assign registry with useRegistry', () => { const compositorRegistry = new Registry({ id: 'Compositor' }) const Element = (_props: ICommonProps) => content @@ -559,7 +553,7 @@ describe('@bem-react/di', () => { compositorRegistry.set('Element', Element) const CompositorPresenter = (_props: ICommonProps) => { - const { Element } = useComponentRegistry('Compositor') + const { Element } = useRegistry('Compositor') return }