Try   HackMD

How to build Keyboard trainer app (html/css/js/?vue)

Initial idea and features

When my son was 1.5 y.o. I noticed that he enjoy playing with keyboard, and I thought: it would be good if he get more feedback from the laptop than password hidden symbols on Ubuntu lock screen, or text somewhere as text editor. The idea became more relevalt after a news: “Two kids found a screensaver bypass in Linux Mint it became dangerous to leave a child with Ubuntu lock sreen (joking).

That's how I planned to code a screen keyboard with:

  • event handler “on key down”
  • popup-like animation highlighting the letter itself and its place on the keyboard,
  • playing sound with its name
  • switching between languages (English, Arabic, Russian)

Idea for an educational project

After I made the app, I thought that it can be a good educational project because:

  • it emulates a real world complex looking hardware it is interesting to digitalize it
  • it is easy to layout with a couple of HTML, CSS tricks
  • it touches basic programming topics, like:
    • web app initialization
    • components approach (App > SwitchLang, Keyboard > Key)
    • templating
    • pass parent state to child (via props)
    • change parent state from child (via methods)
    • CSS flex and animation
    • keyboard events handling
    • playing sound

I thought after all that vue without build will be a good stack for newbies because it hasn't:

  • terminal commands (install, run, build)
  • side packages (dependencies)
  • compiler, bundler settings e.t.c.

You just write components in separate files in a code editor, and they work together as an app without extra steps.

Reserch on similar apps

As a smart person (I hope I am), before coding such an app, I did a research to find something similar. And I didn’t find what I wanted. Most similar apps are:

  • screen keyboards — allows you to type text without phisycal keybord, by clicks/taps on the screen
  • keyboard trainers — give predefined text to type, and then give you feedback — did you type right or wrong, and can stop process if you type wrong symbols

Non of these shows/plays additional info about pressed letters (as I know). They are apps for using keyboard in a new way.

My idea was somewhere between these apps. I want informational app, to provide a user with info about pressed keys: how it looks, sounds and what is its official name.

CODING PROCESS

  1. Install code editor VS Code.
  2. Install extension to it Live server. (How to install Visual Studio Code extensions).

Basic HTML layout

In this section we will do simple HTML/CSS layout for 3 rows of 5 keys each to understand: what parameters we should include into HTML and CSS to achieve a realistic view. We will use this test layout (3 rows, 15 keys) as a draft to design a data model and will scale it to the whole app (6 rows, 80 keys).

index.html

Make a folder for the project keybord-trainer, open it with VS code, and create a file index.html. Type ! and press Tab, you will see a template for empty HTML5 document. Write inside <body> tag something like: “Hello world”, save the file (ctrl+s). And run the app with the Live server (mouse right button click on index.html —> Open with Live Server).

Place the code editor on the left side of the screen, and running app on the right side, so you can see immediately how code updates affect on the app.

Key

Let’s write html code for one key:

<div class="key">
	<div class="main">1</div>
	<div class="shifted">!</div>
</div>

And repeat it for a next four keys:

<div class="key">
	<div class="main">`</div>
	<div class="shifted">~</div>
</div>
<div class="key">
	<div class="main">1</div>
	<div class="shifted">!</div>
</div>
<div class="key">
	<div class="main">2</div>
	<div class="shifted">@</div>
</div>
<div class="key">
	<div class="main">3</div>
	<div class="shifted">#</div>
</div>
<div class="key">
	<div class="main">4</div>
	<div class="shifted">$</div>
</div>

It becomes a plain text column:

`
1
!
2
@
3
#
4
$

It’s time to add some styling.

styles.css

Create a file styles.css next to index.html.

Write in it style for our keys:

.key {
	min-height: 3.4375rem; 
	/*3.4375*16 = 55px (16px is default font size)*/
	background-color: black;
	color: white;
	padding: 0.5rem; /*spacing inside the button*/
	margin: 0.2rem; /*spacing outside the button*/
	border-radius: 0.2rem; /*rounded corners*/
	font-size: 1.5rem;
	cursor: pointer;
}

In index.html in the end of a <head> tag type “link” and press tab. There will appear import css code template. Then press ctrl+space and choose in the menu styles.css. Or, if you don’t like using shortcuts, just type this code:

<link rel="stylesheet" href="styles.css">

Save all changed files ctrl+k s (or with other shortcuts, or save files separately by ctrl+s) and you should see the result:

Row

Wrap all keys in index.html with

<div class="row">
	... here is keys code
</div> 

We need row style to wrap our keys. Add to styles.css:

.row {
	display: flex;
	/* 
flex-direction: row; - default value
that's why or divs arranged in a row
*/
}

Save both files. And you will see the result:

Keys have a minimal width. If we want them to take all available place in the row, we should add to styles.css:

.key {
	...
	flex: 1; 
	/*
	1 is proportion compared to other elements in a flex row
	if we set 2 for one of keys, it will be 2 times wider than other
	*/
}

Now the keys look more realistic:

Row with extra wide key

Lets add a second row with first 5 keys: Tab, Q, W, E, R. Copy all previous code from opening <div class="row"> to </div> and paste it below. Then change text inside each <div class="key">:

...
<div class="row">
		<div class="key">
			<div class="main">Tab</div>
			<div class="shifted"></div>
		</div>
		<div class="key">
			<div class="main">Q</div>
			<div class="shifted"></div>
		</div>
		<div class="key">
			<div class="main">W</div>
			<div class="shifted"></div>
		</div>
		<div class="key">
			<div class="main">E</div>
			<div class="shifted"></div>
		</div>
		<div class="key">
			<div class="main">R</div>
			<div class="shifted"></div>
		</div>
	</div>

