-
Notifications
You must be signed in to change notification settings - Fork 3.4k
[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
[Cookbook] Practical use of scoped slots with GoogleMaps #1754
Conversation
|
||
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. |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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"> |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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` |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
Would you be able to take a look anytime soon? :) |
Yes, sorry about the delay, I'll take a look in the next day or so. |
return { | ||
google: null, | ||
map: null | ||
}; |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Need an empty line.
- 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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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? |
There was a problem hiding this comment.
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…"
There was a problem hiding this 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 |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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]"); |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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.
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. |
@sdras 14 looks good 👍, thanks. |
@sdras any chance of getting another review round from you? :) |
Yes, great job! Congrats! |
|
||
methods: { | ||
initializeMap() { | ||
const mapContainer = this.$refs.googleMap) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 👍
There was a problem hiding this comment.
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.
@sdras a PR related to #1698