Skip to content

Commit

Permalink
feat(di): make di able to keep anything
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `HOC` and `IRegistryComponents` aren't exported from di, and generic-param for `Registry.set` has different meaning
  • Loading branch information
Vittly authored and yarastqt committed Jun 22, 2021
1 parent 655f2d8 commit e302953
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 123 deletions.
40 changes: 35 additions & 5 deletions packages/di/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 (
<>
Expand Down Expand Up @@ -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) => (
<div>
<BaseHeader height={200} color={red}/>
<BaseHeader height={200} color={red} />
</div>
))

Expand All @@ -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 (
<header className={cnHeader({ theme })}>
<h1>{title}</h1>
<h2>Greetings ${username}</h2>
</header>
)
}
```
124 changes: 67 additions & 57 deletions packages/di/di.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,154 +90,164 @@ export const useRegistries = () => {
return useContext(registryContext)
}

export const useComponentRegistry = <T extends {}>(id: string) => {
export const useRegistry = <T extends {}>(id: string) => {
const registries = useRegistries()

return registries[id].snapshot<T>()
}

/**
* @deprecated consider using 'useRegistry' instead
*/
export const useComponentRegistry = useRegistry

export interface IRegistryOptions {
id: string
overridable?: boolean
}

const registryHocMark = 'RegistryHoc'
export type HOC<T> = (WrappedComponent: ComponentType) => ComponentType<T>
const registryOverloadMark = 'RegistryOverloadHMark'

type IRegistryEntity<T = any> = ComponentType<T> | IRegistryHOC<T>
export type IRegistryComponents = Record<string, IRegistryEntity>
type SimpleOverload<T> = (Base: T) => T

interface IRegistryHOC<T> extends React.FC<T> {
$symbol: typeof registryHocMark
hoc: HOC<T>
interface IRegistryEntityOverload<T> {
$symbol: typeof registryOverloadMark
overload: SimpleOverload<T>
}

function withBase<T>(hoc: HOC<T>): IRegistryHOC<T> {
const fakeComponent: IRegistryHOC<T> = () => {
throw new Error(`Not found base component for enhance HOC: ${hoc.toString()}`)
}

fakeComponent.$symbol = registryHocMark as typeof registryHocMark
fakeComponent.hoc = hoc
type IRegistryEntity<T = any> = T | IRegistryEntityOverload<T>
export type IRegistryEntities = Record<string, IRegistryEntity>

return fakeComponent
function withOverload<T>(overload: SimpleOverload<T>): IRegistryEntityOverload<T> {
return {
$symbol: registryOverloadMark,
overload,
}
}

function isHoc<T>(component: IRegistryEntity<T>): component is IRegistryHOC<T> {
return (component as IRegistryHOC<T>).$symbol === registryHocMark
function isOverload<T>(entity: IRegistryEntity<T>): entity is IRegistryEntityOverload<T> {
return (entity as IRegistryEntityOverload<T>).$symbol === registryOverloadMark
}

export class Registry {
id: string
overridable: boolean
private components: IRegistryComponents = {}
private entities: IRegistryEntities = {}

constructor({ id, overridable = true }: IRegistryOptions) {
this.id = id
this.overridable = overridable
}

/**
* 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<T>(id: string, component: ComponentType<T>) {
this.components[id] = component
set<T>(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<T>(id: string, hoc: HOC<T>) {
this.components[id] = withBase(hoc)
extends<T>(id: string, overload: SimpleOverload<T>) {
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<T>(id: string): IRegistryEntity<T> {
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>(): 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<IRegistryComponents>()
const otherRegistryEntities = otherRegistry.snapshot<IRegistryEntities>()

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],
)
}

return clone
}

/**
* 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
Expand Down

0 comments on commit e302953

Please sign in to comment.