DEV Community

Cover image for Building a Wordle helper with Vue.js 3.0
Leonel Mandarino
Leonel Mandarino

Posted on

Building a Wordle helper with Vue.js 3.0

Howdy, strangeršŸ¤ . How you doin'? I'm in the process of learning Vue.js and as an easy project to get started with the main concepts I'll be building a Wordle helper.
So... since you are here, you might as well stick around and take this as an introductory article to Vue or as a project idea if you will šŸ˜.

But first let's get...

The basic idea šŸŒ 

How does this "Wordle helper" works? First you need to know what Wordle is... okay I'll give you 2 secs, go google it quick... here let me help you.
Right! It would be very helpful to have a list of Wordle words because I haven't memorized the near 13000 words on the game. Also it would be VERY helpful to filter these words as needed based on the guesses we make in the game.

For example we could have this interface:

Helper initial interface

Please take a breath and be sure to not get your mind blown...

Getting our hands dirty

Okay we have everything we need to start coding fun stuff. Now let's create a simple Vue app:

npm init vue@latest
Enter fullscreen mode Exit fullscreen mode

And after removing the files of the default Vue greeting, I've come up with this directory structure:

Directory structure

Hey but wait a second, what are those folders and files? We'll get there.

Showing the words

First we need the dataset with all the words from Wordle. After 3 intense seconds on the browser I found this repo. Let's define a function to fetch the words, also let's extract this function to it's own file just to remove some unncesessary logic from the main file and keep things simple.

// src/api/wordList.js

export async function retrieveWords() {
  let words_txt = await fetch(
    "https://raw.githubusercontent.com/tabatkins/wordle-list/main/words"
  ).then((res) => res.text());

  // This is useful because it will return an array of just words
  return words_txt.split("\n");
}
Enter fullscreen mode Exit fullscreen mode

We need a way to show the list of possible words given the letters on the inputs. This should do the trick for now.

// src/App.vue

<script setup>
import { ref } from "vue";
import { retrieveWords } from "./api/wordList.js";

retrieveWords().then((data) => {
  total_words = m_words.value = data;
});

// I've kept the the main list with all the words on it's own variable.
// But I've defined another array which will contain the *filtered* words
// after inserting letters on the inputs.

let total_words = [];
const m_words = ref(null);

</script>

<div>
  <h3>Possible words: {{ m_words.length }}</h3>
  <ul>
    <li v-for="(word, index) in m_words" :key="index">{{ word }}</li>
  </ul>
</div>
Enter fullscreen mode Exit fullscreen mode

Inputs

We need to add the inputs that we'll use to filter the list of words! We need to treat each letter on the input as an independent box because the order of the letters matter. So let's define the green, yellow and grey filters.

const word_filters = ref({
  green_letters: ["", "", "", "", ""],
  yellow_letters: ["", "", "", "", ""],
  grey_letters: "",
});
Enter fullscreen mode Exit fullscreen mode

And... we need to render this on the template

  <div>
    <h3>Green letters:</h3>
    <input
      v-for="n in 5"
      :key="n"
      v-model.trim="word_filters.green_letters[n - 1]"
      maxlength="1"
      size="1"
      placeholder="_"
    />
  </div>
  <div>
    <h3>Yellow letters:</h3>
    <input
      v-for="n in 5"
      :key="n"
      v-model.trim="word_filters.yellow_letters[n - 1]"
      maxlength="1"
      size="1"
      placeholder="_"
    />
  </div>
  <div>
    <h3>Grey letters:</h3>
    <input v-model.trim="word_filters.grey_letters" />
  </div>
Enter fullscreen mode Exit fullscreen mode

Filtering

Let's take a 5 minute break from Vue. Before we continue we need to define the pieces that will do the actual computation and filtering. We'll need the following:

  • filterMatching words, for the green letters on the correct position
  • filterContains words, for the yellow letters contained in the word but not in the position specified
  • filterExcludes words, for the grey letters that should not be in the word.

No need to think too hard... final result:

// src/lib/matcher.js

