Skip to content

[Cookbook] Practical use of scoped slots with GoogleMaps #1754

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Oct 19, 2018

Conversation

mateuszRybczonek
Copy link
Contributor

@sdras a PR related to #1698


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 (`GoogleMapLoader.vue`) that initializes `Google Maps API`, creates a `google` and `map` objects and exposes those objects to the parent component in which the `GoogleMapLoader` is used.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It sounds that the whole parent will have access to it, whereas it is limited to children of the rendered component in the parent context. What do you think about: and exposes those objects to its potential children via slot ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and exposes those objects to its potential children in the template of a parent component in which the GoogleMapLoader is used

I think we should additionally point out that those children must exist in the parent template.

```html
<template>
<div>
<div id="map" :style="{ height: mapHeight }"></div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is a component that might be used in multiple places, I think you should avoid using id, and instead use .js- prefixed class or data attribute. WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also shouldn't height ideally come from CSS - that is shouldn't the map element take 100% of available space? For the sake of simplicity.

<template>
<div>
<div id="map" :style="{ height: mapHeight }"></div>
<template v-if="!!this.google && !!this.map">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about using more semantic Boolean() function instead of double negation?

The template part will look as below

```html
<template>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend adding comment above each code snippet - indicating what file is it. It makes reading way easier.


### Create component that uses our initializer component.

`VesselsGoogleMap.vue`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps for the sake of this cookbook example you should use more generic example like ContactMap?


### Expose `google` and `map` properties to the parent component by adding a scoped slot.

So 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 by doing it in the `<slot>` tag reverse the direction of data flow.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the by in but by doing it in the ... sentence is redundant 🤔


- both of them receive `google` from which we extract required object (Marker or Polyline) and `map` which gives as a reference to the map on which we want to place our element.

- each receive also a prop with data required to create a corresponding element.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT? each component specifies/expects also an extra prop to create a ...


To add elements to our map we render the factory component and pass the `google` and `map` objects.

