├── jest.config.js ├── .github ├── assets │ ├── ts_jsx.png │ ├── screenshot.png │ ├── ts_function.png │ ├── ts_add_props_jsx.png │ └── ts_add_props_function.png └── workflows │ ├── publish.yml │ └── test.yml ├── .editorconfig ├── .gitignore ├── tsconfig.eslint.json ├── tsconfig.json ├── .eslintrc.js ├── LICENSE.md ├── src ├── index.ts ├── classNames.ts ├── tags.ts └── types.ts ├── package.json ├── CHANGELOG.md ├── __tests__ └── classed.test.tsx └── README.md /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; -------------------------------------------------------------------------------- /.github/assets/ts_jsx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathieutu/classed-components/HEAD/.github/assets/ts_jsx.png -------------------------------------------------------------------------------- /.github/assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathieutu/classed-components/HEAD/.github/assets/screenshot.png -------------------------------------------------------------------------------- /.github/assets/ts_function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathieutu/classed-components/HEAD/.github/assets/ts_function.png -------------------------------------------------------------------------------- /.github/assets/ts_add_props_jsx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathieutu/classed-components/HEAD/.github/assets/ts_add_props_jsx.png -------------------------------------------------------------------------------- /.github/assets/ts_add_props_function.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathieutu/classed-components/HEAD/.github/assets/ts_add_props_function.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset=utf-8 3 | end_of_line=lf 4 | trim_trailing_whitespace=true 5 | insert_final_newline=true 6 | indent_style=space 7 | indent_size=2 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | 15 | # Dependency directories 16 | node_modules/ 17 | 18 | # Optional npm cache directory 19 | .npm 20 | 21 | # Optional eslint cache 22 | .eslintcache 23 | 24 | # Optional REPL history 25 | .node_repl_history 26 | 27 | # Output of 'npm pack' 28 | *.tgz 29 | 30 | # Yarn Integrity file 31 | .yarn-integrity 32 | 33 | dist 34 | 35 | coverage/ 36 | 37 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "noResolve": false, 5 | "module": "commonjs", 6 | "target": "es6", 7 | "experimentalDecorators": true, 8 | "noImplicitAny": true, 9 | "noImplicitReturns": true, 10 | "noImplicitThis": true, 11 | "noUnusedLocals": true, 12 | "skipLibCheck": true, 13 | "strictNullChecks": true, 14 | "strictPropertyInitialization": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "outDir": "dist" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish on NPM 2 | on: 3 | push: 4 | tags: 5 | - 'v[0-9]+.[0-9]+.[0-9]+*' 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: 16 14 | registry-url: 'https://registry.npmjs.org' 15 | - name: Install dependencies 16 | run: yarn install 17 | - run: npm publish 18 | env: 19 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "noResolve": false, 5 | "module": "commonjs", 6 | "target": "es2015", 7 | "lib": ["esnext"], 8 | "experimentalDecorators": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "noUnusedLocals": true, 14 | "strictNullChecks": true, 15 | "strictPropertyInitialization": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "outDir": "dist", 18 | "jsx": "react" 19 | }, 20 | "files": ["./src/index.ts"] 21 | } 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['airbnb-typescript/base'], 3 | globals: { 4 | describe: false, 5 | expect: false, 6 | it: false, 7 | }, 8 | parserOptions: { 9 | project: './tsconfig.eslint.json', 10 | }, 11 | rules: { 12 | '@typescript-eslint/semi': ['error', 'never'], 13 | 'consistent-return': 'off', 14 | 'import/no-dynamic-require': 'off', 15 | 'import/extensions': 'off', 16 | 'global-require': 'off', 17 | 'max-len': ['warn', 120], 18 | 'object-curly-newline': 'off', 19 | 'import/prefer-default-export': 'off', 20 | "import/no-extraneous-dependencies": "off", 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: 16 17 | - name: Install dependencies 18 | run: yarn install 19 | - name: Run checks 20 | run: yarn run check 21 | - name: Run tests 22 | run: yarn run test:coverage 23 | - name: Report coverage 24 | uses: coverallsapp/github-action@master 25 | with: 26 | github-token: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) Mathieu TUDISCO 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { clsx } from 'clsx' 2 | import { createElement, forwardRef } from 'react' 3 | import { Classes, ClassesValueArray, processClasses } from './classNames' 4 | import { filterPropsToForward, tags } from './tags' 5 | import { CreateClassedComponent, Tag } from './types' 6 | 7 | const tagDisplayName = (tag: Tag) => (typeof tag === 'string' ? tag : tag.displayName || tag.name) 8 | 9 | const createClassed: any = (tag: Tag) => (classes: Classes, ...placeholders: ClassesValueArray) => { 10 | const Hoc = forwardRef((props: { className?: string }, ref) => { 11 | const className = clsx( 12 | processClasses(classes, props, placeholders), 13 | props.className, 14 | ) 15 | 16 | const propsToForward = filterPropsToForward(tag, props) 17 | 18 | return createElement(tag, { ...propsToForward, ref, className }) 19 | }) 20 | 21 | Hoc.displayName = `Classed(${tagDisplayName(tag)})` 22 | 23 | return Hoc 24 | } 25 | 26 | tags.forEach((tagName) => { 27 | createClassed[tagName] = createClassed(tagName) 28 | }) 29 | 30 | export const classed: CreateClassedComponent = createClassed 31 | 32 | export default classed 33 | -------------------------------------------------------------------------------- /src/classNames.ts: -------------------------------------------------------------------------------- 1 | import { clsx } from 'clsx' 2 | 3 | type BasicClassValue = string | { [id: string]: any } | undefined | boolean 4 | type ClassesValue

