创建第一个Vue组件

现在是时候深入了解Vue,并创建我们自己的自定义组件了--我们将从创建一个组件来表示待办事项列表中的每个项目开始。在这一过程中,我们将学习一些重要的概念,例如在其他组件中调用组件,通过道具向它们传递数据,以及保存数据状态。

注意: 如果你需要根据我们的版本检查您的代码, 你可以在我们的 todo-vue 仓库找到中找到示例 Vue 程序代码的完成版本。 有关运行中的实时版本,请参见 https://mdn.github.io/todo-vue/dist/

前提条件:

熟悉HTMLCSSJavaScript核心语言,了解终端或命令行

Vue组件是由管理应用程序数据的JavaScript对象和映射到基础DOM结构的基于HTML的模板语法组成的。对于安装,以及使用Vue的一些更高级的功能(如单文件组件或渲染函数),你需要一个安装了node和npm的终端。

目标: 学习如何创建一个Vue组件,将其渲染到另一个组件中,使用props将数据传递到组件中,并保存其状态。

创建一个ToDoItem组件

让我们创建第一个组件,它将显示一个单一的待办事项。我们将用它来建立我们的待办事项列表。

  1. 在你的moz-todo-vue/src/components目录下,创建一个ToDoItem.vue的新文件。在你的代码编辑器中打开该文件。
  2. 通过在文件顶部添加<template></template>来创建组件的模板部分。
  3. 在你的模板部分下面创建一个<script></script>部分。在<script>标签内,添加一个默认导出对象export default {},这是你的组件对象。

你的文件现在应该是这样的:

<template> </template>
<script>
  export default {};
</script>

现在我们可以开始为ToDoItem添加实际内容了。Vue模板目前只允许一个根元素--一个元素需要包裹模板内的所有内容(Vue 3 发布后会改变这种情况)。我们将为该根元素使用一个<div>

  1. 现在在你的组件模板中添加一个空的<div>

  2. 在那个<div>里面,让我们添加一个checkbox和一个对应的label。给复选框添加一个id,并添加一个for属性,将复选框映射到标签上,如下图所示。.

    <template>
      <div>
        <input type="checkbox" id="todo-item" checked="false" />
        <label for="todo-item">My Todo Item</label>
      </div>
    </template>

在应用程序中使用TodoItem组件

这一切都很顺利,但我们还没有将组件添加到我们的应用程序中,所以没有办法测试它,看看一切是否正常。我们现在就把它添加进去吧。

  1. 再次打开App.vue文件。

  2. <script>标签的顶部,添加以下内容来引入ToDoItem组件:

    import ToDoItem from './components/ToDoItem.vue';
  3. 在你的组件对象里面,添加 components 属性,然后在它里面添加您的ToDoItem组件进行注册。

你的<script>内容现在应该是这样的:

import ToDoItem from './components/ToDoItem.vue';

export default {
  name: 'app',
  components: {
    ToDoItem
  }
};

这和之前Vue CLI注册HelloWorld组件的方式是一样的。

要在应用程序中实际展示ToDoItem组件,你需要在<template>模板内添加一个<to-do-item>/to-do-item>元素。请注意,组件文件名及其在JavaScript中的表示方式总是用大写驼色(例如ToDoList),而等价的自定义元素总是用连字符小写(例如<to-do-list>)。

  1. <h1>下面,创建一个无序列表(<ul>),其中包含一个列表项(<li>)。
  2. 在列表项(<li>)里面添加<to-do-item></to-do-item>.

你的App.vue<template>内容现在应该是这样的:

<div id="app">
  <h1>To-Do List</h1>
  <ul>
    <li>
      <to-do-item></to-do-item>
    </li>
  </ul>
</div>

如果你再次查看你的应用程序的渲染情况,你现在应该看的到渲染的ToDoItem组件,由一个复选框和一个标签组成。

The current rendering state of the app, which includes a title of To-Do List, and a single checkbox and label

Making components dynamic with props

Our ToDoItem component is still not very useful because we can only really include this once on a page (IDs need to be unique), and we have no way to set the label text. Nothing about this is dynamic.

What we need is some component state. This can be achieved by adding props to our component. You can think of props as being similar to inputs in a function. The value of a prop gives components an initial state that affects their display.

Registering props

In Vue, there are two ways to register props:

  • The first way is to just list props out as an array of strings. Each entry in the array corresponds to the name of a prop.
  • The second way is to define props as an object, with each key corresponding to the prop name. Listing props as an object allows you to specify default values, mark props as required, perform basic object typing (specifically around JavaScript primitive types), and perform simple prop validation.

Note: Prop validation only happens in development mode, so you can't strictly rely on it in production. Additionally, prop validation functions are invoked before the component instance is created, so they do not have access to the component state (or other props).

For this component, we’ll use the object registration method.

  1. Go back to your ToDoItem.vue file.
  2. Add a props property inside the export default {} object, which contains an empty object.
  3. Inside this object, add two properties with the keys label and done.
  4. The label key's value should be an object with 2 properties (or props, as they are called in the context of being available to the components).
    1. The first is a required property, which will have a value of true. This will tell Vue that we expect every instance of this component to have a label field. Vue will warn us if a ToDoItem component does not have a label field.
    2. The second property we'll add is a type property. Set the value for this property as the JavaScript String type (note the capital "S"). This tells Vue that we expect the value of this property to be a string.
  5. Now on to the done prop.
    1. First add a default field, with a value of false. This means that when no done prop is passed to a ToDoItem component, the done prop will will have a value of false (bear in mind that this is not required — we only need default on non-required props).
    2. Next add a type field with a value of Boolean. This tells Vue we expect the value prop to be a JavaScript boolean type.

Your component object should now look like this:

<script>
  export default {
    props: {
      label: { required: true, type: String },
      done: { default: false, type: Boolean }
    }
  };
</script>

Using registered props

With these props defined inside the component object, we can now use these variable values inside our template. Let's start by adding the label prop to the component template.

In your <template>, replace the contents of the <label> element with {{label}}.

{{}} is a special template syntax in Vue, which lets us print the result of JavaScript expressions defined in our class, inside our template, including values and methods. It’s important to know that content inside {{}} is displayed as text and not HTML. In this case, we’re printing the value of the label prop.

Your component’s template section should now look like this:

<template>
  <div>
    <input type="checkbox" id="todo-item" checked="false" />
    <label for="todo-item">{{label}}</label>
  </div>
</template>

Go back to your browser and you'll see the todo item rendered as before, but without a label (oh no!). Go to your browser's DevTools and you’ll see a warning along these lines in the console:

[Vue warn]: Missing required prop: "label"

found in

---> <ToDoItem> at src/components/ToDoItem.vue
       <App> at src/App.vue
         <Root>

This is because we marked the label as a required prop, but we never gave the component that prop — we've defined where inside the template we want it used, but we haven't passed it into the component when calling it. Let’s fix that.

Inside your App.vue file, add a label prop to the <to-do-item></to-do-item> component, just like a regular HTML attribute:

<to-do-item label="My ToDo Item"></to-do-item>

Now you'll see the label in your app, and the warning won't be spat out in the console again.

So that's props in a nutshell. Next we'll move on to how Vue persists data state.

Vue's data object

If you change the value of the label prop passed into the <to-do-item></to-do-item> call in your App component, you should see it update. This is great. We have a checkbox, with an updatable label. However, we're currently not doing anything with the "done" prop — we can check the checkboxes in the UI, but nowhere in the app are we recording whether a todo item is actually done.

To achieve this, we want to bind the component's done prop to the checked attribute on the <input> element, so that it can serve as a record of whether the checkbox is checked or not. However, it's important that props serve as one-way data binding — a component should never alter the value of its own props. There are a lot of reasons for this. In part, components editing props can make debugging a challenge. If a value is passed to multiple children, it could be hard to track where the changes to that value were coming from. In addition, changing props can cause components to re-render. So mutating props in a component would trigger the component to rerender, which may in-turn trigger the mutation again.

To work around this, we can manage the done state using Vue’s data property. The data property is where you can manage local state in a component, it lives inside the component object alongside the props property and has the following structure:

data() {
  return {
    key: value
  }
}

You'll note that the data property is a function. This is to keep the data values unique for each instance of a component at runtime — the function is invoked separately for each component instance. If you declared data as just an object, all instances of that component would share the same values. This is a side-effect of the way Vue registers components and something you do not want.

You use this to access a component's props and other properties from inside data, as you may expect. We'll see an example of this shortly.

