Hiding Content Editor Fields Depending on Selected Values

tl;dr Custom Field which hides other fields in the Content Editor depending on the selected value

A recent question on Stackoverflow asked if it was possible to hide fields in the Sitecore Content Editor when certain values are selected. This is a pretty common (and easy task) on any normal web site and it makes sense from a usability point of view to only show those fields which are relevant to the user. I gave a half answer to the question that a Composite Custom Field could be created to fire off the relevant Javascript and hide the sibling fields on change. I was unsure however how to hide the fields on document load.

The field itself was pretty straight forward. I decided to create custom versions of the Checkbox, Droplist and Droplink controls since these were pretty obvious candidates for this type of behaviour.

Almost all the relevant logic for the controls was put in a static helper class since they are pretty much the same customization wise. I use an Interface to pass the fields we need to access, making sure our controls implement it.

public interface IHideDependentField
{
    string Source { get; set; }
    string Class { get; set; }
    AttributeCollection Attributes { get; }
}

And a static helper to add the relevant attributes to the control, extracting them from the passed in Source property.

public static void SetControlProperties(IHideDependentField control)
{
    if (String.IsNullOrEmpty(control.Source))
        return;

    if (!String.IsNullOrEmpty(control.Source.HideByDefault()) || !String.IsNullOrEmpty(control.Source.ValuesToHide()))
    {
        control.Attributes["onchange"] = "HideDependentFields(this, true)";
        control.Attributes["data-hide-default"] = control.Source.HideByDefault();
        control.Attributes["data-hide-count"] = control.Source.HideCount().ToString();

        if (control is HideDependentCheckbox)
        {
            control.Class = "scContentControlCheckbox hide-dependent-fields";
        }
        else
        {
            control.Attributes["data-hide-values"] = control.Source.ValuesToHide();
            control.Class = "scContentControl hide-dependent-fields";
        }
    }
}

private static string HideByDefault(this string Source)
{
    return StringUtil.ExtractParameter("HideByDefault", Source).Trim();
}

private static int HideCount(this string Source)
{
    var hideCount =  StringUtil.ExtractParameter("HideCount", Source).Trim();
    return MainUtil.GetInt(hideCount, 0);
}

private static string ValuesToHide(this string Source)
{
    return StringUtil.ExtractParameter("ValuesToHide", Source).Trim();
}

The checkbox control out of the box does not support passing of parameters through the Source property but simply declaring the Source property will give us access to it. The other controls already pass the Source so we just call our static helper on control load, we just need to make sure our controls inherit from our interface and the appropriate Sitecore Shell Control.

public class HideDependentCheckbox : Checkbox, IHideDependentField
{
    public HideDependentCheckbox()
        : base()
    {
    }

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        DependentFieldHelper.SetControlProperties(this);
    }

    public string Source { get; set; }
}

I knew I would need to trigger some code on document load, it would not be possible to inline this code since I had to wait till all controls in the page had loaded to be able to hide them. It would be tricky to do this server side since I did not have access to the whole collection of controls and keeping track of control counts would make it messy. Eventually I figured out it would be possible to add my own custom JavaScript file to the Content Editor, and then at point we are back on a normal development stream…

Or so it seemed until I realised that calling document.observe("dom:loaded", fn) would only work the first time the editor loaded but not for any subsequent requests for Items (load, save, switch to raw values etc) since the UI is updated via an AJAX call. I have not used the Prototype JS library before, I guessed in jQuery maybe I could just fire off my JS on all AJAX calls but even that was a bit heavy handed. I considered peeking and modifying the response to include a call to my own function but then stumbled across this magical line in the response:

{"command":"Eval","value":"scContentEditorUpdated()"}

And looking at the definition of that function in /sitecore/shell/Applications/Content Manager/Content Editor.js it contains the following call:

body.fire("sc:contenteditorupdated");

