Create Step-by-Step Help in a Wizard Form Directly in your Page
Posted on: 2015-06-01
The main idea in this article is to write a custom help for every page without having the user leaving the page. The help must take the hand of the user through all steps required for a specific task. I decided to use a library built by LinkedIn called HopScotch. This open source library is still maintain by Linked and allow to assign html box with an arrow to specific Html. The library is great but is in JavaScript. If you are working with Asp.Net MVC, you may want to have something that will generate all the JSON for you and also put the help trigger only if the current page contains help. This article will show you a way to do it pretty easily. Also, we will do a quick patch to the current version of LinkedIn HopScotch to allow to use the library on dynamic Dom element.
The first step is to download the code from GitHub. Their is no Nuget package, so you will have to go in the page, download the Zip file and go into the distribution folder (dist
). This one contains a CSS file, a JavaScript file and some images. Once you add all those files into your solution you have to alter few of these files. I usually do not like to alter third-party code because it is hard to keep with the next versions, but this time you have no choice if you want to keep your code with the MVC framework (content and script folder). The first modification was with the CSS file that was expecting the image to be on a sibling folder. This is not a big deal if you do not care to have image next to CSS folder. However, the bigger change that must be done is in the JavaScript file. This one need a new public function to add the next element. This is required because the library setup all the wizard's steps when the page load. If you want during your help's steps generate Html and link your help to that dynamically generated Html, than you must also specify dynamically that the next step will be this instance.
This modification is the following:
- Search for getCurrTarget
- Add the following code under the code found in step 1:
this.setNextTarget = function(el) { currTour.steps[currStepNum+1].target = el; };
This allow later to call the setNextTarget function and specifying the Dom Element dynamically generated.
So now we are all set. We can create the steps for a single page. I decided to create an interface called IHelp
. When my View Model class is passed into the View, if this one implements IHelp, than I will display the button to start the wizard help and also generate the JavaScript. This way, I setup once the system and any further page just need to use that interface without having to care about HopScotch at all.
Here is the interface code:
public interface IHelp { HelpViewModel GetHelp(); }
This interface is just method to get the help ViewModel. The HelpViewModel class is the one that contain codes about the HopScotch JavaScript. This is the class that the Html Helper later will use to generate the JSON object and the JavaScript call. It contains the tour id which is something unique. This is used in the case we use multiple page HopScotch wizard which I do not plan to use. This is why I just generate a random id. The list of steps are every help box. Each one contains the title, the description, which element to link to and also JavaScript action if required. Other than that, this view model can set some JavaScript action when we start the help wizard, stop this one or end it. The difference between close and end is that close is whenever the user stop using the help and end is only after every steps are done.
public class HelpViewModel { private readonly string tourId;
public HelpViewModel() { tourId = Guid.NewGuid().ToString(); this.Steps = new List<HelpStep>(); } public string TourId { get { return tourId; } }
public List<HelpStep> Steps { get; set; } public JRaw OnStart { get; set; } public JRaw OnClose { get; set; } public JRaw OnEnd { get; set; } public string StepsJson { get { return JsonConvert.SerializeObject(this.Steps ,Formatting.Indented , new JsonSerializerSettings{ContractResolver = new CamelCasePropertyNamesContractResolver()}); } }
}
In this code, few things worth to stop by. First, we are using the type JRaw
. JRaw come from Json.Net library and allow to serialize data that won't be proceeded during serialization. This is required for JavaScript function to be generated as a function and not a string. The second interesting part is the StepsJson method. This one is there for steps. The help take care to build the HelpViewModel which is the data for the wizard itself while the steps are done automatically with the serialization.
Steps are pretty straightforward. The class contains a one to one relationship with the JavaScript equivalence.
public class HelpStep { public HelpStep() { }
public HelpStep(string title, string content, JRaw target, string placement) { this.Title = title; this.Content = content; this.Target = target; this.Placement = placement; } public string Title { get; set; } public string Content { get; set; } public JRaw Target { get; set; } public JRaw OnPrev { get; set; } public JRaw OnNext { get; set; } public JRaw OnShow { get; set; } public string Placement { get; set; } }
The Target property is the JQuery selector to the item. This could be also a straight JavaScript selector to the Dom element. The next part is to have something that use the View Model and to generate the HopScotch code. The partial view that hack has an helper is only used when the View Model implement the IHelp interface. Inside the layout.cshtml :
@if (Model is IHelp) { @Html.Partial("_helpScript", Model as IHelp) }
This code call the partial view:
@model ViewModel.Infrastructures.Definitions.IHelp
<script type="text/javascript"> @{
var infoTour = Model.GetHelp();
}
var tour = { id: "@infoTour.TourId", steps: @Html.Raw(infoTour.StepsJson), onClose : @infoTour.OnClose, onError : function (e){console.log(e);}, showPrevButton: false };
//See_Header.cshtml for the ID $('#page-help-button').click(function () { hopscotch.startTour(tour); });
</script>
This create the help (called tour in Hopscotch). Then, it add to the help button the action to start the help wizard. The generated code look like the one that can be found in the official website. To create the first help wizard, you just need to have your view model having the implementation using the IHelp. Here is an example:
public HelpViewModel GetHelp() {
return new HelpViewModel() {
Steps = {
new HelpStep("Join Contest", "You can join one or many contests.", new JRaw("$('#contest-unsubscribed-panel-header')[0]"), "bottom"),
new HelpStep("Your Contest", "All the contest you are subscribed are listed here.", new JRaw("$('#contest-subscribed-panel-header')[0]"), "top")
{OnShow = new JRaw("function(){createTemporaryContest();}")},
new HelpStep("All Existing Contests", "This is the title of the contest", new JRaw("$('tempo-title')[0]"), "top") {OnShow = new JRaw("function(){hopscotch.setNextTarget($('#tempo-description')[0]); }")},
new HelpStep("Time", "This is the remaining time of the contest", new JRaw("$('#tempo-description')[0]"), "bottom") {OnShow = new JRaw("function(){hopscotch.setNextTarget($('#tempo-number')[0]); }")},
new HelpStep("Participants", "This is the number of user already the contest", new JRaw("$('#tempo-number')[0]"), "top") {OnShow = new JRaw("function(){hopscotch.setNextTarget($('#tempo-description')[0]); }")},
new HelpStep("More Details", "Clicking on the contest will show you the full description of the contest. If you see this box having a shining animation, it means that is it a brand new contest.", new JRaw("$('#tempo-click-description')[0]"), "top") {OnShow = new JRaw("function(){deleteTemporaryContest(); }")},
new HelpStep("What Happen If...", "Once you join, the contest will leave this container and get into the joined contest above", new JRaw("$('#contest-subscribed')[0]"), "top") {OnNext = new JRaw("function(){deleteTemporaryContestFromJoinedContest();}")},
new HelpStep("Done", "Now is it your turn!", new JRaw("$('h1')[0]"), "bottom"){OnShow = new JRaw("function(){removeEverything();}")}, }, OnClose = new JRaw("function(){removeEverything();}") };
}
Their is few things that is not perfect there, but it is quite rapid to work this way. It can be easily re-factored later too. As you can see, I setup every steps in order. In that example, string are not yet localized, but we can leverage of .Net Resource file easily by having all steps in the ViewModel. The selector require to use JRaw
as explained earlier. You can see some method call for OnShow. These method create temporary Dom element that are done if the user really do the action. For example, the createTemporaryContest method create some Dom element, and later, the method deleteTemporaryContest or removeEverything take care to remove those temporary html. The fix we did to LinkedIn HopScotch is used in the OnShow, just a step before this one is used. OnShow allows to do an action when the help's step is shown. This is the perfect time to set the next step on the dynamic generated html element.
Here is an animation of the final result.