Skip to content

Commit

Permalink
feat: don't pass mod props to component
Browse files Browse the repository at this point in the history
  • Loading branch information
Sergei Savelev authored and sergcen committed Jul 27, 2020
1 parent 60c2ee7 commit 6ce126c
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 40 deletions.
108 changes: 72 additions & 36 deletions packages/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,15 @@ export type Enhance<T extends IClassNameProps> = (

type Dictionary<T = any> = { [key: string]: T }

type withBemModOptions = {
__passToProps: boolean
__simple: boolean
}

export function withBemMod<T, U extends IClassNameProps = {}>(
blockName: string,
mod: NoStrictEntityMods,
enhance?: Enhance<T & U>,
enhance?: Enhance<T & U> | withBemModOptions,
) {
let entity: ClassNameFormatter
let entityClassName: string
Expand Down Expand Up @@ -93,7 +98,7 @@ export function withBemMod<T, U extends IClassNameProps = {}>(
// Replace first entityClassName for remove duplcates from className.
.replace(`${entityClassName} `, '')

if (enhance !== undefined) {
if (typeof enhance === 'function') {
if (ModifiedComponent === undefined) {
ModifiedComponent = enhance(WrappedComponent as any)

Expand Down Expand Up @@ -127,58 +132,91 @@ export function withBemMod<T, U extends IClassNameProps = {}>(
return BemMod
}

withMod.blockName = blockName
withMod.mod = mod
withMod.isSimple = !enhance
const { __passToProps = true, __simple = false } = (enhance as withBemModOptions) || {}

const keys = Object.keys(mod)
const isSimple = !enhance && keys.length === 1
const name = keys[0]
const value = mod[keys[0]]

withMod.__isSimple = isSimple || __simple

if (withMod.__isSimple) {
withMod.__blockName = blockName
withMod.__mod = name
withMod.__value = value
withMod.__passToProps = __passToProps
}

return withMod
}

export function createClassNameModifier<T>(blockName: string, mod: NoStrictEntityMods) {
return withBemMod<T>(blockName, mod, { __passToProps: false, __simple: true })
}

export type ExtractProps<T> = T extends ComponentType<infer K> ? { [P in keyof K]: K[P] } : never
export type HOC<T> = (WrappedComponent: ComponentType) => ComponentType<T>
export type Wrapper<T> = HOC<T>
export type Composition<T> = <U extends ComponentType<any>>(
fn: U,
) => StatelessComponent<JSX.LibraryManagedAttributes<U, ExtractProps<U>> & T>

type AllMods = {
[key: string]: string[]
}

export function composeSimple(mods: any[]) {
const { blockName } = mods[0]
const allMods: AllMods = {}

const entity = cn(blockName)
function composeSimple(mods: any[]) {
const { __blockName } = mods[0]
const allMods: Record<string, string[]> = {}
const allModsPassProps: Record<string, boolean[]> = {}

for (let index = 0; index < mods.length; index++) {
const { mod } = mods[index]
const entity = cn(__blockName)

Object.keys(mod).forEach((key) => {
allMods[key] = allMods[key] || []
for (let { __mod, __value, __passToProps } of mods) {
allMods[__mod] = allMods[__mod] || []
// для оптимизации поиска вводим простой массив вместо объекта
allModsPassProps[__mod] = allModsPassProps[__mod] || []

allMods[key].push(mod[key])
})
allMods[__mod].push(__value)
allModsPassProps[__mod].push(__passToProps)
}

const modNames = Object.keys(allMods)

return (Base: any) => {
return (props: any) => {
const modifiers = modNames.reduce((acc: NoStrictEntityMods, key: string) => {
const modValues = allMods[key]
const propValue = (props as any)[key]
return (Base: ComponentType<any>) => {
function SimpleComposeWrapper(props: Record<string, any>) {
const modifiers: NoStrictEntityMods = {}
const newProps: any = { ...props }

if (modValues.includes(propValue)) {
acc[key] = propValue
for (let key of modNames) {
const modValues = allMods[key]
const propValue = props[key]

const foundInValues = modValues.indexOf(propValue)
if (foundInValues !== -1) {
modifiers[key] = propValue
// если стоит флаг __passToProps = false, то не добавляем в пропсы
if (!allModsPassProps[key][foundInValues]) {
delete newProps[key]
}
}
}

return acc
}, {})
newProps.className = entity(modifiers, [props.className])

const className = entity(modifiers, [props.className])
return createElement(Base, newProps)
}
if (__DEV__) {
const allModsFormatted = Object.keys(allMods)
.map((key) => {
const mods
= allMods[key].length > 3 ? ` ${allMods[key].length} mods` : allMods[key].join('|')

return `[${key}:${mods}]`
})
.join(',')

return createElement(Base, { ...props, className })
SimpleComposeWrapper.displayName = `SimpleComposeWrapper ${allModsFormatted}`
}

return SimpleComposeWrapper
}
}

Expand Down Expand Up @@ -256,12 +294,10 @@ export function compose() {
// Use arguments instead of rest-arguments to get faster and more compact code.
const fns: any[] = [].slice.call(arguments)

const simple = []
const simple: any[] = []
const enhanced = []
for (let index = 0; index < fns.length; index++) {
const f = fns[index]

f.isSimple ? simple.push(f) : enhanced.push(f)
for (let f of fns) {
f.__isSimple ? simple.push(f) : enhanced.push(f)
}

const oprimizedFns = simple.length ? [composeSimple(simple), ...enhanced] : enhanced
Expand Down
58 changes: 54 additions & 4 deletions packages/core/test/compose.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { FC, ComponentType } from 'react'
import { mount } from 'enzyme'

import { compose, composeU, withBemMod } from '../core'
import { compose, composeU, createClassNameModifier, withBemMod } from '../core'

type BaseProps = {
text: string
Expand All @@ -24,23 +24,46 @@ type SizeAProps = {
}

type SimpleProps = {
simple?: true
simple?: 'somevalue' | 'errorvalue'
}

const getPropsFromSelector = (Component: React.ReactElement<any>, selector: string = 'div') =>
mount(Component)
.find(selector)
.props()

const Component: FC<BaseProps> = ({ children }) => <div>{children}</div>
const withSimpleCompose = withBemMod<SimpleProps>('EnhancedComponent', { simple: true })
const ComponentSpreadProps: FC<BaseProps> = ({ children, ...props }) => (
<div {...props}>{children}</div>
)
const withSimpleCompose = createClassNameModifier<SimpleProps>('EnhancedComponent', {
simple: 'somevalue',
})

const withAutoSimpleCompose = withBemMod<{ autosimple?: 'yes' }>('EnhancedComponent', {
autosimple: 'yes',
})

const withHover = (Wrapped: ComponentType<any>) => (props: HoveredProps) => <Wrapped {...props} />
const withThemeA = (Wrapped: ComponentType<any>) => (props: ThemeAProps) => <Wrapped {...props} />
const withThemeB = (Wrapped: ComponentType<any>) => (props: ThemeBProps) => <Wrapped {...props} />
const withSizeA = (Wrapped: ComponentType<any>) => (props: SizeAProps) => <Wrapped {...props} />

const EnhancedComponent = compose(
withSimpleCompose,
withAutoSimpleCompose,
withHover,
withSizeA,
composeU(withThemeA, withThemeB),
)(Component)

const EnhancedComponentRemoveProp = compose(
withSimpleCompose,
withAutoSimpleCompose,
withHover,
withThemeB,
)(ComponentSpreadProps)

describe('compose', () => {
test('should compile component with theme a', () => {
mount(<EnhancedComponent theme="a" text="" />)
Expand All @@ -55,6 +78,33 @@ describe('compose', () => {
})

test('should compile component with simple mod', () => {
mount(<EnhancedComponent theme="b" simple text="" />)
mount(<EnhancedComponent theme="b" simple="somevalue" text="" autosimple="yes" />)
})

test('remove mod props in simple mod', () => {
expect(
getPropsFromSelector(
<EnhancedComponentRemoveProp theme="b" simple="somevalue" text="" autosimple="yes" />,
),
).toEqual({
autosimple: 'yes',
children: undefined,
className:
'EnhancedComponent EnhancedComponent_simple_somevalue EnhancedComponent_autosimple_yes',
text: '',
theme: 'b',
})
})

test("don't remove mod props in simple mod if value hasn't matched", () => {
expect(
getPropsFromSelector(<EnhancedComponentRemoveProp theme="b" simple="errorvalue" text="" />),
).toEqual({
children: undefined,
className: 'EnhancedComponent',
simple: 'errorvalue',
text: '',
theme: 'b',
})
})
})

0 comments on commit 6ce126c

Please sign in to comment.