3

My "unenhanced" code looks like this:

@using (Html.BeginForm("Index", "home"))
{
    <!-- old group -->
    <div class="form-group">
        @Html.LabelFor(m => m.Query.artist)        
        @Html.TextBoxFor(m => m.Query.artist, new { @class = "form-control" })
        @Html.ValidationMessageFor(m => m.Query.artist)
    </div>
    ...
}

But I want to enhance it and add a toggle feature for each form-group, like this:

<script>
    var toggleProperty = function(targetCheckbox) {
        var id = targetCheckbox.id.split('-')[0];
        var inputDiv = document.getElementById(id + "-inputdiv");
        inputDiv.style.display = (inputDiv.style.display !== "none") ? "none" : "";
    }
</script>

<!-- new group -->
<input type="checkbox" id="property1-toggler" value="false" onchange="return toggleProperty(this)"/> <label for="property1">name for property1</label>
<div id="property1-inputdiv">
    <input class="form-control" id="property1" name="property1" type="text" value="property1 default value">
    <span class="field-validation-valid" data-valmsg-for="property1" data-valmsg-replace="true"></span>
</div>

I would like to know where in the aspnet mvc source code I can find the code for LabelFor etc.

This way I could better understand how it works, and create an extension function like:

public static IHtmlContent FullGroupFor<TModel, TResult>(this IHtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TResult>> expression);
dda
  • 6,030
  • 2
  • 25
  • 34
Xavier Peña
  • 7,399
  • 9
  • 57
  • 99
  • 1
    Would it be easier to just create a new extension that calls `LabelFor`? Start with that result, then add your stuff afterwards inside a new extension method? – krillgar Apr 13 '17 at 15:37
  • @krillgar Thanks for the hint, after some work I've published [a solution to the problem](http://stackoverflow.com/a/43402960/831138). – Xavier Peña Apr 13 '17 at 23:04

2 Answers2

2

All LabelFor is doing is creating a <label> with a for attribute. This attribute points at an id of a form element such as a textbox or checkbox, and will give it focus (or toggle the checkbox).

<label for="my-textbox">
<input id="my-textbox" type="text">

Form groups don't have labels. Those are most likely being created using <fieldset> which has as <legend> tag for displaying an explanation of each set.

Also, it's not a good idea to toggle groups or fieldsets with display: none because hidden form elements will not be submitted.

Soviut
  • 88,194
  • 49
  • 192
  • 260
  • Thanks for the answer, I wasn't aware that `display: none` would have the effect of not submitting hidden form elements. After some work I've published [a solution to the problem](http://stackoverflow.com/a/43402960/831138). I am curious about an alternative to `display: none` for toggling the content. – Xavier Peña Apr 13 '17 at 23:03
1

I went with @krillgar's suggestion, and it worked:

public static class HtmlHelperExtensions
{
    /// <summary>
    /// Given an input property, creates a full block of:
    /// 1. checkbox (to allow toggle)
    /// 2. label
    /// 3. extra content if needed (shown above the textbox)
    /// 4. textbox
    /// 5. validation message
    /// 
    /// 3+4+5 are shown only if the checkbox is clicked (toggle functionality)
    /// </summary>
    public static IHtmlContent FullGroupFor<TModel, TResult>(
        this IHtmlHelper<TModel> htmlHelper, 
        Expression<Func<TModel, TResult>> expression,
        string placeholder = null,
        IHtmlContent extraContent = null)
    {            
        var propertyId = string.Join("_", expression.Body.ToString().Split('.').Skip(1));

        var method = expression.Compile();
        var value = method(htmlHelper.ViewData.Model);
        var hasValue = (value != null);

        var builder = new HtmlContentBuilder();

        builder.AppendHtml("<div class=\"form-group\">");
        builder.AppendHtml($"<input type=\"checkbox\" id=\"{propertyId}-toggler\" {(hasValue ? "checked" : "")} onchange=\"return toggleProperty(this)\"/>");
        builder.AppendHtml("&nbsp");
        builder.AppendHtml(htmlHelper.LabelFor<TResult>(expression, null, null));
        builder.AppendHtml($"<div id=\"{propertyId}-inputdiv\" style=\"display:{(hasValue ? "" : "none")}\">");
        if (extraContent != null)
            builder.AppendHtml(extraContent);
        builder.AppendHtml(htmlHelper.TextBoxFor(expression, new { @class = "form-control", placeholder = placeholder }));
        builder.AppendHtml(htmlHelper.ValidationMessageFor(expression));
        builder.AppendHtml("</div>");
        builder.AppendHtml("</div>");

        return builder;
    }

    /// <summary>
    /// Javascript code necessary to run the toggle code in the previous function.
    /// </summary>
    public static IHtmlContent GetToggleScript(this IHtmlHelper htmlHelper)
        => new HtmlString(@"
            <script>
                var toggleProperty = function(targetCheckbox) {
                    var id = targetCheckbox.id.split('-')[0];
                    var inputDiv = document.getElementById(id + '-inputdiv');
                    inputDiv.style.display = (inputDiv.style.display !== 'none') ? 'none' : '';
                }
            </script>
        ");
}

It is also important what @Soviut's added, which is: "it's not a good idea to toggle groups or fieldsets with display: none because hidden form elements will not be submitted". In my case it's OK because it matches my specifications (a field will be sent as null if it's not been checked by the user).

Xavier Peña
  • 7,399
  • 9
  • 57
  • 99
  • 1
    According to [this answer](http://stackoverflow.com/a/8318442/1195056), modern browsers **do** submit `display:none` elements. However, they still will not submit ones that are disabled. – krillgar Apr 14 '17 at 00:20