Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dependent fields in collection #565

Open
kevinfoerster opened this issue Aug 29, 2017 · 24 comments · May be fixed by #3891
Open

dependent fields in collection #565

kevinfoerster opened this issue Aug 29, 2017 · 24 comments · May be fixed by #3891

Comments

@kevinfoerster
Copy link

hey,

i could not find any information about this therefor i thought i would ask here.

is it possible to show and hide fields depending on some other field?

eg. this is my frontmatter

---
...
structure:
- type: productCircle
- type: divider
- type: copy
  content:
    text: >-
      Lorem ipsum dolor sit amet, consectetur adipisicing elit. Fugiat fugit enim quo consequuntur amet quasi ullam officia assumenda nihil. Corrupti delectus magnam excepturi nobis quae nihil explicabo provident repellat quaerat.
- type: infographic
  content:
    src: http://example.com/text.png
    overlay: http://example.com/text.png
- type: divider
- type: copy
  content:
    text: >-
      Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi nihil error repellendus perspiciatis ratione, dolores, cum dicta sapiente quod labore saepe, quam ab placeat veniam nobis culpa totam vitae nam.
- type: infographic
  content:
    src: http://example.com/text.png
    overlay: http://example.com/text.png
- type: copy
  content:
    text: >-
      Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aspernatur vero, quos velit minima placeat alias, quibusdam at sed quia illum dolorum. A, rem aut quidem excepturi aliquam. Aliquid, ea, cumque.
- type: quote
  variant:
  - with-divider
  content:
    items: 
      - image: http://example.com/text.png
        copy: Lorem ipsum dolor sit amet, consectetur adipisicing elit. Vitae sunt, earum? Perferendis ab blanditiis ut sequi consequuntur repudiandae ad ratione, amet eligendi laborum itaque labore, eius. Illo, quidem tempore quam!
        author: Mario and Luigi
        type: light
- type: divider
- type: copy
  content:
    text: >-
      Lorem ipsum dolor sit amet, consectetur adipisicing elit. Deleniti, illum qui. Adipisci fugit, obcaecati, vel nostrum cumque a dignissimos odit eveniet ab. Aut cum officiis nesciunt necessitatibus, provident, ullam delectus!
- type: image
  content:
    src: http://example.com/text.png
---

based on that type different configurations for this list item might be needed

eg.
if my type is image only a image field is necessary but if the type is infographicand additional overlay image can be added

my config.yml looks like this

- label: 'structure'
        name: structure
        widget: 'list'
        fields: 
          - {label: "type", name: "type", widget: "select", options: ["copy", "divider", "image", "infographic", "productCircle", "quote"]}
          - label: content
            name: content
            widget: object
            fields:
              - {label: 'text', name: 'text', widget: 'markdown'}
              - {label: 'src', name: 'src', widget: 'image'}
              - {label: 'overlay', name: 'overlay', widget: 'image'}

this list has a lot of unused options and i am far from done adding all the fields

what would a good way to implement this kind of structure?

cms version 0.4.6

@kevinfoerster kevinfoerster changed the title dependent fileds in collection dependent fields in collection Aug 29, 2017
@erquhart
Copy link
Contributor

erquhart commented Sep 2, 2017

You could create a custom widget that provides a select input for selecting type, and the type definitions could be an array of objects defined in the widget config. Then you just swap in fields based on the selected type.

@erquhart erquhart mentioned this issue Dec 8, 2017
2 tasks
@erquhart
Copy link
Contributor

erquhart commented Dec 8, 2017

So I've been considering how this might look. We're now focusing heavily on the extensibility story around the CMS, and a big part of that is making widget authoring simpler, and the available API's more robust.

For this particular issue, we should have a widget as I mentioned in my last comment, but I want it to just be a button with configurable text. In the OP case you would configure the label to "Add content", and it's appearance would simply be that of a dropdown, which editors can use to add one of the widgets configured in the "fields" array. The UI around this could be further enhanced by passing control of labels (optionally) to widget authors (currently all widgets automatically receive a label in the UI direct through the CMS). This means an entry could consist of nothing more than this widget and be entirely dynamic, very similar to what ACF provides to WordPress users.

Furthermore, if the added widgets are themselves draggable, we'd have all that's needed for site building. Doing all of this in a widget is pretty compelling.

Thoughts?

@domtalbot
Copy link

@erquhart thanks for reminding me about this, I have been meaning to get it finished.

Anyhow, I did manage to find a solution to an extent, I could only get preview to work through a custom preview layout. Anyhow, here are some snippets that I generated:

config.yml

- name: slides
        label: Slides
        widget: list
        required: false
        fields:
          - label: "Slide"
            name: "slide"
            widget: "dynamic"
            dynamicWidgets:
              - string
              - image
              - file

Control

import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { Map } from 'immutable';
import CMS from 'netlify-cms';

