Skip to content
This repository was archived by the owner on Aug 26, 2021. It is now read-only.

Commit 0b772a4

Browse files
committed
v1 (the first and last version!)
1 parent 955596f commit 0b772a4

File tree

5 files changed

+337
-45
lines changed

5 files changed

+337
-45
lines changed

README.md

+148-34
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,7 @@ Hide or show a Sanity.io field based on a custom condition set by you.
44

55
---
66

7-
🚨 **Warning:** I stopped working on this plugin before it was done as the Sanity team has voiced they're currently working on a native solution.
8-
9-
This can still be useful if you have basic use cases for conditionals, but it [doesn't work well on arrays](https://github.com/hdoro/sanity-plugin-conditional-field/issues/2), has [issues with validation markers](https://github.com/hdoro/sanity-plugin-conditional-field/issues/1) and is visually a bit buggy.
10-
11-
If in the meantime you _must_ rely on conditional fields, [reach me out in Sanity's community Slack](https://sanity-io-land.slack.com/team/UB1QTEXGC) and I'll try to help you out :)
7+
🚨 **Warning:** the Sanity team has voiced they're currently working on a native solution. The goal of this plugin is to become obsolete.
128

139
---
1410

@@ -22,7 +18,9 @@ sanity install conditional-field
2218
yarn install conditional-field
2319
```
2420

25-
Then, you can use the ConditionalField component as the `inputComponent` of whatever field you want to conditionally render:
21+
## Usage
22+
23+
You can use the ConditionalField component as the `inputComponent` of whatever field you want to conditionally render:
2624

2725
```js
2826
import ConditionalField from 'sanity-plugin-conditional-field'
@@ -36,45 +34,161 @@ export default {
3634
name: 'internal',
3735
title: 'Is this article internal?',
3836
type: 'boolean',
39-
validation: Rule => Rule.required(),
40-
},
41-
{
42-
name: 'content',
43-
title: 'Content',
44-
type: 'array',
45-
of: [
46-
{type: 'block'}
47-
]
48-
inputComponent: ConditionalField,
49-
options: {
50-
condition: document => document.internal === true
51-
}
37+
validation: (Rule) => Rule.required(),
5238
},
5339
{
5440
name: 'externalUrl',
55-
title: 'URL to the content',
5641
type: 'url',
5742
inputComponent: ConditionalField,
5843
options: {
59-
condition: document => !document.internal
60-
}
44+
// Simple conditional based on top-level document value
45+
hide: ({ document }) => document.internal, // hide if internal article
46+
},
6147
},
6248
],
6349
}
6450
```
6551

66-
🚨 **Big red alert**: this plugin simply _hides_ fields if conditions aren't met. It doesn't interfere with validation, meaning that if you set a conditioned field as required, editors won't be able to publish documents when it's hidden.
52+
### Async conditions
53+
54+
```js
55+
{
56+
name: 'content',
57+
title: 'Content',
58+
type: 'array',
59+
of: [
60+
{type: 'block'},
61+
]
62+
inputComponent: ConditionalField,
63+
options: {
64+
// Asynchronous conditions
65+
hide: async ({ document }) => {
66+
if (document.internal) {
67+
return true
68+
}
69+
70+
const isValidContent = await fetch(`/api/is-valid-content/${document.externalUrl}`)
71+
return isValidContent ?
72+
}
73+
}
74+
}
75+
```
76+
77+
### Nested conditions
78+
79+
Besides the current `document`, the `hide` function receives a `parents` array for accessing contextual data.
80+
81+
It's ordered from closest parents to furthest, meaning `parents[0]` will always be the object the field is in, and `parents[-1]` will always be the full document. If the field is at the top-level of the document, `parents[0] === document`.
82+
83+
Here's an example of it in practice - notice how the `link` object is nested under an array, which means `parents[1]` will return the array with all the links:
84+
85+
```js
86+
{
87+
name: 'links',
88+
title: 'Links',
89+
type: 'array',
90+
of: [
91+
{
92+
name: 'link',
93+
title: 'Link',
94+
type: 'object',
95+
fields: [
96+
{
97+
name: 'external',
98+
title: "Links to external websites?",
99+
type: 'boolean',
100+
},
101+
{
102+
name: 'url',
103+
title: 'External URL',
104+
type: 'string',
105+
inputComponent: ConditionalField,
106+
options: {
107+
hide: ({ parents }) => {
108+
// Parents array exposes the closest parents in order
109+
// Hence, parents[0] is the current object's value
110+
return parents[0].external
111+
},
112+
},
113+
},
114+
{
115+
name: 'internalLink',
116+
type: 'reference',
117+
to: [{ type: "page" }],
118+
inputComponent: ConditionalField,
119+
options: {
120+
hide: ({ parents }) => !parents[0].external
121+
},
122+
},
123+
{
124+
name: 'flashyLooks',
125+
type: 'boolean',
126+
inputComponent: ConditionalField,
127+
options: {
128+
// Prevent editors from making the link flashy if this link is not in the first position in the array
129+
hide: ({ parents }) => ({
130+
hidden: parents[1]?.indexOf(parents[0]) > 0 || false,
131+
// Clear field's value if hidden - see below
132+
clearOnHidden: true
133+
})
134+
},
135+
},
136+
],
137+
},
138+
],
139+
},
140+
```
141+
142+
### Deleting values if field is hidden
143+
144+
The `hide` function can also return an object to determine whether or not existing values should be cleared when the field is hidden. By default, this plugin won't clear values.
145+
146+
```js
147+
{
148+
name: 'externalUrl',
149+
type: 'url',
150+
inputComponent: ConditionalField,
151+
options: {
152+
hide: ({ document }) => {
153+
hidden: !!document.internal,
154+
// Clear field's value if hidden
155+
clearOnHidden: true
156+
},
157+
},
158+
},
159+
```
160+
161+
### Typescript definitions
67162
68-
Take a look at the roadmap below for an idea on the plugin's shortcomings.
163+
If you use Typescript in your schemas, here's how you type your `hide` functions:
164+
165+
```ts
166+
import { HideOption } from 'sanity-plugin-conditional-field'
167+
168+
const hideBoolean: HideOption = false
169+
const hideFunction: HideOption = ({ document, parents }) => ({
170+
hidden: document._id.includes('drafts.') || parents.length > 2,
171+
})
172+
```
173+
174+
And here's the shape of the `hide` options:
175+
176+
```ts
177+
type ConditionReturn = boolean | { hidden: boolean; clearOnHidden?: boolean }
178+
179+
export type HideFunction = (props: {
180+
document: SanityDocument
181+
parents: Parent[]
182+
}) => ConditionReturn | Promise<ConditionReturn>
183+
184+
export type HideOption = boolean | HideFunction
185+
```
186+
187+
## Shortcomings
188+
189+
🚨 **Big red alert**: this plugin simply _hides_ fields if conditions aren't met. It doesn't interfere with validation, meaning that if you set a conditioned field as required, editors won't be able to publish documents when it's hidden.
69190
70-
## Roadmap
191+
Besides this, the following is true:
71192
72-
- [ ] Prevent the extra whitespace from hidden fields
73-
- [ ] Find a way to facilitate validation
74-
- [ ] Consider adding a `injectConditionals` helper to wrap the `fields` array & automatically use this inputComponent when options.condition is set
75-
- Example: `injectConditionals([ { name: "title", type: "string", options: { condition: () => true } }])`
76-
- [ ] Async conditions
77-
- Would require some debouncing in the execution of the condition function, else it'll fire off too many requests
78-
- Maybe an array of dependencies similar to React.useEffect
79-
- [ ] get merged into `@sanity/base`
80-
- That's right! The goal of this plugin is to become obsolete. It'd be much better if the official type included in Sanity had this behavior from the get-go. Better for users and the platform :)
193+
- Async conditions aren't debounced, meaning they'll be fired _a lot_
194+
- There's no way of using this field with custom inputs

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
22
"name": "sanity-plugin-conditional-field",
3-
"version": "0.0.2",
3+
"version": "1.0.0",
44
"description": "Hide or show a Sanity.io field based on a custom condition set by you.",
55
"main": "lib/ConditionalField.js",
66
"scripts": {
77
"format": "prettier --write .",
88
"clear-lib": "node clearLib.js",
9-
"build": "npm run format && npm run clear-lib && tsc",
9+
"build": "npm run format && npm run clear-lib && tsc --declaration",
1010
"dev": "tsc -w",
1111
"prepublish": "npm run build"
1212
},

src/ConditionalField.tsx

+108-9
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,94 @@
11
import React from 'react'
2+
import { SanityDocument } from '@sanity/client'
23
import {
34
withDocument,
45
withValuePath,
56
FormBuilderInput,
67
} from 'part:@sanity/form-builder'
8+
import { PatchEvent, unset } from 'part:@sanity/form-builder/patch-event'
9+
import HiddenField from './HiddenField'
10+
import getParents, { Parent } from './getParents'
711

8-
class ConditionalField extends React.PureComponent<any> {
12+
type RenderInfo = {
13+
renderField: boolean
14+
clearOnHidden: boolean
15+
}
16+
17+
type ConditionReturn = boolean | { hidden: boolean; clearOnHidden?: boolean }
18+
19+
export type HideFunction = (props: {
20+
document: SanityDocument
21+
parents: Parent[]
22+
}) => ConditionReturn | Promise<ConditionReturn>
23+
24+
export type HideOption = boolean | HideFunction
25+
26+
const DEFAULT_STATE: RenderInfo = {
27+
renderField: true,
28+
clearOnHidden: false,
29+
}
30+
31+
async function parseCondition({
32+
document,
33+
hide,
34+
parents,
35+
}: {
36+
document: SanityDocument
37+
hide?: HideOption
38+
parents: Parent[]
39+
}): Promise<RenderInfo> {
40+
if (typeof hide === 'boolean') {
41+
return {
42+
...DEFAULT_STATE,
43+
renderField: !hide,
44+
}
45+
}
46+
47+
if (!hide || typeof hide !== 'function') {
48+
return DEFAULT_STATE
49+
}
50+
51+
try {
52+
const hideField = await Promise.resolve(hide({ document, parents }))
53+
54+
if (typeof hideField === 'boolean') {
55+
return {
56+
renderField: !hideField,
57+
clearOnHidden: false,
58+
}
59+
}
60+
61+
return {
62+
renderField: !(typeof hideField.hidden === 'boolean'
63+
? hideField.hidden
64+
: // Don't hide by default
65+
false),
66+
clearOnHidden: hideField.clearOnHidden || false,
67+
}
68+
} catch (error) {
69+
console.info('conditional-field: error running your `hide` condition', {
70+
error,
71+
})
72+
73+
return DEFAULT_STATE
74+
}
75+
}
76+
77+
class ConditionalField extends React.PureComponent<any, RenderInfo> {
978
fieldRef: any = React.createRef()
1079

80+
constructor(props: any) {
81+
super(props)
82+
this.state = DEFAULT_STATE
83+
}
84+
1185
focus() {
1286
if (this.fieldRef?.current) {
1387
this.fieldRef.current.focus()
1488
}
1589
}
1690

17-
getContext(level = 1) {
91+
getContext = (level = 1) => {
1892
// gets value path from withValuePath HOC, and applies path to document
1993
// we remove the last 𝑥 elements from the valuePath
2094

@@ -53,9 +127,29 @@ class ConditionalField extends React.PureComponent<any> {
53127
)
54128
}
55129

130+
updateRender = async () => {
131+
const newState = await parseCondition({
132+
document: this.props.document,
133+
hide: this.props.type?.options?.hide,
134+
parents: getParents({
135+
valuePath: this.props.getValuePath(),
136+
document: this.props.document,
137+
}),
138+
})
139+
140+
this.setState(newState)
141+
}
142+
143+
componentDidUpdate() {
144+
this.updateRender()
145+
}
146+
147+
componentDidMount() {
148+
this.updateRender()
149+
}
150+
56151
render() {
57152
const {
58-
document,
59153
type,
60154
value,
61155
level,
@@ -68,13 +162,14 @@ class ConditionalField extends React.PureComponent<any> {
68162
presence = [],
69163
compareValue,
70164
} = this.props
71-
const shouldRenderField = type?.options?.condition
72-
const renderField = shouldRenderField
73-
? shouldRenderField(document, this.getContext.bind(this))
74-
: true
165+
166+
const { renderField, clearOnHidden } = this.state
75167

76168
if (!renderField) {
77-
return <div style={{ marginBottom: '-32px' }} />
169+
if (clearOnHidden && value) {
170+
onChange(PatchEvent.from(unset()))
171+
}
172+
return <HiddenField />
78173
}
79174

80175
const { type: _unusedType, inputComponent, ...usableType } = type
@@ -89,7 +184,11 @@ class ConditionalField extends React.PureComponent<any> {
89184
onFocus={onFocus}
90185
onBlur={onBlur}
91186
ref={this.fieldRef}
92-
markers={markers}
187+
markers={markers.map((marker: any) => ({
188+
...marker,
189+
// Pass the right path for validation markers
190+
path: getValuePath(),
191+
}))}
93192
presence={presence}
94193
compareValue={compareValue}
95194
/>

0 commit comments

Comments
 (0)