// Exporting the functions that we will use
export function filterMatching(words, letters) {
  const matching_words = words.filter((word) =>
    isMatchingWord(word, normalize_letters(letters))
  );
  return matching_words;
}

export function filterContains(words, letters) {
  const matching_words = words.filter((word) =>
    hasLetters(word, normalize_letters(letters))
  );
  return matching_words;
}

export function filterExcludes(words, letters) {
  const matching_words = words.filter((word) =>
    excludeWithLetters(word, normalize_letters(letters))
  );
  return matching_words;
}


// ==============Helper functions===================
function normalize_letters(letters) {
  return Array.from(letters).map((letter) => letter.toLowerCase());
}

function isMatchingWord(word, match) {
  for (let index = 0; index < 5; index++) {
    const match_letter = match[index];
    const word_letter = word[index];

    if (match_letter === "") {
      continue;
    }

    if (match_letter !== word_letter) {
      return false;
    }
  }

  return true;
}

function hasLetters(word, contains) {
  for (let i = 0; i < contains.length; i++) {
    const letter = contains[i];
    if (!word.includes(letter) || word[i] === letter) {
      return false;
    }
  }
  return true;
}

function excludeWithLetters(word, exclude) {
  for (let i = 0; i < exclude.length; i++) {
    const letter = exclude[i];
    if (word.includes(letter)) {
      return false;
    }
  }
  return true;
}
Enter fullscreen mode Exit fullscreen mode

Connecting the dots

Now we need to put all of this together. Let's also define a function to fire the filters and another to reset the list with all the words

<script setup>
// ...

function handleReset() {
  m_words.value = total_words;
}

function handleSubmit() {
  let words = m_words.value;
  const filters = word_filters.value;
  words = filterExcludes(words, filters.grey_letters);
  words = filterContains(words, filters.yellow_letters);
  words = filterMatching(words, filters.green_letters);

  m_words.value = words;
}
</script>
Enter fullscreen mode Exit fullscreen mode

And the template:

<template>
  <h1>Wordle helper!</h1>
  <!-- ... -->
  <div>
    <button @click="handleReset">Reset</button>
    <button @click="handleSubmit">Save</button>
  </div>

  <div>
    <h3>Possible words: {{ m_words.length }}</h3>
    <ul>
      <li v-for="(word, index) in m_words" :key="index">
        {{ word }}
      </li>
    </ul>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Running the extra mile

There is a little flaw in our app. As you might have noticed the app lags a bit when loading the 30000 words, that's because we are rendering 30000 <li> items on the HTML when the page loads.

To fix this, we can opt for using a Virtual Scroller (luckly for us there is one for Vue 3.0) this will keep our page for rendering so many HTML tags and it will create a smooth and clean experience for our users.

Here is the final piece of script

<script setup>
// ...

// This is used by the template to put 3 words per row.
// These "rows" are generated dynamically on scroll by the RecycleScroller component
function sliceIntoChunks(arr, chunkSize) {
  const res = [];
  for (let i = 0; i < arr.length; i += chunkSize) {
    const chunk = arr.slice(i, i + chunkSize);
    res.push(chunk);
  }
  return res;
}
</script>
Enter fullscreen mode Exit fullscreen mode

And template

<template>
  <h1 class="display-1">Wordle helper!</h1>

  <div v-if="m_words !== null" class="words-container">
    <h3 class="display-6">Possible words: {{ m_words.length }}</h3>
    <RecycleScroller
      class="scroller"
      :items="sliceIntoChunks(m_words, 3)"
      :item-size="45"
      :keyField="null"
      page-mode
      v-slot="{ item }"
    >
      <va-button
        class="word-option"
        :rounded="false"
        color="info"
        gradient
        v-for="(word, index) in item"
        :key="index"
      >
        {{ word }}
      </va-button>
    </RecycleScroller>
  </div>
  <div v-else class="loading-div">
    <va-progress-circle indeterminate />
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Also it would be nice to add some extra css to make things pretty and some quality of life changes to filter the words on each keypress. You can check the final result on my github repo!

Wordler

Wordle helper because all the others weren't that nice

Top comments (0)