官术网_书友最值得收藏!

Vue.js

The official Vue.js website suggests that Vue is a progressive JavaScript framework:

Screenshot from the official Vue.js website

What does that mean? In a very simplified way, I can describe Vue.js as a JavaScript framework that brings reactivity to web applications.

It's undeniable that each and every application has some data and some interface. Somehow, the interface is responsible for displaying data. Data might or might not change during runtime. The interface usually has to react somehow to those changes. The interface might or might not have some interactive elements that might or might not be used by the application's users. Data usually has to react to those interactions, and consequently, other interface elements have to react to the changes that have been done to the data. All of this sounds complex. Part of this complex architecture can be implemented on the backend side, closer to where data resides; the other part of it might be implemented on the frontend side, closer to the interface.

Vue.js allows us to simply bind data to the interface and relax. All the reactions that must happen between data and the interface will happen on their own. Let's look at a very simple example where we will bind a message to the page title. Start by defining a simple HTML structure:

<!DOCTYPE html>
<html>
  <head>
  <meta charset="UTF-8">
    <title>Vue.js - binding data</title>
  </head>
  <body>
    <div id="app">
      <h1>Hello, reader! Let's learn Vue.js</h1>
    </div>
  </body>
</html>

Now, let's initialize a Vue.js instance on this page and bind its data to the <h1> element. For this simple example, we will use a standalone Vue.js file. Download it from the Vue.js official page at a Vue instance. The minimum that a Vue.js instance needs is the element to be attached to and the data object. We want to attach our Vue instance to the main <div> tag with the app ID. Let's also create a data object containing an entry for the name:

var data = {name:'Olga'}

Let's create our Vue.js instance with this data:

new Vue({
  el: '#app',
 data
})

Let's now bind data to our HTML element. We will do this using double curly brackets ({{}}). Once the element has been attached to the Vue instance, everything that is inside of it becomes special—even the curly brackets. Everything that you put inside the double curly brackets will be interpreted and evaluated. So, if you put, for example, 2 + 2 inside the curly brackets, 4 will be rendered on the page. Just try it. Any expression, any statement will be compiled and calculated. Don't be too excited though; don't start writing chunks of JavaScript code inside those brackets. Let's leave the computation to the script logic that is written where script resides. Use the brackets to access the data that you pass to your Vue instance. So, in our case, if you insert {{name}} anywhere inside your HTML markup, you will see the name that we passed to the Vue instance within the data object. Let's, for example, replace the word reader inside the <h1> element by {{name}}:

<h1>Hello, {{name}}! Let's learn Vue.js</h1>

If you refresh the page, you will see that the name we passed to the Vue instance is rendered. Try to change the data.name attribute in the developer tools console. You will see the changes immediately propagated. What we see here is a one-way data binding—the changes that happen to data are reactively propagated to the element to which the data is bound. Vue.js also supports two-way data binding; so, the changes that happen to the element on the page are also propagated to the data to which the element is bound.

To achieve this, just bind the given piece of data to the element using the v-model attribute. Let's, for example, add a text input to the page and bind it to the data attribute name:

<input type="text"v-model="name">

Now, once you start typing in the text input, the change is immediately propagated to any other element bound to this piece of data:

The data changes are reactively propagated through all the bound elements

The complete code for the HTML markup and JavaScript code looks like this:

<body>
  <div id="app">
    <div>
      <label for="name">What's your name? </label>
      <input id="name" type="text" v-model="name">
    </div>
    <h1>Hello, <strong>{{name}}</strong>! Let's learn Vue.js</h1>
  </div>
  <script src="vue.js"></script>
  <script>
 var data = {name:'Olga'}

 new Vue({
 el: '#app',
 data
 })
  </script>
</body>

As you can see, there is nothing difficult at all. All you need is to pass data to the Vue instance and bind it to the elements of your page. The Vue framework does everything else. In the upcoming chapters, we will find out what else is possible using Vue.js and how to Bootstrap a Vue.js project.

Vue project – getting started

So, now that we know what Vue.js is for and what its main focus is, we would like to get our hands dirty and start a Vue.js project and explore all the Vue.js features with it. There are plenty of ways of including Vue into the project. Let's explore all of them.

Including directly in script

You can use Vue.js by just downloading it and including it within the <script> tag. Actually, we've done it in the previous section. So, if you have a project already running and want to use some Vue.js features, you can simply include the vue.js file and use it.

CDN

If you don't want to bother downloading and managing Vue versions yourself, you can simply use the CDN version. Just include https://unpkg.com/vue script in your project and you are good to go! It will always be in sync with the latest Vue version:

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

NPM

If you are all into the Node.js development, you can simply add an npm dependency to your package.json file. Just run npm install on your project's root:

npm install vue --save

Vue-cli

Vue provides a nice and clean command-line interface that is perfect for bootstrapping new projects. First of all, you must install vue-cli:

npm install --global vue-cli

Now, you can start a fresh new project using the Vue command-line interface. Check out the vue-cli repository for the detailed documentation at https://github.com/vuejs/vue-cli.

As you can see, it is possible to setup a project using different templates—starting from a simple single HTML page project and going to a complex webpack project setup. The command that should be used for scaffolding a Vue project is as follows:

