Of all the "major" frameworks, Vue is the one I used the least, as it seems like it's most popular in China and not so much here, so I can't guarantee that my Vue code will be all that great.
But it's an adventure, so let's try anyway. I'll try to implement Svelte file manager from episode 27 as a Vue app. After that I'll switch back to the Svelte version for a while, but you can follow along in Vue, or in any other framework you prefer.
src/Footer.vue
This is almost identical to Svelte version - we just wrap the whole thing in <template>
tag, and add a small declaration of what we're exporting.
All that stuff is redundant in Svelte, where we can just write the contents of the template for simple components like this.
<script>
export default {
name: "Footer"
}
</script>
<template>
<footer>
<button>F1 Help</button>
<button>F2 Menu</button>
<button>F3 View</button>
<button>F4 Edit</button>
<button>F5 Copy</button>
<button>F6 Move</button>
<button>F7 Mkdir</button>
<button>F8 Delete</button>
<button>F10 Quit</button>
</footer>
</template>
<style>
footer {
text-align: center;
grid-area: footer;
}
button {
font-family: inherit;
font-size: inherit;
background-color: #66b;
color: inherit;
}
</style>
src/Panel.vue
This component is responsible for one of the panels of the file manager. The styling is identical to the Svelte version:
<style>
.left {
grid-area: panel-left;
}
.right {
grid-area: panel-right;
}
.panel {
background: #338;
margin: 4px;
}
.file {
cursor: pointer;
}
.file.selected {
color: #ff2;
font-weight: bold;
}
.panel.active .file.focused {
background-color: #66b;
}
.debug {
max-width: 40vw;
}
</style>
Template isn't too bad. There's no expressions for flipping each class separately like Svelte's class:a={conditionA} class:b={conditionB}
, but you can have v-bind:class
with object value like v-bind:class={a: conditionA, b: conditionB}
, which is more or less the same thing.
Loops go into v-for
attribute of each element, not outside the element. I find it less intuitive, but I guess it's somewhat more concise. This should be clear enough if you know Svelte:
<template>
<div v-bind:class="{
panel: true,
active: active,
left: (position==='left'),
right: (position==='right'),
}">
<div
v-for="file in files"
v-bind:class="{
file: true,
focused: (file === focused),
selected: (selected.includes(file)),
}"
@click="() => onclick(file)"
@contextmenu="() => onrightclick(file)"
>
{{file}}
</div>
</div>
</template>
Script part is a good deal more complicated than Svelte version:
<script>
export default {
name: "Panel",
props: ["position", "files", "active"],
data: (e) => {
return {
focused: e.$props.files[0],
selected: [],
}
},
methods: {
onclick: function(file) {
this.$data.focused = file
this.$emit("activate")
},
onrightclick: function(file) {
this.$data.focused = file
this.$emit("activate")
this.flipSelected(file)
},
flipSelected: function(file) {
if (this.$data.selected.includes(file)) {
this.$data.selected = this.$data.selected.filter(f => f !== file)
} else {
this.$data.selected = [...this.$data.selected, file]
}
},
goUp: function() {
let i = this.$props.files.indexOf(this.$data.focused)
if (i > 0) {
this.$data.focused = this.$props.files[i - 1]
}
},
goDown: function() {
let i = this.$props.files.indexOf(this.$data.focused)
if (i < this.$props.files.length - 1) {
this.$data.focused = this.$props.files[i + 1]
}
},
handleKey: function(e) {
if (!this.$props.active) {
return
}
if (e.key === "ArrowDown") {
e.preventDefault()
this.goDown()
}
if (e.key === "ArrowUp") {
e.preventDefault()
this.goUp()
}
if (e.key === " ") {
e.preventDefault()
this.flipSelected(this.$data.focused)
this.goDown()
}
},
},
mounted() {
window.addEventListener("keydown", this.handleKey)
},
}
</script>
What is even going on here?
- we declare that our component is a
Panel
- we declare properties - you can also specify their types, validations, and some extra attributes here
- do declare initial state, we make
data
function which returns initial state, it can usethis.$props
- all methods go into
methods
dictionary, inside it we can accessthis.$data
,this.$props
- we can call parent either through events with
this.$emit
, or by passing callbacks as props - Svelte also supports both mechanism, Vue and Svelte documentation differ on which one to promote, but really it's up to you (React only has callbacks, no events) - methods can also call other methods with
this.methodName(...)
- this part tripped me over - everything must use
function()
style functions, not=>
style arrow functions, otherwisethis
will be incorrect - there's no equivalent of
<svelte:window>
to which we can attach and automatically detach event listeners - so I usedmounted()
lifecycle hook; in general we should also callwindow.removeEventListener
when component is unmounted, it's just not something our app does
Vue vs Svelte example
To compare just one small function between Vue and Svelte, Svelte saves us a lot of this.$stuff
, but other than that they follow very similar flow:
// Vue
goUp: function() {
let i = this.$props.files.indexOf(this.$data.focused)
if (i > 0) {
this.$data.focused = this.$props.files[i - 1]
}
},
// Svelte
let goUp = () => {
let i = files.indexOf(focused)
if (i > 0) {
focused = files[i - 1]
}
}
src/App.vue
This is a simpler component. Global styles and component styles go into separate tags:
<style global>
body{
background-color: #226;
color: #fff;
font-family: monospace;
margin: 0;
font-size: 16px;
}
</style>
<style>
.ui {
width: 100vw;
height: 100vh;
display: grid;
grid-template-areas:
"header header"
"panel-left panel-right"
"footer footer";
grid-template-columns: 1fr 1fr;
grid-template-rows: auto 1fr auto;
}
.ui header {
grid-area: header;
}
header {
font-size: 24px;
margin: 4px;
}
</style>
The template is very close to what we did in Svelte:
<template>
<div class="ui">
<header>
File Manager
</header>
<Panel
v-bind:files=filesLeft
position="left"
v-bind:active="(activePanel === 'left')"
@activate="activateLeft"
/>
<Panel
v-bind:files=filesRight
position="right"
v-bind:active="(activePanel === 'right')"
@activate="activateRight"
/>
<Footer />
</div>
</template>
We map with @activate
what to do when specific component emits activate
event.
And in script part we have a bit more boilerplate than in Svelte version, such as explicit list of component we want to use:
<script>
import Panel from "./Panel.vue"
import Footer from "./Footer.vue"
export default {
name: "App",
components: {
Panel,
Footer,
},
data: () => ({
filesLeft: [
"Cat.js",
"ipsum.js",
"dolor.js",
"sit.js",
"amet.js",
"walk.js",
"on.js",
"keyboard.js",
"hide.js",
"when.js",
"guests.js",
"come.js",
"over.js",
"play.js",
"with.js",
"twist.js",
"ties.js",
],
filesRight: [
"Ask.png",
"to.png",
"be.png",
"pet.png",
"then.png",
"attack.png",
"owners.png",
"hand.png",
"need.png",
"to.jpg",
"chase.png",
"tail.png",
],
activePanel: "left",
}),
methods: {
activateLeft: function() {
this.$data.activePanel = "left"
},
activateRight: function() {
this.$data.activePanel = "right"
},
handleKey: function(e) {
if (e.key === "Tab") {
e.preventDefault()
if (this.$data.activePanel === "left") {
this.$data.activePanel = "right"
} else {
this.$data.activePanel = "left"
}
}
}
},
mounted() {
window.addEventListener("keydown", this.handleKey)
},
}
</script>
Result
Here's the results:
As I mentioned, I haven't used Vue much, so it's possible I missed something really obvious here. If so, let me know.
In the next episode we'll go back to our Svelte app, and make it work with real files not just static mock data.
As usual, all the code for the episode is here.
Top comments (0)