Renderless components

September 2020 - Stéphane Reiss

Advanced vue pattern

Who am I?

  • Web developer
  • PHP background
  • I love javascript

Stéphane Reiss

Agenda

  1. What is a renderless component
  2. Why renderless component - The tags input example
  3. Another example - vue-promised
  4. Workshop - Calendar event management
  5. Closing remarks

This presentation is inspired by this excellent blog post:

https://adamwathan.me/renderless-components-in-vuejs

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