Создание компонентов

Компоненты представляют элементы, к которым компилятор Vue прикрепляет некоторое поведение. Компоненты позволяют инкапсулировать код и затем использовать его многократно в различных частях приложения.

Для создания компонента используется функция Vue.component(tagName, options), где параметр tagName - кастомный элемент HTML, который будет представлять компонент, а параметр options представляет конфигуацию компонента.

Например, определим простейший компонент:

<!DOCTYPE html>
<html>
<head>
    <title>Vue.js</title>
    <meta charset="utf-8" />
</head>
<body>
    <div id="app">
        <hello></hello>
        <hello></hello>
    </div>
    <script src="https://unpkg.com/vue"></script>
    <script>
        Vue.component('hello', {
            template: '<h2>Hello</h2>'
        })

        const app = new Vue({
            el: '#app'
        })
    </script>
</body>
</html>

Здесь определен компонент для элемента hello. И неважно, что такого элемента в HTML нет, мы его сами создаем. Более того мы не можем использовать для компонента встроенные элементы HTML типа div или h2. То есть по сути компонент называется "hello". В объекте options, который передается компоненту, через свойство template можно задать html-разметку, которую будет содержать компонент. В итоге данная разметка будет вставляться вместо элемента <hello></hello>.

При этом компонент должен быть определен до элемента Vue, в котором он используется. И при рендеринге страницы вместо элемента hello будет вставлено содержимое компонента.

При этом мы можем многократно использовать компонент в рамках приложения Vue, и в каждом случае будет происходить рендеринг компонента. Причем компонент может использоваться только в рамках того элемента, к которому прикреплен объект Vue. То есть мы не можем написать наподобие, вынеся использование компонента во вне:

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

Локальная и глобальная регистрация компонентов

Компоненты могут быть зарегистрированы локально и глобально. Глобальные компоненты доступны для любого объекта Vue на веб-странице. Локальные компоненты доступны только в рамках определенных объектов Vue.

Для локальной регистрации компонентов у объекта Vue устанавливается свойство components:

<!DOCTYPE html>
<html>
<head>
    <title>Vue.js</title>
    <meta charset="utf-8" />
</head>
<body>
    <div id="app">
        <section-header></section-header>
        <section-content></section-content>
    </div>
    <script src="https://unpkg.com/vue"></script>
    <script>
        Vue.component('section-header', {
            template: '<h3>Header</h3>'
        })

        const component = {
            template: '<div>Hello World</div>'
        }

        const app = new Vue({
            el: '#app',
            components: {
                'section-content': component
            }
        })
    </script>
</body>
</html>

Здесь определен глобальный компонент - section-header. Глобальный компонент определяется с помощью метода Vue.component().

И также здесь определен компонент component. По сути он представляет объект, который передается в качестве второго параметра в Vue.component() и может иметь все те же свойства, например, свойство template. Этот компонент локально регистрируется в объекте Vue:

components: {
    'section-content': component
}

Причем для его рендеринга будет использоваться элемент <section-content>.

Если бы компонент component не был бы зарегистрирован в объекте Vue, то тогда мы бы его не могли использовать, либо пришлось бы делать его глобальным.

Рассмотрим еще один пример:

<!DOCTYPE html>
<html>
<head>
    <title>Vue.js</title>
    <meta charset="utf-8" />
</head>
<body>
    <div id="app1">
        <section-header></section-header>
        <section-content></section-content>
        <section-footer></section-footer>
    </div>
    <hr/>
    <div id="app2">
        <section-header></section-header>
        <section-content></section-content>
    </div>
    <script src="https://unpkg.com/vue"></script>
    <script>
        Vue.component('section-header', {
            template: '<h2>Header</h2>'
        })

        const comp1 = {
            template: '<div>Content 1</div>'
        }

        const comp2 = {
            template: '<div>Content 2</div>'
        }

        const footer = {
            template: '<p><b>Footer</b></p>'
        }

        new Vue({
            el: "#app1",
            components: {
                'section-content': comp1,
                'section-footer': footer
            }
        })

        new Vue({
            el: "#app2",
            components: {
                'section-content': comp2
            }
        })
    </script>
</body>
</html>

