Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions hocs/with-class-names/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# `@byndyusoft-ui/with-class-names`

---

> High Order Component for css styles injection ...

## Installation

```
npm i @byndyusoft-ui/with-class-names
```

## Method "mergeClassNames"
Use this method for merge component styles with custom styles
```ts
mergeClassNames(targetStyles, sourceStyles, options)
```

### Option "withReplace" ("true" by default)


## Usage example

*Checkbox.tsx (Byndyusoft-UI component from "@byndyusoft-ui/Checkbox")*
```tsx
// ... Some imports
import { IClassNames, TClassNamesRecord } from '@byndyusoft-ui/with-class-names';
import styles from './Checkbox.module.css';

// List of classNames for check TypeScript
const styleClassNames = ['container', 'disabled', 'input', 'field', 'label', 'trigger'] as const;

export type TCheckboxClassNames = TClassNamesRecord<(typeof styleClassNames)[number]>;
export const checkboxClassNames = styles as TCheckboxClassNames;

export interface ICheckboxProps extends InputHTMLAttributes<HTMLInputElement>, IClassNames<TCheckboxClassNames> {
// ... some props
}

const Checkbox = forwardRef<HTMLInputElement, ICheckboxProps>(
(
{
classNames = checkboxClassNames,
// ... other props
},
forwardedRef
) => {
// ... component body
}
);
```

*Checkbox.tsx (Project component with custom styles)*
```tsx
import withClassNames, { mergeClassNames } from '@byndyusoft-ui/with-class-names';
import Checkbox, { TCheckboxClassNames, checkboxClassNames } from '@byndyusoft-ui/Checkbox';
import styles from './CheckboxWithReplacedStyles.module.css';

const Checkbox = withClassNames(
Checkbox,
mergeClassNames(checkboxClassNames, styles as TCheckboxClassNames)
);

export default Checkbox;
```
32 changes: 32 additions & 0 deletions hocs/with-class-names/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@byndyusoft-ui/with-class-names",
"version": "0.0.0",
"description": "Byndyusoft UI React High Order Component for css styles injection",
"keywords": [
"byndyusoft",
"byndyusoft-ui",
"react",
"hoc"
],
"author": "Byndyusoft Frontend Stream <frontend@byndyusoft.com>",
"homepage": "https://github.com/Byndyusoft/ui/tree/master/hocs/with-class-names#readme",
"license": "Apache-2.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"repository": {
"type": "git",
"url": "git+https://github.com/Byndyusoft/ui.git"
},
"scripts": {
"build": "tsc",
"clean": "rimraf dist",
"lint": "eslint src --config ../../eslint.config.js",
"test": "jest --config ../../jest.config.js --roots ./hocs/with-class-names/src"
},
"bugs": {
"url": "https://github.com/Byndyusoft/ui/issues"
},
"publishConfig": {
"access": "public"
}
}
3 changes: 3 additions & 0 deletions hocs/with-class-names/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type { IClassNames, TClassNamesRecord } from './withClassNames';
export { default as mergeClassNames } from './mergeClassNames';
export { default } from './withClassNames';
23 changes: 23 additions & 0 deletions hocs/with-class-names/src/mergeClassNames.tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import mergeClassNames from './mergeClassNames';

describe('hocs / withClassNames / mergeClassNames', () => {
test('should return different classes', () => {
expect(mergeClassNames({}, { b: '2' })).toEqual({ b: '2' });
expect(mergeClassNames({ a: '1' }, {})).toEqual({ a: '1' });
expect(mergeClassNames({ a: '1' }, { b: '2' })).toEqual({ a: '1', b: '2' });
});

test('should return replaced classes', () => {
expect(mergeClassNames({ a: '1' }, { a: '' })).toEqual({ a: '' });
expect(mergeClassNames({ a: '1' }, { a: undefined })).toEqual({ a: undefined });
expect(mergeClassNames({ a: '1' }, { a: '2', b: '3' })).toEqual({ a: '2', b: '3' });
});

test('should return merged classes', () => {
expect(mergeClassNames({ a: '1', b: '3' }, { a: '2' }, { withReplace: false })).toEqual({ a: '1 2', b: '3' });
expect(mergeClassNames({ a: '1', b: '' }, { a: '', b: '3' }, { withReplace: false })).toEqual({
a: '1',
b: '3'
});
});
});
37 changes: 37 additions & 0 deletions hocs/with-class-names/src/mergeClassNames.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
interface IMergeOptions {
withReplace?: boolean;
}

const getDefaultOptions = (): IMergeOptions => ({
withReplace: true
});

const joinClassNames = <CN extends object>(target: CN, source: CN): CN => {
const result = Object.assign({}, target);

for (const key in source) {
if (!source[key]) {
continue;
}

if (result[key]) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
result[key] = [result[key], source[key]].join(' ');
} else {
result[key] = source[key];
}
}

return result;
};

export default function mergeClassNames<CN extends object>(target: CN, source: CN, options: IMergeOptions = {}): CN {
const mergedOptions = Object.assign(getDefaultOptions(), options);

if (mergedOptions.withReplace) {
return Object.assign({}, target, source);
}

return joinClassNames(target, source);
}
15 changes: 15 additions & 0 deletions hocs/with-class-names/src/withClassNames.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React, { ComponentType, FunctionComponent, PropsWithChildren } from 'react';

export interface IClassNames<CN> {
classNames?: CN;
}

export type TClassNamesRecord<K extends string> = Partial<Record<K, string>>;

export default function withClassNames<P, CN>(
Component: ComponentType<P & IClassNames<CN>>,
classNames: CN
): FunctionComponent<PropsWithChildren<P>> {
// eslint-disable-next-line react/display-name
return (props: PropsWithChildren<P>) => <Component {...props} classNames={classNames} />;
}
17 changes: 17 additions & 0 deletions hocs/with-class-names/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": true,
"declarationDir": "dist",
"outDir": "dist",
"module": "commonjs",
"target": "es6"
},
"include": [
"src"
],
"exclude": [
"node_modules",
"src/*.tests.ts"
]
}
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module.exports = {
roots: ['<rootDir>/components', '<rootDir>/hooks', '<rootDir>/services'],
roots: ['<rootDir>/components', '<rootDir>/hocs', '<rootDir>/hooks', '<rootDir>/services'],
setupFilesAfterEnv: ['<rootDir>/.jest/setup.ts'],
transform: {
'^.+\\.tsx?$': 'ts-jest'
Expand Down
8 changes: 1 addition & 7 deletions lerna.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
{
"packages": [
"components/*",
"hooks/*",
"packages/*",
"services/*",
"styles/*"
],
"packages": ["components/*", "hocs/*", "hooks/*", "packages/*", "services/*", "styles/*"],
"useWorkspaces": true,
"version": "independent",
"npmClient": "npm",
Expand Down
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"private": true,
"workspaces": [
"components/*",
"hocs/*",
"hooks/*",
"packages/*",
"services/*",
Expand Down