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

Commit

Permalink
v1 (the first and last version!)
Browse files Browse the repository at this point in the history
  • Loading branch information
hdoro committed Jul 9, 2021
1 parent 955596f commit 0b772a4
Show file tree
Hide file tree
Showing 5 changed files with 337 additions and 45 deletions.
182 changes: 148 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ Hide or show a Sanity.io field based on a custom condition set by you.

---

🚨 **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.

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.

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 :)
🚨 **Warning:** the Sanity team has voiced they're currently working on a native solution. The goal of this plugin is to become obsolete.

---

Expand All @@ -22,7 +18,9 @@ sanity install conditional-field
yarn install conditional-field
```

Then, you can use the ConditionalField component as the `inputComponent` of whatever field you want to conditionally render:
## Usage

You can use the ConditionalField component as the `inputComponent` of whatever field you want to conditionally render:

```js
import ConditionalField from 'sanity-plugin-conditional-field'
Expand All @@ -36,45 +34,161 @@ export default {
name: 'internal',
title: 'Is this article internal?',
type: 'boolean',
validation: Rule => Rule.required(),
},
{
name: 'content',
title: 'Content',
type: 'array',
of: [
{type: 'block'}
]
inputComponent: ConditionalField,
options: {
condition: document => document.internal === true
}
validation: (Rule) => Rule.required(),
},
{
name: 'externalUrl',
title: 'URL to the content',
type: 'url',
inputComponent: ConditionalField,
options: {
condition: document => !document.internal
}
// Simple conditional based on top-level document value
hide: ({ document }) => document.internal, // hide if internal article
},
},
],
}
```

🚨 **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.
### Async conditions

```js
{
name: 'content',
title: 'Content',
type: 'array',
of: [
{type: 'block'},
]
inputComponent: ConditionalField,
options: {
// Asynchronous conditions
hide: async ({ document }) => {
if (document.internal) {
return true
}

const isValidContent = await fetch(`/api/is-valid-content/${document.externalUrl}`)
return isValidContent ?
}
}
}
```
### Nested conditions
Besides the current `document`, the `hide` function receives a `parents` array for accessing contextual data.
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`.
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:
```js
{
name: 'links',
title: 'Links',
type: 'array',
of: [
{
name: 'link',
title: 'Link',
type: 'object',
fields: [
{
name: 'external',
title: "Links to external websites?",
type: 'boolean',
},
{
name: 'url',
title: 'External URL',
type: 'string',
inputComponent: ConditionalField,
options: {
hide: ({ parents }) => {
// Parents array exposes the closest parents in order
// Hence, parents[0] is the current object's value
return parents[0].external
},
},
},
{
name: 'internalLink',
type: 'reference',
to: [{ type: "page" }],
inputComponent: ConditionalField,
options: {
hide: ({ parents }) => !parents[0].external
},
},
{
name: 'flashyLooks',
type: 'boolean',
inputComponent: ConditionalField,
options: {
// Prevent editors from making the link flashy if this link is not in the first position in the array
hide: ({ parents }) => ({
hidden: parents[1]?.indexOf(parents[0]) > 0 || false,
// Clear field's value if hidden - see below
clearOnHidden: true
})
},
},
],
},
],
},
```
### Deleting values if field is hidden
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.
```js
{
name: 'externalUrl',
type: 'url',
inputComponent: ConditionalField,
options: {
hide: ({ document }) => {
hidden: !!document.internal,
// Clear field's value if hidden
clearOnHidden: true
},
},
},
```
### Typescript definitions
Take a look at the roadmap below for an idea on the plugin's shortcomings.
If you use Typescript in your schemas, here's how you type your `hide` functions:
```ts
import { HideOption } from 'sanity-plugin-conditional-field'

const hideBoolean: HideOption = false
const hideFunction: HideOption = ({ document, parents }) => ({
hidden: document._id.includes('drafts.') || parents.length > 2,
})
```
And here's the shape of the `hide` options:
```ts
type ConditionReturn = boolean | { hidden: boolean; clearOnHidden?: boolean }

export type HideFunction = (props: {
document: SanityDocument
parents: Parent[]
}) => ConditionReturn | Promise<ConditionReturn>

export type HideOption = boolean | HideFunction
```
## Shortcomings
🚨 **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.
## Roadmap
Besides this, the following is true:
- [ ] Prevent the extra whitespace from hidden fields
- [ ] Find a way to facilitate validation
- [ ] Consider adding a `injectConditionals` helper to wrap the `fields` array & automatically use this inputComponent when options.condition is set
- Example: `injectConditionals([ { name: "title", type: "string", options: { condition: () => true } }])`
- [ ] Async conditions
- Would require some debouncing in the execution of the condition function, else it'll fire off too many requests
- Maybe an array of dependencies similar to React.useEffect
- [ ] get merged into `@sanity/base`
- 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 :)
- Async conditions aren't debounced, meaning they'll be fired _a lot_
- There's no way of using this field with custom inputs
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "sanity-plugin-conditional-field",
"version": "0.0.2",
"version": "1.0.0",
"description": "Hide or show a Sanity.io field based on a custom condition set by you.",
"main": "lib/ConditionalField.js",
"scripts": {
"format": "prettier --write .",
"clear-lib": "node clearLib.js",
"build": "npm run format && npm run clear-lib && tsc",
"build": "npm run format && npm run clear-lib && tsc --declaration",
"dev": "tsc -w",
"prepublish": "npm run build"
},
Expand Down
117 changes: 108 additions & 9 deletions src/ConditionalField.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,94 @@
import React from 'react'
import { SanityDocument } from '@sanity/client'
import {
withDocument,
withValuePath,
FormBuilderInput,
} from 'part:@sanity/form-builder'
import { PatchEvent, unset } from 'part:@sanity/form-builder/patch-event'
import HiddenField from './HiddenField'
import getParents, { Parent } from './getParents'