Здесь определено два объекта Vue. Компонент section-header является глобальным и поэтому доступен из любого объекта Vue. В дополнение к нему первый объект локально регистрирует два компонента - comp1 и footer, а второй объект Vue - один компонент comp2.


Состояние и поведение компонентов

Параметр data

Как и объекты Vue, компоненты могут содержать некоторые данные или состояние в виде параметра data. Но при этом в компонентах параметр data должен представлять функцию, которая в свою очередь и возвращает состояние компонента. Например:

<!DOCTYPE html>
<html>
<head>
    <title>Vue.js</title>
    <meta charset="utf-8" />
</head>
<body>
    <div id="app">
        <counter></counter>
    </div>
    <script src="https://unpkg.com/vue"></script>
    <script>
        Vue.component('counter', {
            data: function() {
                return {
                    header: 'Counter Program'
                }
            },
            template: '<div><h2>{{ header }}</h2></div>'
        })

        new Vue({
            el: "#app"
        })
    </script>
</body>
</html>

Параметр methods

Кроме состояния компоненты могут определять поведение в виде методов, которые определяются через параметр methods, как и в объектах Vue:

<!DOCTYPE html>
<html>
<head>
    <title>Vue.js</title>
    <meta charset="utf-8" />
</head>
<body>
    <div id="app">
        <counter></counter>
    </div>
    <script src="https://unpkg.com/vue"></script>
    <script>
        Vue.component('counter', {
            data: function() {
                return {
                    header: 'Counter Program',
                    count: 0
                }
            },
            template: `<div><h2>{{ header }}</h2>
                        <button v-on:click="increase">+</button>
                        <span>{{ count }}</span>
                    </div>`,
            methods: {
                increase: function() {
                    this.count++
                }
            }
        })

        new Vue({
            el: "#app"
        })
    </script>
</body>
</html>

Здесь по нажатию на кнопку вызывается метод increase, который увеличивает значение свойства count.


Разделяемое состояние компонентов

Для каждого компонента можно определить его собственное состояние. Но также можно определять некоторое общее состояние для нескольких компонентов. Определим следующую веб-страницу:

<!DOCTYPE html>
<html>
<head>
    <title>Vue.js</title>
    <meta charset="utf-8" />
</head>
<body>
    <div id="app">
        <counter></counter>
        <counter></counter>
    </div>
    <script src="https://unpkg.com/vue"></script>
    <script>
        Vue.component('counter', {
            data: function() {
                return {
                    header: 'Counter Program',
                    count: 0
                }
            },
            template: `<div><h2>{{ header }}</h2>
                        <button v-on:click="increase">+</button>
                        <span>{{ count }}</span>
                    </div>`,
            methods: {
                increase: function() {
                    this.count++
                }
            }
        })

        new Vue({
            el: "#app"
        })
    </script>
</body>
</html>

В данном случае объект Vue использует два раза компонент counter. И каждый объект counter работает со своим состоянием. Изменение свойство count в одном компоненте никак не скажется на другом.

Рассмотрим другую ситуацию, когда компоненты используют одно и то же состояние:

<!DOCTYPE html>
<html>
<head>
    <title>Vue.js</title>
    <meta charset="utf-8" />
</head>
<body>
    <div id="app">
        <counter></counter>
        <counter></counter>
    </div>
    <script src="https://unpkg.com/vue"></script>
    <script>
        const data = {
            header: 'Counter Program',
            count: 0
        }

        Vue.component('counter', {
            data: function() {
                return data
            },
            template: `<div><h2>{{ header }}</h2>
                        <button v-on:click="increase">+</button>
                        <span>{{ count }}</span>
                    </div>`,
            methods: {
                increase: function() {
                    this.count++
                }
            }
        })

        new Vue({
            el: "#app"
        })
    </script>
</body>
</html>

В этом случае одно и то же состояние будет разделяться обоими компонентами. Поэтому изменения в состоянии в одном компоненте повлияют и на другой компонент.


Props

Каждый компонент определяет параметр props, через который мы можем передать компоненту извне различные данные. Например:

<!DOCTYPE html>
<html>
<head>
    <title>Vue.js</title>
    <meta charset="utf-8" />