vue init <template-name><project-name>

The following templates are available:

  • webpack: This is a full-featured webpack setup with vue-loader. It supports hot reload, linting, testing, all kind of pre-processors, and so on.
  • webpack-simple: This is a simple webpack setup that is useful for quick prototyping.
  • browserify: This is a full-featured browserify setup with vueify that also supports hot reload, linting, and unit testing.
  • browserify-simple: This is a simple browserify setup with vueify that can be used for quick prototyping.
  • simple: This generates a simple HTML page that includes Vue.js. It is perfect for quick feature exploration.

It is also possible to create custom templates. Check out the documentation at https://github.com/vuejs/vue-cli#custom-templates and try it.

In this book, we will use the webpack template. We will include some loaders, and we will use linters, unit, and end-to-end testing techniques. To bootstrap a project using the webpack template, simply run the following line of code:

vue init webpack my-project

Now that we know how to scaffold a project with vue-cli, let's check what Vue offers besides what we already explored in the previous section.

Vue directives

Vue directives are no more than just attributes attached to your HTML elements. These directives provide some extra functionality to your template.

All these directives start with the prefix v-. Why? Because it's Vue! You have already used some of them in the previous section. Now, we will see what directives exist and what you can do with them.

Conditional rendering

Open our Hello page and remove user's input. Something not really beautiful is happening:

"Hello,!"

It would be interesting to render the Hello, name message conditionally, depending on the user input. If there is a name, render it; if there's no name, don't render.

For example, only render the Hello, name message if there's a name. Directives v-show and v-if are used exactly for the conditional render. Open the index.html file of this example and let's change it. Wrap the Hello, <strong>{{name}}</strong>!part into span and add a v-show attribute with the name value:

<h1><span v-show="name">Hello, <strong>{{name}}</strong>! </span>Let's learn Vue.js</h1>

Now, if you refresh the page and remove the input completely, the message will only say Let's learn Vue.js:

The v-show attribute allows conditional rendering

Try to replace the v-show directive with the v-if directive. The end result will be quite the same. Why do both exist then? Check out the developer tools' elements tab and try to add or remove the text in the input. You will see that in the case of v-show, the conditional span will just gain a display:none property if the condition does not verify. In the case of v-if, the element disappears completely:

Using the v-show attribute manipulates the display CSS property, whereas using the v-if attribute adds/removes an element completely

When do we use either attribute? If you have a lot of elements that should be visible depending on some data (this data is really dynamic, so it will happen a lot during the runtime), I would advise using the v-show attribute, because adding or removing elements in DOM is a rather expensive operation that might affect the application's performance and even the DOM itself. On the other hand, if the elements should be conditionally rendered only once, let's say, at the application startup, use the v-if attribute. If some elements should not appear, they will just not be rendered. Thus, the number of elements on the page will be reduced. Consequently, the computational cost of the application will be also reduced, as, now, it has fewer elements to go through and compute.

Text versus HTML

I am sure you know pretty well from the previous chapter how to bind some data using the mustache syntax {{}}.

Since this is a technical book about programming, we have to have a cat here Text versus HTML! A cat is pretty easy to render. Its Unicode is U+1F638; thus, we just have to add the &#x1f638; code to our HTML:

<div>&#x1f638;</div>

And, surely, we will have a cat:

Emoji cat saying hello to us

It's nice, but if we want to replace the cat with a dog, we will have to use Google to look for another Unicode representing a dog and replace it. If at some point we want to replace it with a unicorn, we will have to run the same procedure. Moreover, just by looking at our code, we will not be able to say what we are actually rendering unless we know all emoji codes by &hearts;. It might be a good idea to map the names of the emojis to their codes.

Let's add a map of some of them. Open your HTML file and add the following lines of code to the <script> area:

//index.html
<script>
  const animalCodes = {
    dog: '&#x1f436;',
    cat: '&#x1f638;',
    monkey: '&#x1f435;',
    unicorn: '&#x1f984;'
  }
  const data = {
    animalCodes
  }

  new Vue({
    el: '#app',
    data
  })
</script>

Now, you can bind the values of this map to your HTML elements. Let's try to do it using the mustache annotation:

<div>{{animalCodes.cat}}</div>

Refresh the page. The result is not exactly the same as we expected, is it?

The code is rendered instead of the actual cat emoji

This is happening because mustache interpolation actually interpolates text. Using mustache interpolation is the same as using the v-text directive:

<div v-text="animalCodes.cat"></div>

What we actually want to render here is not the text; we want the value of the Unicode for the emoji being rendered as HTML! This is also possible with Vue.js. Just replace a v-text directive with the v-html directive:

<div v-html="animalCodes.cat"></div>

Now, we will get our cat back, and we know exactly what we are rendering when we are looking at the code.

So, remember to use the v-text directive or mustache annotation for text interpolation and the v-html directive for interpolating pure HTML.

Loops

In the previous section, we put a cat on our page. In this section, I would like to have a whole zoo! Imagine that our zoo has a cat Loops, a dog Loops , a monkey Loops , and, of course, a unicorn Loops.We would like to display our zoo in an ordered list. Of course, you can write a simple markup that will look like this:

<ol>
  <li>&#x1f638;</li>
  <li>&#x1f436;</li>
  <li>&#x1f435;</li>
  <li>&#x1f984;</li>
