├── .eslintignore ├── .gitattributes ├── .gitignore ├── .ncurc.json ├── .prettierrc.js ├── .watchmanconfig ├── LICENSE ├── README.md ├── babel.config.js ├── metro.config.js ├── package.json ├── react-native-scaled-layout.jpg ├── sample1.jpg ├── src └── index.tsx ├── tsconfig.build.json └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.test.* 2 | **/*.snap.* 3 | **/*.d.ts 4 | 5 | metro.config.js 6 | dist/ 7 | node_modules/ 8 | lib/ -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # node.js 2 | # 3 | dist/ 4 | node_modules/ 5 | npm-debug.log 6 | yarn-error.log 7 | package-lock.json 8 | yarn.lock 9 | .idea/ 10 | lib/ 11 | -------------------------------------------------------------------------------- /.ncurc.json: -------------------------------------------------------------------------------- 1 | { 2 | "upgrade": true, 3 | "reject": [ 4 | "react", 5 | "react-dom", 6 | "react-test-renderer" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'all', 3 | arrowParens: 'always', 4 | singleQuote: true, 5 | jsxSingleQuote: false, 6 | printWidth: 120, 7 | }; 8 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 MJ Studio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simplist Spannable Text Builder support variant styling, child component 2 | ![NPM Version](https://img.shields.io/npm/v/%40mj-studio%2Freact-native-spannable-string) 3 | 4 | How do you make this? 5 | 6 | 7 | 8 | ### Verbose way 😓 9 | 10 | ```tsx 11 | 12 | Using Bold{' '} 13 | in Text 14 | 15 | ``` 16 | 17 | ### SpannableBuilder way 🔥 18 | 19 | ```tsx 20 | SpannableBuilder.getInstance({ fontSize: 24 }) 21 | .append('Using ') 22 | .appendBold('Bold') 23 | .append(' in Text') 24 | .build() 25 | ``` 26 | 27 | 28 | ## Contents 🏆 29 | 30 | * [Install](#install-) 31 | * [Usage](#usage-) 32 | * [Change Logs](#change-logs-) 33 | ## Install 💠 34 | 35 | ``` 36 | npm i @mj-studio/react-native-spannable-string 37 | ``` 38 | 39 | or 40 | 41 | ``` 42 | yarn add @mj-studio/react-native-spannable-string 43 | ``` 44 | 45 | ## Usage 📌 46 | 47 | 0. Import class from package 48 | 49 | ```tsx 50 | import SpannableBuilder from '@mj-studio/react-native-spannable-string'; 51 | ``` 52 | 53 | 1. Create `SpannableBuilder` instance 54 | 55 | Instantiate `SpannableBuiler` instance with static `getInstance` function. 56 | `getInstance` receive *`TextStyle` parameter for base style used by `SpannableBuilder`* 57 | 58 | ```tsx 59 | SpannableBuilder.getInstance({ fontSize: 24 }) 60 | ``` 61 | 62 | We can also instantate it with `Text` component with `getInstanceWithComponent` like this. 63 | 64 | ```tsx 65 | SpannableBuilder.getInstanceWithComponent(Text) 66 | 67 | // or custom Text component 68 | type Props = { fontFamily: string } & TextProps; 69 | function MyText({fontFamily = 'NotoSansKR-Bold', ...rest}: React.PropsWithChildren) { 70 | const { style, children, ...withOutStyle } = rest; 71 | 72 | return ( 73 | 74 | {children} 75 | 76 | ); 77 | } 78 | 79 | SpannableBuilder.getInstanceWithComponent(MyText) 80 | ``` 81 | 82 | 2. Append your texts with spannable free 83 | 84 | ```tsx 85 | 86 | {SpannableBuilder.getInstance({ fontSize: 24 }) 87 | .append('Using ') 88 | .appendBold('Bold') 89 | .append(' in Text') 90 | .appendCustomComponent( 91 | , 97 | ) 98 | .build()} 99 | {SpannableBuilder.getInstance({ fontSize: 24 }) 100 | .append('Using ') 101 | .appendItalic('Italic') 102 | .append(' in Text') 103 | .build()} 104 | {SpannableBuilder.getInstance({ fontSize: 24 }) 105 | .append('Using ') 106 | .appendColored('Color', 'red') 107 | .append(' in Text') 108 | .build()} 109 | {SpannableBuilder.getInstance({ fontSize: 24 }) 110 | .append('Using ') 111 | .appendCustom('Custom Style', { 112 | textShadowColor: 'blue', 113 | textShadowRadius: 8, 114 | }) 115 | .append(' in Text') 116 | .build()} 117 | 118 | 119 | // Sample Title 120 | SpannableBuilder.getInstance({ fontSize: 44 }) 121 | .appendColored('S', 'red') 122 | .appendItalic('p') 123 | .appendCustom('a', { 124 | fontSize: 30, 125 | textShadowColor: 'blue', 126 | textShadowRadius: 12, 127 | }) 128 | .appendColored('n', 'orange') 129 | .appendCustom('n', { 130 | fontSize: 22, 131 | textDecorationLine: 'underline', 132 | }) 133 | .appendColored('a', 'skyblue') 134 | .appendCustom('b', { 135 | backgroundColor: 'black', 136 | color: 'white', 137 | fontSize: 22, 138 | }) 139 | .appendCustom('l', { fontSize: 18, color: 'red' }) 140 | .appendBold('e ') 141 | .build(), 142 | 143 | ``` 144 | 145 | 146 | ## Change Logs 🔧 147 | * 1.0.0 148 | - First Release 🔥 149 | * 1.0.1 150 | - Add `baseStyle` parameter in `getInstanceWithComponent` 151 | * 1.0.4 152 | - Add config options `additionalTextStyle`, `outerTextStyle` 153 | * 1.0.7 154 | - Fix additionalTextStyle bugs in `appendBold`, `appendColored`, `appendItalic` 155 | * 1.0.8 156 | - Enable re-usability of `Builder` 157 | * 1.0.9 158 | - Add `appendBoldWithDelimiter` 159 | * 1.1.1 160 | - Ignore if first parameter of appendXXX is not a string 161 | * 1.1.3 162 | - Add `appendCustomWithDelimiter`, `appendColoredWithDelimiter`, `appendItalicWithDelimiter`, 163 | * 1.1.4 164 | - Add `appendCustomComponent` 165 | 166 | ### feel free your fork or any PR! Thanks 167 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:@react-native/babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require('metro-config'); 2 | 3 | module.exports = (async () => { 4 | const { 5 | resolver: { sourceExts, assetExts }, 6 | } = await getDefaultConfig(); 7 | return { 8 | transformer: { 9 | babelTransformerPath: require.resolve('react-native-svg-transformer'), 10 | }, 11 | resolver: { 12 | assetExts: assetExts.filter((ext) => ext !== 'svg'), 13 | sourceExts: [...sourceExts, 'svg'], 14 | }, 15 | }; 16 | })(); 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mj-studio/react-native-spannable-string", 3 | "version": "1.1.4", 4 | "description": "The simple Text builder for multiple styling in single Text component", 5 | "main": "lib/commonjs/index", 6 | "module": "lib/module/index", 7 | "types": "lib/typescript/src/index.d.ts", 8 | "react-native": "src/index", 9 | "source": "src/index", 10 | "files": [ 11 | "src", 12 | "lib", 13 | "android", 14 | "ios", 15 | "cpp", 16 | "*.podspec", 17 | "!ios/build", 18 | "!android/build", 19 | "!android/gradle", 20 | "!android/gradlew", 21 | "!android/gradlew.bat", 22 | "!android/local.properties", 23 | "!**/__tests__", 24 | "!**/__fixtures__", 25 | "!**/__mocks__", 26 | "!**/.*" 27 | ], 28 | "private": false, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/mym0404/react-native-spannable-string" 32 | }, 33 | "keywords": [], 34 | "author": "MJ Studio ", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/mym0404/react-native-spannable-string/issues" 38 | }, 39 | "homepage": "https://github.com/mym0404/react-native-spannable-string", 40 | "scripts": { 41 | "prepare": "yarn t", 42 | "prepack": "yarn build", 43 | "typecheck": "tsc --noEmit", 44 | "lint": "eslint \"**/*.{js,ts,tsx}\"", 45 | "t": "yarn lint && yarn typecheck", 46 | "test": "jest", 47 | "build": "yarn t && bob build" 48 | }, 49 | "peerDependencies": { 50 | "react": "*", 51 | "react-native": ">=0.60" 52 | }, 53 | "dependencies": {}, 54 | "devDependencies": { 55 | "@types/react-native": "^0.61.21", 56 | "typescript": "5.1.5", 57 | "eslint": "^8.51.0", 58 | "eslint-config-prettier": "^9.0.0", 59 | "eslint-plugin-prettier": "^5.0.1", 60 | "prettier": "^3.0.3", 61 | "@react-native/eslint-config": "^0.73.1", 62 | "react": "18.2.0", 63 | "react-native": "0.73.6", 64 | "@types/react": "^18.2.44", 65 | "react-native-builder-bob": "^0.20.0" 66 | }, 67 | "eslintConfig": { 68 | "root": true, 69 | "extends": [ 70 | "@react-native", 71 | "prettier" 72 | ], 73 | "rules": { 74 | "prettier/prettier": [ 75 | "error", 76 | { 77 | "quoteProps": "consistent", 78 | "singleQuote": true, 79 | "tabWidth": 2, 80 | "trailingComma": "es5", 81 | "useTabs": false 82 | } 83 | ], 84 | "react-native/no-inline-styles": "off", 85 | "@typescript-eslint/no-shadow": "off" 86 | } 87 | }, 88 | "prettier": { 89 | "quoteProps": "consistent", 90 | "singleQuote": true, 91 | "tabWidth": 2, 92 | "trailingComma": "es5", 93 | "useTabs": false 94 | }, 95 | "react-native-builder-bob": { 96 | "source": "src", 97 | "output": "lib", 98 | "targets": [ 99 | "commonjs", 100 | "module", 101 | [ 102 | "typescript", 103 | { 104 | "project": "tsconfig.build.json" 105 | } 106 | ] 107 | ] 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /react-native-scaled-layout.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mj-studio-library/react-native-spannable-string/445c4a4596a478ef3539a7387cad6b58c5a8bcd1/react-native-scaled-layout.jpg -------------------------------------------------------------------------------- /sample1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mj-studio-library/react-native-spannable-string/445c4a4596a478ef3539a7387cad6b58c5a8bcd1/sample1.jpg -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentType, ReactElement } from 'react'; 2 | import React from 'react'; 3 | import type { StyleProp, TextProps, TextStyle } from 'react-native'; 4 | import { StyleSheet, Text, View } from 'react-native'; 5 | 6 | type TextComponent = ComponentType; 7 | type Config = { 8 | additionalStyle?: StyleProp; 9 | outerTextStyle?: StyleProp; 10 | }; 11 | type InnerConfig = { 12 | appendedStyle?: StyleProp; 13 | outerStyle?: StyleProp; 14 | }; 15 | export default class SpannableBuilder { 16 | static getInstanceWithComponent( 17 | baseComponent?: TextComponent, 18 | config?: Config 19 | ): SpannableBuilder { 20 | const BaseText: TextComponent = baseComponent || Text; 21 | 22 | const Wrapped: TextComponent = ( 23 | props: TextProps & InnerConfig 24 | ): ReactElement => { 25 | const { style, children, appendedStyle, outerStyle } = props; 26 | 27 | const flattenStyle = StyleSheet.flatten([ 28 | config?.additionalStyle, 29 | style, 30 | outerStyle, 31 | appendedStyle, 32 | ]); 33 | 34 | return ( 35 | 36 | {children} 37 | 38 | ); 39 | }; 40 | 41 | return new SpannableBuilder(Wrapped, config?.outerTextStyle); 42 | } 43 | 44 | static getInstance( 45 | additionalStyle?: StyleProp, 46 | outerTextStyle?: StyleProp 47 | ): SpannableBuilder { 48 | if (!additionalStyle) return new SpannableBuilder(Text); 49 | 50 | return SpannableBuilder.getInstanceWithComponent(Text, { 51 | additionalStyle, 52 | outerTextStyle, 53 | }); 54 | } 55 | 56 | readonly #TextComponent: ComponentType; 57 | 58 | #order = ''; 59 | readonly #textList: (string | ReactElement)[] = []; 60 | readonly #customStyleList: StyleProp[] = []; 61 | 62 | readonly outerTextStyle?: StyleProp; 63 | 64 | constructor( 65 | textComponent: TextComponent, 66 | outerTextStyle?: StyleProp 67 | ) { 68 | this.#TextComponent = textComponent; 69 | this.outerTextStyle = outerTextStyle; 70 | } 71 | 72 | clear(): void { 73 | this.#order = ''; 74 | this.#textList.splice(0, this.#textList.length); 75 | this.#customStyleList.splice(0, this.#customStyleList.length); 76 | } 77 | 78 | append(text: string): this { 79 | if (typeof text !== 'string') return this; 80 | 81 | this.#textList.push(text); 82 | this.#order += 'T'; 83 | 84 | return this; 85 | } 86 | 87 | appendCustom(text: string, style: StyleProp): this { 88 | if (typeof text !== 'string') return this; 89 | 90 | this.#textList.push(text); 91 | this.#order += 'S'; 92 | this.#customStyleList.push(style); 93 | 94 | return this; 95 | } 96 | 97 | appendBold(text: string): this { 98 | if (typeof text !== 'string') return this; 99 | 100 | this.appendCustom(text, { fontWeight: 'bold' }); 101 | 102 | return this; 103 | } 104 | 105 | appendItalic(text: string): this { 106 | if (typeof text !== 'string') return this; 107 | 108 | this.appendCustom(text, { fontStyle: 'italic' }); 109 | 110 | return this; 111 | } 112 | 113 | appendColored(text: string, color: string): this { 114 | if (typeof text !== 'string') return this; 115 | 116 | this.appendCustom(text, { color }); 117 | 118 | return this; 119 | } 120 | 121 | appendCustomComponent(component: ReactElement): this { 122 | this.#textList.push(component); 123 | this.#order += 'C'; 124 | 125 | return this; 126 | } 127 | 128 | private appendWithDelimiter({ 129 | appender, 130 | delimiter, 131 | text, 132 | }: { 133 | text: string; 134 | delimiter: string; 135 | appender: (text: string) => void; 136 | }): this { 137 | if (typeof text !== 'string') return this; 138 | 139 | text.split(delimiter).forEach((t, i) => { 140 | if (i % 2 === 0) { 141 | this.append(t); 142 | } else { 143 | appender(t); 144 | } 145 | }); 146 | 147 | return this; 148 | } 149 | 150 | appendCustomWithDelimiter( 151 | text: string, 152 | style: StyleProp, 153 | delimiter = '$' 154 | ): this { 155 | return this.appendWithDelimiter({ 156 | text, 157 | delimiter, 158 | appender: (text) => { 159 | this.appendCustom(text, style); 160 | }, 161 | }); 162 | } 163 | 164 | appendBoldWithDelimiter(text: string, delimiter = '$'): this { 165 | return this.appendWithDelimiter({ 166 | text, 167 | delimiter, 168 | appender: (text) => { 169 | this.appendBold(text); 170 | }, 171 | }); 172 | } 173 | 174 | appendItalicWithDelimiter(text: string, delimiter = '$'): this { 175 | return this.appendWithDelimiter({ 176 | text, 177 | delimiter, 178 | appender: (text) => { 179 | this.appendItalic(text); 180 | }, 181 | }); 182 | } 183 | 184 | appendColoredWithDelimiter( 185 | text: string, 186 | color: string, 187 | delimiter = '$' 188 | ): this { 189 | return this.appendWithDelimiter({ 190 | text, 191 | delimiter, 192 | appender: (text) => { 193 | this.appendColored(text, color); 194 | }, 195 | }); 196 | } 197 | 198 | build(): ReactElement { 199 | const BaseText = this.#TextComponent; 200 | 201 | let idx = 0; 202 | let customStyleIdx = 0; 203 | 204 | const result = ( 205 | 206 | {[...this.#order].map((order, index) => { 207 | const key = `${order}${index}`; 208 | 209 | switch (order) { 210 | case 'S': 211 | return ( 212 | 216 | {this.#textList[idx++]} 217 | 218 | ); 219 | case 'C': 220 | return {this.#textList[idx++]}; 221 | default: 222 | return {this.#textList[idx++]}; 223 | } 224 | })} 225 | 226 | ); 227 | 228 | this.clear(); 229 | 230 | return result; 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "exclude": ["example"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "allowUnreachableCode": false, 5 | "allowUnusedLabels": false, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "jsx": "react", 9 | "lib": ["esnext"], 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitReturns": true, 14 | "noImplicitUseStrict": false, 15 | "noStrictGenericChecks": false, 16 | "noUncheckedIndexedAccess": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "resolveJsonModule": true, 20 | "skipLibCheck": true, 21 | "strict": true, 22 | "target": "esnext", 23 | "verbatimModuleSyntax": true 24 | } 25 | } 26 | --------------------------------------------------------------------------------