Adam Wathan, February 7, 2018, 10am EST
OMG Scoped Slots Can Do That?
[Demo of forum thread 'tag' component]
"It would be cool if I could turn this component into a library..." ...if you want to do that, then it needs to be flexible / customizable.
Real-World example: Stripe
This can be a bit of a pain because of all the inputs to pass in.
This forces people to write CSS that adopts your own naming convention, which is less than desirable; you also might run into naming conflicts.
But there is always more customization that people want, and it's hard to provide for all the cases in CSS alone. "Customization is a bottomless pit."
Seriously.
To introduce this subject, let's first talk about boring slots. These allow you to provide placeholders for the end user, but they are really just props under the hood.
How can we apply the slots method to the 'tag' component we saw earlier?
With 'boring slots', you run into a scoping problem (the component sub-template doesn't have access to external properties).
To get around this, you make the sub-template a callback (like a React render prop). In Vue, to accomplish this you use a scoped slot.
[Live code demonstrating the transformation from boring slots to scoped slots.]
<slot name="tags" v-for="tag in tags" :tag="tag"></slot>
...
<span slot="tag" slot-scope="props" class="tags-input-tag">
- State / Data (simple values)
- Actions (removeTag(tag) is an example)
- Event Handlers / Event Handler Groups
- Bindings / Groups of Bindings
<slot name="tags" v-for="tag in tags" :tag="tag" :remove-tag="removeTag"></slot>
...
// Slot scope syntax:
slot-scope="{ newTag, onInput, handleTagBackspace, addTag }"
// Does this pattern force the end user to know too much?
// Enter 'Event Handler Groups'
// Define all the handlers in one object and bind them in:
v-on="{ input: onInput, keydown: onKeydown }"
...
:event-handlers="{
input: (e) => { newTag = e.target.value },
keydown: (e) => {
if (e.keyCode === 8) {
this.handleTagBackspace()
}
if (e.keyCode === 13) {
e.preventDefault()
this.addTag()
}
}"
:bindings="{
value: newTag
}"
slot-scope="{..., event-handlers, bindings }"
v-on="eventHandlers"
v-bind="bindings"
/>
Single-slot abstraction.
<template>
<div class="tags-input">
<slot
:tags="tags"
:remove-tag="removeTag"
:input-bindings="{
}"
:input-event-handlers="{
input: (e) => { newTag = e.target.value },
keydown: (e) => {
if (e.keyCode === 8) {
this.handleTagBackspace()
}
if (e.keyCode === 13) {
e.preventDefault()
this.addTag()
}
}
}"
/></slot>
</div>
</template>
You can also get around the single '
render() {
return this.$scopedSlots.default({
tags: this.tags,
removeTag: this.removeTag,
inputEventHandlers: {
//
},
inputBindings: {
//
}
})
},
You can fix re-implementing the styling with every component instance by writing wrapper components.
[Rapid live coding, pulling the tags input template into a wrapper]
Scoped slots allow, in the way demonstrated, for building of customizable and flexible Vue components for public libraries / distribution.