Maybe you’ve noticed, that these keys don’t have “shifted” values, and it’s ok, we leave related divs empty.

Tab key should have extra width compared to other keys in a row. We need to specify it somehow in html and css.

index.html

<div class="key Tab">...</div>

styles.css

.key.Tab { 
	flex: 1.3
}

Row with smaller keys

Actually in the keyboard this row is first. But it is third inside our working process.

Copy the first row with all code inside it, and paste it above the first row. Then rewrite content of keys to: Esc, F1, F2, F3, F4, F5.

1st row should have smaller keys than other rows. It means that we need to specify row number in every <div class="row">

index.html:

<div class="row row-1">...</div>
<div class="row row-2">...</div>
<div class="row row-3">...</div>

styles.css

.row-1 .key {
	height: 1rem;
	min-height: 1rem;
	font-size: 0.7rem;
}

Language switcher

It will be 3 rounds with language codes. One of them is active (red background).

index.html

<div class="langSwitcher">
		<div class="lang active">en</div>
		<div class="lang">ru</div>
		<div class="lang">ar</div>
	</div>

To get round we need a div with equal width and height (square) 2rem, and border-radius with half of width/height. Cursor pointer (a hand), and opacity changing on hover invites the user to click the element.

styles.css

.lang {
	width: 2rem;
	height: 2rem;
	border-radius: 1rem;
	cursor: pointer;
}

.lang:hover {
	opacity: 0.7;
}

.langSwitcher .active {
	background-color: red;
	color: white;
}

To place lang code in center of the round (vertically and horizontally), add styles:

.lang {
...
	display: flex;
	align-items: center;
	justify-content: center;
}

To display lang switcher as a row and center it on the page, add styles:

.langSwitcher {
 	display: flex;
 	justify-content: center;
	margin-bottom: 1rem;
}

Congratulations. We have made almost all html/css layout for our app. Now we know, that we need to specify:

  • row number, to style row keys
  • key name, to style key extra width
  • active language in a lang switcher

Data model

Now we have 3 rows and 16 buttons, and it is already 90 line HTML file. If we add all 6 rows and 80 buttons, our code becomes messy and not understandable. Approximately, it will be about a 500 line HTML file. It is only 1 language, and we want to add other languages too.

To make code clear, modular, and maintainable, we should split our data and view. We need to design a data model (a JS object) that represents all data inside the keyboard — all rows and keys.

We need this data model to know how to design our app in a modular way. We will create small components that are responsible for each logical part of the app. And before we do that, it's important to know wich data will be passed to these components.

Inside your project folder, create a new folder keyboardData and create there a file en.js with 2-dimensional array (arrays inside array). Each array represents a row, each object inside an array represents a key:

keyboardData/en.js

const keyboard = [
	[
		{ code: 'Escape', label: 'Esc' },
		{ code: 'F1' },
		{ code: 'F2' },
		{ code: 'F3' },
		{ code: 'F4' },
		{ code: 'F5' }
	],
	[
		{
			code: 'Backquote',
			main: '`',
			shifted: '~',
			shiftedName: 'tilde',
			mainName: 'backquote'
		},
		{
			code: 'Digit1',
			main: '1',
			shifted: '!',
			shiftedName: 'exclamation mark'
		},
		{
			code: 'Digit2',
			main: '2',
			shifted: '@',
			shiftedName: 'at sign'
		},
		{
			code: 'Digit3',
			main: '3',
			shifted: '#',
			shiftedName: 'hash'
		},
		{
			code: 'Digit4',
			main: '4',
			shifted: '$',
			shiftedName: 'dollar sign'
		},
		{
			code: 'Digit5',
			main: '5',
			shifted: '%',
			shiftedName: 'percent sign'
		}
	],
	[
		{ code: 'Tab' },
		{ code: 'KeyQ', main: 'q', shifted: 'Q' },
		{ code: 'KeyW', main: 'w', shifted: 'W' },
		{ code: 'KeyE', main: 'e', shifted: 'E' },
		{ code: 'KeyR', main: 'r', shifted: 'R' }
	]
]

export default keyboard

code identifies a place on a physical keyboard. This code is the same for keyboards in all languages.

label is the text on the key. For a key with the code “Escape” it is “Esc”, for “Space” it can is an empty string.

main is the value returned after key pressed.

shifted is the value returned after key pressed while holding shift key.

mainName is the name of the main symbol.

shiftedName is the name of an additional symbol (with shift).

We don’t always specify all of these props, because sometimes we don’t need them, or they can be calculated from other props. E.g. Tab has only code, because it doesn’t have a returned value (symbol), and its name and label are the same as the code. Escape has code: ”Escape” and label: ”Esc” because we want to display on key shorter version of code. And it also hasn’t returned value (symbol).

Also we need key full names:

  • to display them to the user when he presses the key
  • to get a particular audio file for playing

You could ask: why we specify shifted value for letter keys like “q”, “w”, when we can calculate them Q = 'q'.toUpperCase(). Because it doesn’t work for every language. So, we use a general approach for data to write less code in the future.

You can get all keyboard data (for any language) by pressing each of them and looking to the console.

document.body.addEventListener('keydown', event => {
    const {code, key} = event 
    // or lock at whole event and extract what you need 
    console.log (code, key)
})

JavaScript

Browser apps are coded in JavaScript (JS).

