hdoro/sanity-plugin-conditional-field

Not possible to use on repeater fields

dnlmzw opened this issue · 10 comments

Thanks for taking the time to write this plugin.

I don't see any way to use this on repeater fields, as the reference provided is only on a "document"-level.

Did I miss anything?

+1

attempting to use this within an nested object

{
      title: "Select video type",
      name: "selectVideoType",
      type: "string",
      options: {
        list: [
          {
            title: "Url",
            value: "url",
            // icon: AiOutlineBgColors,
          },
          {
            title: "File",
            value: "file",
            // icon: BsCardImage,
          },
        ],
        layout: "dropdown",
      },
    },
    {
      title: "Video Url",
      name: "videoUrl",
      type: "string",
      inputComponent: ConditionalField,
      options: {
        condition: object => object.selectVideoType === 'url'
      }
    },

simply hides the field even if the select field is changed

Tested with boolean and does not work :'(

@jamesryan-dev I'm using the "Hull"-starter for Sanity and found out that it comes with conditional fields built-in, which takes into consideration the context of the field:
https://github.com/ndimatteo/HULL/blob/main/studio/components/conditional-field.js

You can see how it's applied here:
https://github.com/ndimatteo/HULL/blob/main/studio/schemas/modules/hero.js

Thank you so much for sharing - I'll look into this and see if i can emulate within my current Sanity ecosystem

Also Hull looks so cool will deff be using this for future projects =}

I found that the hotspot-field on image type fields was breaking with the script from HULL, so I ended up using this script and copy pasting in the getContext-part from HULL. It now does exactly what I need. It looks as follows:

import React from 'react'
import {
  withDocument,
  withValuePath,
  FormBuilderInput
} from 'part:@sanity/form-builder'

class ConditionalField extends React.PureComponent {
  fieldRef = React.createRef()

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

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

    const valuePath = this.props.getValuePath()
    const removeItems = -Math.abs(level)
    return valuePath.length + removeItems <= 0
      ? this.props.document
      : valuePath.slice(0, removeItems).reduce((context, current) => {
          // basic string path
          if (typeof current === 'string') {
            return context[current] || {}
          }

          // object path with key used on arrays
          if (
            typeof current === 'object' &&
            Array.isArray(context) &&
            current._key
          ) {
            return (
              context.filter(
                item => item._key && item._key === current._key
              )[0] || {}
            )
          }
        }, this.props.document)
  }

  render() {
    const {
      document,
      type,
      value,
      level,
      focusPath,
      onFocus,
      onBlur,
      onChange,
      getValuePath,
      markers = [],
      presence = [],
      compareValue
    } = this.props
    const shouldRenderField = type?.options?.condition
    const renderField = shouldRenderField
      ? shouldRenderField(document, this.getContext.bind(this))
      : true

    if (!renderField) {
      return <div style={{ marginBottom: '-32px' }} />
    }

    const { type: _unusedType, inputComponent, ...usableType } = type
    return (
      <FormBuilderInput
        level={level}
        type={usableType}
        value={value}
        onChange={patchEvent => onChange(patchEvent)}
        path={getValuePath()}
        focusPath={focusPath}
        onFocus={onFocus}
        onBlur={onBlur}
        ref={this.fieldRef}
        markers={markers}
        presence={presence}
        compareValue={compareValue}
      />
    )
  }
}

export default withValuePath(withDocument(ConditionalField))

This totally breaks on nested fields - I did some black magic to get it to work with one level of nesting.

i.e.

conditional field 1
    conditional field 2
hdoro commented

Hey everyone! Thanks for chipping in and pointing to possible solutions :)

I have a clear view of how this API could look like and even how to implement it, but I stopped working on this plugin as the Sanity team has voiced they're currently working on a native solution.

Don't want to compete with native, so I'll archive this repository and package as soon as that lands 😉

Hey everyone 👋 Thanks @hdoro for this component!

I had 2 issues while using this:

  • validation markers not working as expected
  • values for conditional fields polluting the resulting document data even when the field is not displayed

I haven't had time to put up a PR (also probably doesn't make sense if this is not going to be maintained), but I think the following snippet fixes those 2 issues, so leaving it here in case it's useful to anyone:

import React from 'react';
import { withDocument, withValuePath, FormBuilderInput } from 'part:@sanity/form-builder';
import { PatchEvent, unset } from 'part:@sanity/form-builder/patch-event';

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

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

  getContext(level = 1) {
    // gets value path from withValuePath HOC, and applies path to document
    // we remove the last 𝑥 elements from the valuePath
    const valuePath = this.props.getValuePath();
    const removeItems = -Math.abs(level);
    return valuePath.length + removeItems <= 0
      ? this.props.document
      : valuePath.slice(0, removeItems).reduce((context: any, current: any) => {
          // basic string path
          if (typeof current === 'string') {
            return context[current] || {};
          }

          // object path with key used on arrays
          if (typeof current === 'object' && Array.isArray(context) && current._key) {
            return context.filter((item) => item._key && item._key === current._key)[0] || {};
          }
        }, this.props.document);
  }

  render() {
    const { type, document, value, onChange } = this.props;
    const shouldRenderField = type?.options?.condition;
    const renderField = shouldRenderField
      ? shouldRenderField(document, this.getContext.bind(this))
      : true;

    if (!renderField) {
      // Clear the value when hiding the input
      if (value) onChange(PatchEvent.from(unset()));

      return null;
    }

    const {
      level,
      focusPath,
      onFocus,
      onBlur,
      getValuePath,
      markers = [],
      presence = [],
      compareValue,
    } = this.props;

    const { type: _unusedType, inputComponent, ...usableType } = type;

    return (
      <FormBuilderInput
        level={level}
        type={usableType}
        value={value}
        onChange={(patchEvent: any) => onChange(patchEvent)}
        path={getValuePath()}
        focusPath={focusPath}
        onFocus={onFocus}
        onBlur={onBlur}
        ref={this.fieldRef}
        markers={markers.map((marker: any) => ({
          ...marker,
          // Pass the right path for validation markers
          path: getValuePath(),
        }))}
        presence={presence}
        compareValue={compareValue}
      />
    );
  }
}

export default withValuePath(withDocument(ConditionalField));
hdoro commented

¡Hola @juannorris! Thanks a ton for this :)

Given so many people are using this library, I decided to push a v1 with these features you outlined, which also solves this issue. I'm not removing values by default as you suggested because it could be destructive. Instead, use the clearOnHidden value to do that 😉

I strongly recommend upgrading to v1 (take a look at the docs) and benefiting from a rounder API and smoother UX for your editors 🎉

Awesome, thanks @hdoro! ❤️