= BasicClassValue | ClassesValueArray

| ((props: P) => Classes

) 5 | export type Classes

= ClassesValue

| ClassesValueArray

| TemplateStringsArray 6 | 7 | export interface ClassesValueArray

extends Array> { 8 | } 9 | 10 | const isTemplateString = (classes: Classes): classes is TemplateStringsArray => ( 11 | Array.isArray(classes) && Object.prototype.hasOwnProperty.call(classes, 'raw') 12 | ) 13 | 14 | export const processClasses =

( 15 | classes: Classes

, 16 | props: P, templateStringPlaceholders: ClassesValueArray

= [], 17 | ): BasicClassValue => { 18 | if (isTemplateString(classes)) { 19 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 20 | return processAsTemplateString(classes, props, templateStringPlaceholders) 21 | } 22 | 23 | if (typeof classes === 'function') { 24 | return processClasses(classes(props), props) 25 | } 26 | 27 | if (Array.isArray(classes)) { 28 | return classes.map((classesValue) => processClasses(classesValue, props)) 29 | } 30 | 31 | return classes 32 | } 33 | 34 | const preparePlaceholders =

(templateStringPlaceholders: ClassesValueArray

, props: P) => ( 35 | templateStringPlaceholders.map((placeholder) => clsx(processClasses(placeholder, props))) 36 | ) 37 | 38 | const compileClassnames = (placeholders: string[], classes: TemplateStringsArray) => { 39 | const className = placeholders.reduce((acc, placeholder, i) => acc + classes[i] + placeholder, '') 40 | 41 | return (className + classes[classes.length - 1]).replace(/\s+/g, ' ').trim() 42 | } 43 | 44 | const processAsTemplateString =