</ol>

However, this makes your code unreadable, and if you want to add more animals to your zoo or remove one of them, you would have to know all these codes by heart. In the previous section, we added a map for emoji animals Unicode. Let's use it in our markup. You already learned that we must use a v-html directive so that the codes are interpolated as HTML. Hence, our markup will look like this:

<div id="app">
  <ol>
    <li v-html="animalCodes.cat"></li>
    <li v-html="animalCodes.dog"></li>
    <li v-html="animalCodes.monkey"></li>
    <li v-html="animalCodes.unicorn"></li>
  </ol>
</div>

It looks better, but still there's something we could improve. Imagine if you want to render all the animals from the emoji world! There are plenty of them. For each animal, you will have to repeat the code of the list item. Every time you would like to reorder the list, remove some elements, or add new ones, you will have to deal with this markup. Wouldn't it be nice if we just had an array of animals that we want to render and then somehow iterate over it and render what's inside of it? Of course, it would! It is possible using the v-for directive. Create an array of animals using the following lines of code:

const animals = ['dog', 'cat', 'monkey', 'unicorn']

Export it in the vue data object:

var data = {
  name:'Olga',
 animals,
  animalCodes
}

Now, you can use this array in the v-for directive and replace multiple <li> elements by only one:

<ol>
<h2><span>{{name}}! </span>Here's your Zoo</h2> 
  <li v-for="animal in animals" v-html="animalCodes[animal]"></li>
</ol>

The result will be quite nice:

Emoji zoo rendered using the v-for directive

Binding data

We dealt a lot with rendering different data using Vue.js in the previous section; so now, you are already familiar with different ways of binding it. You know how to interpolate data as text and as HTML, and you know how to iterate over arrays of data.

We've also seen that two-way data binding is achieved using the v-model directive. We used it to bind a name to the input element:

<input id="name" type="text" v-model="name">

