Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

Vue .js 2.0 component


May 07, 2021 Vue.js 2.0


Table of contents


Vue .js 2.0 component

What are components?

Components are one of the most powerful .js Vue's vue system. C omponents can extend HTML elements to encapsulate reusable code. A t a higher level, a component is a custom element, .js Vue's compiler adds special functionality to it. In some cases, components can also be in the form of native HTML elements extended by is attributes.

Use components

Registered

As we said earlier, we can create a Vue instance by:

new Vue({
  el: '#some-element',
  // 选项
})

To register a global component, you can use Vue.component (tagName, options). For example:

Vue.component('my-component', {
  // 选项
})

For custom label names, Vue.js does not enforce compliance with the W3C rule (small case, and contains a short bar), although it is better to follow this rule.

After the component is registered, it can be used as a custom element in the module of the parent instance. To make sure that the component is registered before initializing the root instance:

<div id="example">
  <my-component></my-component>
</div>
// 注册
Vue.component('my-component', {
  template: '<div>A custom component!</div>'
})
// 创建根实例
new Vue({
  el: '#example'
})

Rendered as:

<div id="example">
  <div>A custom component!</div>
</div>
A custom component!

Local registration

You do not have to register each component globally. By registering by using the component instance option, you can make a component available only in the scope of another instance/component:

var Child = {
  template: '<div>A custom component!</div>'
}
new Vue({
  // ...
  components: {
    // <my-component> 将只在父模板可用
    'my-component': Child
  }
})

This package also applies to other registerable Vue features, such as instructions.

DOM template resolution instructions

When you use DOM as a template (for example, by mounting the el option on an existing element), you are subject to some html limitations because Vue can only get template content after the browser parses and standardizes HTML. In particular, elements like these, such as those that can be wrapped in them, can only appear inside other elements.

Using these restricted elements in custom components can cause problems, such as:

<table>
  <my-row>...</my-row>
</table>

Custom components are considered invalid content and therefore cause errors when rendered. The workable scenario is to use the special is property:

<table>
  <tr is="my-row"></tr>
</table>

It should be noted that these restrictions do not apply if you use a string template from one of the following sources:

  • <script type="text/x-template">
  • JavaScript inline template string
  • The .vue component

Therefore, use a string template if necessary.

Data must be a function

When using components, most options can be passed into the Vue constructor, with one exception: data must be a function. In fact, if you do:

Vue.component('my-component', {
  template: '<span>{{ message }}</span>',
  data: {
    message: 'hello'
  }
})

Vue then issues a warning to the console that data must be a function in the component. It would be better to understand the existence of such a rule.

<div id="example-2">
  <simple-counter></simple-counter>
  <simple-counter></simple-counter>
  <simple-counter></simple-counter>
</div>
var data = { counter: 0 }
Vue.component('simple-counter', {
  template: '<button v-on:click="counter += 1">{{ counter }}</button>',
  // data 是一个函数,因此 Vue 不会警告,
  // 但是我们为每一个组件返回了同一个对象引用
  data: function () {
    return data
  }
})
new Vue({
  el: '#example-2'
})

Results:


Because the three components share the same data, adding one counter affects all components! We can solve this problem by returning a new data object for each component:

data: function () {
  return {
    counter: 0
  }
}

Now each counter has its own internal state:

The components that make up it

Components mean working together, as is often the case with parent-child components: Component A uses component B in its template. T hey necessarily need to communicate with each other: to pass data to a child component, a child component needs to tell the parent component what is happening inside it. H owever, it is important to decouple parent-child components as much as possible in a well-defined interface. This ensures that each component can be written and understood in a relatively isolated environment and significantly improves component maintainability and re-useability.

In Vue .js, the relationship between parent-child components can be summed up as props down, events up. T he parent component passes data down through props to the child component, which sends messages to the parent component through events. See how they work.

Vue .js 2.0 component

Props

Use Props to pass data

The scope of a component instance is orphaned. T his means that the data of the parent component cannot and should not be referenced directly within the template of the child component. You can use props to pass data to sub-components.

prop is a custom property used by the parent component to pass data. Sub-components need to explicitly declare "prop" with the props option:

Vue.component('child', {
  // 声明 props
  props: ['message'],
  // 就像 data 一样,prop 可以用在模板内
  // 同样也可以在 vm 实例中像 “this.message” 这样使用
  template: '<span>{{ message }}</span>'
})

Then pass a normal string to it:

<child message="hello!"></child>

Results:

hello!

camelCase vs. kebab-case

HTML attributes are not case sensitive. When using a non-string template, the name form of prop changes from camelCase to kebab-case (short horizontal line apart):

Vue.component('child', {
  // camelCase in JavaScript
  props: ['myMessage'],
  template: '<span>{{ myMessage }}</span>'
})
<!-- kebab-case in HTML -->
<child my-message="hello!"></child>

Again, if you use string templates, don't care about these limitations.

Dynamic Props

Similar to binding HTML attributes to an expression with v-bind, you can also dynamically bind the value of props to the data of the parent component. Whenever the data of the parent component changes, the change is also transmitted to the child component:

<div>
  <input v-model="parentMsg">
  <br>
  <child v-bind:my-message="parentMsg"></child>
</div>

Using the abbreviation syntax of v-bind is usually simpler:

<child :my-message="parentMsg"></child>

Results:


Message from parent

Literal syntax vs dynamic syntax

One common mistake beginners make is to pass values using literal syntax:

<!-- 传递了一个字符串"1" -->
<comp some-prop="1"></comp>

Because it is a word-based prop, its value is passed down in the string "1" rather than in the actual number. If you want to pass an actual JavaScript number, you need to use v-bind so that its value is evaluated as a JavaScript expression:

<!-- 传递实际的数字 -->
<comp v-bind:some-prop="1"></comp>

One-way data flow

prop is one-way bound: when the properties of the parent component change, they are transmitted to the child component, but not the other way around. This is to prevent the child component from inadvertently modifying the state of the parent component -- which can make the app's data flow difficult to understand.

In addition, each time the parent component is updated, all props for the child component are updated to the latest value. T his means that you should not change prop within sub-components. If you do, Vue will give a warning in the console.

There are usually two scenarios that change prop:

  1. prop is passed in as the initial value, and the sub-component then uses its initial value only as the initial value of the local data;
  2. prop is passed in as the original value that needs to be transformed.

More precisely, the two scenarios are:

  1. Define a local data property and use the initial value of prop as the initial value of the local data.
    props: ['initialCounter'],
    data: function () {
      return { counter: this.initialCounter }
    }
  2. Defines a computed property that is calculated from the value of prop.
    props: ['size'],
    computed: {
      normalizedSize: function () {
        return this.size.trim().toLowerCase()
      }
    }

Note: In JavaScript, objects and arrays are reference types that point to the same memory space, and if prop is an object or array, changing it inside a child component affects the state of the parent component.

Prop validation

Components can specify validation requirements for props. V ue issues a warning if a validation requirement is not specified. This is useful when components are used by others.

When prop is an object and not an array of strings, it contains validation requirements:

Vue.component('example', {
  props: {
    // 基础类型检测 (`null` 意思是任何类型都可以)
    propA: Number,
    // 多种类型
    propB: [String, Number],
    // 必传且是字符串
    propC: {
      type: String,
      required: true
    },
    // 数字,有默认值
    propD: {
      type: Number,
      default: 100
    },
    // 数组/对象的默认值应当由一个工厂函数返回
    propE: {
      type: Object,
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator: function (value) {
        return value > 10
      }
    }
  }
})

Type can be the following native constructor:

  • String
  • Number
  • Boolean
  • Function
  • Object
  • Array

Type can also be a custom constructor that uses instanceof detection.

When prop validation fails, Vue rejects setting this value on the sub-component and throws a warning if the development version is used.

Custom events

We know that the parent component uses props to pass data to child components, but what should be done if the child component wants to pass the data back? That's a custom event!

Use v-on binding to customize events

Each Vue instance implements the Events interface, which is:

  • Use $on eventName to listen for events
  • Use $emit eventName to trigger an event

Vue's event system is separated from the browser's EventTarget API. Although they work similarly, $on and $emit are not alias for addEventListener and dispatchEvent.

In addition, the parent component can listen directly with v-on for events triggered by the child component where it is used.