Note: Because of the way that this works in arrow functions (binding to the parent’s context), you wouldn’t be able to access any of the necessary attributes from inside data if you used an arrow function. So don’t use an arrow function for the data property.

So let's add a data property to our ToDoItem component. This will return an object containing a single property that we'll call isDone, whose value is this.done.

Update the component object like so:

export default {
  props: {
    label: { required: true, type: String },
    done: { default: false, type: Boolean }
  },
  data() {
    return {
      isDone: this.done
    };
  }
};

Vue does a little magic here — it binds all of your props directly to the component instance, so we don’t have to call this.props.done. It also binds other attributes (data, which you’ve already seen, and others like methods, computed, etc.) directly to the instance. This is, in part, to make them available to your template. The down-side to this is that you need to keep the keys unique across these attributes. This is why we called our data attribute isDone instead of done.

So now we need to attach the isDone property to our component. In a similar fashion to how Vue uses {{}} expressions to display JavaScript expressions inside templates, Vue has a special syntax to bind JavaScript expressions to HTML elements and components: v-bind. The v-bind expression looks like this:

v-bind:attribute="expression"

In other words, you prefix whatever attribute/prop you want to bind to with v-bind:. In most cases, you can use a shorthand for the v-bind property, which is to just prefix the attribute/prop with a colon. So :attribute="expression" works the same as v-bind:attribute="expression".

So in the case of the checkbox in our ToDoItem component, we can use v-bind to map the isDone property to the checked attribute on the <input> element. Both of the following are equivalent:

<input type="checkbox" id="todo-item" v-bind:checked="isDone" />

<input type="checkbox" id="todo-item" :checked="isDone" />

You're free to use whichever pattern you would like. It's best to keep it consistent though. Because the shorthand syntax is more commonly used, this tutorial will stick to that pattern.

So let's do this. Update your <input> element now to replace checked="false" with :checked="isDone".

Test out your component by passing :done="true" to the ToDoItem call in App.vue. Note that you need to use the v-bind syntax, because otherwise true is passed as a string. The displayed checkbox should be checked.

<template>
  <div id="app">
    <h1>My To-Do List</h1>
    <ul>
      <li>
        <to-do-item label="My ToDo Item" :done="true"></to-do-item>
      </li>
    </ul>
  </div>
</template>

Try changing true to false and back again, reloading your app in between to see how the state changes.

Giving Todos a unique id

Great! We now have a working checkbox where we can set the state programmatically. However, we can currently only add one ToDoList component to the page because the id is hardcoded. This would result in errors with assistive technology since the id is needed to correctly map labels to their checkboxes. To fix this, we can programmatically set the id in the component data.

We can use the lodash package's uniqueid() method to help keep the index unique. This package exports a function that takes in a string and appends a unique integer to the end of the prefix. This will be sufficient for keeping component ids unique.

Let’s add the package to our project with npm; stop your server and enter the following command into your terminal:

npm install --save lodash.uniqueid

Note: If you prefer yarn, you could instead use yarn add lodash.uniqueid.

We can now import this package into our ToDoItem component. Add the following line at the top of ToDoItem.vue’s <script> element:

import uniqueId from 'lodash.uniqueid';

Next, add add an id field to our data property, so the component object ends up looking like so (uniqueId() returns the specified prefix — todo- — with a unique string appended to it):

import uniqueId from 'lodash.uniqueid';

export default {
  props: {
    label: { required: true, type: String },
    done: { default: false, type: Boolean }
  },
  data() {
    return {
      isDone: this.done,
      id: uniqueId('todo-')
    };
  }
};

Next, bind the id to both our checkbox’s id attribute and the label’s for attribute, updating the existing id and for attributes as shown:

<template>
  <div>
    <input type="checkbox" :id="id" :checked="isDone" />
    <label :for="id">{{label}}</label>
  </div>
</template>

Summary

And that will do for this article. At this point we have a nicely-working ToDoItem component that can be passed a label to display, will store its checked state, and will be rendered with a unique id each time it is called. You can check if the unique ids are working by temporarily adding more <to-do-item></to-do-item> calls into App.vue, and then checking their rendered output with your browser's DevTools.

Now we're ready to add multiple ToDoItem components to our App. In our next article we'll look at adding a set of todo item data to our App.vue component, which we'll then loop through and display inside ToDoItem components using the v-for directive.  

In this module