( 45 | classes: TemplateStringsArray, 46 | props: P, 47 | templateStringPlaceholders: ClassesValueArray

, 48 | ): string => { 49 | const placeholders = preparePlaceholders(templateStringPlaceholders, props) 50 | 51 | return compileClassnames(placeholders, classes) 52 | } 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "classed-components", 3 | "version": "2.0.1", 4 | "description": "CSS Classes for the component age. Apply your Css with the power of Javascript.", 5 | "keywords": [ 6 | "classNames", 7 | "react", 8 | "components", 9 | "classed", 10 | "class", 11 | "styled" 12 | ], 13 | "main": "dist/index.js", 14 | "types": "dist/index.d.ts", 15 | "repository": "https://github.com/mathieutu/classed-components.git", 16 | "author": "Mathieu TUDISCO ", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/mathieutu/classed-components/issues" 20 | }, 21 | "homepage": "https://github.com/mathieutu/classed-components", 22 | "files": [ 23 | "dist/**/*" 24 | ], 25 | "scripts": { 26 | "build": "rm -rf dist && tsc", 27 | "watch": "yarn run build -- -w", 28 | "lint": "eslint --ignore-path .gitignore --ext .ts --ext .tsx .", 29 | "lint:fix": "yarn lint --fix", 30 | "typecheck": "tsc --noEmit", 31 | "check": "tsc --noEmit && yarn run lint", 32 | "test": "jest", 33 | "test:coverage": "yarn run test --coverage", 34 | "test-watch": "jest --watch", 35 | "prepare": "yarn run build", 36 | "prepublishOnly": "yarn run check && yarn run test", 37 | "changelog": "gitmoji-changelog" 38 | }, 39 | "devDependencies": { 40 | "@types/jest": "^27.0.2", 41 | "@types/react": "^17.0.34", 42 | "@types/react-test-renderer": "^17.0.1", 43 | "@typescript-eslint/eslint-plugin": "^5.3.0", 44 | "@typescript-eslint/parser": "^5.3.0", 45 | "coveralls": "^3.1.1", 46 | "eslint": "^8.2.0", 47 | "eslint-config-airbnb-typescript": "^15.0.0", 48 | "eslint-plugin-import": "^2.22.1", 49 | "gitmoji-changelog": "^2.2.1", 50 | "jest": "^27.3.1", 51 | "react": "^17.0.2", 52 | "react-test-renderer": "^17.0.2", 53 | "ts-jest": "^27.0.7", 54 | "typescript": "^4.4.4" 55 | }, 56 | "dependencies": { 57 | "@emotion/is-prop-valid": "^1.2.1", 58 | "clsx": "^2.1.0" 59 | }, 60 | "peerDependencies": { 61 | "react": "*" 62 | }, 63 | "engines": { 64 | "node": ">=6.0.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ## 2.0.1 (2024-02-14) 5 | 6 | ### Changed 7 | 8 | - ♻️ Replace classnames with clsx ([#8](https://github.com/mathieutu/classed-components/issues/8)) [[ba50307](https://github.com/mathieutu/classed-components/commit/ba5030741f12c692ff298f90260f3a114b26e3cb)] 9 | 10 | 11 | 12 | ## 2.0.0 (2021-11-08) 13 | 14 | ### Added 15 | 16 | - ✨ Add refs forwarding to HOC. ([#5](https://github.com/mathieutu/classed-components/issues/5)) [[4f85a3b](https://github.com/mathieutu/classed-components/commit/4f85a3be12eab10124d3c7f6a25bc734eeff5a36)] 17 | 18 | ### Miscellaneous 19 | 20 | - ⬆️ Upgrade dependencies. ([#6](https://github.com/mathieutu/classed-components/issues/6)) [[325bc59](https://github.com/mathieutu/classed-components/commit/325bc59c5800ce075039ad69b8669f0fb47904c6)] 21 | - 👷 Replace Travis by Github actions. [[ec0987e](https://github.com/mathieutu/classed-components/commit/ec0987eb3b3739c56bbd543c4d279f0b78a74f53)] 22 | - 🔨 Replace TSLint by ESLint. [[8d39630](https://github.com/mathieutu/classed-components/commit/8d396303d87503c1be0fcdbe26368561a8aaf6ec)] 23 | 24 | 25 | 26 | ## 1.0.1 (2019-06-03) 27 | 28 | ### Fixed 29 | 30 | - 🐛 Filter props to forward to intrinsic elements, and avoid React warnings. (fix [#2](https://github.com/mathieutu/classed-components/issues/2)) [[4098c7b](https://github.com/mathieutu/classed-components/commit/4098c7be43cca1c64992b23ac41d48f83007892d)] 31 | 32 | ### Miscellaneous 33 | 34 | - 📝 Add thanks section to readme. [[63bc9ba](https://github.com/mathieutu/classed-components/commit/63bc9ba2fbb6e98fe44682dcbcb0d134d5a76b76)] 35 | 36 | 37 | 38 | ## 1.0.0 (2019-05-21) 39 | 40 | ### Added 41 | - 🚀 Add NPM deployment configuration. [[5c080a0](https://github.com/mathieutu/classed-components/commit/5c080a0c114b294ec5530abb8d0d550d6769dcd1)] 42 | - 👷 Add Travis and Coveralls. [[5d5fe53](https://github.com/mathieutu/classed-components/commit/5d5fe5366e8599f5e2b6ebdb2886f7b7a2f35baf)] 43 | - 📝 Add Readme. [[4863764](https://github.com/mathieutu/classed-components/commit/4863764b6b48d724c7bddd0990655c5067e42f93)] 44 | - ✅ Add tests. [[b6a929b](https://github.com/mathieutu/classed-components/commit/b6a929b40b271eb4a4c722d7bb3d8f6467b52380)] 45 | - ✨ First implementation of classed function. [[99778ff](https://github.com/mathieutu/classed-components/commit/99778ffbb0405a672522f65f3f911daa9fe8f961)] 46 | - 🎉 Project initialisation. [[64a8a31](https://github.com/mathieutu/classed-components/commit/64a8a318d40324dbff392bd290634bcce4b315ae)] 47 | 48 | ### Changed 49 | 50 | - ♻️ Change forEach to reduce. [[bbb44dc](https://github.com/mathieutu/classed-components/commit/bbb44dc0bada7b43d4ef9acb19202c71a994e38f)] 51 | 52 | ### Fixed 53 | 54 | - 💚 Update code to match specs in tests. [[1161ddb](https://github.com/mathieutu/classed-components/commit/1161ddb039626716685351a9b74f1cf8d5b9bd88)] 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/tags.ts: -------------------------------------------------------------------------------- 1 | import isPropValid from '@emotion/is-prop-valid' 2 | import { ElementType } from 'react' 3 | import { HTMLBasicElement } from './types' 4 | 5 | export const tags: HTMLBasicElement[] = [ 6 | 'a', 7 | 'abbr', 8 | 'address', 9 | 'area', 10 | 'article', 11 | 'aside', 12 | 'audio', 13 | 'b', 14 | 'base', 15 | 'bdi', 16 | 'bdo', 17 | 'big', 18 | 'blockquote', 19 | 'body', 20 | 'br', 21 | 'button', 22 | 'canvas', 23 | 'caption', 24 | 'cite', 25 | 'code', 26 | 'col', 27 | 'colgroup', 28 | 'data', 29 | 'datalist', 30 | 'dd', 31 | 'del', 32 | 'details', 33 | 'dfn', 34 | 'dialog', 35 | 'div', 36 | 'dl', 37 | 'dt', 38 | 'em', 39 | 'embed', 40 | 'fieldset', 41 | 'figcaption', 42 | 'figure', 43 | 'footer', 44 | 'form', 45 | 'h1', 46 | 'h2', 47 | 'h3', 48 | 'h4', 49 | 'h5', 50 | 'h6', 51 | 'head', 52 | 'header', 53 | 'hgroup', 54 | 'hr', 55 | 'html', 56 | 'i', 57 | 'iframe', 58 | 'img', 59 | 'input', 60 | 'ins', 61 | 'kbd', 62 | 'keygen', 63 | 'label', 64 | 'legend', 65 | 'li', 66 | 'link', 67 | 'main', 68 | 'map', 69 | 'mark', 70 | 'menu', 71 | 'menuitem', 72 | 'meta', 73 | 'meter', 74 | 'nav', 75 | 'noscript', 76 | 'object', 77 | 'ol', 78 | 'optgroup', 79 | 'option', 80 | 'output', 81 | 'p', 82 | 'param', 83 | 'picture', 84 | 'pre', 85 | 'progress', 86 | 'q', 87 | 'rp', 88 | 'rt', 89 | 'ruby', 90 | 's', 91 | 'samp', 92 | 'script', 93 | 'section', 94 | 'select', 95 | 'small', 96 | 'source', 97 | 'span', 98 | 'strong', 99 | 'style', 100 | 'sub', 101 | 'summary', 102 | 'sup', 103 | 'table', 104 | 'tbody', 105 | 'td', 106 | 'textarea', 107 | 'tfoot', 108 | 'th', 109 | 'thead', 110 | 'time', 111 | 'title', 112 | 'tr', 113 | 'track', 114 | 'u', 115 | 'ul', 116 | 'var', 117 | 'video', 118 | 'wbr', 119 | 120 | // SVG 121 | 'circle', 122 | 'clipPath', 123 | 'defs', 124 | 'ellipse', 125 | 'foreignObject', 126 | 'g', 127 | 'image', 128 | 'line', 129 | 'linearGradient', 130 | 'mask', 131 | 'path', 132 | 'pattern', 133 | 'polygon', 134 | 'polyline', 135 | 'radialGradient', 136 | 'rect', 137 | 'stop', 138 | 'svg', 139 | 'text', 140 | 'tspan', 141 | ] 142 | 143 | const isAnHtmlBasicElement = (tag: ElementType): tag is HTMLBasicElement => ( 144 | typeof tag === 'string' && tags.includes(tag) 145 | ) 146 | 147 | const forwardAll = Symbol('Forward all props') 148 | 149 | const getShouldForwardProp = (tag: ElementType) => (isAnHtmlBasicElement(tag) ? isPropValid : forwardAll) 150 | 151 | export const filterPropsToForward = (tag: ElementType, props: object) => { 152 | const shouldForwardProp = getShouldForwardProp(tag) 153 | 154 | if (shouldForwardProp === forwardAll) { 155 | return { ...props } 156 | } 157 | 158 | return Object.fromEntries(Object.entries(props).filter(([key]) => shouldForwardProp(key))) 159 | } 160 | -------------------------------------------------------------------------------- /__tests__/classed.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { create } from 'react-test-renderer' 3 | import { classed } from '../src' 4 | 5 | const assertEquals = (expected: React.ReactElement, actual: React.ReactElement) => ( 6 | expect(create(expected).toJSON()).toEqual(create(actual).toJSON()) 7 | ) 8 | 9 | describe('Basic tests', () => { 10 | test('it allows passing basic html tag', () => { 11 | const Nav = classed('nav')('') 12 | 13 | assertEquals(