pte-interpolation-react 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -0
- package/dist/index.cjs +36 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -5
- package/dist/index.d.ts +34 -5
- package/dist/index.js +37 -3
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
- package/src/InterpolatedPortableText.tsx +31 -0
- package/src/__tests__/InterpolatedPortableText.test.tsx +204 -0
- package/src/__tests__/createInterpolationComponents.test.tsx +115 -0
- package/src/__tests__/fixtures.ts +106 -0
- package/src/constants.ts +2 -0
- package/src/createInterpolationComponents.tsx +32 -0
- package/src/index.ts +9 -2
- package/src/types.ts +15 -4
- package/src/InterpolatedText.tsx +0 -10
- package/src/__tests__/InterpolatedText.test.tsx +0 -9
package/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# pte-interpolation-react
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/pte-interpolation-react)
|
|
4
|
+
|
|
5
|
+
React rendering library that resolves dynamic variable inline blocks in [Portable Text](https://portabletext.org/) to actual values. Wraps [`@portabletext/react`](https://github.com/portabletext/react-portabletext) with automatic variable substitution.
|
|
6
|
+
|
|
7
|
+
Part of [sanity-pte-interpolation](https://github.com/jordanl17/sanity-pte-interpolation). For adding variable picker inline blocks to Sanity Studio, see [`sanity-plugin-pte-interpolation`](https://www.npmjs.com/package/sanity-plugin-pte-interpolation).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install pte-interpolation-react @portabletext/react
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Peer dependencies
|
|
16
|
+
|
|
17
|
+
- `react` ^18.0.0 || ^19.0.0
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Drop-in component (recommended)
|
|
22
|
+
|
|
23
|
+
`<InterpolatedPortableText>` is a drop-in replacement for `<PortableText>` that handles variable resolution automatically:
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import {InterpolatedPortableText} from 'pte-interpolation-react'
|
|
27
|
+
|
|
28
|
+
function EmailPreview({body, recipient}) {
|
|
29
|
+
return (
|
|
30
|
+
<InterpolatedPortableText
|
|
31
|
+
value={body}
|
|
32
|
+
interpolationValues={{
|
|
33
|
+
firstName: recipient.firstName,
|
|
34
|
+
lastName: recipient.lastName,
|
|
35
|
+
email: recipient.email,
|
|
36
|
+
}}
|
|
37
|
+
/>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Variables render as `<span data-variable-key="firstName">Jo</span>`, making them easy to target with CSS for styling or highlighting.
|
|
43
|
+
|
|
44
|
+
### With custom Portable Text components
|
|
45
|
+
|
|
46
|
+
Pass your own `components` prop and they will be merged with the interpolation types:
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
<InterpolatedPortableText
|
|
50
|
+
value={body}
|
|
51
|
+
interpolationValues={values}
|
|
52
|
+
components={{
|
|
53
|
+
marks: {
|
|
54
|
+
link: ({children, value}) => <a href={value.href}>{children}</a>,
|
|
55
|
+
},
|
|
56
|
+
}}
|
|
57
|
+
/>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Custom fallback for missing values
|
|
61
|
+
|
|
62
|
+
By default, unresolved variables render as `{variableKey}` (e.g. `{firstName}`). Provide a `fallback` function to customise this:
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
<InterpolatedPortableText
|
|
66
|
+
value={body}
|
|
67
|
+
interpolationValues={values}
|
|
68
|
+
fallback={(variableKey) => `[missing: ${variableKey}]`}
|
|
69
|
+
/>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Low-level API
|
|
73
|
+
|
|
74
|
+
If you need full control over component merging, use `createInterpolationComponents` directly with `<PortableText>` from `@portabletext/react`:
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import {useMemo} from 'react'
|
|
78
|
+
import {PortableText} from '@portabletext/react'
|
|
79
|
+
import {createInterpolationComponents} from 'pte-interpolation-react'
|
|
80
|
+
|
|
81
|
+
function EmailPreview({body, values, customComponents}) {
|
|
82
|
+
const components = useMemo(() => {
|
|
83
|
+
const interpolation = createInterpolationComponents(values)
|
|
84
|
+
return {
|
|
85
|
+
...customComponents,
|
|
86
|
+
types: {
|
|
87
|
+
...customComponents.types,
|
|
88
|
+
...interpolation.types,
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
}, [values, customComponents])
|
|
92
|
+
|
|
93
|
+
return <PortableText value={body} components={components} />
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Authoring Variables in Sanity Studio
|
|
98
|
+
|
|
99
|
+
This package handles the **rendering** side. To add the variable picker to Sanity Studio's Portable Text Editor, use [`sanity-plugin-pte-interpolation`](https://www.npmjs.com/package/sanity-plugin-pte-interpolation):
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import {interpolationVariables} from 'sanity-plugin-pte-interpolation'
|
|
103
|
+
|
|
104
|
+
defineField({
|
|
105
|
+
name: 'body',
|
|
106
|
+
type: 'array',
|
|
107
|
+
of: [
|
|
108
|
+
interpolationVariables([
|
|
109
|
+
{id: 'firstName', name: 'First name'},
|
|
110
|
+
{id: 'email', name: 'Email address'},
|
|
111
|
+
]),
|
|
112
|
+
],
|
|
113
|
+
})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Data Shape
|
|
117
|
+
|
|
118
|
+
Variable blocks in stored Portable Text look like this:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"_type": "block",
|
|
123
|
+
"children": [
|
|
124
|
+
{"_type": "span", "text": "Hello, "},
|
|
125
|
+
{"_type": "pteInterpolationVariable", "variableKey": "firstName"},
|
|
126
|
+
{"_type": "span", "text": "!"}
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The `variableKey` maps to the `id` defined in the Studio variable definitions and the keys in the `interpolationValues` record.
|
|
132
|
+
|
|
133
|
+
## API Reference
|
|
134
|
+
|
|
135
|
+
### `<InterpolatedPortableText>`
|
|
136
|
+
|
|
137
|
+
| Prop | Type | Description |
|
|
138
|
+
| --------------------- | --------------------------------- | -------------------------------------------------------------------------- |
|
|
139
|
+
| `value` | `PortableTextBlock[]` | Portable Text content from Sanity |
|
|
140
|
+
| `interpolationValues` | `Record<string, string>` | Map of variable IDs to their resolved values |
|
|
141
|
+
| `components` | `PortableTextComponents` | Optional custom Portable Text components (merged with interpolation types) |
|
|
142
|
+
| `fallback` | `(variableKey: string) => string` | Optional function for unresolved variables (defaults to `{variableKey}`) |
|
|
143
|
+
|
|
144
|
+
Also accepts all other props from `@portabletext/react`'s `<PortableText>`.
|
|
145
|
+
|
|
146
|
+
### `createInterpolationComponents(values, fallback?)`
|
|
147
|
+
|
|
148
|
+
Returns a `PortableTextComponents` object with a `types.pteInterpolationVariable` renderer. Use this when you need manual control over component merging.
|
|
149
|
+
|
|
150
|
+
| Parameter | Type | Description |
|
|
151
|
+
| ---------- | --------------------------------- | ------------------------------------------ |
|
|
152
|
+
| `values` | `Record<string, string>` | Map of variable IDs to values |
|
|
153
|
+
| `fallback` | `(variableKey: string) => string` | Optional fallback for unresolved variables |
|
|
154
|
+
|
|
155
|
+
### `VARIABLE_TYPE_PREFIX`
|
|
156
|
+
|
|
157
|
+
The constant `'pteInterpolationVariable'` — the `_type` string used for variable inline blocks. Exported for advanced use cases.
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -1,8 +1,40 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: !0 });
|
|
3
|
-
var jsxRuntime = require("react/jsx-runtime");
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
var jsxRuntime = require("react/jsx-runtime"), react$1 = require("@portabletext/react"), react = require("react");
|
|
4
|
+
const VARIABLE_TYPE_PREFIX = "pteInterpolationVariable";
|
|
5
|
+
function defaultFallback(variableKey) {
|
|
6
|
+
return `{${variableKey}}`;
|
|
6
7
|
}
|
|
7
|
-
|
|
8
|
+
function createInterpolationComponents(values, fallback = defaultFallback) {
|
|
9
|
+
function VariableComponent(props) {
|
|
10
|
+
const { variableKey } = props.value, resolvedValue = values[variableKey] !== void 0 ? values[variableKey] : fallback(variableKey);
|
|
11
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { "data-variable-key": variableKey, children: resolvedValue });
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
types: {
|
|
15
|
+
[VARIABLE_TYPE_PREFIX]: VariableComponent
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function InterpolatedPortableText({
|
|
20
|
+
interpolationValues,
|
|
21
|
+
components: userComponents,
|
|
22
|
+
fallback,
|
|
23
|
+
...rest
|
|
24
|
+
}) {
|
|
25
|
+
const mergedComponents = react.useMemo(() => {
|
|
26
|
+
const interpolationComponents = createInterpolationComponents(interpolationValues, fallback);
|
|
27
|
+
return userComponents ? {
|
|
28
|
+
...userComponents,
|
|
29
|
+
types: {
|
|
30
|
+
...userComponents.types,
|
|
31
|
+
...interpolationComponents.types
|
|
32
|
+
}
|
|
33
|
+
} : interpolationComponents;
|
|
34
|
+
}, [interpolationValues, fallback, userComponents]);
|
|
35
|
+
return /* @__PURE__ */ jsxRuntime.jsx(react$1.PortableText, { ...rest, components: mergedComponents });
|
|
36
|
+
}
|
|
37
|
+
exports.InterpolatedPortableText = InterpolatedPortableText;
|
|
38
|
+
exports.VARIABLE_TYPE_PREFIX = VARIABLE_TYPE_PREFIX;
|
|
39
|
+
exports.createInterpolationComponents = createInterpolationComponents;
|
|
8
40
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":["../src/
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/constants.ts","../src/createInterpolationComponents.tsx","../src/InterpolatedPortableText.tsx"],"sourcesContent":["/** @public */\nexport const VARIABLE_TYPE_PREFIX = 'pteInterpolationVariable'\n","import type {PortableTextComponents, PortableTextTypeComponentProps} from '@portabletext/react'\n\nimport {VARIABLE_TYPE_PREFIX} from './constants'\nimport type {\n InterpolationFallback,\n InterpolationValues,\n PteInterpolationVariableBlock,\n} from './types'\n\nfunction defaultFallback(variableKey: string): string {\n return `{${variableKey}}`\n}\n\n/** @public */\nexport function createInterpolationComponents(\n values: InterpolationValues,\n fallback: InterpolationFallback = defaultFallback,\n): PortableTextComponents {\n function VariableComponent(props: PortableTextTypeComponentProps<PteInterpolationVariableBlock>) {\n const {variableKey} = props.value\n const resolvedValue =\n values[variableKey] !== undefined ? values[variableKey] : fallback(variableKey)\n\n return <span data-variable-key={variableKey}>{resolvedValue}</span>\n }\n\n return {\n types: {\n [VARIABLE_TYPE_PREFIX]: VariableComponent,\n },\n }\n}\n","import {PortableText} from '@portabletext/react'\nimport {useMemo} from 'react'\n\nimport {createInterpolationComponents} from './createInterpolationComponents'\nimport type {InterpolatedPortableTextProps} from './types'\n\n/** @public */\nexport function InterpolatedPortableText({\n interpolationValues,\n components: userComponents,\n fallback,\n ...rest\n}: InterpolatedPortableTextProps) {\n const mergedComponents = useMemo(() => {\n const interpolationComponents = createInterpolationComponents(interpolationValues, fallback)\n\n if (!userComponents) {\n return interpolationComponents\n }\n\n return {\n ...userComponents,\n types: {\n ...userComponents.types,\n ...interpolationComponents.types,\n },\n }\n }, [interpolationValues, fallback, userComponents])\n\n return <PortableText {...rest} components={mergedComponents} />\n}\n"],"names":["jsx","useMemo","PortableText"],"mappings":";;;AACO,MAAM,uBAAuB;ACQpC,SAAS,gBAAgB,aAA6B;AACpD,SAAO,IAAI,WAAW;AACxB;AAGO,SAAS,8BACd,QACA,WAAkC,iBACV;AACxB,WAAS,kBAAkB,OAAsE;AAC/F,UAAM,EAAC,YAAA,IAAe,MAAM,OACtB,gBACJ,OAAO,WAAW,MAAM,SAAY,OAAO,WAAW,IAAI,SAAS,WAAW;AAEhF,WAAOA,2BAAAA,IAAC,QAAA,EAAK,qBAAmB,aAAc,UAAA,eAAc;AAAA,EAC9D;AAEA,SAAO;AAAA,IACL,OAAO;AAAA,MACL,CAAC,oBAAoB,GAAG;AAAA,IAAA;AAAA,EAC1B;AAEJ;ACxBO,SAAS,yBAAyB;AAAA,EACvC;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,GAAG;AACL,GAAkC;AAChC,QAAM,mBAAmBC,MAAAA,QAAQ,MAAM;AACrC,UAAM,0BAA0B,8BAA8B,qBAAqB,QAAQ;AAE3F,WAAK,iBAIE;AAAA,MACL,GAAG;AAAA,MACH,OAAO;AAAA,QACL,GAAG,eAAe;AAAA,QAClB,GAAG,wBAAwB;AAAA,MAAA;AAAA,IAC7B,IARO;AAAA,EAUX,GAAG,CAAC,qBAAqB,UAAU,cAAc,CAAC;AAElD,SAAOD,2BAAAA,IAACE,QAAAA,cAAA,EAAc,GAAG,MAAM,YAAY,kBAAkB;AAC/D;;;;"}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,16 +1,45 @@
|
|
|
1
1
|
import {JSX} from 'react/jsx-runtime'
|
|
2
|
-
import type {
|
|
2
|
+
import type {PortableTextComponents} from '@portabletext/react'
|
|
3
|
+
import type {PortableTextProps} from '@portabletext/react'
|
|
3
4
|
|
|
4
5
|
/** @public */
|
|
5
|
-
export declare function
|
|
6
|
+
export declare function createInterpolationComponents(
|
|
7
|
+
values: InterpolationValues,
|
|
8
|
+
fallback?: InterpolationFallback,
|
|
9
|
+
): PortableTextComponents
|
|
6
10
|
|
|
7
11
|
/** @public */
|
|
8
|
-
export declare
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
export declare function InterpolatedPortableText({
|
|
13
|
+
interpolationValues,
|
|
14
|
+
components: userComponents,
|
|
15
|
+
fallback,
|
|
16
|
+
...rest
|
|
17
|
+
}: InterpolatedPortableTextProps): JSX.Element
|
|
18
|
+
|
|
19
|
+
/** @public */
|
|
20
|
+
export declare interface InterpolatedPortableTextProps extends Omit<
|
|
21
|
+
PortableTextProps,
|
|
22
|
+
'components'
|
|
23
|
+
> {
|
|
24
|
+
interpolationValues: InterpolationValues
|
|
25
|
+
components?: PortableTextComponents
|
|
26
|
+
fallback?: InterpolationFallback
|
|
11
27
|
}
|
|
12
28
|
|
|
29
|
+
/** @public */
|
|
30
|
+
export declare type InterpolationFallback = (variableKey: string) => string
|
|
31
|
+
|
|
13
32
|
/** @public */
|
|
14
33
|
export declare type InterpolationValues = Record<string, string>
|
|
15
34
|
|
|
35
|
+
/** @public */
|
|
36
|
+
export declare interface PteInterpolationVariableBlock {
|
|
37
|
+
_type: 'pteInterpolationVariable'
|
|
38
|
+
_key: string
|
|
39
|
+
variableKey: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @public */
|
|
43
|
+
export declare const VARIABLE_TYPE_PREFIX = 'pteInterpolationVariable'
|
|
44
|
+
|
|
16
45
|
export {}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,45 @@
|
|
|
1
1
|
import {JSX} from 'react/jsx-runtime'
|
|
2
|
-
import type {
|
|
2
|
+
import type {PortableTextComponents} from '@portabletext/react'
|
|
3
|
+
import type {PortableTextProps} from '@portabletext/react'
|
|
3
4
|
|
|
4
5
|
/** @public */
|
|
5
|
-
export declare function
|
|
6
|
+
export declare function createInterpolationComponents(
|
|
7
|
+
values: InterpolationValues,
|
|
8
|
+
fallback?: InterpolationFallback,
|
|
9
|
+
): PortableTextComponents
|
|
6
10
|
|
|
7
11
|
/** @public */
|
|
8
|
-
export declare
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
export declare function InterpolatedPortableText({
|
|
13
|
+
interpolationValues,
|
|
14
|
+
components: userComponents,
|
|
15
|
+
fallback,
|
|
16
|
+
...rest
|
|
17
|
+
}: InterpolatedPortableTextProps): JSX.Element
|
|
18
|
+
|
|
19
|
+
/** @public */
|
|
20
|
+
export declare interface InterpolatedPortableTextProps extends Omit<
|
|
21
|
+
PortableTextProps,
|
|
22
|
+
'components'
|
|
23
|
+
> {
|
|
24
|
+
interpolationValues: InterpolationValues
|
|
25
|
+
components?: PortableTextComponents
|
|
26
|
+
fallback?: InterpolationFallback
|
|
11
27
|
}
|
|
12
28
|
|
|
29
|
+
/** @public */
|
|
30
|
+
export declare type InterpolationFallback = (variableKey: string) => string
|
|
31
|
+
|
|
13
32
|
/** @public */
|
|
14
33
|
export declare type InterpolationValues = Record<string, string>
|
|
15
34
|
|
|
35
|
+
/** @public */
|
|
36
|
+
export declare interface PteInterpolationVariableBlock {
|
|
37
|
+
_type: 'pteInterpolationVariable'
|
|
38
|
+
_key: string
|
|
39
|
+
variableKey: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @public */
|
|
43
|
+
export declare const VARIABLE_TYPE_PREFIX = 'pteInterpolationVariable'
|
|
44
|
+
|
|
16
45
|
export {}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,42 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { PortableText } from "@portabletext/react";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
const VARIABLE_TYPE_PREFIX = "pteInterpolationVariable";
|
|
5
|
+
function defaultFallback(variableKey) {
|
|
6
|
+
return `{${variableKey}}`;
|
|
7
|
+
}
|
|
8
|
+
function createInterpolationComponents(values, fallback = defaultFallback) {
|
|
9
|
+
function VariableComponent(props) {
|
|
10
|
+
const { variableKey } = props.value, resolvedValue = values[variableKey] !== void 0 ? values[variableKey] : fallback(variableKey);
|
|
11
|
+
return /* @__PURE__ */ jsx("span", { "data-variable-key": variableKey, children: resolvedValue });
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
types: {
|
|
15
|
+
[VARIABLE_TYPE_PREFIX]: VariableComponent
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function InterpolatedPortableText({
|
|
20
|
+
interpolationValues,
|
|
21
|
+
components: userComponents,
|
|
22
|
+
fallback,
|
|
23
|
+
...rest
|
|
24
|
+
}) {
|
|
25
|
+
const mergedComponents = useMemo(() => {
|
|
26
|
+
const interpolationComponents = createInterpolationComponents(interpolationValues, fallback);
|
|
27
|
+
return userComponents ? {
|
|
28
|
+
...userComponents,
|
|
29
|
+
types: {
|
|
30
|
+
...userComponents.types,
|
|
31
|
+
...interpolationComponents.types
|
|
32
|
+
}
|
|
33
|
+
} : interpolationComponents;
|
|
34
|
+
}, [interpolationValues, fallback, userComponents]);
|
|
35
|
+
return /* @__PURE__ */ jsx(PortableText, { ...rest, components: mergedComponents });
|
|
4
36
|
}
|
|
5
37
|
export {
|
|
6
|
-
|
|
38
|
+
InterpolatedPortableText,
|
|
39
|
+
VARIABLE_TYPE_PREFIX,
|
|
40
|
+
createInterpolationComponents
|
|
7
41
|
};
|
|
8
42
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/constants.ts","../src/createInterpolationComponents.tsx","../src/InterpolatedPortableText.tsx"],"sourcesContent":["/** @public */\nexport const VARIABLE_TYPE_PREFIX = 'pteInterpolationVariable'\n","import type {PortableTextComponents, PortableTextTypeComponentProps} from '@portabletext/react'\n\nimport {VARIABLE_TYPE_PREFIX} from './constants'\nimport type {\n InterpolationFallback,\n InterpolationValues,\n PteInterpolationVariableBlock,\n} from './types'\n\nfunction defaultFallback(variableKey: string): string {\n return `{${variableKey}}`\n}\n\n/** @public */\nexport function createInterpolationComponents(\n values: InterpolationValues,\n fallback: InterpolationFallback = defaultFallback,\n): PortableTextComponents {\n function VariableComponent(props: PortableTextTypeComponentProps<PteInterpolationVariableBlock>) {\n const {variableKey} = props.value\n const resolvedValue =\n values[variableKey] !== undefined ? values[variableKey] : fallback(variableKey)\n\n return <span data-variable-key={variableKey}>{resolvedValue}</span>\n }\n\n return {\n types: {\n [VARIABLE_TYPE_PREFIX]: VariableComponent,\n },\n }\n}\n","import {PortableText} from '@portabletext/react'\nimport {useMemo} from 'react'\n\nimport {createInterpolationComponents} from './createInterpolationComponents'\nimport type {InterpolatedPortableTextProps} from './types'\n\n/** @public */\nexport function InterpolatedPortableText({\n interpolationValues,\n components: userComponents,\n fallback,\n ...rest\n}: InterpolatedPortableTextProps) {\n const mergedComponents = useMemo(() => {\n const interpolationComponents = createInterpolationComponents(interpolationValues, fallback)\n\n if (!userComponents) {\n return interpolationComponents\n }\n\n return {\n ...userComponents,\n types: {\n ...userComponents.types,\n ...interpolationComponents.types,\n },\n }\n }, [interpolationValues, fallback, userComponents])\n\n return <PortableText {...rest} components={mergedComponents} />\n}\n"],"names":[],"mappings":";;;AACO,MAAM,uBAAuB;ACQpC,SAAS,gBAAgB,aAA6B;AACpD,SAAO,IAAI,WAAW;AACxB;AAGO,SAAS,8BACd,QACA,WAAkC,iBACV;AACxB,WAAS,kBAAkB,OAAsE;AAC/F,UAAM,EAAC,YAAA,IAAe,MAAM,OACtB,gBACJ,OAAO,WAAW,MAAM,SAAY,OAAO,WAAW,IAAI,SAAS,WAAW;AAEhF,WAAO,oBAAC,QAAA,EAAK,qBAAmB,aAAc,UAAA,eAAc;AAAA,EAC9D;AAEA,SAAO;AAAA,IACL,OAAO;AAAA,MACL,CAAC,oBAAoB,GAAG;AAAA,IAAA;AAAA,EAC1B;AAEJ;ACxBO,SAAS,yBAAyB;AAAA,EACvC;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,GAAG;AACL,GAAkC;AAChC,QAAM,mBAAmB,QAAQ,MAAM;AACrC,UAAM,0BAA0B,8BAA8B,qBAAqB,QAAQ;AAE3F,WAAK,iBAIE;AAAA,MACL,GAAG;AAAA,MACH,OAAO;AAAA,QACL,GAAG,eAAe;AAAA,QAClB,GAAG,wBAAwB;AAAA,MAAA;AAAA,IAC7B,IARO;AAAA,EAUX,GAAG,CAAC,qBAAqB,UAAU,cAAc,CAAC;AAElD,SAAO,oBAAC,cAAA,EAAc,GAAG,MAAM,YAAY,kBAAkB;AAC/D;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pte-interpolation-react",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "React components for rendering Portable Text with interpolated values",
|
|
@@ -47,6 +47,10 @@
|
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@sanity/pkg-utils": "^10.4.4",
|
|
50
|
+
"@types/react": "^19.2.14",
|
|
51
|
+
"@types/react-dom": "^19.2.3",
|
|
52
|
+
"react": "^19.2.4",
|
|
53
|
+
"react-dom": "^19.2.4",
|
|
50
54
|
"rimraf": "^6.1.3",
|
|
51
55
|
"typescript": "~5.9.3"
|
|
52
56
|
},
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {PortableText} from '@portabletext/react'
|
|
2
|
+
import {useMemo} from 'react'
|
|
3
|
+
|
|
4
|
+
import {createInterpolationComponents} from './createInterpolationComponents'
|
|
5
|
+
import type {InterpolatedPortableTextProps} from './types'
|
|
6
|
+
|
|
7
|
+
/** @public */
|
|
8
|
+
export function InterpolatedPortableText({
|
|
9
|
+
interpolationValues,
|
|
10
|
+
components: userComponents,
|
|
11
|
+
fallback,
|
|
12
|
+
...rest
|
|
13
|
+
}: InterpolatedPortableTextProps) {
|
|
14
|
+
const mergedComponents = useMemo(() => {
|
|
15
|
+
const interpolationComponents = createInterpolationComponents(interpolationValues, fallback)
|
|
16
|
+
|
|
17
|
+
if (!userComponents) {
|
|
18
|
+
return interpolationComponents
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
...userComponents,
|
|
23
|
+
types: {
|
|
24
|
+
...userComponents.types,
|
|
25
|
+
...interpolationComponents.types,
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
}, [interpolationValues, fallback, userComponents])
|
|
29
|
+
|
|
30
|
+
return <PortableText {...rest} components={mergedComponents} />
|
|
31
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type {PortableTextComponents} from '@portabletext/react'
|
|
2
|
+
import {renderToStaticMarkup} from 'react-dom/server'
|
|
3
|
+
import {describe, expect, it} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {VARIABLE_TYPE_PREFIX} from '../constants'
|
|
6
|
+
import {InterpolatedPortableText} from '../InterpolatedPortableText'
|
|
7
|
+
import {
|
|
8
|
+
emptyBlocksContent,
|
|
9
|
+
multiBlockContent,
|
|
10
|
+
multipleVariablesBlock,
|
|
11
|
+
plainTextBlock,
|
|
12
|
+
singleVariableBlock,
|
|
13
|
+
styledTextWithVariableBlock,
|
|
14
|
+
} from './fixtures'
|
|
15
|
+
|
|
16
|
+
describe('InterpolatedPortableText', () => {
|
|
17
|
+
it('renders single variable resolved inline', () => {
|
|
18
|
+
const html = renderToStaticMarkup(
|
|
19
|
+
<InterpolatedPortableText
|
|
20
|
+
value={singleVariableBlock}
|
|
21
|
+
interpolationValues={{firstName: 'Jordan'}}
|
|
22
|
+
/>,
|
|
23
|
+
)
|
|
24
|
+
expect(html).toContain('Hello, ')
|
|
25
|
+
expect(html).toContain('<span data-variable-key="firstName">Jordan</span>')
|
|
26
|
+
expect(html).toContain('!')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('renders multiple variables in one block', () => {
|
|
30
|
+
const html = renderToStaticMarkup(
|
|
31
|
+
<InterpolatedPortableText
|
|
32
|
+
value={multipleVariablesBlock}
|
|
33
|
+
interpolationValues={{
|
|
34
|
+
firstName: 'Jordan',
|
|
35
|
+
lastName: 'Lawrence',
|
|
36
|
+
email: 'jordan@example.com',
|
|
37
|
+
}}
|
|
38
|
+
/>,
|
|
39
|
+
)
|
|
40
|
+
expect(html).toContain('<span data-variable-key="firstName">Jordan</span>')
|
|
41
|
+
expect(html).toContain('<span data-variable-key="lastName">Lawrence</span>')
|
|
42
|
+
expect(html).toContain('<span data-variable-key="email">jordan@example.com</span>')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('renders plain text unchanged', () => {
|
|
46
|
+
const html = renderToStaticMarkup(
|
|
47
|
+
<InterpolatedPortableText value={plainTextBlock} interpolationValues={{}} />,
|
|
48
|
+
)
|
|
49
|
+
expect(html).toContain('No variables here.')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('renders fallback for missing values', () => {
|
|
53
|
+
const html = renderToStaticMarkup(
|
|
54
|
+
<InterpolatedPortableText value={singleVariableBlock} interpolationValues={{}} />,
|
|
55
|
+
)
|
|
56
|
+
expect(html).toContain('{firstName}')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('uses custom fallback prop', () => {
|
|
60
|
+
const html = renderToStaticMarkup(
|
|
61
|
+
<InterpolatedPortableText
|
|
62
|
+
value={singleVariableBlock}
|
|
63
|
+
interpolationValues={{}}
|
|
64
|
+
fallback={(key) => `[${key}]`}
|
|
65
|
+
/>,
|
|
66
|
+
)
|
|
67
|
+
expect(html).toContain('[firstName]')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('preserves user components alongside interpolation types', () => {
|
|
71
|
+
const userComponents: PortableTextComponents = {
|
|
72
|
+
block: {
|
|
73
|
+
normal: ({children}) => <div data-testid="custom-block">{children}</div>,
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const html = renderToStaticMarkup(
|
|
78
|
+
<InterpolatedPortableText
|
|
79
|
+
value={singleVariableBlock}
|
|
80
|
+
interpolationValues={{firstName: 'Jordan'}}
|
|
81
|
+
components={userComponents}
|
|
82
|
+
/>,
|
|
83
|
+
)
|
|
84
|
+
expect(html).toContain('data-testid="custom-block"')
|
|
85
|
+
expect(html).toContain('<span data-variable-key="firstName">Jordan</span>')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('renders variables across multiple blocks', () => {
|
|
89
|
+
const html = renderToStaticMarkup(
|
|
90
|
+
<InterpolatedPortableText
|
|
91
|
+
value={multiBlockContent}
|
|
92
|
+
interpolationValues={{
|
|
93
|
+
firstName: 'Jordan',
|
|
94
|
+
email: 'jordan@example.com',
|
|
95
|
+
}}
|
|
96
|
+
/>,
|
|
97
|
+
)
|
|
98
|
+
expect(html).toContain('<span data-variable-key="firstName">Jordan</span>')
|
|
99
|
+
expect(html).toContain('<span data-variable-key="email">jordan@example.com</span>')
|
|
100
|
+
expect(html).toContain('Dear ')
|
|
101
|
+
expect(html).toContain('Your email is ')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('interpolation types take precedence over user types with same key', () => {
|
|
105
|
+
const userComponents: PortableTextComponents = {
|
|
106
|
+
types: {
|
|
107
|
+
[VARIABLE_TYPE_PREFIX]: () => <span>user-override</span>,
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const html = renderToStaticMarkup(
|
|
112
|
+
<InterpolatedPortableText
|
|
113
|
+
value={singleVariableBlock}
|
|
114
|
+
interpolationValues={{firstName: 'Jordan'}}
|
|
115
|
+
components={userComponents}
|
|
116
|
+
/>,
|
|
117
|
+
)
|
|
118
|
+
expect(html).not.toContain('user-override')
|
|
119
|
+
expect(html).toContain('<span data-variable-key="firstName">Jordan</span>')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('renders user custom types alongside interpolation types', () => {
|
|
123
|
+
const customWidgetBlock = [
|
|
124
|
+
{
|
|
125
|
+
_type: 'block',
|
|
126
|
+
_key: 'block-1',
|
|
127
|
+
style: 'normal' as const,
|
|
128
|
+
markDefs: [],
|
|
129
|
+
children: [
|
|
130
|
+
{_type: 'span', _key: 'span-1', text: 'Hello ', marks: []},
|
|
131
|
+
{_type: 'pteInterpolationVariable', _key: 'var-1', variableKey: 'firstName'},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
_type: 'customWidget',
|
|
136
|
+
_key: 'widget-1',
|
|
137
|
+
label: 'Click me',
|
|
138
|
+
},
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
const userComponents: PortableTextComponents = {
|
|
142
|
+
types: {
|
|
143
|
+
customWidget: ({value}: {value: {label: string}}) => (
|
|
144
|
+
<button data-testid="widget">{value.label}</button>
|
|
145
|
+
),
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const html = renderToStaticMarkup(
|
|
150
|
+
<InterpolatedPortableText
|
|
151
|
+
value={customWidgetBlock}
|
|
152
|
+
interpolationValues={{firstName: 'Jordan'}}
|
|
153
|
+
components={userComponents}
|
|
154
|
+
/>,
|
|
155
|
+
)
|
|
156
|
+
expect(html).toContain('data-testid="widget"')
|
|
157
|
+
expect(html).toContain('Click me')
|
|
158
|
+
expect(html).toContain('<span data-variable-key="firstName">Jordan</span>')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('preserves user mark components', () => {
|
|
162
|
+
const userComponents: PortableTextComponents = {
|
|
163
|
+
marks: {
|
|
164
|
+
strong: ({children}) => <strong data-testid="custom-strong">{children}</strong>,
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const html = renderToStaticMarkup(
|
|
169
|
+
<InterpolatedPortableText
|
|
170
|
+
value={styledTextWithVariableBlock}
|
|
171
|
+
interpolationValues={{firstName: 'Jordan'}}
|
|
172
|
+
components={userComponents}
|
|
173
|
+
/>,
|
|
174
|
+
)
|
|
175
|
+
expect(html).toContain('data-testid="custom-strong"')
|
|
176
|
+
expect(html).toContain('<span data-variable-key="firstName">Jordan</span>')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('renders without errors when value array is empty', () => {
|
|
180
|
+
const html = renderToStaticMarkup(
|
|
181
|
+
<InterpolatedPortableText value={emptyBlocksContent} interpolationValues={{}} />,
|
|
182
|
+
)
|
|
183
|
+
expect(html).toBe('')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('default fallback renders exactly {variableKey} format', () => {
|
|
187
|
+
const html = renderToStaticMarkup(
|
|
188
|
+
<InterpolatedPortableText value={singleVariableBlock} interpolationValues={{}} />,
|
|
189
|
+
)
|
|
190
|
+
expect(html).toContain('<span data-variable-key="firstName">{firstName}</span>')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('renders styled text marks alongside variables', () => {
|
|
194
|
+
const html = renderToStaticMarkup(
|
|
195
|
+
<InterpolatedPortableText
|
|
196
|
+
value={styledTextWithVariableBlock}
|
|
197
|
+
interpolationValues={{firstName: 'Jordan'}}
|
|
198
|
+
/>,
|
|
199
|
+
)
|
|
200
|
+
expect(html).toContain('<strong>Welcome </strong>')
|
|
201
|
+
expect(html).toContain('<span data-variable-key="firstName">Jordan</span>')
|
|
202
|
+
expect(html).toContain('<em> aboard</em>')
|
|
203
|
+
})
|
|
204
|
+
})
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {PortableText} from '@portabletext/react'
|
|
2
|
+
import {renderToStaticMarkup} from 'react-dom/server'
|
|
3
|
+
import {describe, expect, it} from 'vitest'
|
|
4
|
+
|
|
5
|
+
import {VARIABLE_TYPE_PREFIX} from '../constants'
|
|
6
|
+
import {createInterpolationComponents} from '../createInterpolationComponents'
|
|
7
|
+
import {
|
|
8
|
+
consecutiveVariablesBlock,
|
|
9
|
+
multipleVariablesBlock,
|
|
10
|
+
singleVariableBlock,
|
|
11
|
+
variableOnlyBlock,
|
|
12
|
+
} from './fixtures'
|
|
13
|
+
|
|
14
|
+
describe('createInterpolationComponents', () => {
|
|
15
|
+
it('returns object with types.pteInterpolationVariable component', () => {
|
|
16
|
+
const components = createInterpolationComponents({firstName: 'Jordan'})
|
|
17
|
+
expect(components.types).toBeDefined()
|
|
18
|
+
expect(components.types).toHaveProperty('pteInterpolationVariable')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('resolves variable value from the values map', () => {
|
|
22
|
+
const components = createInterpolationComponents({firstName: 'Jordan'})
|
|
23
|
+
const html = renderToStaticMarkup(
|
|
24
|
+
<PortableText value={singleVariableBlock} components={components} />,
|
|
25
|
+
)
|
|
26
|
+
expect(html).toContain('Jordan')
|
|
27
|
+
expect(html).toContain('data-variable-key="firstName"')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('renders {variableKey} fallback for missing values', () => {
|
|
31
|
+
const components = createInterpolationComponents({})
|
|
32
|
+
const html = renderToStaticMarkup(
|
|
33
|
+
<PortableText value={singleVariableBlock} components={components} />,
|
|
34
|
+
)
|
|
35
|
+
expect(html).toContain('{firstName}')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('renders empty string when value is explicitly ""', () => {
|
|
39
|
+
const components = createInterpolationComponents({firstName: ''})
|
|
40
|
+
const html = renderToStaticMarkup(
|
|
41
|
+
<PortableText value={singleVariableBlock} components={components} />,
|
|
42
|
+
)
|
|
43
|
+
expect(html).toContain('<span data-variable-key="firstName"></span>')
|
|
44
|
+
expect(html).not.toContain('{firstName}')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('calls custom fallback function for missing values', () => {
|
|
48
|
+
const customFallback = (variableKey: string) => `[MISSING: ${variableKey}]`
|
|
49
|
+
const components = createInterpolationComponents({}, customFallback)
|
|
50
|
+
const html = renderToStaticMarkup(
|
|
51
|
+
<PortableText value={singleVariableBlock} components={components} />,
|
|
52
|
+
)
|
|
53
|
+
expect(html).toContain('[MISSING: firstName]')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('renders inside <span> with data-variable-key attribute', () => {
|
|
57
|
+
const components = createInterpolationComponents({
|
|
58
|
+
firstName: 'Jordan',
|
|
59
|
+
lastName: 'Lawrence',
|
|
60
|
+
email: 'jordan@example.com',
|
|
61
|
+
})
|
|
62
|
+
const html = renderToStaticMarkup(
|
|
63
|
+
<PortableText value={multipleVariablesBlock} components={components} />,
|
|
64
|
+
)
|
|
65
|
+
expect(html).toContain('<span data-variable-key="firstName">Jordan</span>')
|
|
66
|
+
expect(html).toContain('<span data-variable-key="lastName">Lawrence</span>')
|
|
67
|
+
expect(html).toContain('<span data-variable-key="email">jordan@example.com</span>')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('returns components with only a types key', () => {
|
|
71
|
+
const components = createInterpolationComponents({firstName: 'Jordan'})
|
|
72
|
+
expect(Object.keys(components)).toEqual(['types'])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('registers component under VARIABLE_TYPE_PREFIX key', () => {
|
|
76
|
+
const components = createInterpolationComponents({firstName: 'Jordan'})
|
|
77
|
+
expect(Object.keys(components.types!)).toEqual([VARIABLE_TYPE_PREFIX])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('falls back for all variables when values map is empty', () => {
|
|
81
|
+
const components = createInterpolationComponents({})
|
|
82
|
+
const html = renderToStaticMarkup(
|
|
83
|
+
<PortableText value={multipleVariablesBlock} components={components} />,
|
|
84
|
+
)
|
|
85
|
+
expect(html).toContain('{firstName}')
|
|
86
|
+
expect(html).toContain('{lastName}')
|
|
87
|
+
expect(html).toContain('{email}')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('escapes HTML special characters in values', () => {
|
|
91
|
+
const components = createInterpolationComponents({firstName: '<script>alert("xss")</script>'})
|
|
92
|
+
const html = renderToStaticMarkup(
|
|
93
|
+
<PortableText value={singleVariableBlock} components={components} />,
|
|
94
|
+
)
|
|
95
|
+
expect(html).not.toContain('<script>')
|
|
96
|
+
expect(html).toContain('<script>')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('renders consecutive variables without interference', () => {
|
|
100
|
+
const components = createInterpolationComponents({firstName: 'Jordan', lastName: 'Lawrence'})
|
|
101
|
+
const html = renderToStaticMarkup(
|
|
102
|
+
<PortableText value={consecutiveVariablesBlock} components={components} />,
|
|
103
|
+
)
|
|
104
|
+
expect(html).toContain('<span data-variable-key="firstName">Jordan</span>')
|
|
105
|
+
expect(html).toContain('<span data-variable-key="lastName">Lawrence</span>')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('renders a variable as the only child in a block', () => {
|
|
109
|
+
const components = createInterpolationComponents({firstName: 'Jordan'})
|
|
110
|
+
const html = renderToStaticMarkup(
|
|
111
|
+
<PortableText value={variableOnlyBlock} components={components} />,
|
|
112
|
+
)
|
|
113
|
+
expect(html).toContain('<span data-variable-key="firstName">Jordan</span>')
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type {PortableTextBlock} from '@portabletext/react'
|
|
2
|
+
|
|
3
|
+
export const singleVariableBlock: PortableTextBlock[] = [
|
|
4
|
+
{
|
|
5
|
+
_type: 'block',
|
|
6
|
+
_key: 'block-1',
|
|
7
|
+
style: 'normal',
|
|
8
|
+
markDefs: [],
|
|
9
|
+
children: [
|
|
10
|
+
{_type: 'span', _key: 'span-1', text: 'Hello, ', marks: []},
|
|
11
|
+
{_type: 'pteInterpolationVariable', _key: 'var-1', variableKey: 'firstName'},
|
|
12
|
+
{_type: 'span', _key: 'span-2', text: '!', marks: []},
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
export const multipleVariablesBlock: PortableTextBlock[] = [
|
|
18
|
+
{
|
|
19
|
+
_type: 'block',
|
|
20
|
+
_key: 'block-1',
|
|
21
|
+
style: 'normal',
|
|
22
|
+
markDefs: [],
|
|
23
|
+
children: [
|
|
24
|
+
{_type: 'span', _key: 'span-1', text: 'Name: ', marks: []},
|
|
25
|
+
{_type: 'pteInterpolationVariable', _key: 'var-1', variableKey: 'firstName'},
|
|
26
|
+
{_type: 'span', _key: 'span-2', text: ' ', marks: []},
|
|
27
|
+
{_type: 'pteInterpolationVariable', _key: 'var-2', variableKey: 'lastName'},
|
|
28
|
+
{_type: 'span', _key: 'span-3', text: ', Email: ', marks: []},
|
|
29
|
+
{_type: 'pteInterpolationVariable', _key: 'var-3', variableKey: 'email'},
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
export const plainTextBlock: PortableTextBlock[] = [
|
|
35
|
+
{
|
|
36
|
+
_type: 'block',
|
|
37
|
+
_key: 'block-1',
|
|
38
|
+
style: 'normal',
|
|
39
|
+
markDefs: [],
|
|
40
|
+
children: [{_type: 'span', _key: 'span-1', text: 'No variables here.', marks: []}],
|
|
41
|
+
},
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
export const variableOnlyBlock: PortableTextBlock[] = [
|
|
45
|
+
{
|
|
46
|
+
_type: 'block',
|
|
47
|
+
_key: 'block-1',
|
|
48
|
+
style: 'normal',
|
|
49
|
+
markDefs: [],
|
|
50
|
+
children: [{_type: 'pteInterpolationVariable', _key: 'var-1', variableKey: 'firstName'}],
|
|
51
|
+
},
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
export const consecutiveVariablesBlock: PortableTextBlock[] = [
|
|
55
|
+
{
|
|
56
|
+
_type: 'block',
|
|
57
|
+
_key: 'block-1',
|
|
58
|
+
style: 'normal',
|
|
59
|
+
markDefs: [],
|
|
60
|
+
children: [
|
|
61
|
+
{_type: 'pteInterpolationVariable', _key: 'var-1', variableKey: 'firstName'},
|
|
62
|
+
{_type: 'pteInterpolationVariable', _key: 'var-2', variableKey: 'lastName'},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
export const styledTextWithVariableBlock: PortableTextBlock[] = [
|
|
68
|
+
{
|
|
69
|
+
_type: 'block',
|
|
70
|
+
_key: 'block-1',
|
|
71
|
+
style: 'normal',
|
|
72
|
+
markDefs: [],
|
|
73
|
+
children: [
|
|
74
|
+
{_type: 'span', _key: 'span-1', text: 'Welcome ', marks: ['strong']},
|
|
75
|
+
{_type: 'pteInterpolationVariable', _key: 'var-1', variableKey: 'firstName'},
|
|
76
|
+
{_type: 'span', _key: 'span-2', text: ' aboard', marks: ['em']},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
export const emptyBlocksContent: PortableTextBlock[] = []
|
|
82
|
+
|
|
83
|
+
export const multiBlockContent: PortableTextBlock[] = [
|
|
84
|
+
{
|
|
85
|
+
_type: 'block',
|
|
86
|
+
_key: 'block-1',
|
|
87
|
+
style: 'normal',
|
|
88
|
+
markDefs: [],
|
|
89
|
+
children: [
|
|
90
|
+
{_type: 'span', _key: 'span-1', text: 'Dear ', marks: []},
|
|
91
|
+
{_type: 'pteInterpolationVariable', _key: 'var-1', variableKey: 'firstName'},
|
|
92
|
+
{_type: 'span', _key: 'span-2', text: ',', marks: []},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
_type: 'block',
|
|
97
|
+
_key: 'block-2',
|
|
98
|
+
style: 'normal',
|
|
99
|
+
markDefs: [],
|
|
100
|
+
children: [
|
|
101
|
+
{_type: 'span', _key: 'span-3', text: 'Your email is ', marks: []},
|
|
102
|
+
{_type: 'pteInterpolationVariable', _key: 'var-2', variableKey: 'email'},
|
|
103
|
+
{_type: 'span', _key: 'span-4', text: '.', marks: []},
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
]
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type {PortableTextComponents, PortableTextTypeComponentProps} from '@portabletext/react'
|
|
2
|
+
|
|
3
|
+
import {VARIABLE_TYPE_PREFIX} from './constants'
|
|
4
|
+
import type {
|
|
5
|
+
InterpolationFallback,
|
|
6
|
+
InterpolationValues,
|
|
7
|
+
PteInterpolationVariableBlock,
|
|
8
|
+
} from './types'
|
|
9
|
+
|
|
10
|
+
function defaultFallback(variableKey: string): string {
|
|
11
|
+
return `{${variableKey}}`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** @public */
|
|
15
|
+
export function createInterpolationComponents(
|
|
16
|
+
values: InterpolationValues,
|
|
17
|
+
fallback: InterpolationFallback = defaultFallback,
|
|
18
|
+
): PortableTextComponents {
|
|
19
|
+
function VariableComponent(props: PortableTextTypeComponentProps<PteInterpolationVariableBlock>) {
|
|
20
|
+
const {variableKey} = props.value
|
|
21
|
+
const resolvedValue =
|
|
22
|
+
values[variableKey] !== undefined ? values[variableKey] : fallback(variableKey)
|
|
23
|
+
|
|
24
|
+
return <span data-variable-key={variableKey}>{resolvedValue}</span>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
types: {
|
|
29
|
+
[VARIABLE_TYPE_PREFIX]: VariableComponent,
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export
|
|
1
|
+
export {InterpolatedPortableText} from './InterpolatedPortableText'
|
|
2
|
+
export {createInterpolationComponents} from './createInterpolationComponents'
|
|
3
|
+
export {VARIABLE_TYPE_PREFIX} from './constants'
|
|
4
|
+
export type {
|
|
5
|
+
InterpolatedPortableTextProps,
|
|
6
|
+
InterpolationFallback,
|
|
7
|
+
InterpolationValues,
|
|
8
|
+
PteInterpolationVariableBlock,
|
|
9
|
+
} from './types'
|
package/src/types.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {PortableTextComponents, PortableTextProps} from '@portabletext/react'
|
|
2
2
|
|
|
3
3
|
/** @public */
|
|
4
4
|
export type InterpolationValues = Record<string, string>
|
|
5
5
|
|
|
6
6
|
/** @public */
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
export type InterpolationFallback = (variableKey: string) => string
|
|
8
|
+
|
|
9
|
+
/** @public */
|
|
10
|
+
export interface PteInterpolationVariableBlock {
|
|
11
|
+
_type: 'pteInterpolationVariable'
|
|
12
|
+
_key: string
|
|
13
|
+
variableKey: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** @public */
|
|
17
|
+
export interface InterpolatedPortableTextProps extends Omit<PortableTextProps, 'components'> {
|
|
18
|
+
interpolationValues: InterpolationValues
|
|
19
|
+
components?: PortableTextComponents
|
|
20
|
+
fallback?: InterpolationFallback
|
|
10
21
|
}
|
package/src/InterpolatedText.tsx
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import {describe, expect, it} from 'vitest'
|
|
2
|
-
import {InterpolatedText} from '../InterpolatedText'
|
|
3
|
-
|
|
4
|
-
describe('InterpolatedText', () => {
|
|
5
|
-
it('should be defined', () => {
|
|
6
|
-
expect(InterpolatedText).toBeDefined()
|
|
7
|
-
expect(typeof InterpolatedText).toBe('function')
|
|
8
|
-
})
|
|
9
|
-
})
|