JS was made in 1995 and DOM (document object model in the browser) — in 1998. Writing code in plain (vanilla) JS and direct manipulations with DOM is kinda hard (and old fashioned).

But there is no alternative.

Framework

So programmers invented modern frameworks, that make coding web apps easier, clearer and faster. We will use the simplest of them: Vue. Also frameworks allow us to write JS in a component way.

Setup

In index.html comment all code inside tag <body>. We need it in the future, but not now.

index.html

<body>
    <!-- 
    ...
    -->
</body>

Copy code example from: https://vuejs.org/guide/quick-start.html#without-build-tools (or from here):

<script src="https://unpkg.com/vue@3"></script>

<div id="app">{{ message }}</div>

<script>
  const { createApp } = Vue

  createApp({
    data() {
      return {
        message: 'Hello Vue!'
      }
    }
  }).mount('#app')
</script>

and paste it to index.html after <body> tag. If on top of the page appeared “Hello Vue!” it means that setup works.

Even though it works, we need to orginize code better.

Entry point — index.js

Put first <script> tag into the <head> tag.

index.html

<head>
    ...
    <script src="https://unpkg.com/vue@3"></script>    
</head>

Copy second <script> tag content (and remove whole tag). Create in project root directory a file index.js and paste there what you've copied before.

index.js

const { createApp } = Vue

  createApp({
    data() {
      return {
        message: 'Hello Vue!'
      }
    }
  }).mount('#app')

In index.html, just before closing </body> tag add string:

index.html

    ...
    <script src="./index.js" type="module"></script>
</body>

Attribute type="module" allows us to use ES6 feature import/export. We need it on the next step.

index.html (result)

<head>
    ...
    <script src="https://unpkg.com/vue@3"></script>
</head>
<body>
    <div id="app">{{ message }}</div>
    <!-- ... -->	
    <script src="./index.js" type="module"></script>
</body>

index.js is called the entry point. It mounts all our app code into index.html document.

If you did everything right, you should see "Hello Vue!" at the top of the page as before.

Root component — App.js

Open index.js. The code inside createApp() is a vue component (root component) — cut it (ctrl+x or copy and delete). We will move it to separate file: App.js — create it in a project root directory. Paste there a code you copied before (ctrl+v) to const App. Then export it.

App.js

const App = {
    data() {
        return {
            message: 'Hello Vue!!'
        }
    }
}

export default App

Then import it in index.js and put into createApp()

index.js

import App from './App.js'

const { createApp } = Vue

createApp(App).mount('#app')

If you did everything right, you should see "Hello Vue!" at the top of the page as before.

Components hierarchy

First we create all components as a colored rectangles to test how works our framework. That’s the hierarchy:

<App>
    <LangSwitcher />
        <Keyboard>
            <Key />
        </Keyboard>
</App>

<App> is parent for <LangSwitcher> and <Keyboard>.

<Keyboard> is parent for <Key>.

<App> is grandpa for <Key> :smile:.

App

We already have App component. Just add to

styles.css

...
#app {
	background-color: red;
	padding: 10px;
}

All styles in this section are temporary, we need them to see nesting of the components. Then we'll delete them.

Now App is a red rectangle.

LangSwitcher

Create a directory components in the project root directory, and create there a file LangSwitcher.js

const LangSwitcher = {
	template: `<div class="langSwitcher">LangSwitcher</div>`
}

export default LangSwitcher

and add to

styles.css

...

.langSwitcher {
	background-color: green;
	padding: 10px;
}

Add a newly created component to

App.js

import LangSwitcher from './components/LangSwitcher.js'

const App = {
	template: `App <vue-lang-switcher />`,
	components: {
		'vue-lang-switcher': LangSwitcher
	}
}

export default App

Result:

We see here that App contains LangSwitcher wich is correct.

Keyboard

Create a file Keyboard.js in a components folder

Keyboard.js

const Keyboard = {
	template: `<div class="keyboard">Keyboard</div>`,
}

export default Keyboard

Add to

styles.css

...
.keyboard {
	background-color: blue;
	padding: 10px;
	display: flex; /*to display keys in a row, on next step*/
}

Add a new component Keyboard to

App.js

import Keyboard from './components/Keyboard.js'
import LangSwitcher from './components/LangSwitcher.js'

const App = {
	template: `App 
	<vue-lang-switcher />
	<vue-keyboard />
	`,
	components: {
		'vue-lang-switcher': LangSwitcher,
		'vue-keyboard': Keyboard
	}
}

export default App

Now the app looks like:

Key

Create file Key.js in components directory

Key.js

const Key = {
	template: `<div class="key">Key</div>`
}

export default Key

Add to

styles.css

.key {
	background-color: yellow;
	padding: 10px;
	color: black;
}

As you remember Key is the child of Keyboard so it should be imported in Keyboard, not in App.js.

Keyboard.js

import Key from './Key.js'

const Keyboard = {
	template: `<div class="keyboard">
                    Keyboard
                    <vue-key />
                    <vue-key />
                    <vue-key />
                </div>`,
	components: {
		'vue-key': Key
	}
}

export default Keyboard

After saving all files, that’s how our app looks:

Our component hierarchy works well. All components have correct nesting. How we said it the beginning of the chapter:

<App> is parent for <LangSwitcher> and <Keyboard>.
<Keyboard> is parent for <Key>.

Components (real)

For now our components are a colored rectangles with some static content (text). In real components all content is dynamic, passed by props.

Props are external params (variables) that we pass from parent to child.

The idea of a component is that we recieve external data from parent (props), and put them into html-like template with empty slots for these props.

Key

passing prop (v-bind)

