This is a post about what I worked on this month, why I was motivated to work on it, and the lessons that I learned. As the title says, the project is a clone of Typeform, which is a web application for building and sharing nice looking questionnaires. Here is a demo. Here is the source. Also here is a gif:
It has an amazing User Experience:1. It looks great with its choice of colors and layout2. It is smooth. Navigating between questions is snappy.3. It has keyboard shortcuts for answering questions and navigating the form.4. There are dynamic questions that can change depending on what the user has entered.
But so what? Why does that make it a good candidate for cloning?
Dan Pink talks about 3 pillars of motivation: Autonomy, Mastery, and Purpose.
“Why reach for something you can never fully attain? But it’s also a source of allure. Why not reach for it? The joy is in the pursuit more than the realization. In the end, mastery attracts precisely because mastery eludes.”― Daniel H. Pink, Drive: The Surprising Truth About What Motivates Us
The act of creating an interface with the features listed above using Elm was on the edge of my skillset (Mastery). In other words, I have never been able to achieve that fidelity in the past, but I knew I was capable of it and was therefore highly motivated.
About 6 months ago, I was creating a new static website, but I didn’t want it to look like every bootstrap site ever. I could have found a CSS theme that matched the given design, but those typically have edge cases to work around. Plus, if the design only used 20% of the CSS in the theme, the extra 80% is hard to get rid of because of complications like media queries and nesting rules.
I chose Tachyons for both that project and this one because I can create a fully designed site without ever touching CSS. Literally the only CSS I wrote for the previous project was a couple colors.
Here is a quick example I made showing how quick the iteration process is with tachyons.
Sketching with Tachyons
It is also easier to debug a tachyon design. Normally your browser’s development tools show something like this when investigating how something looks:
Not only is there a lot of classes to debug, but each one can have many properties!
If there was a problem with the layout that needed to be debugged, one would have to open up the HTML and CSS code. This might just be 2 files, but it could be more if the CSS includes other files via a pre-processor, or if the HTML is controlled by JS.
With Tachyons, the only code to worry about is the HTML. Even if the HTML is heavily controlled by JS, it is still easier to debug the element and test different solutions.
For example, here is a similarly complex layout:
Still many CSS classes on those elements, but each one only has one property.
Here, we see all the properties affecting the element and its parents. While it might look just as complex as the previous example, each class only does one thing, so debugging it is much easier.
Another benefit to not having to write CSS is not having to come up with class names. Naming stuff is hard. Conventions like BEM and SMACCS are effective for standardizing names but you still have to come up with the name and they can get long: sidebar__section sidebar__section — large
Wouldn’t it be better if you didn’t have to name anything to begin with? One final note to choosing tachyons for this project: I know CSS better now. Bootstrap and similar frameworks make it very easy to put something together that looks nice in most environments, but instead of learning CSS, you learn those framework’s specific class names and idioms.
For more examples of how using Tachyons can simplify a design, Simon Vrachliotis has a great series about it where he rewrote the design of SocietyOne.
Lots of discussion has already happened on why to chose Elm for front-end development. If articles aren’t your style, then YouTube is your friend.
Two strengths in particular are its flexible type system, and its compiler errors.
Haskell users would be quick to point out that their type system is more flexible, and they would be right! While Elm does not have type classes, compared to JavaScript, it is streets ahead.
When I used to build Angular apps, I might have represented a list of questions in JSON like this:
[{title: "Question 1 - Enter your name",type: "text",value: ""},{title: "Question 2 - Select a Country",type: "dropdown",choices: ["Abkhazia", "Afghanistan", "Aland", "..."]filteredChoices: [],selectedChoice: ""},{title: "Question 3 - Select a Gender",type: "select",choices: ["Male", "Female", "Other"],selectedChoice: ""}]
But we can have Elm give us guarantees with stronger types:
type alias Question ={ title : String, type: QuestionType, answer : String}
type QuestionType= Text TextOptions| Select SelectOptions| Dropdown DropdownOptions
type alias TextOptions ={ internalValue : String}
type alias DropdownOptions ={ choices : List String, filteredChoices : List String, inputValue : String}
type alias SelectOptions ={ choices : List String}
Elm will make us do more work up front to write code that follows this schema, but if we mess up, we have nice errors:
-- NAMING ERROR --------------------------------------------------- elm/Main.elm
Cannot find variable `answr`
510| List.map (\q -> { q | answer = answr }) questions^^^^^^^^^Maybe you want one of the following?
answer
Refactoring data effectively is a good skill to have as a developer. Having both of these features in Elm meant that when changing the application model: 1. Those changes were done correctly. 2. Everything that depends on those changes knows about it.
“When it compiles, it works” — The Elm Community
There were a few parts of this project that I got hung up on for a day or so. I will attempt to cover most of them:
How to get keyboard navigation? Use keyboard-extra, add a Msg, add some case
logic to route to different functions depending on what keys were pressed (Enter, Shift+Up/Down, Escape)
How to smoothly scroll between questions? I used a smoothScroll Javascript library and added a port to tell it what to do.
How to do multiple color schemes without separate CSS files? Just make a type alias to store colors (primary, secondary, background, hover, etc.). Then in the view functions, simply set HTML style attributes. One gotcha is not being able to use CSS hover psuedo classes, so I used elm-dynamic-style.
How to add Markdown to the question text? Easy: elm-markdown
How to navigate Up and Down between Questions and between dropdown items? Use a Zipper structure…. a what?
I first heard about this data structure from Richard Feldman’s talk about Making Impossible States Impossible. If you are new to Elm or learning it, I recommend watching the entire video as it is mind opening.
A Zipper is a technique for traversing and updating a data structure. In my case the data structure was a List, but it can also be applied to Trees. The type signature for a list-based Zipper is: type Zipper a = Zipper (List a) a (List a)
The elm-listzipper library implements an API to use the zipper technique. With it, I can change my Lists to Zippers and easily traverse them. The most common functions I used were map, mapCurrent, next, previous, and find.
For example, the list of questions in the demo is a “Zipper Question”, and the dropdown options in the demo is a “Zipper String”. This allowed common operations (next item, previous item, get selected item, update selected item) to be performed with one line of code.
-- The updated model for the filterable dropdown, notice the Zippertype alias DropdownOptions ={ choices : List String, filteredChoices : Zipper String, inputValue : String, showList : Bool}
Because of Elm’s compiler messages, you can simply change your definition and follow the trail of errors until the code compiles.
Here is a snippet that demonstrates handling a navigation event like Up and Down keypresses. If I didn’t use a zipper, I would either have to set a “selected id” or an “isSelected” flag and handle the edge-case logic. With the zipper, I can just call next or previous and give it a default in case there’s an error.
handleUpOrDown : Direction -> Model -> ModelhandleUpOrDown direction model =letnextZipper =case direction ofUp ->Zipper.previous model.filteredChoicesDown ->Zipper.next model.filteredChoicesin{ model | filteredChoices = nextZipper |> Zipper.withDefault "Not Found :(" }
Updating the item that the zipper is focused on is also easy with mapCurrent.
setQuestionIsFocused : Zipper Question -> Zipper QuestionsetQuestionIsFocused zipper =Zipper.mapCurrent (\x -> { x | isFocused = True }) zipper
The main goal of this project was to learn more Elm and use Tachyons with it. If there is enough interest to clone more of Typeform, I will build an interface to interactively build questionnaires and add user support. For now, the code needs refactoring, which is the easy part!
I am in the market for remote web development work. If you’re interested in hiring me, send me an email!
David Streeter Consulting_Website Optimizations | Re-designs | New Sites | Webdev consulting_davidstreeterconsulting.com