export default class DynamicControl extends Component {
  static propTypes = {
    onChange: PropTypes.func.isRequired,
    onAddAsset: PropTypes.func.isRequired,
    onRemoveAsset: PropTypes.func.isRequired,
    getAsset: PropTypes.func.isRequired,
    value: PropTypes.oneOfType([
      PropTypes.node,
      PropTypes.object,
      PropTypes.bool,
    ]),
    field: PropTypes.object,
    forID: PropTypes.string,
    dynamicWidgets: PropTypes.object
  };

  constructor(props) {
    super(props);

    const fieldValue = this.props.value && Map.isMap(this.props.value) ?
      this.props.value.get(this.props.field.get('name')) :
      '';

    if (!fieldValue) {
      this.state = {
        widget: null,
      };
    } else {
      this.state = {
        widget: CMS.getWidget(fieldValue),
      };
    }
  }

  handleChange = (e) => {
    this.props.onChange(Map().set(e.target.id, e.target.value));

    if (!e.target.value) {
      this.setState({
        widget: null,
      });
    } else {
      this.setState({
        widget: CMS.getWidget(e.target.value),
      });
    }
  };

  render() {
    const { field, value, forID, onChange, onAddAsset, onRemoveAsset, getAsset } = this.props;
    const { widget } = this.state;

    const name = field.get('name');
    const selectedName = `${ field.get('name') }_selected`;

    const fieldValue = this.props.value && Map.isMap(this.props.value) ?
      this.props.value.get(name || '') :
      '';

    const fieldValueSelected = this.props.value && Map.isMap(this.props.value) ?
      this.props.value.get(selectedName || '') :
      '';

    let options = field.get('dynamicWidgets').map((option) => {
      if (typeof option === 'string') {
        return { label: option, value: option };
      }
      return option;
    });

    options = options.insert(0, {
      label: 'Please Select',
      value: '',
    });

    return (
      <div>
        <div>
          <select id={forID} value={fieldValue || ''} onChange={this.handleChange}>
            {options.map((option, idx) => <option key={idx} value={option.value}>
              {option.label}
            </option>)}
          </select>
        </div>
        <div>
          {
            widget ?
              <div key={selectedName}>
                <div key={selectedName}>
                  <label htmlFor={selectedName}>{`${ field.get('label') } Data`}</label>
                  {
                    React.createElement(widget.control, {
                      id: selectedName,
                      field,
                      value: fieldValueSelected,
                      onChange: (val, metadata) => {
                        onChange((value || Map()).set(selectedName, val), metadata);
                      },
                      onAddAsset,
                      onRemoveAsset,
                      getAsset,
                      forID: selectedName,
                    })
                  }
                </div>
              </div>
            :
              ''
          }
        </div>
      </div>
    );
  }
}

Hope that helps kick start this conversation ;)

@erquhart
Copy link
Contributor

@domtalbot can you open a "WIP" PR with a working implementation?

@domtalbot
Copy link

domtalbot commented Dec 21, 2017 via email

@cwahlfeldt
Copy link

Started another issue similar to this. Wondering if you got anywhere @domtalbot and if you need any help? #1066

@erquhart
Copy link
Contributor

While considering #2405 I think I found a way to statically configure conditional fields right in the yaml while still providing flexibility. Using the original case from @kevinfoerster's OP:

- label: structure
  name: structure
  widget: list
  fields:
    - label: type
      name: type
      widget: select
      options:
        - copy
        - divider
        - image
        - infographic
        - productCircle
        - quote
    - label: content
      name: content
      widget: object
      fields:
        - label: text
          name: text
          widget: markdown
          condition: {type: fieldValue, field: type, oneOf: [copy, productCircle, quote]}
        - label: src
          name: src
          widget: image
          condition: {type: fieldValue, field: type, oneOf: [image, infographic, productCircle]}
        - label: overlay
          name: overlay
          widget: image
          condition: {type: fieldValue, field: type, equals: infographic}

fieldValue is just one potential type, and each type can accept different properties, here we use equals and oneOf.

condition could accept either an object or an array. The example above is shorthand for condition.oneOf, which accepts an object or array of objects, and can be used if condition itself can be configured with other properties for more control, or alternatives to condition.oneOf, such as condition.allOf, which would require all condition objects in an array to validate.

@ghost
Copy link

ghost commented Jul 17, 2019

I love this idea of the condition property suggested above ☝️ Do we have any forward progression on this? Just ran into a couple of scenarios where this would be very helpful. Thanks in advance.

@erquhart
Copy link
Contributor

Not yet, just a concept for now.

Sent with GitHawk

@tomrutgers
Copy link
Contributor

Looks great already @erquhart. Maybe add condition.not to the list of conditions

@stale
Copy link

stale bot commented Oct 29, 2019

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@caseyjkey
Copy link