Open index.html, copy one of the key code, that we commented before, and paste it to template in

Key.js

const Key = {
                template: `<div class="key">
                                <div class="main">1</div>
                                <div class="shifted">!</div>
                            </div>`
}

export default Key

We described our "Data model" in a section above. Open keyboardData/en.js and find the data for a single key:

keyboardData/en.js

{
	code: 'Digit1',
	main: '1',
	shifted: '!',
	mainName: 'one',
	shiftedName: 'exclamation mark'
}

copy it.

We will pass this object to Key component as a prop keyContent.

Keyboard.js

import Key from './Key.js'

const keyData = { /*paste here copied data*/ 
	code: 'Digit1',
	main: '1',
	shifted: '!',
	mainName: 'one',
	shiftedName: 'exclamation mark'
}

const Keyboard = {
	template: `<div class="keyboard">
                        Keyboard
                        <vue-key :keyContent="keyData" />
		    </div>`,
	components: {
		'vue-key': Key
	},
	data() {
		return { keyData }
	}
}

export default Keyboard

Here we:

  • described data of Key in const keyData
  • made the component see this data with data(){} method
  • passed keyData as a prop to child Key component

Notice, when we pass prop, we use colon : before its name.

<vue-key :keyContent="keyData" />

In that case framework interpet “keyData” as a variable name.

Otherwise (wihout :) it would be interpreted as a string.

<vue-key keyContent="keyData" /> —> Key component will recieve string “keyData” instead of the object keyData.

Now we should warn Key about a new prop.

Key.js

const Key = { template: `<div class="key"> <div class="main"> {{ keyContent.main }} </div> <div class="shifted"> {{ keyContent.shifted }} </div> </div>`, props: { keyContent: Object, } } export default Key
  1. We told component about a prop props: {keyContent: Object}
  2. We told template how to use props by {{ }}.

Result:

Return to Keyboard.js. We already have all key data in keyboardData/en.js so let’s import and use it, instead of a single key data:

Keyboard.js

import Key from './Key.js'
import keyboardData from '../keyboardData/en.js'

const Keyboard = {
	template: `<div class="keyboard">
                        Keyboard
                        <vue-key :keyContent="keyboardData[1][0]" />
                        <vue-key :keyContent="keyboardData[1][1]" />
                        <vue-key :keyContent="keyboardData[1][2]" />
                        <vue-key :keyContent="keyboardData[1][3]" />
                        <vue-key :keyContent="keyboardData[1][4]" />
                        <vue-key :keyContent="keyboardData[1][5]" />
                    </div>`,
	components: {
		'vue-key': Key
	},
	data() {
		return { keyboardData }
	}
}

export default Keyboard

Now we have less code and more keys:

computed (template variables)

If we try to display first row keyboardData[0] (Esc, F1, F2, …)

    ...
    template: `<div class="keyboard">
                        <vue-key :keyContent="keyboardData[0][0]" />
                        <vue-key :keyContent="keyboardData[0][1]" />
                        <vue-key :keyContent="keyboardData[0][2]" />
                        <vue-key :keyContent="keyboardData[0][3]" />
                        <vue-key :keyContent="keyboardData[0][4]" />
                        <vue-key :keyContent="keyboardData[0][5]" />
                </div>`,
    ...

we will get empty yellow rectangles:

That’s because these keys doesn’t have main or shifted values:

[
    { code: 'Escape', label: 'Esc' },
    { code: 'F1' },
    { code: 'F2' },
    { code: 'F3' },
    { code: 'F4' },
    { code: 'F5' }
]

So we need to compute them from other params: code and label. Vue component has especial property computed for such computations.

Key.js

const Key = {
	template: `<div class="key">
                        <div class="main">
                            {{main}}
                        </div>
                        <div class="shifted">
                            {{shifted}}
                        </div>
                    </div>`,
	props: {
		keyContent: Object
	},
	computed: {
		main() {
			const { main, label, code } = this.keyContent
			return label || main || code
		},
		shifted() {
			const { shifted } = this.keyContent
			return shifted
		}
	}
}

export default Key

We added to component object a new property computed with 2 methods: main() and shifted(). Also we changed template to use this new values:

{{keyboardData.main}} —> {{main}}

{{keyboardData.shifted}} —> {{shifted}}

Result:

Before we output all rows, remove all temporary styles, that we added to see how component hierarchy works. Remove these lines from the end of

styles.css

#app {
	background-color: red;
	padding: 10px;
}

.langSwitcher {
	background-color: green;
	padding: 10px;
}

.keyboard {
	background-color: blue;
	padding: 10px;
	display: flex;
}

.key {
	background-color: yellow;
	padding: 10px;
	color: black;
}

Let’s ouput all rows from our data model

Keyboard.js

...
template: `<div class="keyboard">
                <div class="row row-1">
                    <vue-key :keyContent="keyboardData[0][0]" />
                    <vue-key :keyContent="keyboardData[0][1]" />
                    <vue-key :keyContent="keyboardData[0][2]" />
                    <vue-key :keyContent="keyboardData[0][3]" />
                    <vue-key :keyContent="keyboardData[0][4]" />
                    <vue-key :keyContent="keyboardData[0][5]" />
                </div>
                <div class="row row-2">
                    <vue-key :keyContent="keyboardData[1][0]" />
                    <vue-key :keyContent="keyboardData[1][1]" />
                    <vue-key :keyContent="keyboardData[1][2]" />
                    <vue-key :keyContent="keyboardData[1][3]" />
                    <vue-key :keyContent="keyboardData[1][4]" />
                    <vue-key :keyContent="keyboardData[1][5]" />
                </div>
                <div class="row row-3">
                    <vue-key :keyContent="keyboardData[2][0]" />
                    <vue-key :keyContent="keyboardData[2][1]" />
                    <vue-key :keyContent="keyboardData[2][2]" />
                    <vue-key :keyContent="keyboardData[2][3]" />
                    <vue-key :keyContent="keyboardData[2][4]" />
                </div>
            </div>`,
