Card Templates: User Input 101 (buttons, keyboard shortcuts, etc.) [Guide]

This is a response to Cloze one by one uncovering - #14 by shallash

I’ll briefly explain how basic user input works in JS, so you can add buttons and keyboard shortcuts yourself whenever you need them in the future:

Foundation: The HTML element

The foundation for everything JS-related is the HTML element. Any content that isn’t plain text ( → textNode) is contained inside an element:

  • various content containers (<div>, <span>, <p>, …)
  • links (<a>)
  • images (<img>)
  • etc.

Elements can be accessed in lots of different ways, but the most universally usable function is

element.querySelector

and

element.querySelectorAll

where element can be document (global interface for the whole page) or any other HTML element, and the argument is a CSS selector for the element(s) you want to get.

Examples:

var element = document.querySelector("#elementID")
var chilrenWithSomeClass = element.querySelector(".class")
var allLinks = document.querySelectorAll("a")

You’re probably already familiar with document.getElementById("id") - which is a bit faster than querySelector("#id") because it’s more specific ( → less overhead), but that’s irrelevant unless you’re making hundreds of thousands of calls.

Elements are stored as objects and as such can have lots of different attributes/properties, which can either be accessed with dot-notation or bracket notation.

element.property // standard
element["property"] // useful in specific cases

I recommend analyzing the attributes of different elements with the AnkiWebView Inspector to get a sense for what can be accessed.

User Interaction 101

1: Choose your target Element

Unless its height/width is set to 0, an element occupies a box-shaped area which you can interact with (you can extend that area beyond its actual bounds with CSS pseudo elements):

image

Pick an existing element or wrap the content you want to interact with inside an HTML tag with an id or class. In this sample image, a <div> element with the id “container” is highlighted using AnkiWebView Inspector.

2: Find the right Event

Elements constantly fire events: when you hover them, click/activate them, press keys while they’re in focus etc. You can use these events as triggers for your functions - and there’s tons of them. We only need to find the right one. Google is usually the fastest way - look for Mozilla Dev Network (MDN) pages, as they are of the highest quality. Here’s an overview: Event reference | MDN

I will use the click event for this example, which can - perhaps unintuitive at first - be used on all elements, not just the <button> element.

3: Attach an EventListener to your target

EventListener vs. GlobalEventHandler

To make a function execute when an element is clicked, you got two popular options:

element.addEventListener("click", function)

and the GlobalEventHandler method (via attribute, often used inline)

element.onclick = function

Syntax: oneventname

Only one onclick handler can be assigned to an object at a time. You may prefer to use the EventTarget.addEventListener() method instead, since it’s more flexible.

Beware: Don’t use function() with parentheses as an argument.
We just want to assign the function to call when the event triggers, not execute it right away.

4: Create event handler

Once you assigned an EventListener to your target element, you just need to write a function that executes once the event is triggered.

Depending on the type of function we assigned to our EventListener, there are different options for how to access the element that triggered the event:

this

If we pass a traditional function, we can use this to access the triggering element:

element.addEventListener("click", convertToUpperCase)

function convertToUpperCase() {
    this.innerText = this.innerText.toUpperCase()
}

Notice how this function doesn’t have a return value? The element’s innerText attribute is altered as a side-effect.

event.target

If we got an arrow function, we cannot use this, but we can use the target attribute of the event interface, which is implicitly passed to our arrow function:

var convertToLowerCase = (event) => {
    let element = event.target
    element.innerText = element.innerTex.toLowerCase()
}

element.addEventListener("click", convertToLowerCase)

Notice how I add the eventListener after the initialization of our variable convertToLowerCase? That is something you have to be aware of when using this form of functions. In most cases, you’ll probably be better off with traditional functions, because they are available anywhere in the current scope.

The event interface can also be used in traditional functions of course (target is just one of many useful properties).

Most useful events

  • click (works with mouse and touch input)
    – there’s also a dblclick event
  • keydown (used for keyboard shortcuts)
  • focus & blur (i.e. when tabbing through things)
  • mouseover (for hover effects)

Troubleshooting, common pitfalls

Most of the time, the reason why your interactive button isn’t working lies within the function that handles the event. Because the possibilites for errors are endless, I can’t give you a miracle cure for that sort of issue. But I can give you some common pitfalls I have encountered in the past that do not have anything to do with the event handling itself.

Passing function() instead of function

As mentioned above, when you pass a function with parentheses into an EventListener, JS will invoke the function immediately, not when the event fires.

The element you're trying to attach an EventListener to has not been loaded into the DOM yet

A web page is built up step by step. Sometimes you’ll encounter a situation where you think you successfully attaching an EventListener to an element, but it still doesn’t work, because at that time it wasn’t mounted into the DOM tree yet. This can be incredibly frustrating as it tends to be the last thing you’re looking at when trying to debug your template.

Solution: add the eventListener asynchronously (i.e. with a setTimeout, MutationObserver or Promise)

Cross-client differences (e.g. Anki Desktop and AnkiMobile)

The webview of each Anki client differs a bit. The documents have slightly different structures, classes and most importantly, run on different operating systems. That means some things that work on one client do not work on the other (e.g. lookahead and lookbehind assertions for regular expressions do not work on AnkiMobile, as it uses Safari for mobile, which kinda sucks).

AnkiDroid is not persistent

The most important difference you have to be aware of when striving for cross-platform compatibility is between AnkiDroid and the rest of the ecosystem: AnkiDroid rebuilds the whole document on each card flip, the other clients don’t. That means that elements/variables that remain persistent on those clients are not persistent on AnkiDroid.

I recommend you build your code for the persistent paradigm of Anki Desktop, AnkiMobile and AnkiWeb, as a reliance on the periodic destruction of the document (AnkiDroid) makes your code unusable on the other platforms, but it’s way easier to adjust the other way around.

10 Likes

Thank you! I really appreciate the time you took to write this. I went through the MDN documents referred and to be honest I didn’t understand all of what they said, because my JS knowledge has been built top-down and rather spottily. Still, I think I have a better understanding of userinput now, even though the chokepoint in the problem I stated was making the function for unobscuring the clozes one by one, and not the input required to trigger that function. :slight_smile:

But with the power of [search engine], I was able to cobble together something to solve my problem.

1 Like