DEV Community

Bryan Ollendyke
Bryan Ollendyke

Posted on

Click the word game

Web components pose an existential threat to the educational technology industry [hard stop]. By producing free, complex, and platform portable functionality, we can liberate educational experiences of traditionally entrenched, poorly constructed ecosystems that universities pay millions for.

For today's example of what's possible I built a simple "click the word" game for my students as a codepen. Here's the code and then we'll step into notable concepts of this you could use in your own projects

Implementation

<x-ample answers="implemented,because,is,going">This is a whole bunch of text that is going to be in the middle here but yet we won't see it because of the slot not being implemented</x-ample>
Enter fullscreen mode Exit fullscreen mode

The implementation has the answer in it (OH NOZE!) but at run-time, reads this into a variable and deletes the answer from the dom.

Inspecting the element

There's two ways I accomplish this. The 1st is reading in the Light DOM as data via .innerText. Because we're just trying to mark words we don't need the nodes that might live there.

this.textAry = this.innerText.split(/\s+/g);
Enter fullscreen mode Exit fullscreen mode

Now we've got an array of text like ["word", "another"] as we .split using a regex to ensure all spaces / end-lines get picked up.

We do a similar practice with the answer. To hide the answer from the attributes in the DOM we listen for answers to change in our updated() life cycle of LitElement and split on , in order to generate another Array.

updated(changedProperties) {
    changedProperties.forEach((oldValue, propName) => {
      if (propName === "answers" && this[propName]) {
        this.validAnswers = this[propName].split(",");
        // unset so that people can't cheat
        this.answers = null;
      }
    });
  }
Enter fullscreen mode Exit fullscreen mode

Then when we want to make evaluations we use validAnswers as opposed to answers. Setting this to null we wipe it from the Light DOM.

Rebuilding the "game" aspect

So now we've got an array of values that are correct, and an array of original text.

 updated(changedProperties) {
    changedProperties.forEach((oldValue, propName) => {
      if (propName === "textAry") {
        this.rebuildContents(this[propName]);
      }
    });
  }
rebuildContents(ary) {
    // wipe out inner
    this.shadowRoot.querySelector("#area").innerHTML = "";
    ary.forEach((word) => {
      let span = document.createElement("span");
      span.innerText = word;
      span.setAttribute("tabindex", "-1");
      span.addEventListener("click", this.clickWord.bind(this));
      this.shadowRoot.querySelector("#area").appendChild(span);
    });
  }
Enter fullscreen mode Exit fullscreen mode

.rebuildContents takes an array of text and does the following:

  • Wipes a shadowRoot id marked as area
  • Creates a span element
  • Sets .innerText to the word we're on in the array
  • Sets a tabindex so that keyboard users can focus the element
  • add's an event listener for clicking.

When a span get's clicked, we toggle a data-selected attribute on it which we can then visualize using a basic css selector

span[data-selected] {
  outline: 2px solid purple;
}
Enter fullscreen mode Exit fullscreen mode

CSS applied to selected items

Now we can toggle by "click" the status of each word so that we know what someone wants to mark for testing correctness in identification.

Testing answers

When we tap our Test button, we capture this event and then walk the shadowRoot for what the user selected previously. This attribute can then be used to do a .querySelectorAll and obtain what the user clicked previously without us actually having stored that information!
HTML structure showing span tags that have a attribute set

  testAnswers(e) {
    const selected = this.shadowRoot.querySelectorAll("#area span[data-selected]");
    console.log(selected);
    for (var i=0; i < selected.length; i++) {
      const el = selected[i];
      if (this.validAnswers.includes(el.innerText)) {
        el.setAttribute("data-status", "correct");
      }
      else {
        el.setAttribute("data-status", "incorrect");        
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode

We then loop through these span[data-selected] elements that matched our query and see if their innerText was included in the initial validAnswers array. If we get a match, we set (and visualize) correctness, otherwise we show incorrect.

While this element isn't finished, it's a great example of the transformative nature of modern standards. A previously unapproachable problem requiring Flash or Meta's R_act framework can now be achieved with a slim library like Lit and honestly could be VanillaJS with minor tweaks.

In project EdtechJoker my students explore problems like this. Using the Web component standard we can liberate pedagogical approaches like this to empower people to educate others regardless of access to overpriced platforms.

Discussion (1)

Collapse
dannyengelman profile image
Danny Engelman • Edited on