Any updates on this? I want to display a datetime widget only if a boolean widget is false.

@erezrokah
Copy link
Contributor

Hi @caseykey, we're working our way through the most upvoted issues first, so best way to push something forward is to upvote it.

@caseyjkey
Copy link

Hi @caseykey, we're working our way through the most upvoted issues first, so best way to push something forward is to upvote it.

Like give it a thumbs up?

@erezrokah
Copy link
Contributor

Hi @caseykey, we're working our way through the most upvoted issues first, so best way to push something forward is to upvote it.

Like give it a thumbs up?

Exactly

@kylekirkby
Copy link

@erezrokah Where is this on the roadmap? Would be great to get this feature added! I've got a situation that looks like this:

netlifycms

It's very confusing for content editors and hinders their productivity and the potential flexibility of NetlifyCMS.

Thanks to all for your hard work on this project !

Kyle

@erezrokah
Copy link
Contributor

Hi @kylekirkby, see my comment #565 (comment) and the most upvoted features here https://github.com/netlify/netlify-cms/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc.
I think one could implement this in a custom widget, and if that works well we can import it into the main repo.

@domtalbot
Copy link

domtalbot commented May 11, 2020 via email

@mcfarlanedev
Copy link

Hi guys, I would love to see this feature added too. I have a requirement for a page builder. The way I would like it to work would be that you create a new page based on the following in your config.yml:

collections:
  - name: 'pages'
    label: 'Pages'
    label_singular: 'Page'
    folder: 'src/_pages/pages'
    create: true
    identifier_field: name
    fields:
      - {
          label: 'Page Template',
          name: 'templateKey',
          widget: 'select',
          default: 'standardPage',
          options:
            [
              { label: 'Standard Layout', value: 'standardPage' },
              { label: 'Background Layout', value: 'backgroundPage' },
              { label: 'Carousel Layout', value: 'carouselPage' },
              { label: 'Gallery Layout', value: 'galleryPage' },
            ],
        }

The relevant fields for each type of Layout would be rendered out in the editor panel depending on which dropdown option you selected. Although, I'm not sure how I would associate each dropdown value with a unique set of fields (is this where the beta Variable Types widget comes in to play?) When I publish the page, the page's markdown would just include the frontmatter fields that correspond to the selected layout template (e.g a columns field) that was selected during page creation - all pre-populated of course:

---
templateKey: standardPage
title: Page Title
columns: 
  columnOne: Column One Text
  columnTwo: Column Two Text
  columnThree: Column Three Text
---

@barthc barthc self-assigned this Jun 7, 2020
@barthc barthc linked a pull request Jun 12, 2020 that will close this issue
2 tasks
@filipburian
Copy link

Hi, any updates on this?

@miguelt1
Copy link

miguelt1 commented Mar 5, 2021

Hi, I need this feature for a client requirement, how is going on?

@erezrokah
Copy link
Contributor

erezrokah commented Mar 8, 2021

Hi @filipburian and @miguelt1, we have some in progress work in #3891 if you'd like to pick it up.

@patrulea
Copy link

6 years and multiple solutions proposed and still nothing🥲

@DwanW
Copy link

DwanW commented Jun 11, 2024

In my case, I just needed a boolean widget that is attached to a list widget, and boolean widget can show/hide the list.
you can use a custom widget similar to this:

// custom widget
import React, { Component } from "react"
import CMS from "netlify-cms-app"
import _ from "lodash"
import { List } from "immutable"

const booleanWidget = CMS.getWidget("boolean")
const listWidget = CMS.getWidget("list")

export class ToggleListControl extends Component {
  constructor(props) {
    super(props)
    this.state = {
      show: !_.isNil(this.props.value),
    }
  }
  render() {
    const {
      value,
      forID,
      onChange,
      classNameWrapper,
      setActiveStyle,
      setInactiveStyle,
    } = this.props

    return (
      <div>
        <div>
          {React.createElement(booleanWidget.control, {
            value: !_.isNil(value),
            onChange: () => {
              if (!_.isNil(value)) {
                this.setState({ show: false })
                onChange(undefined)
              } else {
                this.setState({ show: true })
                onChange(List())
              }
            },
            forID: `${forID}-toggle`,
            classNameWrapper,
            setActiveStyle,
            setInactiveStyle,
          })}
        </div>
        <div style={{ marginTop: "16px" }}>
          {!_.isNil(this.props.value) && (
            <div>
              {React.createElement(listWidget.control, { ...this.props })}
            </div>
          )}
        </div>
      </div>
    )
  }
}

export const ToggleListPreview = ({ value }) => {
  return <div></div>
}

and the config would be the same as the one you use for list:

      - label: Toggle List
        name: fieldname
        widget: toggleList
        required: false
        field: { label: Text, name: text, widget: string }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.