...

We wrapped rows with <div class="row row-{{index}}">...</div>.

Result:

Last line looks not correct.

For languages with upper case letters (e.g. cyrillic, latin alphabets), we should show in main slot shifted value (uppercase), and don't show main value at all. Otherwise it makes our keaboard lookin unrealistic and overwhelmed. We add function getKeyLabels(keyContent) that does all this work for us:

Key.js

const getKeyLabels = keyContent => {
	const { main = '', shifted = '', label, code } = keyContent
	const isUpperCaseLang = main.toUpperCase() === shifted
	const mainOutput = isUpperCaseLang ? shifted : main
	const shiftedOutput = isUpperCaseLang ? '' : shifted
    
	return {
		main: label || mainOutput || code,
		shifted: shiftedOutput
	}
}

const Key = {
	template: `<div class="key">
                        <div class="main">{{main}}</div>
                        <div class="shifted">{{shifted}}</div>
                    </div>`,
	props: {
		keyContent: Object
	},
	computed: {
		main() {
			const { main } = getKeyLabels(this.keyContent)
			return main
		},
		shifted() {
			const { shifted } = getKeyLabels(this.keyContent)
			return shifted
		}
	}
}

export default Key

In main() and shifted() we use the new function getKeyLabels.

Result is ok:

Rows — loop in template (v-for)

Keyboard.js

Did you already think that it is annoying to ouput data with template in such way:

Keyboard.js

...
template: `<div class="keyboard">
                <div class="row row-1">
                    <vue-key :keyContent="keyboardData[0][0]" />
                    <vue-key :keyContent="keyboardData[0][1]" />
                    <vue-key :keyContent="keyboardData[0][2]" />
                    <vue-key :keyContent="keyboardData[0][3]" />
                    <vue-key :keyContent="keyboardData[0][4]" />
                    <vue-key :keyContent="keyboardData[0][5]" />
                </div>
                <div class="row row-2">
                    <vue-key :keyContent="keyboardData[1][0]" />
                    <vue-key :keyContent="keyboardData[1][1]" />
                    <vue-key :keyContent="keyboardData[1][2]" />
                    <vue-key :keyContent="keyboardData[1][3]" />
                    <vue-key :keyContent="keyboardData[1][4]" />
                    <vue-key :keyContent="keyboardData[1][5]" />
                </div>
                <div class="row row-3">
                    <vue-key :keyContent="keyboardData[2][0]" />
                    <vue-key :keyContent="keyboardData[2][1]" />
                    <vue-key :keyContent="keyboardData[2][2]" />
                    <vue-key :keyContent="keyboardData[2][3]" />
                    <vue-key :keyContent="keyboardData[2][4]" />
                </div>
            </div>`,
...

v-for directive allows us to loop elements in template.

We could guess by template structure , that there are 2 nested loops.

  • 1st for rows
  • 2nd for keys inside the row

1st loop — rows of the keyboard

First let’s ouput just row containers.

Keyboard.js

import Key from './Key.js'
import keyboardData from '../keyboardData/en.js'

const Keyboard = {
	template: `<div class="keyboard">
                        <div 
                            v-for="(row, index) in keyboardData"
                            :class="['row', 'row-'+(index+1)]" 
                        >
                            row {{index+1}}
                        </div>
                    </div>`,
	components: {
		'vue-key': Key
	},
	data() {
		return { keyboardData }
	}
}

export default Keyboard

v-for="(row, index) in keyboardData" loops through keyboardData array.

On each iteration v-for creates the same element (like element that contains v-for), with 2 new params: index and row that we can use inside our template. For now we use only index.

Result:

:class="['row', 'row-'+(index+1)]" generates class="row row-1" e.t.c. Сolon : tells framework that class value should be interpreted as a varible, not string. We use in class a variable index gotten from v-for.

On the image above there is opened Developer tools. (In browser click mouse right button —> Inspect).

In the code (DevTools tab elements) we see that each row are represented by a div with classes row and row-index. That’s important for us, because 1st row has different styles: smaller buttons and font size, if you remember.

<vue-key 
	v-for="keyContent in row" 
	:keyContent="keyContent" 
/>

2nd loop — keys of a row

In Keyboard.js let’s replace row {{index+1}} with another loop with keys.

This template part recieves row from the loop before, and makes another loop for keys of the row.

Keyboard.js

import Key from './Key.js'
import keyboardData from '../keyboardData/en.js'

const Keyboard = {
	template: `<div class="keyboard">
                        <div 
                            v-for="(row, index) in keyboardData" 
                            :class="['row', 'row-'+(index+1)]"
                        >
                        <vue-key 
                            v-for="keyContent in row" 
                            :keyContent="keyContent" 
                        />
                        </div>
                    </div>`,
	components: {
		'vue-key': Key
	},
	data() {
		return { keyboardData }
	}
}

export default Keyboard

Result:

Add to keyboardData/en.js a new key F6 and you'll see the result immidiatly.

Now we don’t care even if our keyboard data contains hundreds of rows and keys — they will be displayed automatically by the 2 loops, with these 25 lines of code. We separated view and data.

