Chapter 3: Components

As we briefly touched on in Chapter 1, Vue was designed for creating component-based UIs. The idea is that you can structure your application around self-contained, reusable elements that represent a discrete part of the interface.

One example of this might be an Avatar component that displays a user’s profile picture as a circular image. Once created, this component can be dropped into your application code anywhere you want to display the user’s avatar:

<div class="sidebar">
  <avatar :src="user.image" />
  <h2>{{ user.nick }}</h2>
</div>

As you can see in the above example, using a Vue component in your templates is like having a new HTML element available: you use the component’s name as the tag (<avatar>, in this case) and pass in any data it needs in the form of custom attributes. (Here a URL is being passed as the src attribute. We’ll look at this in more detail soon.)

Defining Custom Components

So how do we go about creating our own components with Vue?

The Vue object (which is a global, if you include it from a CDN) provides a component method. This can be used to register new components by passing a name and an options object, similar to the one we used to create our Vue instance in Chapter 1:

Vue.component('MyCustomComponent', {
  // ...
});

A Note on Naming

You can choose kebab case (my-custom-component) or Pascal case (MyCustomComponent) when naming your component. I recommend Pascal case, as this will allow you to use either when referencing your component within a template. It’s important to note that when using your component directly in the DOM (as in Chapter 1), only the kebab case tag name is valid.

Registering a component in this way makes it available anywhere in your Vue app for other components to use within their templates. This is very handy for components (for example, general layout components) that you’re going to be using a lot throughout your app.

When using single file components (SFCs), it’s possible to import and use these directly within the components where they’re needed, which we’ll cover in the next section.

Component Options

Components take almost all the same options you can use when creating an instance, with a couple of important differences.

Firstly, components don’t accept an el property: you can supply a component with a template by setting the template property to a string containing markup.

Template String

Vue.component('HelloWorld', {
  template: '<p>Hello, world!</p>',
});

If the string starts with a #, Vue will treat it as a selector and look for a matching element in the DOM. If it finds one, it will use its contents as the template.

In-DOM Template

Vue.component('HelloWorld', {
  template: '#my-template-container-id'
});

If you’re using SFCs, you can provide your markup within <template></template> tags in your .vue component file.

HelloWorld.vue

<template>
  <p>Hello, world!</p>
</template>

<script>
  export default {
    name: 'HelloWorld'
  }
</script>

The second change is that a component’s data property must be a function that returns a new object literal. Think of this function like a factory, providing a fresh data object to each instance of the component that’s created:

Vue.component('HelloWorld', {
  template: 'Hello, {{message}}!',
  data() {
    return {
      message: 'World'
    }
  }
});

Let’s revisit our staff directory example from Chapter 1 and convert it into a component. This way, we can easily reuse it on different screens of a web app.

Online Example

Note that I’m using the expanded example from the online example.

We’ll implement it as a .vue SFC, so if you haven’t already, install the Vue CLI as described in Chapter 2 and create a new project using the default preset.

Open the project folder, navigate into src/components, and create a file called StaffDirectory.vue. Inside this file, add the following code.

src/components/StaffDirectory.vue (template)

<template>
  <div class="ui container">
    <input v-model="filterBy" placeholder="Filter By Last Name">
    <table class="ui celled table">
      <thead>
        <tr>
          <th>Avatar</th>
          <th @click="sortBy = 'firstName'">First Name</th>
          <th @click="sortBy = 'lastName'">Last Name</th>
          <th @click="sortBy = 'email'">Email</th>
          <th @click="sortBy = 'phone'">Phone</th>
          <th @click="sortBy = 'department'">Department</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="(employee, index) in sortedEmployees" :key="index">
          <td>
            <img :src="employee.photoUrl" class="ui mini rounded image" />
          </td>
          <td>{{ employee.firstName }}</td>
          <td>{{ employee.lastName }}</td>
          <td>{{ employee.email }}</td>
          <td>{{ employee.phone }}</td>
          <td>{{ employee.department }}</td>
        </tr>
      </tbody>
      <tfoot>
        <tr>
          <th colspan="6">{{sortedEmployees.length}} employees</th>
        </tr>
      </tfoot>
    </table>
  </div>
