How to implement progressive disclosure UI for non-JS users

Table of Contents

What is Progressive Disclosure UI?

How do you achieve this?

Issues with disabling non-JS/JavaScript browsers

Understand the code

solution


What is Progressive Disclosure UI?

Progressive disclosure UI is a user interface in which content and actions change based on user response. For example, see the following scenario.

The UI changes dynamically based on the user’s answer to the question – “Do you have a doctor’s appointment?” If the user answers yes, the next question will be “Is this your first visit? Otherwise, it will be “Do you want to schedule an appointment?” See the flowchart below.

Depending on the answer to the second question, the next question/content and action will change.

How do you achieve this?

In browsers that support JavaScript, this is easy. You could use yes/no radio buttons and have them automatically submit true, or better yet use an Ajax call and load only the part of the page that contains the appropriate content and actions.

This is not the case for browsers with JavaScript disabled (non-JS users).

The problem of disabling non-JS/JavaScript browsers

In non-JS, auto-submit and Ajax calls will not work. Therefore, any solution discussed above will fail for non-JS users. If using radio buttons, the page update will not happen when the user clicks Yes/No, which will cause a UI error. This application will not be responsive and accurate as it is expected to be in progressive disclosure status. For example, see the screenshot below.

When you select “Yes” and click Submit“, the following page will be displayed.

Now select No for the first question. Don’t click to submit and pay attention to the UI. This can cause UI errors. See the flowchart above, it says that if there is no doctor’s appointment, you need to ask the user to schedule an appointment.

The second question should be “Do you want to schedule a date?” Compare the error and correct UI below.

In this buggy UI, the content, questions, and actions are incorrect, which may result in incorrect entries in the database when “Submit” is clicked. See the following example:

Note that all these issues exist in non-JS browsers (browsers with JavaScript disabled). To test this, you can disable JavaScript in your browser or use the “ToggleJavaScript” extension in Chrome.

So how to solve this problem?

Here is the solution.

In Visual Studio 2022, create a new ASP.NET Core Web App project-NonJSProgressiveDisclosureUI:

Here is the final Solution Explorer:

Add the Enum folder and create a new Enum YesNoType.

namespace NonJSProgressiveDisclosureUI.Enums
{
    public enum YesNoType
    {
        Unknown,
        Yes,
        No
    }
}

Add a new IndexViewModelmodel in the “Models” folder:

using NonJSProgressiveDisclosureUI.Enums;

namespace NonJSProgressiveDisclosureUI.Models
{
    public class IndexViewModel
    {
        public YesNoType AnswerToHaveTheDoctorAppointmentQuestionIs { get; set; }

        public YesNoType AnswerToIsThisYourFirstVisitQuestionIs { get; set; }

        public YesNoType AnswerToScheduleAnAppointmentQuestionIs { get; set; }

        public bool ShowRadioButtons { get; set; }
    }
}

Update Program.cs – add caching service:

// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddMemoryCache();

Update the main controller:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using NonJSProgressiveDisclosureUI.Enums;
using NonJSProgressiveDisclosureUI.Models;
using System.Diagnostics;

