Бывают ситуации, когда может потребоваться чтобы шаблон внутри слота имел доступ к данным дочернего компонента, отвечающего за отрисовку содержимого слота. Это особенно полезно, когда требуется свобода в создании пользовательских шаблонов, использующих свойства данных дочернего компонента. Это типичный случай для использования слотов с ограниченной областью видимости.
Представьте себе компонент, который настраивает и подготавливает внешний API для использования в другом компоненте, но не имеет тесной связи с каким-либо конкретным шаблоном. Такой компонент может быть повторно использован в нескольких местах, отрисовывая различные шаблоны, но используя один и тот же базовый объект с определенным API.
Мы создадим компонент (GoogleMapLoader.vue
) который:
google
и map
.GoogleMapLoader
.Ниже приведён пример как этого можно добиться. Мы проанализируем код по частям и посмотрим, что на самом деле происходит в следующем разделе.
Сначала создадим шаблон GoogleMapLoader.vue
:
<template>
<div>
<div class="google-map" ref="googleMap"></div>
<template v-if="Boolean(this.google) && Boolean(this.map)">
<slot
:google="google"
:map="map"
/>
</template>
</div>
</template>
Теперь скрипту нужно передать некоторые данные компоненту, который позволяет установить Google Maps API и Map object:
import GoogleMapsApiLoader from 'google-maps-api-loader'
export default {
props: {
mapConfig: Object,
apiKey: String
},
data() {
return {
google: null,
map: null
}
},
async mounted() {
const googleMapApi = await GoogleMapsApiLoader({
apiKey: this.apiKey
})
this.google = googleMapApi
this.initializeMap()
},
methods: {
initializeMap() {
const mapContainer = this.$refs.googleMap
this.map = new this.google.maps.Map(
mapContainer, this.mapConfig
)
}
}
}
Это всего лишь часть рабочего примера, который можно найти в Codesandbox ниже.
GoogleMapLoader.vue
В шаблоне создаём вместилище для карты, в который будет монтироваться объект Map из Google Maps API.
<template>
<div>
<div class="google-map" ref="googleMap"></div>
</div>
</template>
Далее, скрипт должен получить данные от родительского компонента, который позволит установить Google Map. Эти данные состоят из:
import GoogleMapsApiLoader from 'google-maps-api-loader'
export default {
props: {
mapConfig: Object,
apiKey: String
}
Затем устанавливаем null
в качестве начального значения google
и map
:
data() {
return {
google: null,
map: null
}
}
В перехватчике mounted
определяем объекты googleMapApi
и Map
из GoogleMapsApi
и устанавливаем значения google
и map
для создаваемых экземпляров:
async mounted() {
const googleMapApi = await GoogleMapsApiLoader({
apiKey: this.apiKey
})
this.google = googleMapApi
this.initializeMap()
},
methods: {
initializeMap() {
const mapContainer = this.$refs.googleMap
this.map = new this.google.maps.Map(mapContainer, this.mapConfig)
}
}
}
Пока все хорошо. Сделав всё это при необходимости можно продолжать добавлять на карту другие объекты (Маркеры, Ломанные и т.д.) и использовать её как обычный компонент карты.
Но мы хотим использовать наш компонент GoogleMapLoader
только в качестве загрузчика, который подготавливает карту — мы не хотим ничего на ней отрисовывать.
Для этого нам необходимо разрешить родительскому компоненту, который будет использовать наш GoogleMapLoader
, получать доступ к this.google
и this.map
, определённые внутри компонента GoogleMapLoader
. Вот где слоты с ограниченной областью видимости действительно выручают. Эти слоты позволяют в родительском компоненте получить доступ к свойствам, заданным в дочернем компоненте. Звучит с чего уже можно начинать, но потерпите ещё одну минуту, мы разберёмся с этим далее.
TravelMap.vue
В шаблоне мы отрисовываем компонент GoogleMapLoader
и передаём данные, необходимые для инициализации карты.
<template>
<GoogleMapLoader
:mapConfig="mapConfig"
apiKey="yourApiKey"
/>
</template>
Логика компонента будет выглядеть так:
<script>
import GoogleMapLoader from './GoogleMapLoader'
import { mapSettings } from '@/constants/mapSettings'
export default {
components: {
GoogleMapLoader
},
computed: {
mapConfig () {
return {
...mapSettings,
center: { lat: 0, lng: 0 }
}
}
}
}
</script>
До сих пор нет слотов с ограниченной областью видимости, так что давайте добавим один.
google
и map
в родительском компоненте с помощью слота с ограниченной областью видимостиНаконец, мы можем добавить слот с ограниченной областью видимости, который позволит нам получить доступ к данным дочернего компонента в родительском. Мы сделаем это, добавив тег <slot>
в дочерний компонент и передав данные, к которым хотим предоставить доступ (используя директиву v-bind
или её сокращение :propName
). Это не отличается от передачи данных в дочерний компонент, но если сделать это в теге <slot>
то направление потока данных изменится.
GoogleMapLoader.vue
<template>
<div>
<div class="google-map" ref="googleMap"></div>
<template v-if="Boolean(this.google) && Boolean(this.map)">
<slot
:google="google"
:map="map"
/>
</template>
</div>
</template>
Теперь, когда есть слот в дочернем компоненте, мы должны получать и использовать предоставленные входные параметры в родительском компоненте.
slot-scope
.Для получения данных в родительском компоненте, мы объявляем элемент шаблона и используем атрибут slot-scope
. Этот атрибут имеет доступ к объекту со всеми данными из дочернего компонента, к которым открываем доступ. Мы можем использовать объект целиком или деструктурировать его и использовать только необходимое.
Давайте воспользуемся деструктуризацией.
TravelMap.vue
<GoogleMapLoader
:mapConfig="mapConfig"
apiKey="yourApiKey"
>
<template slot-scope="{ google, map }">
{{ map }}
{{ google }}
</template>
</GoogleMapLoader>
Несмотря на то, что свойства google
и map
не существуют в области видимости TravelMap
, компонент имеет доступ к ним и мы можем использовать их в шаблоне.
Вы можете задаться вопросом, зачем нам делать такие вещи, и какая от этого польза ?
Слоты с ограниченной областью видимости позволяют передавать шаблон в слот вместо элемента создателя. Он называется scoped
-слотом, потому что у него будет доступ к определенным данным дочернего компонента, даже если шаблон отрисовывается в области видимости родительского компонента. Это предоставляет нам свободу по наполнению шаблона пользовательским содержимым из родительского компонента.
Теперь, когда карта готова, создадим два фабричных компонента, которые будут использоваться для добавления элементов в TravelMap
.
GoogleMapMarker.vue
import { POINT_MARKER_ICON_CONFIG } from '@/constants/mapSettings'
export default {
props: {
google: {
type: Object,
required: true
},
map: {
type: Object,
required: true
},
marker: {
type: Object,
required: true
}
},
mounted() {
new this.google.maps.Marker({
position: this.marker.position,
marker: this.marker,
map: this.map,
icon: POINT_MARKER_ICON_CONFIG
})
}
}
GoogleMapLine.vue
import { LINE_PATH_CONFIG } from '@/constants/mapSettings'
export default {
props: {
google: {
type: Object,
required: true
},
map: {
type: Object,
required: true
},
path: {
type: Array,
required: true
}
},
mounted() {
new this.google.maps.Polyline({
path: this.path,
map: this.map,
...LINE_PATH_CONFIG
})
}
}
Оба они получают google
, который мы используем для извлечения нужного объекта (Marker
или Polyline
), а также map
, в котором будет ссылка на карту, на которой можем разместить элемент.
Каждый компонент также ожидает дополнительные данные для создания соответствующего элемента. В этом случае это marker
и path
соответственно.
В перехватчике mounted
создаём элемент (Marker/Polyline) и прикрепляем его к карте, передавая свойство map
конструктору объекта.
Остается ещё один шаг…
Давайте используем наши фабричные компоненты для добавления элементов на карту. Мы должны отобразить фабричные компоненты и передать объекты google
и map
так, чтобы данные попали в нужные места.
Нам также необходимо предоставить данные, которые требуются самому элементу. В нашем случае это объект marker
с положением маркера и объект path
с координатами ломанной.
Итак, сопрягаем точки данных непосредственно в шаблон:
<GoogleMapLoader
:mapConfig="mapConfig"
apiKey="yourApiKey"
>
<template slot-scope="{ google, map }">
<GoogleMapMarker
v-for="marker in markers"
:key="marker.id"
:marker="marker"
:google="google"
:map="map"
/>
<GoogleMapLine
v-for="line in lines"
:key="line.id"
:path.sync="line.path"
:google="google"
:map="map"
/>
</template>
</GoogleMapLoader>
Нам нужно импортировать необходимые фабричные компоненты в наш скрипт и задать данные, которые будут передаваться маркерам и линиям:
import { mapSettings } from '@/constants/mapSettings'
export default {
components: {
GoogleMapLoader,
GoogleMapMarker,
GoogleMapLine
},
data () {
return {
markers: [
{ id: 'a', position: { lat: 3, lng: 101 } },
{ id: 'b', position: { lat: 5, lng: 99 } },
{ id: 'c', position: { lat: 6, lng: 97 } },
],
lines: [
{ id: '1', path: [{ lat: 3, lng: 101 }, { lat: 5, lng: 99 }] },
{ id: '2', path: [{ lat: 5, lng: 99 }, { lat: 6, lng: 97 }] }
],
}
},
computed: {
mapConfig () {
return {
...mapSettings,
center: this.mapCenter
}
},
mapCenter () {
return this.markers[1].position
}
}
}
Может возникнуть соблазн создать очень сложное решение на основе примера, но в какой-то момент можно попасть в ситуацию, когда эта абстракция становится самостоятельной частью кода, живущей в нашей кодовой базе. Если мы дойдём до этого момента, то, возможно, стоит подумать об извлечении в добавку.
Вот так. После создания всех этих маленьких частей мы теперь можем повторно использовать компонент GoogleMapLoader
в качестве базы для всех наших карт, передавая в каждом случае различные шаблоны. Представьте, что вам нужно создать другую карту с разными Маркерами или просто Маркеры без Ломанных. Используя вышеприведенный шаблон, это становится очень просто, так как просто нужно будет передать разное содержимое компоненту GoogleMapLoader
.
Этот шаблон не связан строго с Google Maps; он может быть использован с любой библиотекой для реализации базового компонента и предоставления API библиотеки, которое может быть затем использовано в компоненте, вызвавшем базовый компонент.