Housetab 1 - Creating a first run tutorial
This is the first in (hopefully) a series of posts coming out of creating the web application HouseTab with the haskell web framework Snap. All the source for the application is available at http://darcsden.com/position/housetab, and these posts will draw heavily from what is there (linking back where relevant).
One feature that this application has is a tutorial that runs the first time you log on, guiding you through setting up your account. We decided that we did not want this to be something that was separate from the application, but rather a set of dialogs / numbers that appeared within the application, so that not only would the tutorial explain how to set up your account, you would be doing it while going through the tutorial.
If you want to see this in action, feel free to create an account on HouseTab - you can always delete it (permanently and easily) from the Settings page.
There were a few decisions that we made. First, the tutorial would run the first time a new user signed in, but anyone could prompt it to start at any point. Second, the tutorial should be able to remember where you are beyond a page reload (so don’t store state in javascript). The third decision was that the tutorial should follow your actions - recognizing when you have completed a step and automatically proceeding to the next step.
A couple tools made this possible: the Heist templating library and the ajax add-on to it that I released at https://github.com/dbp/heist-async. I also use the snap-auth library to provide session support (and user auth, but that is not covered here). From now on, I’m going to assume familiarity with those, so briefly look over them now if you haven’t already seen them.
To maintain the active status of the tutorial, there is a simple boolean stored with every account. The step in the tutorial is stored in a session variable. Originally I was going to store it permanently, but it seemed that it would be more confusing to log in again days, weeks, or months later and end up half-way through the tutorial. The session variable is a simple number, and a library function is provided that changes it, given what it currently is (ie, set to 3 if it currently is 2):
tutorialStep user old new =
if not (tutorialActive user) then return () else do
st <- getFromSession "tutorial-step"
if st == Just old then setInSession "tutorial-step" new
else return ()
This provides half of the functionality - when a user performs an action (for example, the first step of the tutorial is creating a person on the account), provided they are in the tutorial and at the given step, proceed to the next step. This means that they won’t go backwards if they repeat an earlier step, and can’t accidentally skip ahead by performing another action. An example from the code is:
...
do saveHouseTabPerson $ person' { pHTId = htid}
nu <- recalculateTotals user
tutorialStep user "1" "2"
... $ renderHT "people/add_success"
...
The other half is where the tutorial is actually displayed. First, we created a splice (this is Heist terminology, if it doesn’t make sense, read the Heist tutorial linked to above) that displays it’s children if the current tutorial step is that specified in the step attribute (from Views/Account.hs):
import qualified Data.Text.Encoding as TE
import qualified Text.XmlHtml as X
tutorialSplice :: Splice Application
tutorialSplice = do
node <- getParamNode
s <- lift $ getFromSession "tutorial-step"
case X.getAttribute "step" node of
Just step | Just (TE.encodeUtf8 step) == s
-> return (X.elementChildren node)
_ -> return []
Now for the simple cases, we show the tutorial box on the event of a full page load. This looks, in the page template, like:
...
<tutorial step="1">
<apply template="tutorial/1"></apply>
</tutorial>
<tutorial step="2">
<apply template="tutorial/2"></apply>
</tutorial>
<tutorial step="3">
<apply template="tutorial/3"></apply>
</tutorial>
...
One of these pages looks like (collapsed the base and template into one for conciseness):
<div-async name="tutorial" id="tutorial">
<div class="content">
<p>
Welcome to the HouseTab Tutorial.
Follow these steps to finish setting up your account:
</p>
<p>
<span class="num">1.</span>
Add at least one user to your account.
</p>
</div>
<a-async class="end" href="/tutorial/deactivate">
End Tutorial
</a-async>
</div-async>
If this were a non-ajax application, this would probably be good enough - when the next page load occurred, the correct step of the tutorial would be selected. To do it with ajax, using heist-async, it isn’t actually much more difficult. First, be sure that the tutorial box is wrapped inside a <div-async> (which it is above), so that it can be replaced be a later page fragment. Then, in the template that is sent down to update the page (in the example above, this template is “people/add_success”), simply conditionally include the next step of the tutorial, which will replace the tutorial box, using the exact same code as it is included for the case of a non-ajax page load:
<tutorial step="2">
<apply template="tutorial/2"></apply>
</tutorial>
One thing that is important is that this box is not included twice - so the second load should only accur in the special page that is only for ajax responses - in this case, "people/add_success".
Now the last part is the numbered prompts - the idea is, when you get to step 2, there is the box at the top of the page that tells you what to do, but there is also a red circle with a 2 in it that appears on the page where the action you should be performing is. Again, this is simply a matter of conditionally including the proper div’s in the places they belong. They will be positioned and styled with CSS. Since the fragments of the pages are the same templates whether they are loaded partially via ajax or via a full page load, there is nothing special that needs to be done to make it work for ajax. An example is the add person form (which is the first step of the tutorial. form cut down for presentation):
<div-async name="add-person" class="addPerson" id="adduser">
<form-async action="/people/add" method="POST">
<h2><label for="name">Add a new user:</label></h2>
<input name="name" type="text" value="$(name-value)" />
<button type="submit" title="" class="addform_submit" />
</form-async>
<tutorial step="1">
<div id="tutorial-1">
</div>
</tutorial>
</div-async>
That’s all for this short first post!