The v-model directive can only be used with the input, select, and textarea elements. It also accepts some modifiers to be used with. Modifiers are special keywords that affect the input in some way. There are three modifiers that can be used with this directive:

  • .lazy: This will only update the data on a change event (try it with our input and you'll see that changes in the input will only affect other parts where the name is used when the Enter button is pressed and not on each key press)
  • .number: This will cast your input to number
  • .trim: This will trim the user's input

It is also possible to chain the modifiers:

<input id="name" type="text"v-model.lazy.trim="name">

So now, we know nearly everything about binding data to the elements. What if we want to bind some data to the elements' properties? Imagine, for example, the dynamic value for the image's source property or class property depending on some data value. How could we do that?

For this, Vue provides a v-bind directive. With this directive, you can bind whatever you want!

As an example, let's show a sad picture when the name is not defined and a glad picture when the name is defined. For this, I've created two pictures, glad.png and sad.png, and put them into the images folder of my application. I will also export their paths into the data object:

//index.html
var data = {
  name:'Olga',
  animals,
  animalCodes,
 sadSrc: 'images/sad.png',
 gladSrc: 'images/glad.png'
}

Now, I can create an image and bind its source using v-bind:src, and I'll provide a JavaScript expression as the value. This expression will check the value of the name. If it's defined, the glad image will be applied, and if not, the sad image will be applied instead:

<img width="100%" v-bind:src="name ? gladSrc : sadSrc">

The shortcut for the v-bind directive is :, so we can just write the following line of code:

<img width="100%" :src="name ? gladSrc : sadSrc">

Here is how our page looks when the value of name is defined:

Happy face image appears when the name is defined

If you remove the name from the input field, the image will automatically change! Open the page and try to remove the text from the input field and add it again. Continue removing and adding, and you will see how fast the image is changed to the corresponding one. This is how the page looks when the name is undefined:

Once the input is cleaned, the image source is immediately changed

Basically, you can do exactly the same with any property binding, for example, class:

<label for="name" v-bind:class="{green: name, red: !name}">What's your name? </label>

You can also bind properties to pass to the children components. We will see how to do it in the section about components.

Handling events

Besides the direct form of data binding to the elements, we want to handle some events because this is what our users do on the page—trigger some events so that they happen. They click, they hover, they submit forms—and all these events must be handled somehow by us. Vue provides a very nice way of attaching listeners to events on any DOM element and provides methods that can handle those events. The good thing about these methods is that they have direct access to Vue data using the this keyword. In this way, we can use methods to manipulate data, and since this data is reactive, all the changes will be immediately propagated to the elements to which this data is bound.

In order to create a method, you just have to add a methods object to the export section of your Vue application. In order to attach this method to any event listener, use the v-on directive with the corresponding event after the colon. Here is an example:

v-on:sumbit="handleSubmit"
v-on:click="handleClick"
v-on:hover="handleHover"

The shortcut for this directive is @, so we could rewrite all these directives as follows:

@sumbit="handleSubmit"
@click="handleClick"
@hover="handleHover"

It should sound familiar to you. Do you remember the tutorial that we followed in the Chapter 1, Please Introduce Yourself – Tutorial? Do you remember that we were listening on the submit method of the message, adding form and calling addMessage? Check it out. Our form with its submit directive looked like this:

//please-introduce-yourself/src/App.vue
<template>
  <form @submit="addMessage">
  <...>
  </form>
</template>

Then, inside the methods section, we actually had the addMessage method defined:

//please-introduce-yourself/src/App.vue
<script>
  <...>
  export default {
  <...>
    methods: {
      addMessage (e) {
      <...>
      },
    },
  }
</script>

Does it start to make more sense now?

Just to understand it better, let's add some methods to our zoo page! Wouldn't it be nice if you could compose your own zoo? Let's add a multiple select element that will contain all possible options, and your zoo will be populated from something that you actually choose! So, let's do the following:

  • Add more animals to our animalCodes map
  • Add another array called animalsForZoo
  • Use this new array in our ordered list that displays the zoo
  • Add a multiple select box composed of the keys of the animalCodes map
  • Attach a @change listener to this select box that will call the populateAnimalsForZoo method
  • Create a populateAnimalsForZoo method that will populate the animalsForZoo array with the selected options from our multiple select element

Doesn't it sound easy? Of course, it does! Let's get started. So, at first, add more animals to our animalCodes map:

var animalCodes = {
  dog     : '&#x1f436;',
  cat     : '&#x1f638;',
  monkey  : '&#x1f435;',
  unicorn : '&#x1f984;',
  tiger   : '&#x1f42f;',
  mouse   : '&#x1f42d;',
  rabbit  : '&#x1f430;',
  cow     : '&#x1f42e;',
  whale   : '&#x1f433;',
  horse   : '&#x1f434;',
  pig     : '&#x1f437;',
  frog    : '&#x1f438;',
  koala   : '&#x1f43c;'
}

Let's also rethink our animals array and generate it out of our map. In this way, every time we need to add some new animal, we just add its key-value name-unicode to the mapping object instead of maintaining both object and array. So, our animals array will look like this:

var animals = Object.keys(animalCodes)

Now, we need another empty array. Let's call it animalsForZoo, and let's populate our zoo from this new array. Since it is empty, our zoo will also be empty. However, we are about to create a method that will populate this array. So, creating an array is easy, and don't forget to export it in a data object:

<script>
<...>
 var animalsForZoo = []
  var data = {
    name:'Olga',
    animals,
    animalCodes,
 animalsForZoo,
    sadSrc: 'images/sad.png',
    gladSrc: 'images/glad.png'
  }
  new Vue({
    el: '#app',
 data
  })
</script>

Don't forget to replace the usage of the animals array in our zoo display with the new animalsForZoo array:

<ol>
  <li v-for="animal in animalsForZoo"><span class="animal" v-html="animalCodes[animal]"></span></li>
</ol>

I know that now you are worried that your zoo on the page is empty, but give us a couple of minutes and we will take care of that!

First of all, let's create a multiple select element that will be populated based on the animals array:

<select multiple="multiple" name="animals" id="animals">
  <option v-for="animal in animals" :value="animal">{{animal}}</option>
</select>

Now, finally, we will attach an event listener to our select box. Let's attach a listener to the change event. Let's tell it to call the populateAnimalsForZoo method. Our directive will look like this:

@change="populateAnimalsForZoo"

The whole select element will obtain a new attribute:

<select @change="populateAnimalsForZoo" multiple="multiple" name="animals" id="animals">
  <option v-for="animal in animals" :value="animal">{{animal}}</option>
</select>

Great! But there's no such method as populateAnimalsForZoo. But there's us! Let's create it. This method will just iterate through the checked options of the animals selected as input and push them into the animalsForZoo array:

new Vue({
  el: '#app',
  data,
  methods: {
 populateAnimalsForZoo(ev) {
 this.animalsForZoo = []
 const selected = document.querySelectorAll('#animals option:checked')
 for (var i = 0; i < selected.length; i++) {
 this.animalsForZoo.push(selected[i].value)
 }
 }
  }
})

Check out how the whole HTML and JavaScript code look after all these changes in the chapter2/example1-vue-intro/index.html file. This is how our testing page looks after the changes:

The zoo is being populated based on the user's choice

The page is messy, right? However, look how many things you have already learned just by using this page. And, admit it, it's a fun learning process! And we are not done with it yet.

Now that you have learned how to add methods and event listeners, I will teach you how we could have done the exact same thing without this method and v-bind:change. Remove all the code we just added and just add v-model to our select element with the animalsForZoo value :

<select v-model="animalsForZoo" multiple="multiple" name="animals" id="animals">
  <option v-for="animal in animals" :value="animal">{{animal}}</option>
</select>

Now, everything we have just done inside the method is handled automatically by Vue! Isn't it great?

Vue components

We came to this chapter having a midsize HTML page in our hands that contains a lot of different parts. We could have thought of more things, for example, adding interactivity to each animal of our zoo, adding the possibility of feeding animals, or having some interesting facts about each animal showing up every time you hover over the animal's icon. At some point, let's face it, the HTML file along with its JavaScript will become unmaintainable.

Can you also see that our visualization layer (HTML) works along with our logical layer (JavaScript)? So, they kind of form blocks, items, bricks… For example, we have a piece of code that is responsible for the Hello name section. We have another block that contains our zoo. Each animal in the zoo is another item.

Call these things whatever you want, but they are undeniably separated pieces of structure and logic that, when brought together, form the whole puzzle. If you build a wall from a unique piece of material and decide to change some parts of the wall, it will not be the easiest task.

So, imagine, you build this wall and incorporate some yellow stars, blue polygons, red squares, and so on into it. Then, you decide that your yellow stars should be black. You have to change all your stars. Then, you decide that your green ellipsis should be a smiling face instead. What now? Change all ellipses, but first you have to find all the places in the wall that contain those ellipses. This is your wall, try to find all ellipses in it:

The wall built as a whole piece with incorporated parts of different colors and forms

Now, imagine that each piece actually resides on its individual brick. You can change them, add them, and remove them as much as you want. If you want to change the appearance of some of the wall elements, you just change this one brick and all the wall pieces containing this brick will change, because all in all, it's just another brick in the wall. So, instead of having the wall full of incorporated strange pieces, you have four bricks, and you change them whenever you need to change the piece of wall that relies on that brick:

If you need to change the appearance of an element in the wall, you just change the corresponding brick

The wall is composed of bricks. These bricks are our components. What if we could also have components built with HTML, CSS, and JavaScript and our application could be built of those components? Did I just say "what if"? There's no "what if." We already have it. Vue.js supports component-based application structure. It's really easy to create components with Vue.js. The only three things you have to do are as follows:

  1. Create a component, and give it a template, data, methods, and whatever you need to give to it.
  2. Register it in the Vue app under the components object.
  3. Use it within the application's template.

For example, let's create a component that will simply render a header element saying Hello. Let's call it HelloComponent. It will only contain the template string:

var HelloComponent = {
  template: '<h1>Hello!</h1>'
}

Now, we can register this component inside our Vue application initialization code:

new Vue({
  el: '#app',
  components: {
 HelloComponent
  }
})

Now, this component can actually be used inside the HTML section of the Vue application's element:

<div id="app">
 <hello-component></hello-component>
</div>

So, the whole section will look something like this:

<body>
  <div id="app">
    <hello-component></hello-component>
  </div>
  <script src="vue.js"></script>
  <script>
    var HelloComponent = {
      template: '<h1>Hello!</h1>'
    }
    new Vue({
      el: '#app',
      components: {
        HelloComponent
      }
    })
  </script>
</body>

Someone might ask, "What's so powerful in these components?" The amount of written code is actually the same as if I would have just written a piece of HTML that does the same. What's the point? Yes, sure, but in this example, our component had just one template inside. A template composed of one line only. We could have a huge template in there, and we could have some methods in this component and also its own data! Let's, for example, add an input for the name to this component and the name to its data object:

var HelloComponent = {
  template: '<div>' +
  '<input v-model="name" />' +
  '<h1>Hello! <strong>{{name}}</strong></h1>' +
  '</div>',
 data() {
 return {
 name: ''
 }
 }
}

If you need to reuse this component, you can do it as many times as you want:

<div id="app">
 <hello-component></hello-component>
 <hello-component></hello-component>
 <hello-component></hello-component>
</div>

Then, you will end up with three independent components on your page:

Using components helps avoid repeated code

These components are very nice, but there's still a big amount of code written within the same JavaScript code block. We declare components all in one place, and if there are too many of them, the application will become unmanageable again. Besides that, this HTML code within the template string is also not the most maintainable thing ever.

Well, if you are thinking so, I have some good news for you. Each component can be stored in its own file with its own HTML, JavaScript, and CSS code. These are special files with the .vue extension. Inside each file, there's a <script> section for the JavaScript code, a <style> section for the CSS code, and a<template> section for the HTML code. Isn't it convenient? Such components are called single-file components. Have a look at the first chapter's code—there's a main component called App.vue and there's also the MessageCard.vue component created by us. Isn't it nice?

If you want to use single-file components in your application, you must scaffold this application using some modular bundler, for example, webpack. We already talked about vue-cli and how easy it is to bootstrap a Vue application using the webpack template. Let's port the messy page with zoo to the webpack bundled application. Run the initialization and installation scripts:

vue init webpack zoo
cd zoo
npm install
npm run dev

Now, open the App.vue file and let's fill it up with our messy zoo application. The <script> section looks like this:

<script>
  <...>
  var data = {
    name: 'Olga',
    animals,
    animalCodes,
    animalsForZoo,
 sadSrc: '../static/images/sad.png',
 gladSrc: '../static/images/glad.png'
  }
  export default {
    name: 'app',
 data () {
 return data
 }
  }
</script>

Note the highlighted areas. I've copied the images into the static folder. Another important thing is that the data inside the component should be used as a function that returns an object and not as an object itself. Since the data object will still be one single instance across multiple components, the whole data object with its properties must be assembled in a dedicated function.

The rest of the script is completely the same.

The template area of the component is pretty much the same as the HTML structure from the previous example. Check out the code in the chapter2/example3-components-started folder.

Let's extract some of the functionality into the individual component. What do you think if we extract the zoo to its individual component? Create a Zoo.vue file in the components folder. Copy the template for the animals list to this component's <template> area:

//Zoo.vue
<template>
  <div v-if="animals.length > 0">
    <h2><span v-if="name">{{name}}! </span>Here's your Zoo</h2>
    <ol>
      <li v-for="animal in animals"><span class="animal"v-html="animalCodes[animal]"></span></li>
    </ol>
  </div>
</template>

Now, we should tell this component that it will receive animals, name, and animalCodes properties from the parent component that will call the following component:

//Zoo.vue
<script>
  export default {
 props: ['animals', 'animalCodes', 'name']
  }
</script>

Now, open the main App.vue component, import the Zoo component, and export it in the components object:

//App.vue
<script>
 import Zoo from './components/Zoo'
  <...>
  export default {
    name: 'app',
 components: {
 Zoo
 }
  }
</script>

Now, we can use this component inside the template! So, replace the whole div tag that contains our zoo with just the following code:

//App.vue
<template>
  <...>
 <zoo :animals="animalsForZoo" :animalCodes="animalCodes":name="name"></zoo>
  <...>
</template>

Check out the page! Everything works as it did earlier!

Exercise

Extract an animal to the individual component and call it inside the zoo within the v-for directive. Each animal has to have a small functionality that will display a small description when clicking its face (on click). I am pretty sure you will easily solve this exercise. If you need help, check out this chapter's code inside the example4-components/zoo directory.

Vue router

Single Page Applications (SPA) are great. They came to make our life easier. And it definitely is. With a bit of JavaScript code, you can achieve all the functionality that had to be done on the server side before, and the whole page should have been replaced just to display the result of that functionality. It is a golden era for web developers now. However, there is a problem that SPAs are trying to solve—navigation. History API and the pushState method (https://developer.mozilla.org/en-US/docs/Web/API/History_API) are already solving it, but it has been a long process until it became an established technology.

Our users are used to controlling their where I am and where I want to be using browsers' navigation buttons. If the whole functionality is located on the same page, how will these buttons help with the navigation? How do you use Google analytics to check which page (that, in fact, is the same) is being accessed more by your users? The whole concept is totally different. Of course, these kinds of applications are a lot faster because the number of requests is significantly reduced, and of course, our users are grateful for that, but they are not changing their web surfing habits just because we changed the way we implement things. They still want to go back. They expect that if they refresh the page, the page will open on exactly the same place where they were right before hitting the refresh button. They expect that they will understand where they are just by looking at the page's URL and checking what's behind the slash. For example, if it's http://mySite/store then it's a store; if it's http://mySite/settings, then most likely I'm somewhere where I can check my current settings and change them.

There are a lot of ways to achieve navigation without having to transform single-page applications into multiple-page applications. You can include an extra layer of logic on your application and change window.location.href every time a different URL is required—this will cause the page to refresh, which is not nice. You can also use HTML5 history API. It would not be the simplest thing to maintain, but it might work.

We all know that good developers are lazy, right? Being lazy means not solving problems that are already solved by someone else. This problem of navigation is being solved by many frameworks and libraries. Not only can you use some third-party libraries that help you deal with the routing in your application, but you can also use the mechanisms provided by the framework of your choice. Vue.js is one of the frameworks that offers a way of dealing with routing. You just map the URL path to your components and everything just works! Check out the official documentation of vue-router library at https://router.vuejs.org/en/.

In order to be able to use vue-router, you must install it for your project:

npm install vue-router –save

Optionally, vue-router usage can be selected on the Vue project initialization with vue init.

Now, you can use Vue router in your application. Just tell Vue to use it:

//main.js
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

Let's create a simple routing example. We will have three components, one of which we consider as the Home component, meaning that it should be shown when someone navigates to the root route /. Let's call the second one Hello component and the third one Bye component. Open the example5-router-started code files from Chapter 2, Under the Hood – Tutorial Explained. You will find all the described components in the components directory:

The structure of the example application where we are going to try Vue router

Now, we must create a router instance. The constructor receives the options object as a parameter. This object can contain different configurable values. The most important one is the array of routes. Each entry of this array should consist of an object that indicates the path of the route and its corresponding component.

First, we will import all the needed components, and then, our router instance will look like this:

//main.js
import Home from '@/components/Home'
import Hello from '@/components/Hello'
import Bye from '@/components/Bye'
<...>
var router = new Router({
  mode: 'history',
  routes: [
    {
      name: 'home',
 component: Home,
 path: '/'
    },
    {
      name: 'hello',
 component: Hello,
 path: '/hello'
    },
    {
      name: 'bye',
 component: Bye,
 path: '/bye'
    }
  ]
})

If you want to understand better what the mode: history option is, check out the documentation page at have to pass the router option to our Vue application. This option will point to our new router instance:

//main.js
new Vue({
  el: '#app',
  template: '<App/>',
  components: { App },
 router
})

Now, the whole application knows that we use this router. One more important step: we need to include the router component into the main component's template. For this, it is enough to just include the <router-view> tag in the App.vue component's template:

//App.vue
<template>
  <div id="app">
    <img src="./assets/logo.png">
 <router-view></router-view>
  </div>
</template>

Check out in more detail the router-view component at https://router.vuejs.org/en/api/router-view.html.

Voilà! Run the application if you haven't done so already:

npm run dev

Open the page at http://localhost:8080 and check that it is displaying our home page component. Then, type http://localhost:8080/hello and http://localhost:8080/bye in the browser's address bar. Check that the content of the page actually changes according to the URL path:

Basic routing with vue-router

Of course, you are already thinking about how to create a simple menu, pointing an anchor <a> element to your defined paths in the router. Don't think too much. Just use a <router-link> component with the to attribute pointing to the path of your choice. For example, to display a simple navigational menu for our router example application, we could write something like this:

//App.vue
<template>
  <div id="app">
 <router-link to="/">Home</router-link>
 <router-link to="hello">Hello</router-link>
 <router-link to="bye">Bye</router-link>
    <router-view></router-view>
  </div>
</template>

Alternatively, if you don't want to write your paths all over again, you can reference your routes by name and use the v-bind:to directive or simply use :to:

//App.vue
<template>
  <div id="app">
 <router-link :to="{name: 'home'}">Home</router-link>
 <router-link :to="{name: 'hello'}">Hello</router-link>
 <router-link :to="{name: 'bye'}">Bye</router-link>
    <router-view></router-view>
  </div>
</template>

Check how the code looks in the example6-router folder.

Open the page and check whether all the links actually work! Click on them several times and check whether you will actually go back if you click on the browser's go back button. Isn't it fantastic?

Vuex state management architecture

Do you remember our example with the Zoo and animal components? There was some data that had to be propagated from the main component to the child component of the child component. If this grandchild component had the possibility of somehow changing data, this change would have to be propagated from the child component to its parent component and so on until data reaches the main component. Don't think that you would do it simply with a v-model binding attribute. Vue has some restrictions regarding binding data to the children components via props. It is strictly one way. So, if the parent component changes the data, the child component's bindings will be affected, but it will never happen the other way around. Check out Vue's official documentation in regarding this at https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow.

If you don't believe me, let's try it. Imagine that in our zoo page example, we would extract the introduction part to the separate component. I am talking about this part of our messy zoo page:

What if we'd like to extract this part to the separate component?

It seems easy. We have to declare a component, let's say Introduction, tell it that it will receive the name property, and just copy-paste HTML from App.vue to this new component. Inside App.vue, we will import this new component and export it inside the components object of the Vue instance. Of course, we will replace the HTML that we already copied to the new component with the <introduction> tag and bind the name property to it. Isn't it easy? Our Introduction.vue file will look like this:

//Introduction.vue
<template>
  <div>
    <label for="name" :class="{green: name, red: !name}">What's your name? </label>
    <input id="name" type="text" v-model.trim="name">
  </div>
</template>
<script>
  export default {
 props: ['name']
  }
</script>

Our App.vue file will import, export, and call:

//App.vue
<template>
  <div id="app" class="jumbotron">
    <...>
 <introduction :name="name"></introduction>
    <...>
  </div>
</template>

<script>
  <...>
 import Introduction from './components/Introduction'

  <...>
  export default {
    components: {
      Zoo,
 Introduction
    }
    <...>
  }
</script>  

Check out this code in the code bundle of the Chapter 2, Under the Hood – Tutorial Explained in the example7-events-started/zoo folder. Run npm install and npm run inside this folder:

cd example7-events-started/zoo
npm install
npm run dev

Check out the page. It looks like it did before. Try to change the name inside the input. First of all, it doesn't change in other places where it should change, and second, our dev tools console is full of warnings and errors:

The name is not updated where it should have been updated, and the console is full of errors

It seems the documentation is right: we can't change the value of data passed as property to the child component. What can we do then? We can emit events and attach event listeners to the component, and change the data on the event. How do we do this? It's simple. First of all, let's call the property being passed by something that is not name, for example, initialName. Then, open the Introduction component and create a data function that will bind this component's name object to initialValueprops. In this way, we are at least telling Vue that it is not our intention to try to change parent's data from the child. So, script of the Introduction.vue component will look like this:

//Introduction.vue
<script>
  export default {
 props: ['initialName'],
    data () {
      return {
 name: this.initialName
      }
    }
  }
</script>

We also have to change the way we bind name to the component inside App.vue:

//App.vue
<introduction :initialName="name"></introduction>

Now, if you check the page, you will at least see that Vue doesn't complain anymore about something illegal that we try to do. However, still, if we try to change the name, the changes are not propagated to the parent, which is quite understandable; these changes only affect the data of the component itself. Now, we have to attach the event to the input element. This event will call a method that will finally emit the event to the parent component:

//Introduction.vue
<template>
  <div>
    <...>
    <input id="name" type="text" v-model.trim="name"@input="onInput">
  </div>
</template>
<script>
  export default {
    <...>
    methods: {
 onInput () {
 this.$emit('nameChanged', this.name)
 }
    }
  }
</script>

Now, the only thing we have to do is to bind the nameChanged event listener to the <introduction> component and call the method that will change the name of the App.vue data object:

//App.vue
<template>
<...>
<introduction @nameChanged="onNameChanged" :initialName="name"></introduction>
<...>
</template>
<script>
  export default {
<...>
 methods: {
 onNameChanged (newName) {
 this.name = newName
 }
 }
  }
</script>

Check the page. Now, everything works as before! Check the code for this solution inside the example7-events/zoo code folder for this chapter.

Well, it was not very difficult, but do we want to emit all these events every time we need to update the state? And what if we have components inside the components? And what if we have other components inside those components? Will it be the events handling hell? And if we have to change something, will we have to go to all those components? Argh! Wouldn't it be great to have the application's data in some kind of centralized storage that would provide a simple API for its management and then we could just call this storage's methods in order to retrieve and update the data? Well, this is exactly what Vuex is for! Vuex is a centralized state management inspired by Redux. Check out its official documentation at http://vuex.vuejs.org/en/.

Now, in a nutshell, the three most important parts of a Vuex store are state, getters, and mutations:

  • State: This is an initial state of the application, basically the data of the application
  • Getters: These are exactly what you think, functions that return data from the store
  • Mutations: These are functions that can mutate data on the store

A store can also have actions. These things are like wrappers for mutations with a bit more capacity. If you want to check what are they about, refer to the official documentation at http://vuex.vuejs.org/en/mutations.html.

Let's add the Vuex store to our Zoo application to check how it works. First of all, we need to install vuex. Open the code for Chapter 2, Under the Hood – Tutorial Explained from the example8-store-started/zoo folder and run npm install:

cd example8-store-started/zoo
npm install vuex --save

Let's create our store. Start by creating a folder named store with the index.js file inside. We will put all our store data inside this file. Before doing this, tell Vue that we will use Vuex:

//store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

Now, we can create a new Vuex instance. It should receive state, getters, and mutations. Let's define them:

//store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const state = {
}

const getters = {
}

const mutations = {
}

export default new Vuex.Store({
  state,
  getters,
  mutations
})

Nice! Now, let's add all the data that resides in our application to the state:

//store/index.js
const animalCodes = {
  dog: '&#x1f436;',
  <...>
  koala: '&#x1f43c;'
}
const animalsDescriptions = {
  dog: 'I am a dog, I bark',
  <...>
  koala: 'I am a koala, I love eucalyptus!'
}
const animals = Object.keys(animalCodes)
const state = {
  name: 'Olga',
  animals,
  animalCodes,
  animalsDescriptions,
  animalsForZoo: [],
  sadSrc: '../static/images/sad.png',
  gladSrc: '../static/images/glad.png'
}

Now, if you inject the store on the Vue application initialization, all the components and their children will have access to the this.$store instance. Let's inject it:

//main.js
import Vue from 'vue'
import App from './App'
import store from './store'

new Vue({
  el: '#app',
  template: '<App/>',
  components: { App },
 store
})

Now, if we replace all the data with computed properties from the store in App.vue (except animalsForZoo, which is bound as a property for our zoo), the application will look quite the same:

//App.vue
<script>
  import Zoo from './components/Zoo'
  import Introduction from './components/Introduction'

  export default {
    name: 'app',
    components: {
      Zoo,
      Introduction
    },
    data () {
      return {
        animalsForZoo: []
      }
    },
 computed: {
 name () {
 return this.$store.state.name
 },
 animals () {
 return this.$store.state.animals
 },
 animalCodes () {
 return this.$store.state.animalCodes
 },
 sadSrc () {
 return this.$store.state.sadSrc
 },
 gladSrc () {
 return this.$store.state.gladSrc
 }
 },
    methods: {
      onNameChanged (newName) {
        this.name = newName
      }
    }
  }
</script>

If you open the page, nothing has changed. However, our changing name interaction doesn't work again!

Let's add mutation to change the name. Mutations are just methods that receive a state as first argument and anything you call them with as other parameters. So, let's call our mutation updateName and pass newName to it as a second argument:

//store/index.js
const mutations = {
 updateName (state, newName) {
 state.name = newName
 }
}

Now, we can use this mutation to access the this.$store.mutation property inside the component responsible for updating the name—Introduction.vue. We have to just change the onInput method:

//Introduction.vue
methods: {
  onInput (ev) {
 this.$store.commit('updateName', ev.currentTarget.value)
  }
}

By the way, we can also remove the properties and pass the name directly from the store, just like we did in the App.vue component. Then, you can remove the name binding to the introduction component inside the App.vue component's template. Now, you can replace the properties that are bound to the Zoo component by computed properties coming from the store. See how elegant the code becomes! For example, look at this line of code:

<introduction></introduction>

Doesn't it look better than the following line of code:

<introduction @nameChanged="onNameChanged" :initialName="name"></introduction>

Check out the final code for this chapter in the example8-store/zoo code folder for not even used any getters. For a more sophisticated use, we would create getters and actions, and they would have been located in their own actions.js and getters.js files. We would also use mapGetters and mapActions helpers. However, for basic understanding, what we have done is enough. Refer to the official documentation to find out more about Vuex store and how to use it.

主站蜘蛛池模板: 华安县| 城市| 新竹市| 青河县| 宁明县| 阳春市| 新郑市| 射洪县| 望都县| 北碚区| 专栏| 伊吾县| 湖州市| 平罗县| 阳泉市| 长沙县| 蓝田县| 巩留县| 九江市| 嘉鱼县| 云龙县| 吉木乃县| 娄底市| 华坪县| 柞水县| 延边| 蕲春县| 沂源县| 南宁市| 五家渠市| 万全县| 新建县| 咸丰县| 延津县| 洛宁县| 金乡县| 西昌市| 珠海市| 台东市| 日喀则市| 兴仁县|