├── .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 | 
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 |
--------------------------------------------------------------------------------