LangSwitcher — refactor with props and v-for

Open index.html and copy commented code for LangSwitcher, then paste it to template in

LangSwitcher.js

const LangSwitcher = {
	template: `<div class="langSwitcher">
                            <div class="lang active">en</div>
                            <div class="lang">ru</div>
                            <div class="lang">ar</div>
                    </div>`
}

export default LangSwitcher

Result:

The idea is to move the red round to the lang code that we clicked. Also we need to store selected lang in some variable. Smells as reactivity, yeah?

But first we rewrite LangSwitcher with props and v-for.

In App.js we add a new param langs to data(). Then in template we pass it to <vue-lang-switcher />

App.js

import Keyboard from './components/Keyboard.js'
import LangSwitcher from './components/LangSwitcher.js'

const App = {
	template: `App 
	<vue-lang-switcher :langs="langs" />
	<vue-keyboard />
	`,
	components: {
		'vue-lang-switcher': LangSwitcher,
		'vue-keyboard': Keyboard
	}, 
	data(){
		return {
			langs: ['en', 'ru', 'ar']
		}
	}
}

export default App

In <LangSwitcher.js> we recieve this new param (array) langs, and ouput it in a loop with v-for.

LangSwitcher.js

const LangSwitcher = {
	template: `<div class="langSwitcher">
                        <div 
                            v-for="lang in langs" 
                            class="lang"
                        >
                            {{lang}}
                        </div>
                    </div>`, 
	props: {
		langs: Array
	}, 		
}

export default LangSwitcher

Result:

The red round disappeared because style active not attached to any element.

App async state keyboardData

We need keyboardData on every levels of our app. So it's wrong to store it in Keyboard component and we should move it up to the root App component.

For now we have only 1 keyboard layout English (en). But then we'll have different layouts (langs), so we need a feature to load them from different files.

Open App.js and:

  • add data() and a new state there keyboardData
  • add mounted(), and load there keyboardData from a file
  • update state with recieved data
  • in template pass keyboardData to the <vue-keyboard> as a prop.

App.js

import Keyboard from './components/Keyboard.js'
import LangSwitcher from './components/LangSwitcher.js'

const App = {
	template: `App 
	<vue-lang-switcher :langs="langs" />
	<vue-keyboard :keyboardData="keyboardData" />
	`,
	components: {
		'vue-lang-switcher': LangSwitcher,
		'vue-keyboard': Keyboard
	},
        /* add: */
	mounted() {
        /* dynamic import from file */
		import(`./keyboardData/en.js`).then(result => {
			const { default: keyboardData } = result
            /* update state with recieved data */
			this.keyboardData = keyboardData
		})
	},
	data() {
		return {
            /* add a new state */
			keyboardData: []
		}
	}
}

export default App

Method mounted() will be called when user opens the App at the first time.

import('path-to-file') works as import in the top of a page. But you can put it anywhere and call it anytime. It is a promise (works asyncronous), so it returns after a while a result object {default: } with code from an external module. We wait it and .then we use recieved code (keyboardData) to update our App state.

Open Keyboard.js and add a new prop.

Keyboard.js

import Key from './Key.js'
/* delete: 
import keyboardData from '../keyboardData/en.js'
* we receive it from prop 
*/

const Keyboard = {
	template: `<div class="keyboard">
                        <div 
                            v-for="(row, index) in keyboardData" 
                            :class="['row', 'row-'+(index+1)]"
                        >
                        <vue-key 
                            v-for="keyContent in row" 
                            :keyContent="keyContent" 
                        />
                        </div>
                </div>`,
	components: {
		'vue-key': Key
	},
	 /* add a new prop  */
	props: {
		keyboardData: Array
	}, 
	/* delete: 
	data() {
		return { keyboardData }
	}
	*/
}

export default Keyboard

If you have done everything right, the app will work just as before, without any visible changes. But we made our code better. Now keyboardData is available in App, and we can pass it down to any child component. And now we import keyboardData dynamically, that allows us on next steps to switch between different language keyboards.

Interactivity

What we coded until now was a static elements, that doesn’t react on user input, and doesn’t change dynamically.

Using components with props we made our code modular.

Using loops in templates we made their code short, clear, extansible and maintanable. Now we can display data of any length with a small template with a loop.

Now it's turn to handle user generated events.

Interactivity it is when user interact with an app, and see results.

Reactive state, @click event, calling method

When we change a variable value, and it causes change in a visible app (view), it is called reactive state. Reactivity means connection between variable and view.

  • In vue such reactive variables are placed in the method data().
  • The most common approach to change them — by methods.
  • Methods are called from event listeners placed in a template (e.g. @click).

Let’s we add to LangSwitcher:

  • a method data() with a returned property (state) currentLang: 'en' (‘en’ as default)
  • a property methods with a new method switchLang(lang)
  • in template — a new event handler @click to element <div class="lang">
  • in template — a new div to display reactive variable currentLang. It is temporaty, after testing we’ll delete it.

LangSwitcher.js

const LangSwitcher = {
	template: `<div class="langSwitcher">
                        <div 
                            v-for="lang in langs" 
                            class="lang"
                            @click="switchLang(lang)"
                        >
                            {{lang}}
                        </div>
                    </div>
                    <div style="text-align: center;">
                        {{currentLang}}
                    </div>`,
	props: {
		langs: Array
	},
	data() {
		return {
			currentLang: 'en'
		}
	},
	methods: {
		switchLang(lang) {
			this.currentLang = lang
		}
	}
}