namespace NonJSProgressiveDisclosureUI.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;
        private readonly IMemoryCache _memoryCache;

        public HomeController(ILogger<HomeController> logger, IMemoryCache memoryCache)
        {
            _logger = logger;
            _memoryCache = memoryCache;
        }

        [HttpGet]
        public IActionResult Index()
        {
            var viewModel = new IndexViewModel();
            viewModel.ShowRadioButtons = false;
            return View(viewModel);
        }

        [HttpPost]
        public IActionResult Index(IndexViewModel viewModel, string submit)
        {
            var updatedViewModel = viewModel;
            if (!viewModel.ShowRadioButtons)
            {
                updatedViewModel = SetIndexViewModel(viewModel, submit);
            }
            return View(updatedViewModel);
        }

        public IActionResult Privacy()
        {
            return View();
        }

        [ResponseCache(Duration = 0,
         Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResultError()
        {
            return View(new ErrorViewModel
            { RequestId = Activity.Current?.Id  HttpContext.TraceIdentifier });
        }

        private IndexViewModel SetIndexViewModel
                (IndexViewModel viewModel, string submit)
        {
            var cacheKey = "doctorAppointmentKey";

            IndexViewModel cachedViewModel =
                 (IndexViewModel)_memoryCache.Get(cacheKey)  viewModel;

            switch (submit)
            {
                case "AnswerToHaveTheDoctorAppointmentQuestionIsYes":
                    {
                        cachedViewModel.AnswerToHaveTheDoctorAppointmentQuestionIs =
                                        YesNoType.Yes;
                        break;
                    }
                case "AnswerToHaveTheDoctorAppointmentQuestionIsNo":
                    {
                        cachedViewModel.AnswerToHaveTheDoctorAppointmentQuestionIs =
                                        YesNoType.No;
                        break;
                    }
                case "AnswerToIsThisYourFirstVisitQuestionIsYes":
                    {
                        cachedViewModel.AnswerToIsThisYourFirstVisitQuestionIs =
                                        YesNoType.Yes;
                        break;
                    }
                case "AnswerToIsThisYourFirstVisitQuestionIsNo":
                    {
                        cachedViewModel.AnswerToIsThisYourFirstVisitQuestionIs =
                                        YesNoType.No;
                        break;
                    }
                case "AnswerToScheduleAnAppointmentQuestionIsYes":
                    {
                        cachedViewModel.AnswerToScheduleAnAppointmentQuestionIs =
                                        YesNoType.Yes;
                        break;
                    }
                case "AnswerToScheduleAnAppointmentQuestionIsNo":
                    {
                        cachedViewModel.AnswerToScheduleAnAppointmentQuestionIs =
                                        YesNoType.No;
                        break;
                    }
                case "Submit":
                    break;
            }

            //Cache
            var cacheExpiryOptions = new MemoryCacheEntryOptions
            {
                AbsoluteExpiration = DateTime.Now.AddMinutes(20)
            };
            _memoryCache.Set(cacheKey, cachedViewModel, cacheExpiryOptions);

            return cachedViewModel;
        }
    }
}

Update Index.cshtml:

@using NonJSProgressiveDisclosureUI.Enums
@model IndexViewModel

@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Welcome to Our Hospitals</h1>
        @using (Html.BeginForm("Index", "Home", FormMethod.Post))
        {
            @Html.AntiForgeryToken()

        <partial name="../_DoctorAppointment" model="Model" />

            if (Model.ShowRadioButtons)
            {
              <div style="display:flex; margin-top:35px">
                   <button type="submit" id="submit" name="submit" value="submit">
                    Submit
                   </button>
              </div>
            }
        }
</div>

In the View“>Sharedfolder< Add the following views.

@using NonJSProgressiveDisclosureUI.Enums
@model IndexViewModel

<partial name="../_DoctorAppointmentQuestion" model="Model"/>

@if(Model.AnswerToHaveTheDoctorAppointmentQuestionIs == YesNoType.Yes)
{
    <partial name="../_IsThisYourFirstVisitQuestion" model="Model" />

    @if (Model.AnswerToIsThisYourFirstVisitQuestionIs == YesNoType.Yes)
    {
        <partial name="../_SubmitIdProof" model="Model" />
    }
    else if (Model.AnswerToIsThisYourFirstVisitQuestionIs == YesNoType.No)
    {
        <partial name="../_ProvideRegistrationNumber" model="Model" />
    }
}
else if (Model.AnswerToHaveTheDoctorAppointmentQuestionIs == YesNoType.No)
{
    <partial name="../_ScheduleAnAppointmentQuestion" model="Model" />
}

__DoctorAppointmentQuestion.cshtml

@using NonJSProgressiveDisclosureUI.Enums
@model IndexViewModel