</head>
<body>
    <div id="app">
        <message-comp message="hello"></message-comp>
    </div>
    <script src="https://unpkg.com/vue"></script>
    <script>
        Vue.component('message-comp', {
            props: ['message'],
            template: '<h2>{{ message }}</h2>'
        })

        new Vue({
            el: "#app"
        })
    </script>
</body>
</html>

Параметр props хранит массив ключей или свойств, которым извне можно передать значения. В данном случае определено одно свойство message. Это свойство также можно использовать в шаблоне компонента (<span>{{ message }}</span>).

Чтобы передать свойству значение, у элемента компонента применяется атрибут, который называется также, как и свойство:

<message-comp message="hello"></message-comp>

В некотором плане свойства props похожи на те свойства, которые определяются через параметр data. При этом надо учитывать, что мы не можем определить свойство с одним и тем же именем и в props, и в data.

Динамические свойства

В примере выше свойству message передается статическое литеральное значение - строка "hello". Однако можно также устанавливать значения свойств динамически в зависимости от введенных в поля ввода данных:

<!DOCTYPE html>
<html>
<head>
    <title>Vue.js</title>
    <meta charset="utf-8" />
</head>
<body>
    <div id="app">
        <input type="text" v-model="welcome" /><br><br>
        <message-comp v-bind:message="welcome"></message-comp>
    </div>
    <script src="https://unpkg.com/vue"></script>
    <script>
        Vue.component('message-comp', {
            props: ['message'],
            template: '<h2>{{ message }}</h2>'
        })

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

Теперь свойство message устанавливается динамически в зависимости от введеного значения.

Для этого применяется механизм привязки - перед атрибутом указывается директива v-bind, которая определяет, к какому значению идет привязка:

<message-comp v-bind:message="welcome"></message-comp>

Также можно использовать сокращенную форму:

<message-comp :message="welcome"></message-comp>

Привязка к сложным объектам

Допустим, наш компонент выводит данные о пользователе - имя и возраст. Передадим эти данные в компонент:

<!DOCTYPE html>
<html>
<head>
    <title>Vue.js</title>
    <meta charset="utf-8" />
</head>
<body>
    <div id="app">
        <input type="text" v-model="name" /><br><br>
        <input type="number" v-model.number="age" /><br><br>
        <user :name="name" :age="age"></user>
    </div>
    <script src="https://unpkg.com/vue"></script>
    <script>
        Vue.component('user', {
            props: ['name', 'age'],
            template: '<div><h2>User</h2><p>Name: {{ name }}</p><p>Age: {{ age }}</p></div>'
        })

        new Vue({
            el: "#app",
            data: {
                name: '',
                age: 18
            }
        })
    </script>
</body>
</html>

Здесь компонент получает значения для своих свойств name и age.

Но в данном случае свойства name и age можно оформить как единый объект и передавать этот объект целиком в компонент:

<!DOCTYPE html>
<html>
<head>
    <title>Vue.js</title>
    <meta charset="utf-8" />
</head>
<body>
    <div id="app">
        <input type="text" v-model="user.name" /><br><br>
        <input type="number" v-model.number="user.age" /><br><br>
        <user v-bind="user"></user>
    </div>
    <script src="https://unpkg.com/vue"></script>
    <script>
        Vue.component('user', {
            props: ['name', 'age'],
            template: '<div><h2>User</h2><p>Name: {{ name }}</p><p>Age: {{ age }}</p></div>'
        })

        new Vue({
            el: "#app",
            data: {
                user: {
                    name: '',
                    age: 18
                }
            }
        })
    </script>
</body>
</html>

С помощью атрибута v-bind="user" объект user сразу передается компоненту и автоматически сопоставляется со свойствами name и age, которые определены в компоненте. Но естественно должно быть соответствие между названиями свойств объекта user и названиями свойств компонента. В остальном результат работы будет тот же самый, что и в предыдущем случае. В то же время несмотря на то, что вроде бы идет привязка к объекту user, тем не менее компонент получает значения его свойств по отдельности.


Валидация props

С помощью props можно передавать извне данные в компонент. Однако не всегда данные, которые передаются приложению, являются корректными. Например, для возраста передать отрицательное числового значение или при вводе имени ввести пустую строку. Чтобы избежать подобных ситуаций, когда передаваемые данные не отвечают нашим ожиданиям и не являются корректными, применяется механизм валидации.

