Practical use of scoped slots with GoogleMaps

    Imagine a component that configures and prepares an external API to be used in another component, but is not tightly coupled with any specific template. Such a component could then be reused in multiple places rendering different templates but using the same base object with specific API.

    We’ll create a component () that:

    • Initializes the
    • Creates google and map objects
    • Exposes those objects to the parent component in which the GoogleMapLoader is usedBelow is an example of how this can be achieved. We will analyze the code piece-by-piece and see what is actually happening in the next section.

    Let’s first establish our GoogleMapLoader.vue template:

    Now, our script needs to pass some props to the component which allows us to set the Google Maps API and :

    1. import GoogleMapsApiLoader from 'google-maps-api-loader'
    2. export default {
    3. props: {
    4. mapConfig: Object,
    5. apiKey: String,
    6. },
    7. data() {
    8. return {
    9. google: null,
    10. map: null
    11. }
    12. },
    13. async mounted() {
    14. const googleMapApi = await GoogleMapsApiLoader({
    15. apiKey: this.apiKey
    16. })
    17. this.google = googleMapApi
    18. this.initializeMap()
    19. },
    20. methods: {
    21. initializeMap() {
    22. const mapContainer = this.$refs.googleMap
    23. this.map = new this.google.maps.Map(
    24. mapContainer, this.mapConfig
    25. )
    26. }
    27. }
    28. }

    This is just part of a working example, you can find the whole example in the Codesandbox below.

    GoogleMapLoader.vue

    In the template, we create a container for the map which will be used to mount the Map object extracted from the Google Maps API.

    1. <template>
    2. <div>
    3. <div class="google-map" ref="googleMap"></div>
    4. </div>
    5. </template>

    Next up, our script needs to receive props from the parent component which will allow us to set the Google Map. Those props consist of:

    • : Google Maps config object
    • apiKey: Our personal api key required by Google Maps
    1. import GoogleMapsApiLoader from 'google-maps-api-loader'
    2. export default {
    3. props: {
    4. mapConfig: Object,
    5. apiKey: String,
    6. },

    Then, we set the initial values of google and map to null:

    On mounted hook we instantiate a googleMapApi and Map objects from the GoogleMapsApi and we set the values of google and map to the created instances:

    1. async mounted() {
    2. const googleMapApi = await GoogleMapsApiLoader({
    3. apiKey: this.apiKey
    4. })
    5. this.google = googleMapApi
    6. this.initializeMap()
    7. methods: {
    8. initializeMap() {
    9. const mapContainer = this.$refs.googleMap
    10. this.map = new this.google.maps.Map(mapContainer, this.mapConfig)
    11. }
    12. }
    13. }

    So far, so good. With all that done, we could continue adding the other objects to the map (Markers, Polylines, etc.) and use it as an ordinary map component.

    But, we want to use our GoogleMapLoader component only as a loader that prepares the map — we don’t want to render anything on it.

    2. Create component that uses our initializer component.

    TravelMap.vue

    In the template, we render the GoogleMapLoader component and pass props that are required to initialize the map.

    1. <template>
    2. <GoogleMapLoader
    3. :mapConfig="mapConfig"
    4. apiKey="yourApiKey"
    5. />
    6. </template>

    Our script tag will look like this:

    1. <script>
    2. import GoogleMapLoader from './GoogleMapLoader'
    3. import { mapSettings } from '@/constants/mapSettings'
    4. export default {
    5. components: {
    6. GoogleMapLoader
    7. },
    8. computed: {
    9. mapConfig () {
    10. return {
    11. ...mapSettings,
    12. center: { lat: 0, lng: 0 }
    13. }
    14. },
    15. },
    16. }
    17. </script>

    Still no scoped slots, so let’s add one.

    Finally, we can add a scoped slot that will do the job and allow us to access the child component props in the parent component. We do that by adding the <slot> tag in the child component and passing the props that we want to expose (using v-bind directive or :propName shorthand). It does not differ from passing the props down to the child component, but doing it in the <slot> tag will reverse the direction of data flow.

    GoogleMapLoader.vue

    Now, when we have the slot in the child component, we need to receive and consume the exposed props in the parent component.

    4. Receive exposed props in the parent component using slot-scope attribute.

    To receive the props in the parent component, we declare a template element and use the slot-scope attribute. This attribute has access to the object carrying all the props exposed from the child component. We can grab the whole object or we can and only what we need.

    Let’s de-structure this thing to get what we need.

    TravelMap.vue

    1. <GoogleMapLoader
    2. :mapConfig="mapConfig"
    3. apiKey="yourApiKey"
    4. >
    5. <template slot-scope="{ google, map }">
    6. {{ map }}
    7. {{ google }}
    8. </template>
    9. </GoogleMapLoader>

    Even though the google and map props do not exist in the TravelMap scope, the component has access to them and we can use them in the template.

    You might wonder why would we do things like that and what is the use of all that?

    Now when we have our map ready we will create two factory components that will be used to add elements to the TravelMap.

    GoogleMapMarker.vue

    1. import { POINT_MARKER_ICON_CONFIG } from '@/constants/mapSettings'
    2. export default {
    3. props: {
    4. google: {
    5. type: Object,
    6. required: true
    7. },
    8. map: {
    9. type: Object,
    10. required: true
    11. },
    12. marker: {
    13. type: Object,
    14. required: true
    15. }
    16. },
    17. mounted() {
    18. new this.google.maps.Marker({
    19. position: this.marker.position,
    20. marker: this.marker,
    21. map: this.map,
    22. })
    23. }

    GoogleMapLine.vue

    1. import { LINE_PATH_CONFIG } from '@/constants/mapSettings'
    2. export default {
    3. props: {
    4. google: {
    5. type: Object,
    6. required: true
    7. },
    8. map: {
    9. type: Object,
    10. required: true
    11. },
    12. path: {
    13. type: Array,
    14. required: true
    15. }
    16. },
    17. mounted() {
    18. new this.google.maps.Polyline({
    19. path: this.path,
    20. map: this.map,
    21. ...LINE_PATH_CONFIG
    22. })
    23. }
    24. }

    Both of these receive google that we use to extract the required object (Marker or Polyline) as well as map which gives as a reference to the map on which we want to place our element.

    Each component also expects an extra prop to create a corresponding element. In this case, we have marker and path, respectively.

    On the mounted hook, we create an element (Marker/Polyline) and attach it to our map by passing the map property to the object constructor.

    There’s still one more step to go…

    6. Add elements to map

    Let’s use our factory components to add elements to our map. We must render the factory component and pass the google and map objects so data flows to the right places.

    We also need to provide the data that’s required by the element itself. In our case, that’s the marker object with the position of the marker and the path object with Polyline coordinates.

    Here we go, integrating the data points directly into the template:

    We need to import the required factory components in our script and set the data that will be passed to the markers and lines:

    1. import { mapSettings } from '@/constants/mapSettings'
    2. export default {
    3. components: {
    4. GoogleMapLoader,
    5. GoogleMapMarker,
    6. GoogleMapLine
    7. },
    8. data () {
    9. return {
    10. markers: [
    11. { id: 'a', position: { lat: 3, lng: 101 } },
    12. { id: 'b', position: { lat: 5, lng: 99 } },
    13. { id: 'c', position: { lat: 6, lng: 97 } },
    14. ],
    15. lines: [
    16. { id: '1', path: [{ lat: 3, lng: 101 }, { lat: 5, lng: 99 }] },
    17. { id: '2', path: [{ lat: 5, lng: 99 }, { lat: 6, lng: 97 }] }
    18. ],
    19. }
    20. },
    21. computed: {
    22. mapConfig () {
    23. return {
    24. ...mapSettings,
    25. center: this.mapCenter
    26. }
    27. },
    28. mapCenter () {
    29. return this.markers[1].position
    30. }
    31. },
    32. }

    It might be tempting to create a very complex solution based on the example, but at some point we can get to the situation where this abstraction becomes an independent part of the code living in our codebase. If we get to that point it might be worth considering extraction to an add-on.

    This pattern is not strictly connected to Google Maps; it can be used with any library to set the base component and expose the library’s API that might be then used in the component that summoned the base component.