In this post, we try to achieve progressive disclosure UI on JavaScript disabled browsers.
What Is Progressive Disclosure UI?
Progressive Disclosure UI is a user interface where the content and actions change based on user responses. For example, see the below scenario.
Based on the user response to the question — ‘ Do you have the doctor appointment?’, the UI changes dynamically. If the user response is Yes, then the next question will be ‘Is this your first visit?’. Otherwise, it will be ‘Do you want to schedule an appointment?’. Please see the flow chart below.
Based on the response to the second question, the next question/content and actions change.
How Do You Achieve This?
In JavaScript enabled browsers, it is easy. You can use radio buttons for Yes/No and make them auto-submit true or a better approach is to use Ajax calls and load just the partial page with appropriate content and actions.
It is not the same case with JavaScript disabled browsers (non JS users).
Problems with Non JS / JavaScript Disabled Browsers
In non JS, auto submit and Ajax calls will not work. So whatever solution is discussed above will fail for non JS users. If you use radio buttons, when the user clicks on yes/no, the page updates will not happen and this will cause faulty UI. The application will not be responsive and accurate as it is expected to be in progressive disclosure. For example, see the following screen shots.
When you choose Yes and click on Submit, the following page is displayed.
Now choose No for the first question. Don’t click on Submit yet and notice the UI. This results in a faulty UI. Please see the flowchart above which says if the doctor appointment is not there, you need to ask the user to schedule an appointment.
The second question should be ‘Do you want to schedule an appointment’. Compare the faulty and Correct UI below.
In this faulty UI, the content, questions and actions are incorrect which might cause wrong entries in DB when Submit is clicked. See the example below:
Please note that all these issues are in non JS browsers (browsers where JavaScript is disabled). To test this, you can either disable JavaScript in your browser or use ‘Toggle JavaScript’ extension in Chrome.
So how do you fix this issue?
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 Enums folder and create new Enum YesNoType
.
namespace NonJSProgressiveDisclosureUI.Enums
{
public enum YesNoType
{
Unknown,
Yes,
No
}
}
Add New Model IndexViewModel
in 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 services:
builder.Services.AddControllersWithViews();
builder.Services.AddMemoryCache();
Update Home 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 IActionResult Error()
{
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;
}
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>
Add the following views in Views > Shared folder.
__DoctorAppointment.cshtml
@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>
Understanding Code
- Home controller >
SetIndexViewModel
method — Based on the user selections, the model values are updated and cached. - All the relevant partial pages are created.
- _DoctorAppointment.cshtml — This is where the logic to load appropriate partial pages with correct questions, content and applications is there.
- Instead of radio buttons, buttons are used and the model values are updated in
SetIndexViewModel
method in Home Controller.
Solution
Now run the application and see how the new solution fixes all the issues. You may compare it with the flowchart above.
When user selects Yes, it checks if it is the first visit.
When user selects Yes for the second question, it asks for ID proof to register him as new user.
When user selects No for the second question, it asks for the registration number.
Now when the user selects No for the first question, the second question changes accordingly.
You might have noticed that the second question has changed appropriately.
Say the user selects Yes again for the first question. The UI should retain his previous selection for second question as well, i.e., No in this case. Also, the second question is to be changed appropriately.
So it is showing the correct UI in all the cases.
If you want to test the radio button ones, set ShowRadioButtons=true
in Home Controller and run the app.
Hope you find this interesting and useful.
History
- 7th February, 2023: Initial version