Here's an example:

<div id="counter-event-example">
  <p>{{ total }}</p>
  <button-counter v-on:increment="incrementTotal"></button-counter>
  <button-counter v-on:increment="incrementTotal"></button-counter>
</div>
Vue.component('button-counter', {
  template: '<button v-on:click="increment">{{ counter }}</button>',
  data: function () {
    return {
      counter: 0
    }
  },
  methods: {
    increment: function () {
      this.counter += 1
      this.$emit('increment')
    }
  },
})
new Vue({
  el: '#counter-event-example',
  data: {
    total: 0
  },
  methods: {
    incrementTotal: function () {
      this.total += 1
    }
  }
})

0

In this case, the sub-component is completely decoupled from its external. All it does is trigger an internal event that the parent component cares about.

Binding native events to components

Sometimes, you may want to listen for a native event on the root element of a component. Y ou can decorate v-on with .native. For example:

<my-component v-on:click.native="doTheThing"></my-component>

Enter components using the form of a custom event

Custom events can also be used to create custom form input components that use v-models for two-way data binding. Remember:

<input v-model="something">

Just a syntax:

<input v-bind:value="something" v-on:input="something = $event.target.value">

So when used in components, it is equivalent to the following short case:

<input v-bind:value="something" v-on:input="something = arguments[0]">

So for the v-model of the component to take effect, it must:

  • Accept a value property
  • The input event is triggered when there is a new value

A very simple currency input:

<currency-input v-model="price"></currency-input>
Vue.component('currency-input', {
  template: '\
    <span>\
      $\
      <input\
        ref="input"\
        v-bind:value="value"\
        v-on:input="updateValue($event.target.value)"\
      >\
    </span>\
  ',
  props: ['value'],
  methods: {
    // Instead of updating the value directly, this
    // method is used to format and place constraints
    // on the input's value
    updateValue: function (value) {
      var formattedValue = value
        // Remove whitespace on either side
        .trim()
        // Shorten to 2 decimal places
        .slice(0, value.indexOf('.') + 3)
      // If the value was not already normalized,
      // manually override it to conform
      if (formattedValue !== value) {
        this.$refs.input.value = formattedValue
      }
      // Emit the number value through the input event
      this.$emit('input', Number(formattedValue))
    }
  }
})

Results:

$

The above implementation is too idealistic. F or example, the user can even enter more than one dial or period - slug! So we need a more meaningful example, and here's a better currency filter:

This interface can be used not only to connect form inputs within components, but also to easily integrate input types that you create yourself. Imagine:

<voice-recognizer v-model="question"></voice-recognizer>
<webcam-gesture-reader v-model="gesture"></webcam-gesture-reader>
<webcam-retinal-scanner v-model="retinalImage"></webcam-retinal-scanner>

Non-parent-child component communication

Sometimes components that are not parent-child also need to communicate. In a simple scenario, an empty Vue instance is used as the central event bus:

var bus = new Vue()
// 触发组件 A 中的事件
bus.$emit('id-selected', 1)
// 在组件 B 创建的钩子中监听事件
bus.$on('id-selected', function (id) {
  // ...
})

In more complex cases, you should consider using a dedicated state management model .

Distribute content using Slots

When using components, you often combine them like this:

<app>
  <app-header></app-header>
  <app-footer></app-footer>
</app>

Note two points:

  1. The component does not know what will be on its mount point. The contents of the mount point are determined by the parent component of the .lt;app.
  2. The component is likely to have its own template.

In order for components to be combined, we need a way to mix the contents of the parent component with the child component's own template. T his process is called content distribution (or "transclusion" if you are familiar with Angular). Vue.js implemented a content distribution API that uses a special slot for the original content, referring to the current draft Web component specification.

Compilation scope

Before we dive into the content distribution API, let's clarify the compilation scope of the content. Suppose the template is:

<child-component>
  {{ message }}
</child-component>

Should message be bound to the data of the parent component, or to the data of the child component? T he answer is the parent component. The component scope is simply:

The contents of the parent component template are compiled within the scope of the parent component;

A common mistake is an attempt to bind an instruction to a child component's properties/methods within the parent component template:

<!-- 无效 -->
<child-component v-show="someChildProperty"></child-component>