<input asp-for="ShowRadioButtons" type="hidden" />
@{
    var buttonStyleSelected = "text-align:center; width:30%;
        font-weight:600; color:darkslateblue; background-color:yellow";
    var buttonStyleDefault = "text-align:center; width:30%;
        font-weight:600; color:darkslateblue";
    var buttonStyleYes = buttonStyleDefault;
    var buttonStyleNo = buttonStyleDefault;
}

<div style="text-align:left;margin-top:25px">
    Do you have the doctor appointment
</div>

@if (Model.AnswerToHaveTheDoctorAppointmentQuestionIs == YesNoType.Yes)
{
    buttonStyleYes = buttonStyleSelected;
}
else if (Model.AnswerToHaveTheDoctorAppointmentQuestionIs == YesNoType.No)
{
    buttonStyleNo = buttonStyleSelected;
}

@if (!Model.ShowRadioButtons)
{
   <div style="display:flex">
    <button style="@buttonStyleYes" type="submit"
     id="AnswerToHaveTheDoctorAppointmentQuestionIsYes"
     name="submit" value="AnswerToHaveTheDoctorAppointmentQuestionIsYes">Yes</button>
    <button style="@buttonStyleNo" type="submit"
     id="AnswerToHaveTheDoctorAppointmentQuestionIsNo"
     name="submit" value="AnswerToHaveTheDoctorAppointmentQuestionIsNo">No</button>
    </div>
}
else
{
    <div style="text-align:left;margin-top:15px">
        @Html.RadioButtonFor(m => m.AnswerToHaveTheDoctorAppointmentQuestionIs,
        YesNoType.Yes, new { id="AnswerToHaveTheDoctorAppointmentQuestionIsYes" } )
        <label for="yes" style="width:8%">Yes</label>
        @Html.RadioButtonFor(m => m.AnswerToHaveTheDoctorAppointmentQuestionIs,
        YesNoType.No, new { id="AnswerToHaveTheDoctorAppointmentQuestionIsNo" } )
        <label for="yes" style="width:8%">No</label>
    </div>
}

_IsThisYourFirstVisitQuestion.cshtml

@using NonJSProgressiveDisclosureUI.Enums
@model IndexViewModel

@{
    var buttonStyleSelected = "text-align:center; width:30%;
        font-weight:600; color:darkslateblue; background-color:yellow";
    var buttonStyleDefault = "text-align:center; width:30%;
        font-weight:600; color:darkslateblue";
    var buttonStyleYes = buttonStyleDefault;
    var buttonStyleNo = buttonStyleDefault;
}

<div style="text-align:left; margin-top:25px">
    Is this your first visit
</div>

@if (Model.AnswerToIsThisYourFirstVisitQuestionIs == YesNoType.Yes)
{
    buttonStyleYes = buttonStyleSelected;
}
else if (Model.AnswerToIsThisYourFirstVisitQuestionIs == YesNoType.No)
{
    buttonStyleNo = buttonStyleSelected;
}

@if (!Model.ShowRadioButtons)
{
    <div style="display:flex">
        <button style="@buttonStyleYes" type="submit"
         id="AnswerToIsThisYourFirstVisitQuestionIsYes" name="submit"
         value="AnswerToIsThisYourFirstVisitQuestionIsYes">Yes</button>
        <button style="@buttonStyleNo" type="submit"
         id="AnswerToIsThisYourFirstVisitQuestionIsIsNo" name="submit"
         value="AnswerToIsThisYourFirstVisitQuestionIsNo">No</button>
    </div>
}
else
{
    <div style="text-align:left;margin-top:15px">
        @Html.RadioButtonFor(m => m.AnswerToIsThisYourFirstVisitQuestionIs,
         YesNoType.Yes, new { id="AnswerToIsThisYourFirstVisitQuestionIsYes" } )
        <label for="yes" style="width:8%">Yes</label>
        @Html.RadioButtonFor(m => m.AnswerToIsThisYourFirstVisitQuestionIs,
         YesNoType.No, new { id="AnswerToIsThisYourFirstVisitQuestionIsNo" } )
        <label for="yes" style="width:8%">No</label>
    </div>
}