export default LangSwitcher

@click="switchLang(lang)" by clicking on an element where it placed (<div class=”lang”>) will be called method switchLang with a parameter lang, which is particular to each <div> and can be ‘en’, ‘ru’ or ‘ar’.

That’s how a dynamic generated elements changes a reactive state by user input (click).

Result:

You see, after click on a lang code, currentLang state of the component changes.

Conditional styling

Instead of additional text with currentLang code we need a red round background under active lang.

When we apply some styling to an element, only if a particular condition is true, it is called — conditional styling.

:class='["lang", {active: currentLang === lang}]' this string will do all work for us.

1st element in the array is a string, that means that class lang will be attached to <div> in any case (without condition).

2nd element is object like {styleName: boolean condition}. Class “active” will be attached to <div class="lang"> only if prop lang of element is equal to state currentLang.

In styles.css we defined before:

.lang {
	width: 2rem;
	height: 2rem;
	border-radius: 1rem;
	...
}

.langSwitcher .active {
	background-color: red;
	color: white;
}

That’s why the red round follows our clicks on lang codes — because of attaching and deattaching class active.

Result:

Change parent state from a child

Another important approach to share data between components — is changing parent state from a child. It is kinda opposite to passing props from parent to child.

  • In a parent we create a reactive state and a method to change it
  • we pass this method to child as a prop
  • we call it (with different params) from the child

When you call a method recieved from a prop, NOTICE that actually it happens where it was defined and passed down.

If the parent state was passed as a prop to multiple components, if we change this state (by method from any child, which received the method as a prop) — then all components with this state (as prop) will be updated. So that is how a little child component on the bottom of the hierarchy can globally affect on the whole app — by calling a method, that changes parent state.

In a small apps as our, it is common to have reactive state and main logic in the top level component as <App> and pass the state and methods to childs (<Keyboard>, <LangSwitcher>) as props.

For now currentLang is placed in <LangSwitcher>. But we need this value also in <Keyboard> and <Key>.

<LangSwitcher> and <Keyboard> are siblings (they haven’t relations parent-child, but have common parent). So, to share the state currentLang between siblings, we should lift it to a common ancestor <App>.

Let’s we move state currentLang and method switchLang from <LangSwitcher> to <App> and then pass them as props to <LangSwitcher> and use there.

Open LangSwitcher.js and remove data() and methods. Add to props: currentLang, switchLang.

const LangSwitcher = {
	template: `<div class="langSwitcher">
                        <div 
                            v-for="lang in langs" 
                            :class='["lang", {active: currentLang === lang}]'
                            @click="switchLang(lang)"
                        >
                            {{lang}}
                        </div>
                    </div>`,
	props: {
		langs: Array,
		/* add: */
		currentLang: String,
		switchLang: Function
	}
	/* delete: 
	data() {
		return {
			currentLang: 'en'
		}
	},
	methods: {
		switchLang(lang) {
			this.currentLang = lang
		}
	} 
	*/
}

export default LangSwitcher

Open App.js. Add to data() a new state currentLang: 'en'. Paste whole methods from old LangSwitcher.js.

In thetemplate pass to <vue-lang-switcher> 2 new props: switchLang and currentLang. Also add somewhere {{currentLang}} to test our changes.

App.js

import Keyboard from './components/Keyboard.js'
import LangSwitcher from './components/LangSwitcher.js'

const App = {
	template: `App-{{currentLang}}
	<vue-lang-switcher 
		:langs="langs" 
		:switchLang="switchLang" 
		:currentLang="currentLang" 
	/>
	<vue-keyboard :keyboardData="keyboardData" />
	`,
	components: {
		'vue-lang-switcher': LangSwitcher,
		'vue-keyboard': Keyboard
	},
	mounted() {
		import(`./keyboardData/en.js`).then(result => {
			const { default: keyboardData } = result
			this.keyboardData = keyboardData
		})
	},
	data() {
		return {
			langs: ['en', 'ru', 'ar'],
			keyboardData: [],
			/* add: */
			currentLang: 'en'
		}
	},
	/* add: */ 
	methods: {
		switchLang(lang) {
			this.currentLang = lang
		}
	}
}

export default App

Result:

Notice, now when we do something in LangSwitcher it changes App state. We change the parent state from the child with method. The method we passed from parent to child as a prop.

Switching keyboards (languages)

Another languages data

Open a folder keyboardData. Copy and paste there a file en.js twice. Rename clones to ru.js and ar.js. Change a content (copy from here or type).

ru.js

const keyboard = [
	[
		{ code: 'Escape', label: 'Esc' },
		{ code: 'F1' },
		{ code: 'F2' },
		{ code: 'F3' },
		{ code: 'F4' },
		{ code: 'F5' }, 
		{ code: 'F6' }
	],
	[
		{
			code: 'Backquote',
			main: 'ё',
			shifted: 'Ё'
		},
		{
			code: 'Digit1',
			main: '1',
			shifted: '!',
			shiftedName: 'восклицательный знак'
		},
		{
			code: 'Digit2',
			main: '2',
			shifted: '"',
			shiftedName: 'двойная кавычка'
		},
		{
			code: 'Digit3',
			main: '3',
			shifted: '№',
			shiftedName: 'знак номер'
		},
		{
			code: 'Digit4',
			main: '4',
			shifted: ';',
			shiftedName: 'точка с запятой'
		},
		{
			code: 'Digit5',
			main: '5',
			shifted: '%',
			shiftedName: 'процент'
		}
	],
	[
		{ code: 'Tab' },
		{
			code: 'KeyQ',
			main: 'й',
			shifted: 'Й'
		},
		{
			code: 'KeyW',
			main: 'ц',
			shifted: 'Ц'
		},
		{
			code: 'KeyE',
			main: 'у',
			shifted: 'У'
		},
		{
			code: 'KeyR',
			main: 'к',
			shifted: 'К'
		}
	]
]

