decaporg / decap-cms

A Git-based CMS for Static Site Generators
https://decapcms.org
MIT License
17.88k stars 3.04k forks source link

dependent fields in collection #565

Open kevinfoerster opened 7 years ago

kevinfoerster commented 7 years ago

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

erquhart commented 7 years ago

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 commented 6 years ago

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 commented 6 years ago

@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 commented 6 years ago

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

domtalbot commented 6 years ago

Hi Shawn,

I should be able to do this over the christmas period yes, will keep this thread updated :)

On 19 December 2017 at 21:48, Shawn Erquhart notifications@github.com wrote:

@domtalbot https://github.com/domtalbot can you open a "WIP" PR with a working implementation?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/netlify/netlify-cms/issues/565#issuecomment-352896609, or mute the thread https://github.com/notifications/unsubscribe-auth/AG11467bYwSm0kzYwWEBDGURYYZlLx5Lks5tCC9LgaJpZM4PGGOv .

--

Kind Regards

Dominic Talbot 07791193905

cwahlfeldt commented 6 years ago

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

erquhart commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

Not yet, just a concept for now.

Sent with GitHawk

tomrutgers commented 5 years ago

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

stale[bot] commented 4 years ago

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 commented 4 years ago

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

erezrokah commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

Hi @kylekirkby, see my comment https://github.com/netlify/netlify-cms/issues/565#issuecomment-576140240 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 commented 4 years ago

I managed to get it working as a custom widget for my client site so is perfectly doable. I don't have access to the code anymore though :(

On Mon, 11 May 2020, 11:29 Erez Rokah, notifications@github.com wrote:

Hi @kylekirkby https://github.com/kylekirkby, see my comment #565 (comment) https://github.com/netlify/netlify-cms/issues/565#issuecomment-576140240 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.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/netlify/netlify-cms/issues/565#issuecomment-626618054, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABWXLY65G5PJCLFOZAPCOHLRQ7HQPANCNFSM4DYYMOXQ .

mcfarlanedev commented 4 years ago

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
---
filipburian commented 3 years ago

Hi, any updates on this?

miguelt1 commented 3 years ago

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

erezrokah commented 3 years ago

Hi @filipburian and @miguelt1, we have some in progress work in https://github.com/netlify/netlify-cms/pull/3891 if you'd like to pick it up.

patrulea commented 10 months ago

6 years and multiple solutions proposed and still nothing🥲

DwanW commented 4 months ago

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 }