Assuming that someChildProperty is a property of a sub-component, the example above does not work as expected. The parent component template should not know the status of the child component.

If you want to bind instructions within a sub-component to the root node of a component, you should do so within its template:

Vue.component('child-component', {
  // 有效,因为是在正确的作用域内
  template: '<div v-show="someChildProperty">Child</div>',
  data: function () {
    return {
      someChildProperty: true
    }
  }
})

Similarly, distribution content is compiled within the scope of the parent component.

A single Slot

Unless the child component template contains at least one slot, the contents of the parent component are discarded. When a child component template has only one slot without properties, the entire content fragment of the parent component is inserted into the DOM location where the slot is located and replaces the slot label itself.

Initially, anything in the label is considered alternate. The alternate content is compiled within the scope of the subcompony and is displayed only if the host element is empty and there is nothing to insert.

Suppose the my-component component has the following template:

<div>
  <h2>I'm the child title</h2>
  <slot>
    如果没有分发内容则显示我。
  </slot>
</div>

Parent component template:

<div>
  <h1>I'm the parent title</h1>
  <my-component>
    <p>This is some original content</p>
    <p>This is some more original content</p>
  </my-component>
</div>

Rendering results:

<div>
  <h1>I'm the parent title</h1>
  <div>
    <h2>I'm the child title</h2>
    <p>This is some original content</p>
    <p>This is some more original content</p>
  </div>
</div>

Named Slots

The element can be configured to distribute content with a special property name. M ultiple slots can have different names. The name slot matches elements in the content fragment that correspond to the slot attribute.

You can still have an anonymous slot, which is the default slot, as an alternate slot where a matching content fragment cannot be found. If there is no default slot, these unsetched content fragments are discarded.

For example, suppose we have an app-layout component with a template that:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

Parent component template:

<app-layout>
  <h1 slot="header">Here might be a page title</h1>
  <p>A paragraph for the main content.</p>
  <p>And another one.</p>
  <p slot="footer">Here's some contact info</p>
</app-layout>

The rendering results are:

<div class="container">
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div>

The content distribution API is a useful mechanism when combining components.

Dynamic components

Multiple components can use the same mount point and then dynamically switch between them. Dynamically bind to its is attribute, using a reserved element:

var vm = new Vue({
  el: '#example',
  data: {
    currentView: 'home'
  },
  components: {
    home: { /* ... */ },
    posts: { /* ... */ },
    archive: { /* ... */ }
  }
})
<component v-bind:is="currentView">
  <!-- 组件在 vm.currentview 变化时改变! -->
</component>

You can also bind directly to a component object:

var Home = {
  template: '<p>Welcome home!</p>'
}
var vm = new Vue({
  el: '#example',
  data: {
    currentView: Home
  }
})

keep-alive

If you keep a switched component in memory, you can preserve its state or avoid re-rendering. To do this, you can add a key-alive instruction parameter:

<keep-alive>
  <component :is="currentView">
    <!-- 非活动组件将被缓存! -->
  </component>
</keep-alive>

See more details on the API reference.

Miscellaneous

Write re-usable components

When writing components, remember whether it is beneficial to re-use them. It doesn't matter that one-time components are tightly coupled with other components, but reflumpable components should define a clear public interface.

The APIs for the Vue components come from three parts - props, events, and slots:

  • Props allows the external environment to pass data to the component
  • Events allows components to trigger side effects of the external environment
  • Slots allows the external environment to combine additional content into components.

Using the short-form syntax of v-bind and v-on, the indentation of the template is clear and concise:

<my-component
  :foo="baz"
  :bar="qux"
  @event-a="doThis"
  @event-b="doThat"
>
  <img slot="icon" src="...">
  <p slot="main-text">Hello!</p>
</my-component>

The sub-component index

Despite props and events, there are times when you need direct access to sub-components in JavaScript. T o do this, you can use ref to specify an index ID for the sub-components. For example:

<div id="parent">
  <user-profile ref="profile"></user-profile>
</div>
var parent = new Vue({ el: '#parent' })
// 访问子组件
var child = parent.$refs.profile

When ref is used with v-for, ref is an array or object that contains the appropriate sub-components.

$refs is filled only after the component rendering is complete, and it is non-responsive. It serves only as a contingency plan for direct access to sub-components -- you should avoid using the property in templates or compute properties $refs.

Asynchronous components

In large applications, we may need to split the app into small modules and download them from the server on demand. T o make things easier, Vue .js allows components to be defined as a factory function, dynamically parsing the definition of components. V ue.js triggers the factory function only when the component needs to render, and caches the results for subsequent rendering. For example:

Vue.component('async-example', function (resolve, reject) {
  setTimeout(function () {
    resolve({
      template: '<div>I am async!</div>'
    })
  }, 1000)
})

The factory function receives a resolve callback, called when it receives a component definition downloaded from the server. Y ou can also call reject (reason) to indicate a failed load. H ere setTimeout is just for demonstration. H ow to get components is entirely up to you. Recommended for: Webpack's code segmentation feature:

Vue.component('async-webpack-example', function (resolve) {
  // 这个特殊的 require 语法告诉 webpack
  // 自动将编译后的代码分割成不同的块,
  // 这些块将通过 Ajax 请求自动下载。
  require(['./my-async-component'], resolve)
})

You can return a Promise resolve function using the syntax of Webpack 2 plus ES2015:

Vue.component(
  'async-webpack-example',
  () => System.import('./my-async-component')
)

If you're a Browserify user, you may not be able to use asynchronous components, and its authors have shown that Browserify doesn't support asynchronous loading. If this feature is important to you, use Webpack.

The component naming convention

When registering components (or props), you can use kebab-case, camelCase, or TitleCase. Vue doesn't care about that.

// 在组件定义中
components: {
  // 使用 camelCase 形式注册
  'kebab-cased-component': { /* ... */ },
  'camelCasedComponent': { /* ... */ },
  'TitleCasedComponent': { /* ... */ }
}

In the HTML template, use the kebab-case form:

<!-- 在HTML模版中始终使用 kebab-case -->
<kebab-cased-component></kebab-cased-component>
<camel-cased-component></camel-cased-component>
<title-cased-component></title-cased-component>

When using string patterns, you can not be restricted by the case-insensitive of HTML. This means that in the template, you can actually use camelCase, PascalCase, or kebab-case to refer to your components and props:

<!-- 在字符串模版中可以用任何你喜欢的方式! -->
<my-component></my-component>
<myComponent></myComponent>
<MyComponent></MyComponent>

If a component does not pass content without a slot element, you can even use / make it self-closed after the component name:

<my-component/>

Of course, this only works in string templates. Because an autistic custom element is invalid HTML, the browser's native parser cannot recognize it.

Recursive components

A component can call itself recursively within its template, but only if it has a name option:

name: 'unique-name-of-my-component'

When you register a component globally with Vue.component, the global ID is automatically set as the component's name option.

Vue.component('unique-name-of-my-component', {
  // ...
})

If you are not careful, recursive components can cause a dead loop:

name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'

The above component causes an error "max stack size exceeded", so make sure that the recursive call has a termination condition (such as using v-if when the recursive call and letting him eventually return false).

Inline template

If a child component has an inline-template attribute, the component treats its content as its template rather than as distribution. This makes templates more flexible.

<my-component inline-template>
  <div>
    <p>These are compiled as the component's own template.</p>
    <p>Not parent's transclusion content.</p>
  </div>
</my-component>

But inline-template makes the scope of the template difficult to understand. The best practice is to use the template option to define a template within a component or to use a template element in a .vue file.

X-Templates

Another way to define templates is to use the text/x-template type in the JavaScript label and specify an id. For example:

<script type="text/x-template" id="hello-world-template">
  <p>Hello hello hello</p>
</script>
Vue.component('hello-world', {
  template: '#hello-world-template'
})

This is useful in many templates or small applications, otherwise it should be avoided because it isolates templates from other definitions of components.

Use the low-level static components of v-once (Cheap Static Component)

Although rendering HTML in Vue is quick, consider using v-once to cache the rendering results when the component contains a large amount of static content, as this is:

Vue.component('terms-of-service', {
  template: '\
    <div v-once>\
      <h1>Terms of Service</h1>\
      ... a lot of static content ...\
    </div>\
  '
})