</template>

Nothing has changed in our template markup; it’s just been dropped into an SFC between a pair of <template> tags.

One important thing to note is that (for now, at least) your templates must have a single root element (in this case, the <div class="ui container"></div> wrapper). You can’t have multiple root-level elements.

src/components/StaffDirectory.vue (script)

<script>
  export default {
    name: 'StaffDirectory',
    data() {
      return {
        filterBy: "",
        sortBy: 'department',
        employees: [
          // omitted for brevity
        ]
      }
    },
    computed: {
      sortedEmployees() {
        return this.employees.filter(
          employee => employee.lastName.includes(this.filterBy)
        ).sort(
          (a, b) => a[this.sortBy].localeCompare(b[this.sortBy])
        );
      }
    }
  }
</script>

For the code, we have to add an export statement to allow it to be imported and used by the other components in our app.

You might notice that our component has a name property. This is important for components that aren’t registered globally. (We’ll look at how to import components locally in a moment.) It also helps with debugging: without it, the Vue devtools will show <AnonymousComponent> in the component tab, making debugging difficult.

src/StaffDirectory.vue (style)

<style>
  h1.ui.center.header {
    margin-top: 3em;
  }

  .ui.table th:not(:first-child):hover {
    cursor: pointer;
  }

  input {
    padding: 3px;
  }
</style>

We’ve also got some additional CSS styles to tweak the layout and change the cursor when hovering over the column headers. These are included within the style block.

We’re going to use this component from within the existing src/App.vue file. As we’ve not registered it globally with Vue (that is, by calling Vue.component()), we have to import our new file and register it locally.

src/App.vue

<template>
  <div id="app">
    <h1 class="ui center aligned header">Staff Directory</h1>
    <StaffDirectory />
  </div>
</template>

<script>
import 'semantic-ui-css/semantic.css'
import StaffDirectory from './components/StaffDirectory.vue'

export default {
  name: 'app',
  components: {
    StaffDirectory
  }
}
</script>

Within the <script> section, we import the StaffDirectory component. You can replace the HelloWorld import here, as we aren’t going to use it.

Importing Styles

In Chapter 1, I included a CSS library called Semantic UI to style the staff directory. In the example above, we’re importing it into our SFC instead of including it from a CDN. If you’re following along, you’ll need to run npm install semantic-ui-css from within your project directory.

The next thing we do is create a components property. This should be an object where each key/value pair is the component name and the component object respectively. I’ve used the ES2015 shorthand syntax here, as I want to use the same name we import the component with.

Lastly, we modify the template to include our new component element. We can use it in self-closing form unless we want it to wrap any child elements (we’ll look at this later).

Live Demo

You can find this example online at CodeSandbox.

Lifecycle Methods

If you’ve used React before, you’re probably familiar with the concept of component lifecycle methods. The idea behind this is that there are various events that happen in the lifetime of a component—from when it’s created, through to when it’s destroyed. The lifecycle methods are called at these points, allowing you to execute code—for example, to perform an Ajax request when the component is initialized.

The Vue documentation includes a useful chart of all of the various methods and when in the lifecycle they’re called.

The methods you’ll probably find yourself using most often are:

  • created. This is the place to kick off any Ajax requests for fetching data.
  • mounted. When this hook is fired, the component has been rendered and inserted into the DOM. This is where you can manipulate the DOM elements—for example, if you’re wrapping a non-Vue library of some sort.
  • beforeDestroy. Just before the component is removed from the DOM, this method is called. You can do any cleanup here.

If we wanted to load in the staff data for our component from an API, we could do something like the following.

src/StaffDirectory.vue

