Vue.js
Magpie is based on a JavaScript framework called Vue.js. To learn how magpie works, it is thus beneficial to take a brief look at Vue.js first.
Vue.js allows you to compose a web application out of small self-contained components, that you can re-use and share with other people.
Components
A Vue.js component is a bundle of HTML, JavaScript and Optionally CSS, packaged together, usually in a file with the extension .vue
. Such a file generally looks like this:
<template>
<div>
<h1>hello</h1>
</div>
</template>
<script>
export default {
name: 'MyComponent',
}
</script>
<style>
h1 {
font-weight: bold;
}
</style>
The HTML content resides in a template
element, the JavaScript part resides in a script
element and the CSS code resides in a style
element.
Somewhere else, we could now use this component like a normal HTML element and Vue.js will render the component's contents instead.
<div>
<MyComponent />
</div>
This will render
hello
Data
To make components more flexible, you can use a special syntax, the Vue.js template language inside the <template>
tag.
The string {{ name }}
will be replaced on a website with the value of the variable name
defined in the data
function inside the <script>
tag.
<template>
<div>
<h1>hello {{ name }}</h1>
</div>
</template>
<script>
export default {
name: 'MyComponent',
data() {
return {
name: 'Theodor'
}
}
}
</script>
The data
function initializes the internally stored data of the component, whenever it is used.
Any property we define in data
is available in the template as a variable.
We can then use such a variable in a text block using
the {{ variable_name }}
notation.
Technically, anything inside these braces is just normal JavaScript, too,
so you could use the built-in JavaScript method toUpperCase
to do something like this:
<template>
<div>
<h1>hello {{ name.toUpperCase() }}</h1>
</div>
</template>
<script>
export default {
name: 'MyComponent',
data() {
return {
name: 'Theodor'
}
}
}
</script>
toUpperCase
takes a string and returns it with all letters in upper case.
This will render
hello THEODOR
Computed data
As we've seen in the last section, we can compute arbitrary JavaScript inside of template tags. However, we often want to use the same values at multiple places in the code and we would rather don't want to repeat ourselves. This is where computed data comes into play.
<template>
<div>
<h1>hello {{ name.toUpperCase() }}</h1>
</div>
</template>
<script>
export default {
name: 'MyComponent',
data() {
return {
name: 'Theodor'
}
}
}
</script>
Let's have a look at this example again. Turning a string into an upper case representation is luckily not very resource-heavy, but we could very well imagine other transformations that are.
<template>
<div>
<h1>hello {{ upperCaseName }}</h1>
</div>
</template>
<script>
export default {
name: 'MyComponent',
data() {
return {
name: 'Theodor'
}
},
computed: {
upperCaseName() {
return this.name.toUpperCase()
}
}
}
</script>
Here we define a computed variable based on our name variable and we now use the computed variable directly in the template as if it was a regular variable. This way
- we don't have to repeatedly write the same code, but can put it in a computed variable
- we make sure to only execute code when necessary
The second point is a very nice bonus: upperCaseName
is only recomputed when the value of name
changes, allowing us to cache
heavy computations while automatically recomputing these values when necessary.
Note: When writing computed variables, make sure to not add any side effects to them. Ie. we should not alter any other
variables in this computation or even name
itself, because we would end up in infinite loops and hard to track down rabbit holes.
Component props
To make components even more useful, we usually want to generalize them. So, we don't want a component for each person that can be greeted, but we want a general "greeter component".
To do this, we want to be able to pass options to our components.
Vue.js calls these props
and we also have to define them in our component definition.
<template>
<div>
<h1>hello {{ name }}</h1>
</div>
</template>
<script>
export default {
name: 'Greeter',
props: {
name: {
type: String,
required: true
}
}
}
</script>
Here, instead of setting the variable name
with a pre-defined value, we define a prop
called name
which accepts string values and always has to be set,
when using this component (required
). Notice that we can use props in the same way as variables defined in data
.
We can then use the "Greeter" component from within another component, as follows:
<template>
<div>
<Greeter name="Theodor" />
</div>
</template>
<script>
export default {
name: 'MyComponent'
}
</script>
A normal attribute declaration like this will only allow passing strings. However, you often want to pass another variable or an expression to a component, and of course Vue.js's template language supports that, too.
<template>
<div>
<Greeter :name="name" />
</div>
</template>
<script>
export default {
name: 'MyComponent',
data() {
return {
name: 'Theodor'
}
}
}
</script>
Here we use the colon-prefix for the prop to indicate that we want to pass a JavaScript expression.
Note that even though the Greeter component internally also uses a variable called name
, we still have to explicitly pass
the value as a prop, because components do not implicitly share any state other than the props we pass to them.
Syntax
In templates, props with long names can either be specified in camelCase e.g. firstName
, or kebab-case e.g. first-name
.
In the JavaScript component definition, you can only use camelCase.
Listening to events
Static web pages are relatively boring.
We want the user to interact with the browser.
We can achieve this by listening to HTML events.
Vue.js allows us to do this using the @-shorthand.
For example, to be notified when the user clicks on a specific element, we use @click
on that element.
The listener attribute accepts either a JavaScript statement, like a function call, or a function value.
The HTML event object is available as $event
.
<template>
<div>
<h1 @click="greeting = 'Bye'">{{greeting}} {{name}}</h1>
</div>
</template>
<script>
export default {
name: 'Greeter',
props: {
name: {
type: String,
required: true,
}
},
data() {
return {
greeting: 'Hello'
}
}
}
</script>
This component is a bit more complex. We define a prop, called name
, which takes
the name of the person to be greeted as a string. And in data
we define the greeting we want
to greet this person with. By default, this is set to 'Hello'
.
If we passed the string 'Theodor'
to the prop, the message would then read: 'Hello Theodor'.
However, in the template, we listen to clicks on the heading. When the user clicks,
we change the variable greeting
to 'Bye'
, and now the heading reads 'Bye Theodor'.
Defining methods
To avoid having to squeeze our code inside these @-declarations, we can define methods below in the component definition.
<template>
<div>
<h1 @click="changeGreeting">{{greeting}} {{name}}</h1>
</div>
</template>
<script>
export default {
name: 'Greeter',
props: {
name: {
type: String,
required: true,
}
},
data() {
return {
greeting: 'Hello'
}
},
methods: {
changeGreeting() {
this.greeting = 'Bye'
}
}
}
</script>
Again, we listen to a click on the heading, which initially reads e.g. 'Hello Theodor' (depending on the name prop, of course).
However, we set a method as the listener for the click
event. When the user clicks,
the changeGreeting
method will be called, which then changes the greeting to 'Bye'
. Notice that we have to use the magic this
variable to access the variable inside methods.
Emitting events
Additionally, you can also emit events in your components using $emit
.
For example, we might want to pass along the click
event to our parent component.
<template>
<div>
<h1 @click="changeGreeting">{{greeting}} {{name}}</h1>
</div>
</template>
<script>
export default {
name: 'Greeter',
props: {
name: {
type: String,
required: true,
}
},
data() {
return {
greeting: 'Hello'
}
},
methods: {
changeGreeting(event) {
this.greeting = 'Bye'
this.$emit('click', event)
}
}
}
</script>
Here, we've changed the changeGreeting
method to emit a click event on our Greeter component, using the magic $emit
method. (Generally, names that start with $
are built-in methods of Vue.js or associated libraries.)
From within a component that uses the Greeter component, we can now listen for this event similarly:
<template>
<div>
<Greeter :name="'Theodor'" @click="onClick"/>
</div>
</template>
<script>
export default {
name: 'MyComponent',
methods: {
onClick(event) {
// Do something, when Theodor is clicked.
}
}
}
</script>
Syncing properties
Properties can be used to define inputs for a component. Events can be used to pass data back up the component tree. We often want components to manipulate data for us and pass it back up, for example with a text input. While we can use properties and events in the conventional way for this, Vue.js has a special built-in syntax to deal with this situation.
<template>
<div>
<h1 @click="changeGreeting">{{ hello? 'Hello' : 'Goodbye' }} {{name}}</h1>
</div>
</template>
<script>
export default {
name: 'Greeter',
props: {
name: {
type: String,
required: true,
},
hello: {
type: Boolean,
required: true,
}
},
methods: {
changeGreeting(event) {
this.$emit('update:hello', !this.hello)
}
}
}
</script>
Here, we add a second prop for our Greeter component which is a boolean value indicating whether we want to say 'Hello' or 'Goodbye'.
The complex-looking expression in the template is simply a ternary operator acting as an inline if-else statement: hello? 'Hello' : 'Goodbye'
means "If hello is true, insert 'Hello', otherwise insert 'Goodbye'".
Once the user clicks on the text, the changeGreeting method is called, which this time only sends an event with the new value for our hello
property,
inverting the previous value of hello
.
In our upstream component MyComponent
, we can now use the .sync
modifier when setting the hello
property. This will make sure
that our variable sayHello will be updated when the Greeter component sends an update and rerender it with the new value.
<template>
<div>
<Greeter :name="'Theodor'" :hello.sync="sayHello" />
</div>
</template>
<script>
export default {
name: 'MyComponent',
data() {
return {
sayHello: true
}
}
}
</script>
Component slots
Component props allow passing JavaScript values as options to Components. However, sometimes you want to pass HTML snippets to another component. The template language also supports this.
<template>
<div>
<h1>hello <slot /></h1>
</div>
</template>
<script>
export default {
name: 'Greeter',
}
</script>
Instead of defining a prop in our component definition, we used the magic <slot>
element to define a component slot.
In a component that uses this Greeter component, you can now pass in arbitrary HTML that will be put at the position of the slot element within the Greeter component. We pass HTML to a slot by placing it as a child of the Greeter component.
<template>
<div>
<Greeter>
<i>Theodor</i>
</Greeter>
</div>
</template>
<script>
export default {
name: 'MyComponent',
}
</script>
Here we pass the HTML partial <i>Theodor</i>
, which renders 'Theodor'
in italic.
The Greeter component will then render
hello Theodor
By default, children will be put into the #default
slot. A component may also define multiple slots.
These will then get names.
<template>
<div>
<h1>hello <slot name="person" /></h1>
<blockquote><slot name="bio" /></blockquote>
</div>
</template>
<script>
export default {
name: 'Greeter',
}
</script>
Here we define two slots for our greeter component, one called "person" and one called "bio". In another component we can then fill those slots as follows.
<template>
<div>
<Greeter>
<template #person>
<b>Theodor</b>
</template>
<template #bio>
<p>The 45th president of the United States.</p>
</template>
</Greeter>
</div>
</template>
<script>
export default {
name: 'MyComponent',
}
</script>
Here, we use template elements with #-directives to declare which children go into which slot.
If and for
To make things even more interesting, Vue.js introduces two special attributes: v-if
and v-for
,
which allow you to render elements conditionally and iterate over them, respectively.
Conditionals
<template>
<div>
<h1>hello {{ name }}</h1>
<img v-if="name === 'Theodor'" src="theodor.jpg" />
</div>
</template>
<script>
export default {
name: 'Greeter',
props: {
name: {
type: String,
required: true,
}
},
}
</script>
Here, we only render the image if the value of name
equals "Theodor"
. As you might have guessed, v-if
also allows arbitrary JavaScript expressions.
You can also use v-else
and v-else-if
to catch alternative conditions. You can use as many instances of v-else-if
as you need or omit it directly.
<template>
<div>
<h1>hello {{ name }}</h1>
<img v-if="name === 'Theodor'" src="theodor.jpg" />
<img v-else-if="name === 'Hillary'" src="hillary.jpg" />
<img v-else src="person.jpg" />
</div>
</template>
<script>
export default {
name: 'Greeter',
props: {
name: {
type: String,
required: true,
}
},
}
</script>
Loops
The following renders multiple headings, with the different names that we defined in data
.
<template>
<div>
<h1 v-for="name in names" :key="name">hello {{ name }}</h1>
</div>
</template>
<script>
export default {
name: 'MyComponent',
data() {
return {
names: ['Theodor', 'Hillary', 'Joe', 'Barack']
}
}
}
</script>
In every iteration, we create a new <h1>
element and the variable name
will be bound to one of the values in the names
array we defined in our data function.
This will render the following:
hello Theodor
hello Hillary
hello Joe
hello Barack
Notice that, in addition to the v-for
attribute, we also have to provide a :key
attribute when iterating over lists,
which helps Vue.js distinguish between the different elements. Here, we simply use the name
variable again for this purpose.
We can also count during iterating over lists. This looks as follows:
<template>
<div>
<h1 v-for="(name, i) in names" :key="i">hello {{ name }}</h1>
</div>
</template>
<script>
export default {
name: 'MyComponent',
data() {
return {
names: ['Theodor', 'Hillary', 'Joe', 'Barack']
}
}
}
</script>
Here, the variable ì
is bound to the indices of the names in the list, so that we can use it as a key
for the loop.
Wrapping multiple elements with v-if or v-for
You can apply a v-if
or a v-for
to multiple elements at once, by wrapping them in a template
element.
For example, instead of adding the same condition to multiple elements...
<template>
<div>
<h1>hello {{ name }}</h1>
<blockquote v-if="name === 'Theodor'">
A duck in a comic.
</blockquote>
<img src="theodor.jpg" v-if="name === 'Theodor'" />
</div>
</template>
<script>
export default {
name: 'MyComponent',
props: {
name: {
type: String,
required: true,
}
},
}
</script>
... we can wrap them in a template
element and write the condition only once:
<template>
<div>
<h1>hello {{ name }}</h1>
<template v-if="name === 'Theodor'">
<blockquote>
A duck in a comic.
</blockquote>
<img src="theodor.jpg" />
</template>
</div>
</template>
<script>
export default {
name: 'MyComponent',
props: {
name: {
type: String,
required: true,
}
},
}
</script>
Styling
All HTML elements have a default style, but for your experiments you may want to change the display of some elements. In the browser this can be done using Cascading Style Sheets (CSS).
Inline styles
The simplest way to style your HTML is using inline styles:
<template>
<div>
<h1 style="color: green;">hello {{ name }}</h1>
</div>
</template>
<script>
export default {
name: 'Greeter',
props: {
name: {
type: String,
required: true,
}
},
}
</script>
Here, we display the greeting with green text.
Inline styles are convenient for small adjustments, but quickly become unwieldy when changing many display properties of an element:
<template>
<div>
<h1 style="color: green; text-decoration: underline; font-style: italic;">hello {{ name }}</h1>
</div>
</template>
<script>
export default {
name: 'Greeter',
props: {
name: {
type: String,
required: true,
}
},
}
</script>
CSS classes
When changing many display properties of an element or when you want to conditionally apply style changes based on your component state, CSS classes come in handy.
<template>
<div>
<h1 class="greeter-heading big-text">hello {{ name }}</h1>
</div>
</template>
<script>
export default {
name: 'Greeter',
props: {
name: {
type: String,
required: true,
}
},
}
</script>
<style>
.greeter-heading {
color: green;
text-decoration: underline;
font-style: italic;
}
.big-text {
font-size: 40px;
}
</style>
Instead of placing our CSS in a style
attribute directly within our HTML, we declare abstract CSS classes in a separate
style
element. We can then apply one or more of these CSS classes to our HTML elements using the class
attribute.
This allows us to reuse the same styles on multiple elements without having to repeat ourselves.
Applying CSS classes conditionally
Another advantage of CSS classes is that we can easily apply a set of styles conditionally to an element.
<template>
<div>
<h1 :class="{ 'greeter-heading': true, 'big-text': name === 'Theodor' }">hello {{ name }}</h1>
</div>
</template>
<script>
export default {
name: 'Greeter',
props: {
name: {
type: String,
required: true,
}
},
}
</script>
<style>
.greeter-heading {
color: green;
text-decoration: underline;
font-style: italic;
}
.big-text {
font-size: 40px;
}
</style>
We can use the colon prefix for our class
attribute to be able to pass in JavaScript expressions. This way,
we can pass in a JavaScript object whose keys are CSS class names, with the associated values indicating whether the class should be applied or not.
In this example, we only apply the class big-text
if the Greeter
component was created with the name 'Theodor'
.
More
There's much more to learn about Vue.js. If you are curious, head over to the official guide.