class ConditionalField extends React.PureComponent<any> {
type RenderInfo = {
renderField: boolean
clearOnHidden: boolean
}

type ConditionReturn = boolean | { hidden: boolean; clearOnHidden?: boolean }

export type HideFunction = (props: {
document: SanityDocument
parents: Parent[]
}) => ConditionReturn | Promise<ConditionReturn>

export type HideOption = boolean | HideFunction

const DEFAULT_STATE: RenderInfo = {
renderField: true,
clearOnHidden: false,
}

async function parseCondition({
document,
hide,
parents,
}: {
document: SanityDocument
hide?: HideOption
parents: Parent[]
}): Promise<RenderInfo> {
if (typeof hide === 'boolean') {
return {
...DEFAULT_STATE,
renderField: !hide,
}
}

if (!hide || typeof hide !== 'function') {
return DEFAULT_STATE
}

try {
const hideField = await Promise.resolve(hide({ document, parents }))

if (typeof hideField === 'boolean') {
return {
renderField: !hideField,
clearOnHidden: false,
}
}

return {
renderField: !(typeof hideField.hidden === 'boolean'
? hideField.hidden
: // Don't hide by default
false),
clearOnHidden: hideField.clearOnHidden || false,
}
} catch (error) {
console.info('conditional-field: error running your `hide` condition', {
error,
})

return DEFAULT_STATE
}
}

class ConditionalField extends React.PureComponent<any, RenderInfo> {
fieldRef: any = React.createRef()

constructor(props: any) {
super(props)
this.state = DEFAULT_STATE
}

focus() {
if (this.fieldRef?.current) {
this.fieldRef.current.focus()
}
}

getContext(level = 1) {
getContext = (level = 1) => {
// gets value path from withValuePath HOC, and applies path to document
// we remove the last 𝑥 elements from the valuePath

Expand Down Expand Up @@ -53,9 +127,29 @@ class ConditionalField extends React.PureComponent<any> {
)
}

updateRender = async () => {
const newState = await parseCondition({
document: this.props.document,
hide: this.props.type?.options?.hide,
parents: getParents({
valuePath: this.props.getValuePath(),
document: this.props.document,
}),
})

this.setState(newState)
}

componentDidUpdate() {
this.updateRender()
}

componentDidMount() {
this.updateRender()
}

render() {
const {
document,
type,
value,
level,
Expand All @@ -68,13 +162,14 @@ class ConditionalField extends React.PureComponent<any> {
presence = [],
compareValue,
} = this.props
const shouldRenderField = type?.options?.condition
const renderField = shouldRenderField
? shouldRenderField(document, this.getContext.bind(this))
: true

const { renderField, clearOnHidden } = this.state

if (!renderField) {
return <div style={{ marginBottom: '-32px' }} />
if (clearOnHidden && value) {
onChange(PatchEvent.from(unset()))
}
return <HiddenField />
}

const { type: _unusedType, inputComponent, ...usableType } = type
Expand All @@ -89,7 +184,11 @@ class ConditionalField extends React.PureComponent<any> {
onFocus={onFocus}
onBlur={onBlur}
ref={this.fieldRef}
markers={markers}
markers={markers.map((marker: any) => ({
...marker,
// Pass the right path for validation markers
path: getValuePath(),
}))}
presence={presence}
compareValue={compareValue}
/>
Expand Down
Loading

0 comments on commit 0b772a4

Please sign in to comment.