<script>
  export default {
    name: 'StaffDirectory',
    data() {
      return {
        heading: "Staff Directory",
        sortBy: 'department',
        employees: []
      }
    },
    created() {
      fetch('https://randomuser.me/api/?nat=us,dk,fr,gb&results=5')
        .then(response => response.json())
        .then(json => this.employees = json.results);
    },
    computed: {
      // ...
    }
  }
</script>

This way, as soon as the component is created, the created() method will be fired, making an Ajax request to the API and assigning the results to the employees data property.

Passing Data In

Of course, being able to create reusable segments of your UI is quite handy in itself, but to be really useful we need to be able to pass information to our components—both data for it to display, and options to configure how it behaves.

Vue’s way of doing this is to allow your components to declare a set of props, which are essentially custom attributes. To declare the props that a component will accept, you can simply add a property called props whose value is an array containing the names of the props as strings.

In the case of our imaginary Avatar components, we would declare the src prop that’s passed in the following way:

{
  template: '<div class="avatar"><img :src="src" /></div>'
  name: 'Avatar',
  props: ['src']
}

Props are automatically available in the component’s template and are accessible as properties of the component (that is, this) within methods and computed properties.

Prop Validation and Defaults

Declaring props as an array of strings is fine for some situations—such as when you’re just experimenting and haven’t settled on your component design yet. To build more robust components, though, it’s essential to be able to enforce the type and content of a component’s props and provide default values. These abilities are especially important for situations where other developers may be using components created by you.

To apply some basic type validation to your props, you have to set your component’s props property to an object where the prop name is the key, and the type is the value:

props: {
  myString: String,
  myNumber: Number,
  myBoolean: Boolean,
  myArray: Array,
  myObject: Object
}

If you try to pass a value of the wrong type, you’ll get a warning in the browser’s console.

With our props object, we now have the flexibility to do more in terms of prop validation. If we want, instead of using the prop type as the value, we can set it to an object:

props: {
  isAdminUser: {
    type: Boolean,
    required: true
  }
}

As you can see, we’re explicitly setting a type property now, along with a required boolean. There are two other options we can also now set: default, and validate.

The default property is used in the event that no value is passed to the component for that prop:

props: {
  isAdminUser: {
    type: Boolean,
    default: false
  }
}

Defaults for Arrays and Objects

As arrays and objects in JavaScript are passed by reference, any default value must be a factory function that returns a new object or array each time.

In cases where you want to do some more complex validation than just checking the prop’s type, you can provide a validate() function, which will receive the incoming prop and return a boolean value to indicate validity:

props: {
  user: {
    validate(user) {
      return ['admin', 'editor', 'author'].includes(user.role);
    }
  }
}

In the example above, the validation function checks to ensure the user prop is an object with one of the specified roles.

Communicating with the Outside World

So far, we’ve seen how to get data into a component, using props, but how do we allow our component to communicate information back? In React, it’s common to pass a callback function to a component to allow for data to be sent back. Although it’s also possible to do this in Vue, the preferred way is to use events.

We previously looked at how to listen for DOM events (such as mouse clicks) on HTML elements in the template and connect them up to handler methods. It’s actually very straightforward to have components emit their own, custom events, and listen for them in exactly the same way.

SearchBox.vue

<template>
  <div class="searchbox">
    <label>
      Terms
      <input v-model="terms" />
    </label>
    <button @click="$emit('search', terms)">Search</button>
  </div>
</template>

<script>
export default {
  name: 'SearchBox',
  data() {
    return {
      terms: ''
    }
  }
}
</script>

In the code above, we have a simple SearchBox component that renders a text input with a label, and a button. When the button is clicked, the component emits a search event that the parent component can listen for.

Parent Component

<template>
  ...
  <SearchBox @search="onSearch" />
  ...
</template>

<script>
import SearchBox from 'src/components/SearchBox.vue';

export default {
  components: {
    SearchBox
  },
  methods: {
    onSearch(terms) {
      // make API call with search terms
    }
  }
}
</script>