We also need to provide data required by the element itself (in our case `marker` object with position of the marker, `path` object with polyline coordinates.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the parenthesis is not closed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and keyword would probably sound a bit nicer: of the marker and .... WDYT?


data () {
return {
mapSettings,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mapSettings in data seems to be redundant, isn't it?

methods: {
initializeMap() {
const mapContainer = this.$el.querySelector("#map");
const { Map } = this.google.maps;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be careful about destructuring Map as this is also a native constructor in ES6. I think we shouldn't avoid shadowing these kind of objects as it might have unintended consequences and might introduce confusion. Do you agree?

@mateuszRybczonek
Copy link
Contributor Author

@sdras

Would you be able to take a look anytime soon? :)

@sdras
Copy link
Member

sdras commented Aug 27, 2018

Yes, sorry about the delay, I'll take a look in the next day or so.

return {
google: null,
map: null
};
Copy link
Member

@phanan phanan Sep 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's follow Vue's code style by removing semicolons :)


## Wrapping Up
That's it. With all those bits and pieces created we can now re-use the `GoogleMapLoader` component as a base for all our maps by passing different templates to each one of them. Imagine that you need to create another map with different Markers or just Markers without Polylines. By using the above pattern it becomes very easy as we just need to pass different content to the `GoogleMapLoader` component.
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Need an empty line.
  2. Grammatically speaking, it should be a semicolon instead of a comma (; it can be…).

```

## When To Avoid This Pattern
It might be tempting to create a very complex solutions 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

solution (singular).


Yeah, ok, but why would I do things like that, what is the use of all that?

Scoped slots allow us to pass a template to the slot instead of passing a rendered element. It’s called a “scoped” slot because although the template is rendered in the parent component scope, it will have access to certain child component data. That gives us a freedom to fill the template with custom content from the parent component.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest "This gives us the freedom" instead.


After doing that even though the `google` and `map` props does not exist in the `TravelMap` scope, the component has access to them and we can use them in the template.

Yeah, ok, but why would I do things like that, what is the use of all that?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This question here is kind of out of the blue. We can instead go for something like "You might wonder…"

Copy link
Member

@sdras sdras left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this looks really good! I think it's a great addition, we really need something that covers scoped slots in depth and google maps is a great and common example.

I saw your post come through CSS-Tricks too, which is cool! I think it's cool to cross-post when it's good quality. Do you want to mention and link from one to the other?

I think there's a little bit of clean up work from comments from me and @phanan, but it's getting there. I also noticed some really nice additions in the CSS-Tricks version that might be stronger if added here as well.

Thanks!

---
title: Practical use of scoped slots with GoogleMaps
type: cookbook
order: 11
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This order needs to be updated as 11 is already taken now. I think we're on 13 with base 0 index but worth double checking.

```html
<template>
<div>
<div class="google-map" data-google-map></div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be better served as a ref like ref="googlemap" so you don't have to use a query selector below.


methods: {
initializeMap() {
const mapContainer = this.$el.querySelector("[data-google-map]");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See earlier comment, this would become const mapContainer = this.$refs.googlemap


Inside the script part:

- we receive props from the parent component which will allow us to set the Google Map:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these bullets should be capitalized as they are far from eachother.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the bullets from here

},
```

- on `mounted` hook we create an instance of `GoogleMapsApi` and `Map` object from the `GoogleMapsApi` and we set the values of `google` and `map` to the created instances
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's a bit confusing here to use the term "created" in the same sentence that you're using to describe another lifecycle hook as created is one as well. Please use a different word here, thanks.


So far so good, with that done we could continue adding the other objects to the map (Markers, Polylines, etc.) and using it as a ordinary map component. But we want to use our `GoogleMapLoader` component only as a loader that prepares the map, not renders anything on it.

To achieve that we need to allow parent component that will use our `GoogleMapLoader` to access `this.google` and `this.map` that are set inside the `GoogleMapLoader` component. That's where `scoped slots` really shine. Scoped slots allow us to expose the properties set in a child component to the parent component. It may sound like an inception, but bear with me one more minute.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"inception" literally means to begin something, so it wouldn't apply in this case. I think what you're referring to is the movie. This would need then to be rephrased to "It may sound like Inception, but..."

<script>
import GoogleMapLoader from './GoogleMapLoader'

import {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need this to be split across three lines- actually there's a bit of formatting that's off in this code sample. Can you please correct to:

import GoogleMapLoader from './GoogleMapLoader'
import { mapSettings } from '@/constants/mapSettings'

export default {
  components: {
    GoogleMapLoader
  },
  computed: {
    mapConfig() {
      return {
        ...mapSettings,
        center: { lat: 0, lng: 0 }
      }
    }
  }
}

};
```

- both of them receive `google` from which we extract required object (Marker or Polyline) and `map` which gives as a reference to the map on which we want to place our element
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please capitalize these as well :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bullets removed from here as well.

@mateuszRybczonek
Copy link
Contributor Author

@sdras @phanan, suggestions applied, please re-check 👁 when you find some time.

@sdras
Copy link
Member

sdras commented Oct 8, 2018

This is looking really great! The order should actually be 14, I just looked and there is already a 13. That's probably why the netlify build is failing. I think we're almost there.

@mateuszRybczonek
Copy link
Contributor Author

@sdras 14 looks good 👍, thanks.

@mateuszRybczonek
Copy link
Contributor Author

@sdras any chance of getting another review round from you? :)

@sdras
Copy link
Member

sdras commented Oct 19, 2018

Yes, great job! Congrats!

@sdras sdras merged commit dbd90a2 into vuejs:master Oct 19, 2018

methods: {
initializeMap() {
const mapContainer = this.$refs.googleMap)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've got a parenthesis loitering at the end there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! @mateuszRybczonek do you want to put in another PR for that or if not I can grab it sometime

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR with fix here - #1848, @alexdilley thanks for spotting that 👍

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

np! I'm selfishly only using merged commit notifications as a way to keep up to speed on new documentation hence not picking up sooner; your entry was an interesting read!

It's maybe not the remit of a recipe in a cookbook, but I, as a reader, was curious to understand why this approach is more appropriate than simply setting up the loader in the parent component, for example, or perhaps more globally in a Vuex store. I think the answer is "reusability", "encapsulation" and "separation of concerns", but I then question how many types of map-related workflows you'd typically find in a project whose requirements couldn't be satisfied by a single, general-purpose map component...splitting concerns and avoiding an unwieldy "god component", maybe? Please appreciate that I may be completely missing the point, for which I apologise!!

There's a lot of buzz around this pattern, with it being a common go-to for those "in the know", and it might be pertinent – with such an example as this as context – to explain why this would be the tool to reach for in this case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants