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:
After I made the app, I thought that it can be a good educational project because:
App > SwitchLang, Keyboard > Key
)I thought after all that vue without build
will be a good stack for newbies because it hasn't:
You just write components in separate files in a code editor, and they work together as an app without extra steps.
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:
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.
Live server
. (How to install Visual Studio Code extensions).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).
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.
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.
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:
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:
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
}
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;
}
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:
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:
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)
})
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.
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.
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.
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.
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.
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:.
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.
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.
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:
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>
.
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.
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:
const keyData
data(){}
methodkeyData
as a prop to child Key
componentNotice, 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
props: {keyContent: Object}
{{ }}
.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:
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:
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.
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"
/>
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.
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.
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:
data()
and a new state there keyboardData
mounted()
, and load there keyboardData
from a filekeyboardData
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.
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.
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.
vue
such reactive variables are placed in the method data()
.@click
).Let’s we add to LangSwitcher
:
data()
with a returned property (state) currentLang: 'en'
(‘en’ as default)methods
with a new method switchLang(lang)
<div class="lang">
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.
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:
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.
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.
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.
<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.
In the file App.js
add:
data()
add a new state activeKey
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.