export default keyboard

ar.js

const keyboard = [
	[
		{ code: 'Escape', label: 'Esc' },
		{ code: 'F1' },
		{ code: 'F2' },
		{ code: 'F3' },
		{ code: 'F4' },
		{ code: 'F5' },
		{ code: 'F6' }
	],
	[
		{
			code: 'Backquote',
			main: '٫',
			shifted: '٬',
			mainName: 'decimal point',
			shiftedName: 'inverted comma'
		},
		{
			code: 'Digit1',
			main: '١',
			shifted: '!',
			mainName: '1',
			shiftedName: 'exclamation mark'
		},
		{
			code: 'Digit2',
			main: '٢',
			shifted: '@',
			mainName: '2',
			shiftedName: 'at sign'
		},
		{
			code: 'Digit3',
			main: '٣',
			shifted: '#',
			mainName: '3',
			shiftedName: 'hash'
		},
		{
			code: 'Digit4',
			main: '٤',
			shifted: '$',
			mainName: '4',
			shiftedName: 'dollar sign'
		},
		{
			code: 'Digit5',
			main: '٥',
			shifted: '٪',
			mainName: '5',
			shiftedName: 'percent sign'
		}
	],
	[
		{ code: 'Tab' },
		{
			code: 'KeyQ',
			main: 'ض',
			shifted: 'َ',
			shiftedName: 'fatha'
		},
		{
			code: 'KeyW',
			main: 'ص',
			shifted: 'ً',
			shiftedName: ''
		},
		{
			code: 'KeyE',
			main: 'ث',
			shifted: 'ُ',
			shiftedName: ''
		},
		{
			code: 'KeyR',
			main: 'ق',
			shifted: 'ٌ',
			shiftedName: ''
		}
	]
]

export default keyboard

Arabic diacritic symbols don't look good in a code. But don't worry about it. It will work well for our purpuses.

Refactor <App>

Open App.js and add there a new method getKeyboardData. Put call of this method to method switchLang, and mounted().

import Keyboard from './components/Keyboard.js'
import LangSwitcher from './components/LangSwitcher.js'

const App = {
	template: `App-{{currentLang}}
                <vue-lang-switcher 
                    :langs="langs" 
                    :switchLang="switchLang" 
                    :currentLang="currentLang" 
                />
                <vue-keyboard :keyboardData="keyboardData" />
	`,
	components: {
		'vue-lang-switcher': LangSwitcher,
		'vue-keyboard': Keyboard
	},
	mounted() {
		/* replace  
		import(`./keyboardData/en.js`).then(result => {
			const { default: keyboardData } = result
			this.keyboardData = keyboardData
		})
		*/
		this.getKeyboardData(this.currentLang)
	},
	data() {
		return {
			langs: ['en', 'ru', 'ar'],
			keyboardData: [],
			currentLang: 'en'
		}
	},
	methods: {
		switchLang(lang) {
			this.currentLang = lang
			/* add: */
			this.getKeyboardData(lang)
		},
		/* add: */
		async getKeyboardData(lang) {
			const { default: keyboardData } = await import(
				`./keyboardData/${lang}.js`
			)
			this.keyboardData = keyboardData
		}
	}
}

export default App

If you noticed async/await in the method getKeyboardData that is alternative syntax for promises. This code is asyncronous, because reading of a file takes time and we should wait for result to move furthere through our scenario.

Result:

With a couple lines of code we achived big improvment of functionality. That is because we organized code well: in a modular way with an intuitive props, methods and structure.

Keydown event handling

In the file App.js add:

  • to data() add a new state activeKey
  • to template an output of this new state
  • to mounted() an event listener on keydown

App.js

import Keyboard from './components/Keyboard.js' import LangSwitcher from './components/LangSwitcher.js' const App = { template: `App-{{currentLang}} <div>activeKey: {{activeKey}}</div> <vue-lang-switcher :langs="langs" :switchLang="switchLang" :currentLang="currentLang" /> <vue-keyboard :keyboardData="keyboardData" /> `, components: { 'vue-lang-switcher': LangSwitcher, 'vue-keyboard': Keyboard }, mounted() { this.getKeyboardData(this.currentLang) /* add: */ window.addEventListener('keydown', e => { e.preventDefault() const { code, shiftKey } = e this.activeKey = { code, shiftKey } }) }, data() { return { langs: ['en', 'ru', 'ar'], keyboardData: [], currentLang: 'en', /* add: */ activeKey: null } }, methods: { switchLang(lang) { this.currentLang = lang this.getKeyboardData(lang) }, async getKeyboardData(lang) { const { default: keyboardData } = await import( `./keyboardData/${lang}.js` ) this.keyboardData = keyboardData } } } export default App

Save the file. And click by mouse on the app, so it became active window, and we can handle keydown there. Then press different keys, with and without shift, and lock what happens.

Result:

activeKey updates on pressing buttons. And it is the same for any language.

It isn't a bug, it is a feature
(Programmers' proverb)

As we said before, at the beginning of the tutorial, we will calculate real key from currentLang, code and shiftKey. These 3 values uniquely identify the pressed key in any language. It is important for us to control the state currentLang from our web app, regardless of which language is selected in the operating system.