_ProvideRegistrationNumber.cshtml

<div style="display:flex;margin-top:30px;">
    <label for="regNo" style="width:30%;text-align:left">
     Please provide registration number: </label>
    <input type="text" style="width:30%;" id="regNo" name="regNo">
</div>

_ScheduleAnAppointmentQuestion.cshtml

@using NonJSProgressiveDisclosureUI.Enums
@model IndexViewModel

@{
    var buttonStyleSelected = "text-align:center; width:30%;
    font-weight:600; color:darkslateblue; background-color:yellow";
    var buttonStyleDefault = "text-align:center; width:30%;
    font-weight:600; color:darkslateblue";
    var buttonStyleYes = buttonStyleDefault;
    var buttonStyleNo = buttonStyleDefault;
}

<div style="text-align:left; margin-top:25px">
    Do you want to schedule an appointment
</div>

@if (Model.AnswerToScheduleAnAppointmentQuestionIs == YesNoType.Yes)
{
    buttonStyleYes = buttonStyleSelected;
}
else if (Model.AnswerToScheduleAnAppointmentQuestionIs == YesNoType.No)
{
    buttonStyleNo = buttonStyleSelected;
}

@if (!Model.ShowRadioButtons)
{
    <div style="display:flex">
        <button style="@buttonStyleYes" type="submit"
         id="AnswerToScheduleAnAppointmentQuestionIsYes" name="submit"
         value="AnswerToScheduleAnAppointmentQuestionIsYes">Yes</button>
        <button style="@buttonStyleNo" type="submit"
         id="AnswerToScheduleAnAppointmentQuestionIsNo" name="submit"
         value="AnswerToScheduleAnAppointmentQuestionIsNo">No</button>
    </div>
}
else
{
        <div style="text-align:left;margin-top:15px">
            @Html.RadioButtonFor(m => m.AnswerToScheduleAnAppointmentQuestionIs,
            YesNoType.Yes, new { id="AnswerToScheduleAnAppointmentQuestionIsYes" } )
            <label for="yes" style="width:8%">Yes</label>
            @Html.RadioButtonFor(m => m.AnswerToScheduleAnAppointmentQuestionIs,
            YesNoType.No, new { id="AnswerToScheduleAnAppointmentQuestionIsNo" } )
            <label for="yes" style="width:8%">No</label>
        </div>
}

_SubmitIdProof.cshtml

<div style="text-align:left; margin-top:30px">
    <label for="idProof" style="width:25%;text-align:left">
     Please Submit ID Proof for Registration: </label>
    <button>Browse</button>
</div>

Understand the code

  • Main Controller> SetIndexViewModel method-updates and caches model values based on user selection.
  • All relevant partial pages will be created.
  • _DoctorAppointment.cshtml – This is the logic that loads the appropriate section of the page with the correct question, content, and application.
  • Use buttons instead of radio buttons and update the model value in the SetIndexViewModel method of the main controller.

Solution

Now run the application and see how the new solution solves all the problems. You can compare this to the flowchart above.

When the user selects “Yes” , it checks if it is the first time visiting.

When the user selects “Yes” for the second question, it asks for proof of identity to register them as a new user.

When the user selects No” for the second question, it will ask for the registration number.

Now, when the user selects “No” for the first question, the second question will change accordingly.

You may have noticed that the second question has been changed appropriately.

Suppose the user selects “Yes” again for the first question. The UI should also retain his previous choice for the second question, which was no in this case. In addition, the second issue is to make appropriate changes.

Therefore, it displays the correct UI in all situations.

If you want to test radio buttons, set ShowRadioButtons=true in the main controller and run the app.

Hope you found this interesting and useful.

https://www.codeproject.com/Articles/5353997/How-to-Achieve-Progressive-Disclosure-UI-for-Non-J