Web CNLearn - Vue Frontend 1

You’ve heard whispers, you’ve heard rumours, but you’ve never seen anything. It’s time to present the beta version of CNLearn (fyi, a lot easier to make a web app than GUI…kind of annoying but oh well..), or at least, present its creation :) at the end of this series it will be live. Now it’s only live on an internal server..so what’s the stack?

As it is currently, the backend is a FastAPI one with PostgreSQL (but I’m looking at Mongo for the flashcards), and the frontend is a Vue application. Vue? Is that React? Why not React? I honestly prefer working with Vue but both are good. So on the fronted is Vue using Vuex for state management.

I do all my development in containers so that my computer doesn’t have a billion different versions of every single package out there and force me to figure out why some dependency is not the right version..I could write a story about why I develop like that, why I’m using Vue, but you’re not here for that. So let’s get started!

Install Vue

Specifically, let’s install vue-cli. Assuming you have a recent version of NodeJS installed:

npm install -g @vue/cli

I do it globally cause it stays in that container. Check if it installed correctly by checking its version. At the time of writing, I have @vue/cli 4.5.12.

Create a project using specific settings

vue create cnlearn_frontend

That will take you through a “questionnaire”. On the first screen, select Manually select features. Make sure Choose Vue Version, Babel, Router, Vuex, Linter/Formatter and Unit Testing are selected (use space to toggle the selection). We won’t be using TypeScript now. We will rewrite it when it’s finished but let’s first write it in JS. Press enter. Choose 3.x for Vue version. Enter. Use history mode (default is Y): Enter. For the Linter, it’s up to you. I’ll use ESLint + Airbnb for this one. Enter. Lint on save. Enter. I’ll be using Jest for the tests. Enter. Config files in dedicated config files. Enter. Don’t save preset (unless you want).

Create initial commit

cd into the directory and let’s create a git repo.

git init

I then added everything in that folder in an initial commit to the repo. The commit can be found here.

Using a UI library

I’m sadly not an amazing designer, so I will use some components that are built by others. I didn’t want to go with Vuetify since they don’t yet support Vue 3, so I went with PrimeVue. Let’s install it and add it to our project.

npm install primevue@^3.3.5 --save
npm install primeicons --save

While that’s downloading and installing, let’s discuss what the frontend will look like. The initial v0.1 will be a search textinput where a Chinese phrase will be entered. It will then be segmented into words (using a third-party library), then the words will appear as result cards underneath. Clicking on them will lead to a Word page, where definitions and information will be presented. On that page, one will also be able to click on the component characters in order to get to a Character page (still Word page) that will display character information including etymology/stroke diagram/etc.

Perfect, writing that was just long enough to get everything installed. In this post we will only add an InputText component that will act as our search. But first, let’s remove the About router link and view. I will also delete the HelloWord component.

Adding PrimeVue to the App

In the main.js file, according to the instructions from here, we should do:

import { createApp } from 'vue';
import PrimeVue from 'primevue/config';
import App from './App.vue';
import router from './router';
import store from './store';
import 'primevue/resources/themes/saga-blue/theme.css';
import 'primevue/resources/primevue.min.css';
import 'primeicons/primeicons.css';

const app = createApp(App);
app.use(store);
app.use(router);
app.use(PrimeVue);
app.mount('#app');

Please note that I am not registering the components with the App in the main.js file. I have before, but was getting some strange behaviour when testing with the newest version of vue-test-utils. When I figure out how to do it with globally registered components, I’ll change back to that.

Creating the SearchInput component

In the components directory, create a SearchInput.vue file. What will it have? For now, just an InputText component. The input field will be connected to a data property called searchString via a v-model. Please note I am using a few data-test attributes to make testing easier.

<template>
  <div>
    <p data-test="searched_for">You searched for:</p>
    <p data-test="search_string" v-if="searchString.length > 0">
      {{ searchString }}
    </p>
    <span class="p-input-icon-left">
      <i class="pi pi-search" />
      <InputText
        type="text"
        class="search_box"
        id="search_box"
        v-model="searchString"
        placeholder="Search"
        data-test="search_box"
      />
    </span>
  </div>
</template>

<script>
import InputText from 'primevue/inputtext';

export default {
  name: 'SearchInput',
  props: {},
  components: {
    InputText,
  },
  data() {
    return {
      searchString: '',
    };
  },
};
</script>

<style>
</style>

In our router/index.js file we only have the Home view at “/”.

import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue';

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
];

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
});

export default router;

The Home.vue view is as follows:

<template>
  <div class="home">
    <SearchInput />
  </div>
</template>

<script>
import SearchInput from '@/components/SearchInput.vue';

export default {
  name: 'Home',
  components: {
    SearchInput,
  },
};
</script>

Running the app

Let’s see if it works. Run npm run serve and check your browser at the address indicated. Then enter a few words in the search box and they should appear above it. Yay! The app is now complete…

Testing

But what is that? You say you want more tests??? You got it. In the tests/unit/ directory, create a SearchInput.spec.js file. We will test to see if the component mounts properly. If the search string is empty, we shouldn’t be able to see the “search_string” but we should see the “Searched for” text as well as the text input. Once there is some text in the text input field, we should see all three and the text should match. This is what we are currently testing.

import { mount } from '@vue/test-utils';
import SearchInput from '@/components/SearchInput.vue';
import PrimeVue from 'primevue/config';


describe('SearchInput', () => {
  test('Checks if the component got mounted', async () => {
    const wrapper = mount(SearchInput, {
      global: {
        plugins: [PrimeVue]
      }
    });
    expect(wrapper.find('[data-test="searched_for"]').exists()).toBe(true)
    expect(wrapper.find('[data-test="search_box"]').exists()).toBe(true)
    expect(wrapper.find('[data-test="search_string"]').exists()).toBe(false)
  });
  test('Checks if inputting values works', async () => {
    const wrapper = mount(SearchInput, {
      global: {
        plugins: [PrimeVue]
      }
    });
    // set the searchString data using setData method
    await wrapper.setData({ searchString: "hello" })
    expect(wrapper.find('[data-test="search_string"]').exists()).toBe(true)
    const search_string = wrapper.find('[data-test="search_string"]').element.textContent
    expect(search_string).toBe("hello")

    // let's also "write some text" in the textfield using setValue
    await wrapper.find('[data-test="search_box"]').setValue("general")
    const updated_search_string = wrapper.find('[data-test="search_string"]').element.textContent
    expect(updated_search_string).toBe("general")
  });
});

And that’s it for this post. In the next one we will connect our app to the Vuex store. See you soon!

Also, the git commit for this post is here.