In our imaginary parent component, we’re rendering the SearchBox component and attaching a handler to the search event. When the event fires, the onSearch method is called, receiving the terms as a parameter, which it can then use to make an API call.

Slots

In all the examples so far, I’ve shown the components used as self-closing elements. In order to make components that can be composed together in useful ways, we need to be able to nest them inside one another as we do with HTML elements.

If you try to use a component with a closing tag and put some content inside, you’ll see that Vue just swallows this up. Anything within the component’s opening and closing tags is replaced with the rendered output from the component itself:

<StaffDirectory>
  <p>This content will be replaced.</p>
</StaffDirectory>

Vue lets us output the children of a component by providing a <slot> component that can be placed in the template at the location you want to render the child elements.

TodoList.vue

<template>
  <div class="todo-list">
    <h2>To-do List</h2>
    <ul>
      <slot><li>All done!</li></slot>
    </ul
  </div>
</template>

<script>
  export default {
    name: 'TodoList'
  }
</script>

Parent Template

<TodoList>
  <li>Buy some milk</li>
  <li>Feed the cats</li>
  <li>Have some pie</li>
</TodoList>

In the example above, the TodoList component will render any child elements inside a <ul> with a heading.

Slot Fallback Content

The <slot> element can have its own child content, which will be rendered in the event that the component itself has no children. This is useful for providing default or fallback content.

Named Slots

Your components’ slots can actually be named, meaning you can have multiple slots rendered in different locations in the template. This allows you to design very flexible components that are highly configurable.

As an example, let’s say we wanted to create a reusable Bootstrap card component for our application, with slots to provide header, footer, and body content.

HeaderFooterCard.vue (template only)

<template>
<div class="card text-center">
  <div class="card-header">
    <slot name="header"></slot>
  </div>
  <div class="card-body">
    <slot name="body"></slot>
  </div>
  <div class="card-footer text-muted">
    <slot name="footer"></slot>
  </div>
</div>
</template>

Usage

<HeaderFooterCard>
  <template slot="header">Featured</template>

  <template slot="body">
    <h5 class="card-title">Special title treatment</h5>
    <p class="card-text">With supporting text below as a natural lead-in to
    additional content.</p>
    <a href="#" class="btn btn-primary">Go somewhere</a>
  </template>

  <template slot="footer">Two days ago</template>
</HeaderFooterCard>

Now, you might be thinking that we could have used props to pass the header and footer text to the component, but using slots means you’re free to pass in HTML elements and even other Vue components.

Scoped Slots

Vue has one last trick up its sleeve where slots are concerned. It provides a clever little feature called scoped slots. Scoped slots allow child components to access data from the parent.

We could, for example, build a component that fetches data from an API and makes it available to any children.

AjaxLoader.vue

<template>
  <div>
    <slot :data="data"></slot>
  </div>
</template>

<script>
  export default {
    name: 'AjaxLoader',
    props: {
      url: {
        type: String,
        required: true
      }
    },
    data() {
      return {
        data: []
      }
    },
    created() {
      fetch(this.url)
        .then(res => res.json())
        .then(json => this.data = json);
    }
  }
</script>

Note that the <slot> element is wrapped in a <div> here, as it can’t be the route element in a template. This is because whoever uses the AjaxLoader component might pass multiple child elements.

Usage

<AjaxLoader url="/api/staff">
  <template slot-scope="{ data }">
    <StaffDirectory :staff="data" />
  </template>
</AjaxLoader>

As you can see in the code above, you can use the slot-scope property to access the data from the parent component and pass it into any child components as props.

AjaxLoader Example

There’s an example of the AjaxLoader component that you can check out online.

Summary

In this chapter, we delved into components—how to create them, how to register them either globally or locally, and what additional options there are when creating them.

We also saw how to use props to get data into components, using type validation and default values to build more reliable code, and we looked at using custom events to communicate with parent components.

Lastly, we took a look at slots, and how they enable us to build more flexible components that can accept children, and even make their data available to them.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.140.185.147