Прежде всего мы можем указать тип для свойств. В качестве типов можно использовать следующие: String, Number, Boolean, Function, Object, Array, Symbol.

Например, в прошлой теме параметр props определялся следующим образом:

props: ['name', 'age']

Но фактически это объект, который мы можем переписать иным образом:

Vue.component('user', {
    props: {name: String, age: Number},
    template: '<div><h2>User</h2><p>Name: {{ name }}</p><p>Age: {{ age }}</p></div>'
})

Здесь параметр props определен как объект. name и age в этом объекте выступают в качестве свойств, при этом для каждого свойства устанавливается тип.

Для более точной валидации свойства для него можно задать ряд параметров:

  • type: тип свойства
  • required: если этот параметр имеет значение true, то для данного свойства обязательно надо ввести значение
  • default: значение по умолчанию, которое устанавливается, если для свойства извне не передается никакого значения
  • validator: функция, которая валидирует значение свойства. Если значение корректно, то функция валидатора должна возвращать true, иначе возвращается false.

Если свойства не проходят валидацию, то в консоли браузера отображается соответствующее предупреждение.

Применим эти параметры:

<!DOCTYPE html>
<html>
<head>
    <title>Vue.js</title>
    <meta charset="utf-8" />
</head>
<body>
    <div id="app">
        <input type="text" v-model="user.name" /><br><br>
        <input type="number" v-model.number="user.age" /><br><br>
        <user v-bind="user"></user>
    </div>
    <script src="https://unpkg.com/vue"></script>
    <script>
        Vue.component('user', {
            props: {
                name: {
                    type: String,
                    required: true,
                    default: 'Tom',
                    validator: function(value) {
                        return value != 'admin' && value != ''
                    }
                },
                age: {
                    type: Number,
                    required: true,
                    default: 18,
                    validator: function(value) {
                        return value >= 0 && value < 100
                    }
                }
            },
            template: '<div><h2>User</h2><p>Name: {{ name }}</p><p>Age: {{ age }}</p></div>'
        })

        new Vue({
            el: "#app",
            data: {
                user: {
                    name: '',
                    age: 0
                }
            }
        })
    </script>
</body>
</html>

Передача массивов и сложных объектов

Отдельно рассмотрим передачу в компонент через props сложных объектов и массивов.

Например, передадим сложный объект:

<!DOCTYPE html>
<html>
<head>
    <title>Vue.js</title>
    <meta charset="utf-8" />
</head>
<body>
    <div id="app">
        <input type="text" v-model="user.name" /><br><br>
        <input type="number" v-model.number="user.age" /><br><br>
        <userinfo v-bind:user="user"></userinfo>
    </div>
    <script src="https://unpkg.com/vue"></script>
    <script>
        Vue.component('userinfo', {
            props: ["user"],
            template: `<div>
                        <h2>User</h2>
                        <p>Name: {{ user.name }}</p>
                        <p>Age: {{ user.age }}</p>
                    </div>`
        })

        new Vue({
            el: "#app",
            data: {
                user: {
                    name: 'Tom',
                    age: 18
                }
            }
        })
    </script>
</body>
</html>

Здесь компонент получает данные в целом именно как один объект user. И после этого в шаблоне компонента можно обращаться к свойствам этого объекта через user.name и user.age.

Но если мы не передадим значение для свойства user из props:

<userinfo></userinfo>

То в этом случае свойство user в компоненте будет неопределено, и программа завершит свою работу с ошибкой. В этом случае мы можем предусмотреть значение по умолчанию для этого свойства с помощью параметра default. При этом для сложных объектов, а также для массивов этот параметр должен представлять функцию, которая возращает начальное значение для свойства.

В частности, изменим компонент следующим образом:

<!DOCTYPE html>
<html>
<head>
    <title>Vue.js</title>
    <meta charset="utf-8" />