Bingo. As long as I could register my custom function to this event it would act like a dom:loaded call for my purposes. I found out that most, if not all, modern JS libraries append when event handlers are registered rather than overwrite any existing since I did not want to overwrite any Sitecore stuff. In my initial testing I didn’t see any events registered so I wonder if Sitecore have been nice to us and given this for our customization?

Anyway, it turned out to be pretty simple:

document.observe("sc:contenteditorupdated", function(event) {
    $$('.hide-dependent-fields').each( function(element, index) {
        HideDependentFields(element, false);
    });
});

I wanted some visual animation to make it a little more obvious that something had happened rather than an abrupt hide, but also did not want to take the additional time on initial load so added the additional boolean flag to indicate whether to use animation.

The corresponding JavaScript method is triggered on change. Only fields within the same collapsible section are hidden. I decided to stick with Prototype as a learning process, which turned out to be a good call anyway…

function HideDependentFields(element, useEffects) {

    var action = 'show',
        $element = $(element),
        hideByDefault = $element.readAttribute('data-hide-default'),
        hideCount = $element.readAttribute('data-hide-count'),
        hideValues = $element.readAttribute('data-hide-values'),
        siblingElements = $element.up('.scEditorFieldMarker').nextSiblings();

    // get array of elements to be hidden
    if (hideCount > 0 && siblingElements.length > hideCount) {
        siblingElements = siblingElements.slice(0, hideCount);
    }

    // for checkbox type elements check if checked
    if (element.type == 'checkbox') {
        if ((!element.checked && hideByDefault == 'true') || (element.checked && hideByDefault == 'false')) {
            action = 'hide';
        }
    }

    // for select elements check the selected value
    if (element.nodeName.toLowerCase() == 'select') {
        var elVal = $element.getValue();
        if (hideByDefault && elVal == '') {
            action = 'hide';
        }
        else if (hideValues.length > 0) {
            var arrValues = hideValues.split('|');
            if (arrValues.indexOf(elVal) != -1) {
                action = 'hide';
            }
        }
    }

    // hide immediatelty if firing from DOM Load event
    if (!useEffects) {
        siblingElements.invoke(action);
        return;
    }

    // otherwise use fade effects
    var effectFn = (action == 'show') ? function(el) { el.appear({ duration: 0.5 }) } : function(el) { el.fade({ duration: 0.5 }) };
    Effect.multiple(siblingElements, effectFn, { speed: 0 });

};

The results:
Hide Dependent Fields Configuration

Hide Dependent Fields in Content Editor

Hide Dependent Fields [animated gif]

The custom control in itself is pretty straight forward code but I think the JavaScript opens up a lot of possibilities for us to further customize the content editor.

Usage:
In your template select the appropriate Hide Dependent Field type.
For checkbox type, you must set HideDefault bool in the source.
For the drop types, you must set HideDefault or HideValues in the source.
If these values are not set then a standard control is rendered.
If you need to set a datasource then set it using parameters, e.g. Datasource=/sitecore/content/home&HideDefault=true
Values:
HideDefault: [boolean] – true/false value indicating whether the fields should be hidden by default (i.e. when checkbox is not checked or no value selected in select list)
HideCount: [int] – number of siblings to hide. Default (or passing 0) means all.
HideValues [string] – pipe (|) separated list of values that siblings should be hidden when selected, e.g. Item1|Item2 or {guid}|{guid}. Not applicable for checkbox type.
Example config:
HideByDefault=true&HideCount=1
DataSource=/sitecore/content/TestLookup&HideByDefault=false&ValuesToHide=Item1|Item2
DataSource=/sitecore/content/TestLookup&HideByDefault=true&HideCount=2&ValuesToHide={0C016725-E94D-4DA5-B6D0-100A66E28247}|{FD7B5774-C673-450D-9B14-D02F8CD4D979}

The source code can be found on GitHub or you can download the module from the Sitecore Marketplace. The code has been compiled with .Net 3.5 and tested against Sitecore 6.5 and 7.2 rev 140314, so should work on everything in between.

Advertisements

2 comments

  1. Torrey Garland · February 8

    Is this no longer available at the Sitecore market place?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s