--- title: How to build Keyboard trainer app (html/css/js/?vue) description: How to build Keyboard trainer app (html/css/js/?vue) --- # How to build Keyboard trainer app (html/css/js/?vue) [TOC] ## 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](https://securityaffairs.co/wordpress/113518/hacking/screensaver-bypass-linux-mint.html)” -- 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](https://code.visualstudio.com/learn/get-started/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. ![](https://i.imgur.com/uOhlIco.png) ### Key Let’s write html code for one key: ```html <div class="key"> <div class="main">1</div> <div class="shifted">!</div> </div> ``` And repeat it for a next four keys: ```html <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: ```yaml ` 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: ```css .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: ```jsx <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: ![](https://i.imgur.com/ZomxeZM.png) ### Row Wrap all keys in `index.html` with ```html <div class="row"> ... here is keys code </div> ``` We need row style to wrap our keys. Add to `styles.css`: ```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: ![](https://i.imgur.com/MzTcj6s.png) Keys have a minimal width. If we want them to take all available place in the row, we should add to `styles.css`: ```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: ![](https://i.imgur.com/3l4V4bx.png) ### 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">`: ```html ... <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. ![](https://i.imgur.com/G2zOhkp.png) `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 ```html <div class="key Tab">...</div> ``` styles.css ```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. ![](https://i.imgur.com/QZwiZXm.png) 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: ```html <div class="row row-1">...</div> <div class="row row-2">...</div> <div class="row row-3">...</div> ``` styles.css ```css .row-1 .key { height: 1rem; min-height: 1rem; font-size: 0.7rem; } ``` ![](https://i.imgur.com/wRpIHqo.png) ### Language switcher It will be 3 rounds with language codes. One of them is active (red background). index.html ```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 ```css .lang { width: 2rem; height: 2rem; border-radius: 1rem; cursor: pointer; } .lang:hover { opacity: 0.7; } .langSwitcher .active { background-color: red; color: white; } ``` ![](https://i.imgur.com/efh3mky.png) To place lang code in center of the round (vertically and horizontally), add styles: ```css .lang { ... display: flex; align-items: center; justify-content: center; } ``` ![](https://i.imgur.com/PWBsIgw.png) To display lang switcher as a row and center it on the page, add styles: ```css .langSwitcher { display: flex; justify-content: center; margin-bottom: 1rem; } ``` ![](https://i.imgur.com/6uoiwPr.png) 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 ```jsx 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. ```javascript 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 ```htmlmixed <body> <!-- ... --> </body> ``` Copy code example from: https://vuejs.org/guide/quick-start.html#without-build-tools (or from here): ```htmlembedded <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 ```htmlmixed <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 ```javascript const { createApp } = Vue createApp({ data() { return { message: 'Hello Vue!' } } }).mount('#app') ``` In index.html, just before closing `</body>` tag add string: index.html ```htmlmixed ... <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) ```htmlmixed <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 ```javascript const App = { data() { return { message: 'Hello Vue!!' } } } export default App ``` Then import it in `index.js` and put into createApp(...) index.js ```javascript 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: ```HTML <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 ```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. ![](https://i.imgur.com/vqKCjka.png) ### LangSwitcher Create a directory `components` in the project root directory, and create there a file `LangSwitcher.js` ```javascript const LangSwitcher = { template: `<div class="langSwitcher">LangSwitcher</div>` } export default LangSwitcher ``` and add to styles.css ```CSS ... .langSwitcher { background-color: green; padding: 10px; } ``` Add a newly created component to App.js ```javascript import LangSwitcher from './components/LangSwitcher.js' const App = { template: `App <vue-lang-switcher />`, components: { 'vue-lang-switcher': LangSwitcher } } export default App ``` Result: ![](https://i.imgur.com/0IH6k8h.png) We see here that App contains LangSwitcher wich is correct. ### Keyboard Create a file `Keyboard.js` in a `components` folder Keyboard.js ```javascript const Keyboard = { template: `<div class="keyboard">Keyboard</div>`, } export default Keyboard ``` Add to styles.css ```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 ```javascript 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: ![](https://i.imgur.com/dstRG1x.png) ### Key Create file `Key.js` in `components` directory Key.js ```javascript const Key = { template: `<div class="key">Key</div>` } export default Key ``` Add to styles.css ```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 ```javascript 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: ![](https://i.imgur.com/TTPpN1C.png) 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 ```javascript const Key = { template: `<div class="key"> <div class="main">1</div> <div class="shifted">!</div> </div>` } export default Key ``` We described our ["Data model"](#data-model) in a section above. Open `keyboardData/en.js` and find the data for a single key: keyboardData/en.js ```javascript { 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 ```javascript 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. ```javascript <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 ```javascript= 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: ![](https://i.imgur.com/eEYGm8v.png) 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 ```javascript 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: ![](https://i.imgur.com/sqA6ZDH.png) #### computed (template variables) If we try to display first row `keyboardData[0]` (Esc, F1, F2, …) ```javascript ... 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: ![](https://i.imgur.com/xatTSkM.png) That’s because these keys doesn’t have `main` or `shifted` values: ```javascript [ { 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 ```javascript 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: ![](https://i.imgur.com/c0P0n5F.png) 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 ```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 ```javascript ... 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: ![](https://i.imgur.com/ooZKB3x.png) 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 ```javascript 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: ![](https://i.imgur.com/yZW0Tc2.png) ### 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 ```javascript ... 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 ```javascript 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: ![](https://i.imgur.com/my0DVwH.png) `: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. ```javascript <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 ```javascript 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: ![](https://i.imgur.com/cFIbVIN.png) 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 ```javascript 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: ![](https://i.imgur.com/l4oO9M4.png) 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 ```javascript 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 ```javascript const LangSwitcher = { template: `<div class="langSwitcher"> <div v-for="lang in langs" class="lang" > {{lang}} </div> </div>`, props: { langs: Array }, } export default LangSwitcher ``` Result: ![](https://i.imgur.com/bDu0Mom.png) 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 ```javascript 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 ```javascript 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 ```jsx 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: ![](https://i.imgur.com/7rwpUuG.gif) 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: ```css .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: ![](https://i.imgur.com/w9ZNbnz.gif) ### 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`. ```javascript 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 the`template` pass to `<vue-lang-switcher>` 2 new props: `switchLang` and `currentLang`. Also add somewhere `{{currentLang}}` to test our changes. App.js ```javascript 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: ![](https://i.imgur.com/JJw7bBO.gif) 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 ```javascript 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 ```javascript 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()`. ```javascript 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: ![](https://i.imgur.com/4q6JLOq.gif) 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 ```javascript= 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: ![](https://i.imgur.com/Otrcbpx.gif) `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.