</head>
<body>
    <div id="app">
        <input type="text" v-model="user.name" /><br><br>
        <input type="number" v-model.number="user.age" /><br><br>
        <userinfo v-bind:user="user"></userinfo>
    </div>
    <script src="https://unpkg.com/vue"></script>
    <script>
        Vue.component('userinfo', {
            props: {
                user: {
                    type: Object,
                    default: function() {
                        return {
                            name: 'Bob',
                            age: 22
                        }
                    }
                }
            },
            template: `<div>
                            <h2>User</h2>
                            <p>Name: {{ user.name }}</p>
                            <p>Age: {{ user.age }}</p>
                        </div>`
        })

        new Vue({
            el: "#app",
            data: {
                user: {
                    name: 'Tom',
                    age: 18
                }
            }
        })
    </script>
</body>
</html>

Теперь если извне не передается значение для props, программа будет использовать значение по умолчанию.

Массивы

Рассмотрим передачу массивов:

<!DOCTYPE html>
<html>
<head>
    <title>Vue.js</title>
    <meta charset="utf-8" />
</head>
<body>
    <div id="app">
        <userslist :users="users"></userslist>
    </div>
    <script src="https://unpkg.com/vue"></script>
    <script>
        Vue.component('userslist', {
            props: ["users"],
            template: `<ul>
                        <li v-for="user in users">
                            <p>Name: {{ user.name }}</p>
                            <p>Age: {{ user.age }}</p>
                        </li>
                    </ul>`
        })

        new Vue({
            el: "#app",
            data: {
                users: [
                    {
                        name: 'Tom',
                        age: 18
                    },
                    {
                        name: 'Bob',
                        age: 23
                    },
                    {
                        name: 'Alice',
                        age: 21
                    }
                ]
            }
        })
    </script>
</body>
</html>

Здесь извне компоненту передается массив users, состоящий из сложных объектов. В компоненте мы можем с помощью директивы v-for пробежаться по всему массиву и получить отдельные его элементы.


Родительские и дочерние компоненты

Одни компоненты (родительские компоненты) могут содержать другие (дочерние компоненты). Например, один компонент выводит список объект, а для вывода отдельного объекта используется еще один компонент:

<!DOCTYPE html>
<html>
<head>
    <title>Vue.js</title>
    <meta charset="utf-8" />
    <style>
        .userdetails {
            border-bottom: 1px solid #888;
        }
    </style>
</head>
<body>
    <div id="app">
        <userslist :users="users"></userslist>
    </div>
    <script src="https://unpkg.com/vue"></script>
    <script>
        Vue.component('userdetails', {
            props: ["user"],
            template: `<div class="userdetails">
                            <p>Name: {{ user.name }}</p>
                            <p>Age: {{ user.age }}</p>
                        </div>`
        })
        Vue.component('userslist', {
            props: ["users"],
            template: `<div>
                        <userdetails v-for="user in users" :key="user.name" :user="user"></userdetails>
                    </div>`
        })
        new Vue({
            el: "#app",
            data: {
                users: [
                    {
                        name: 'Tom',
                        age: 18
                    },
                    {
                        name: 'Bob',
                        age: 23
                    },
                    {
                        name: 'Alice',
                        age: 21
                    }
                ]
            }
        })
    </script>
</body>
</html>

Здесь в компонент userslist передается список объектов users. Для вывода каждого отдельного объекта userslist использует еще один компонент userdetails. С помощью директивы v-for происходит перебор списка объектов, и каждый объект передается в компонент userdetails.

В принципе мы могли бы определить все в одном компоненте, однако выделение отдельного компонента userdetails позволяет развивать и обновлять его разметку отдельно от родительского компонента. Например, если потребуется изменить структуру разметки HTML в компоненте, то достаточно это сделать в коде компонента userdetails. К тому же мы можем повторно использовать данный компонент в других компонентах и частях программы.


Получение состояния вложенного компонента

С помощью ссылки this.$refs из объекта Vue мы можем ссылаться на различные элементы веб-станицы внутри объекта шаблона Vue. Но кроме того, подобным образом мы можем ссылаться также и на вложенные компоненты и, таким образом, обращаться к внутреннему состоянию компонента.

Например, установим в объекте Vue изменение свойства, которое определено в компоненте:

<!DOCTYPE html>
<html>
<head>
    <title>Vue.js</title>
    <meta charset="utf-8" />
