Renderless components
September 2020 - Stéphane Reiss
Advanced vue pattern
Who am I?
- Web developer
- PHP background
- I love javascript
Stéphane Reiss
Agenda
- What is a renderless component
- Why renderless component - The tags input example
- Another example - vue-promised
- Workshop - Calendar event management
- Closing remarks
This presentation is inspired by this excellent blog post:
What is a renderless component
Components that don’t render anything
<script>
export default {
render() {
return this.$scopedSlots.default();
}
};
</script>
Renderless components relies on Scoped Slots
<template>
<RenderlessComponent>
<div>whatever</div>
</RenderlessComponent>
</template>
default slot
slots
<template>
<RenderlessComponent v-slot="slotProps">
<div>whatever + {{ slotProps }}</div>
</RenderlessComponent>
</template>
default slot with scope from RenderlessComponent
scoped slots
Why renderless component
How to update the look of a component while keeping its functionalities, without sacrificing maintainability?
Solution: renderless components
Why renderless component - Tags input
<script>
export default {
props: ['value'],
data() {
return {
newTag: '',
}
},
methods: {
addTag() {
if (this.newTag.trim().length === 0 || this.value.includes(this.newTag.trim())) {
return
}
this.$emit('input', [...this.value, this.newTag.trim()])
this.newTag = ''
},
removeTag(tag) {
this.$emit('input', this.value.filter(t => t !== tag))
}
},
render() {
return this.$scopedSlots.default({
tags: this.value,
addTag: this.addTag,
removeTag: this.removeTag,
inputAttrs: {
value: this.newTag,
},
inputEvents: {
input: (e) => { this.newTag = e.target.value },
keydown: (e) => {
if (e.keyCode === 13) {
e.preventDefault()
this.addTag()
}
}
}
})
},
})
</script>
renderless-tags-input.vue
vue-promised
vue-promised is a renderless component that provide usefull interface to use promise states in templates.
See https://github.com/posva/vue-promised/blob/master/src/index.js
Transform your Promises into components !
vue-promised
<template>
<div>
<p v-if="error">Error: {{ error.message }}</p>
<p v-else-if="isLoading && isDelayElapsed">Loading...</p>
<ul v-else-if="!isLoading">
<li v-for="user in data">{{ user.name }}</li>
</ul>
</div>
</template>
<script>
export default {
data: () => ({
isLoading: false,
error: null,
data: null,
isDelayElapsed: false,
}),
methods: {
fetchUsers() {
this.error = null
this.isLoading = true
this.isDelayElapsed = false
getUsers()
.then(users => {
this.data = users
})
.catch(error => {
this.error = error
})
.finally(() => {
this.isLoading = false
})
setTimeout(() => {
this.isDelayElapsed = true
}, 200)
},
},
created() {
this.fetchUsers()
},
}
</script>
<template>
<Promised :promise="usersPromise">
<!-- Use the "pending" slot to display a loading message -->
<template v-slot:pending>
<p>Loading...</p>
</template>
<!-- The default scoped slot will be used as the result -->
<template v-slot="data">
<ul>
<li v-for="user in data">{{ user.name }}</li>
</ul>
</template>
<!-- The "rejected" scoped slot will be used if there is an error -->
<template v-slot:rejected="error">
<p>Error: {{ error.message }}</p>
</template>
</Promised>
</template>
<script>
export default {
data: () => ({ usersPromise: null }),
created() {
this.usersPromise = this.getUsers()
},
}
</script>
without vue-promised
with vue-promised
Workshop - Calendar events
Let's code!
Source code can be found here:
https://github.com/T0RAT0RA/renderless-components
Workshop - What about tests?
/* eslint-disable no-unused-vars */
import Vue from "vue";
import { mount } from "@vue/test-utils";
import WithEvents from "./WithEvents";
// Mock events.js module with fake events
let mockEvents = [
{ title: "event 1", date: "2020-08-02 10:00:00" },
{ title: "event 2", date: "2020-08-03 09:00:00" }
];
jest.mock("./events", () => ({
get events() {
return mockEvents;
}
}));
jest.useFakeTimers();
const scopedSlots = {
default: '<span slot-scope="data">{{ data }}</span>'
};
describe("WithEvents", () => {
let wrapper;
beforeEach(() => {
wrapper = mount(WithEvents, {
scopedSlots
});
});
it("is loading while fetching events", () => {
const expected = {
events: [],
isLoading: true
};
expect(wrapper.text()).toBe(JSON.stringify(expected, null, 2));
});
it("is not loading after fetching events", async () => {
const expected = {
events: mockEvents,
isLoading: false
};
jest.runAllTimers();
await Vue.nextTick();
expect(wrapper.text()).toBe(JSON.stringify(expected, null, 2));
});
it("can subscribe to events", async () => {
console["log"] = jest.fn();
wrapper = mount(WithEvents, {
scopedSlots: {
default: `
<span slot-scope="{ events, subscribe }">
<span @click="subscribe(events[0])" class="event">{{ events[0] }}</span>
</span>`
}
});
jest.runAllTimers();
await Vue.nextTick();
wrapper.find(".event").trigger("click");
expect(console["log"]).toHaveBeenCalledWith("Subscribe to", "event 1");
});
});
WithEvents.test.js
Will Vue 3 bring the end of renderless components?
I'll answer that question at the next World Vue Summit, October 2nd 2020
Thank you!
Renderless Components
By Stéphane Reiss
Renderless Components
- 462