</head>
<body>
    <div id="app">
        <userdetails :user="user" ref="details"></userdetails>
        <button v-on:click="toggle()">Show</button>
    </div>
    <script src="https://unpkg.com/vue"></script>
    <script>
        Vue.component('userdetails', {
            props: ["user"],
            template: `<div>
                        <h2>Информация о пользователе</h2>
                        <div v-if="visible">
                            <p>Name: {{ user.name }}</p>
                            <p>Age: {{ user.age }}</p>
                        </div>
                    </div>`,
            data: function() {
                return {
                    visible: false
                }
            }
        })

        new Vue({
            el: "#app",
            data: {
                user: {
                    name: 'Tom',
                    age: 18
                }
            },
            methods: {
                toggle: function() {
                    this.$refs.details.visible = !this.$refs.details.visible
                }
            }
        })
    </script>
</body>
</html>

Здесь для компонента userdetails с помощью атрибута ref установлена ссылка details, через которую можно ссылаться на данный компонент.

<userdetails :user="user" ref="details"></userdetails>

В самом компоненте userdetails определено свойство visible, которое управляет видимостью частью шаблона компонента. Для изменения значения этого свойства в объекте Vue предусмотрена кнопка, по нажатию на которую срабатывает метод toggle():

toggle: function() {
    this.$refs.details.visible = !this.$refs.details.visible
}

Передача функций обратного вызова в компоненты

Функции обратного вызова представляют еще один способ взаимодействия между родительским и дочерним компонентами: родительские компоненты могут определять функции, а вызываются они в дочерних компонентах.

Например, определим следующую веб-страницу:

<!DOCTYPE html>
<html>
<head>
    <title>Vue.js</title>
    <meta charset="utf-8" />
</head>
<body>
    <div id="app">
        <h2>Список пользователей</h2>
        <userform :addfn="add"></userform>
        <div>
            <useritem v-for="(user, index) in users"
                :user="user"
                :key="index"
                :index="index"
                :removefn="remove">
            </useritem>
        </div>
    </div>
    <script src="https://unpkg.com/vue"></script>
    <script>
        Vue.component('userform', {
            props: ["addfn"],
            data: function () {
                return {
                    user: {
                        name: '',
                        age: 18
                    }
                }
            },
            template: `<div>
                            <input type="text" v-model="user.name" />
                            <input type="number" v-model="user.age" />
                            <button v-on:click="addfn({name: user.name, age: user.age})">Add</button>
                        </div>`
        })

        Vue.component('useritem', {
            props: ["user", "index", "removefn"],
            template: `<div>
                            <p>Name: {{ user.name }} <br> Age: {{ user.age }}</p>
                            <button v-on:click="removefn(index)">Delete</button>
                        </div>`
        })

        new Vue({
            el: "#app",
            data: {
                users: [
                    { name: 'Tom', age: 23 },
                    { name: 'Bob', age: 26 },
                    { name: 'Alice', age: 28 }
                ]
            },
            methods: {
                remove: function(index) {
                    this.users.splice(index, 1)
                },
                add: function(user) {
                    this.users.push(user)
                }
            }
        })
    </script>
</body>
</html>

Объект Vue выводит на страницу массив элементов и определяет два метода для управления элементами: add (для добавления) и remove (для удаления).

Для добавления нового элемента определен компонент userform, который представляет форму с полями ввода. В этот компонент передается метод add в виде функции addfn:

<userform :addfn="add"></userform>

Причем функция addfn определена в компоненте userform через props:

Vue.component('userform', {
    props: ["addfn"],

При нажатии на кнопку эта функция будет вызываться, и ей будут передаваться введенные данные. Что фактически приведет к вызову метода add в родительском объекте Vue.

Аналогичная ситуация с компонентом useritem, который получает метод remove в виде функции removefn, которая также определяется через props. При нажатии на кнопку удаления вызывается функция removefn, ей передается индекс удаляемого элемента, и фактически будет идти вызов метода remove из объекта Vue.

Таким образом, в самих дочерних компонентах никаких действий по управлению массивом и объектами и по генерации событий не определено. Дочерние компоненты просто вызывают те методы, которые определены в родительском объекте.