Post

Easily migrate Vue 2 component to Composition API (setup)

Harianto van Insulinde

Converting your Vue Component to Composition API shouldn’t be hard if you want to keep that orderly grouped with data, computed and methods and without changing
this.

Also I want to point out the current explanation really doesn’t make Vue fun, and that I want to bring that back.

Install Composition API

bash
npm i -S @vue/composition-api

package.json: "@vue/composition-api": "^1.0.0-beta.22"

One of the advantages could be from migrating to Composition API what I could think of is: Reusing a piece of code (in this case a composition) to a different theme of Page Components.

Quick Template Composition

With this template we can use similar logic to make a modular Composition.

js
import { computed } from '@vue/composition-api'

const toComputed = object => Object.entries(object).reduce((acc, [key, method]) => {
    acc[key] = computed(method)
    return acc
}, {})

export default toComputed

Quick Template toComputed: toComputed.js

js
import { reactive, toRefs } from '@vue/composition-api'
import toComputed from '../toComputed'

const setup = props => {
    // data: state
    const state = reactive({
    })
    // computed: compute
    const compute = {
    }
    // methods
    const methods = {
    }
    
    return {
        ...toRefs(state),
        ...toComputed(compute),
        ...methods
    }
}

export default setup

Quick Template Composition API: composition/setup-template.js

js
import setup from '../composition/setup-template.js'

export default {
    name: 'TemplateComponent',
    setup,
    // Life Cycle Hooks
    mount() {}
}

Quick Template Component: vue/TemplateComponent.vue

Things you should mind (for this example).

data

When you have data in your Component.

js
export default {
    data: () => ({
        list: [],
        filters: []
    })
}

Snippet: data of .vue

Change data as state to:

js
import { reactive, toRefs } from '@vue/composition-api'

const setup = props => {
    // data
    const state = reactive({
        list: [],
        filters: []
    })
    
    return {
        ...toRefs(state)
    }
}

export default setup

Snippet: data as state of .js

Easy access with by simply: state.list and state.list.push('addMe').

js
// I find it tedious to write like this:

import { ref } from '@vue/composition-api'

const setup = props => {
    // data
    const list = ref([]),
        filters = ref([])
    
    return {
        list,
        filters
    }
}

export default setup

Tedious...

I find it tedious to write it like this: const list = ref([]), and accessing values doing this list.value.push('something'). This takes fun out of coding zen using Vue.

computed

When you have computed in your Component.

js
export default {
    computed: {
        selected: {
            get() {
                return this.filters[0]
            },
            set(value) {
                // console.log({ value })
                if (value == undefined) {
                    this.filters = []
                    return
                }

                const { filters } = this
                const index = filters.indexOf(value)

                if (index > 0) {
                    filters.unshift(...filters.splice(index, 1))
                } else {
                    filters.splice(0, 1, value)
                }
            }
        },
        mappedOptionsFromUniqueTagsFromList() {
            const options = this.uniqueTagsFromList.map(item => ({
                value: item.id,
                text: item.name
            }))
            const label = {
                selected: true,
                // disabled: true,
                value: null,
                text: 'Choose Category'
            }

            return [label, ...options]
        }
    }
}

Snippet: computed of .vue

Change computed as compute to:

js
import { computed } from '@vue/composition-api'

const toComputed = object => Object.entries(object).reduce((acc, [key, method]) => {
    acc[key] = computed(method)
    return acc
}, {})

const setup = props => {
    // computed
    const compute = {
        selected: {
            get() {
                return this.filters[0]
            },
            set(value) {
                // console.log({ value })
                if (value == undefined) {
                    this.filters = []
                    return
                }

                const { filters } = this
                const index = filters.indexOf(value)

                if (index > 0) {
                    filters.unshift(...filters.splice(index, 1))
                } else {
                    filters.splice(0, 1, value)
                }
            }
        },
        mappedOptionsFromUniqueTagsFromList() {
            const options = this.uniqueTagsFromList.map(item => ({
                value: item.id,
                text: item.name
            }))
            const label = {
                selected: true,
                // disabled: true,
                value: null,
                text: 'Choose Category'
            }

            return [label, ...options]
        }
    }
    
    return {
        ...toComputed(compute)
    }
}

export default setup

Snippet: computed as compute of .js

methods

When you have methods in your Component

js
export default {
    methods: {
        foundByTags(queryElements, item) {
            let found = true

            queryElements.forEach(categoryId => {
                let foundIndex = item.categories.findIndex(el => el['id'] == categoryId)
                found *= foundIndex > -1
            })

            return found
        }
    }
}

Snippet: methods of .vue

Change methods to:

js
const setup = props => {
    // methods
    const methods = {
        foundByTags(queryElements, item) {
            let found = true

            queryElements.forEach(categoryId => {
                let foundIndex = item.categories.findIndex(el => el['id'] == categoryId)
                found *= foundIndex > -1
            })

            return found
        }
    }
    
    return {
        ...methods
    }
}

export default setup

Snippet: methods of .js

Combined result as a module

js
import { reactive, toRefs, computed } from '@vue/composition-api'

const toComputed = object => Object.entries(object).reduce((acc, [key, method]) => {
    acc[key] = computed(method)
    return acc
}, {})

const setup = props => {
    // data
    const state = reactive({
        list: [],
        filters: []
    })
    // computed
    const compute = {
        selected: {
            get() {
                return this.filters[0]
            },
            set(value) {
                // console.log({ value })
                if (value == undefined) {
                    this.filters = []
                    return
                }

                const { filters } = this
                const index = filters.indexOf(value)

                if (index > 0) {
                    filters.unshift(...filters.splice(index, 1))
                } else {
                    filters.splice(0, 1, value)
                }
            }
        },
        mappedOptionsFromUniqueTagsFromList() {
            const options = this.uniqueTagsFromList.map(item => ({
                value: item.id,
                text: item.name
            }))
            const label = {
                selected: true,
                // disabled: true,
                value: null,
                text: 'Choose Category'
            }

            return [label, ...options]
        }
    }
    // methods
    const methods = {
        foundByTags(queryElements, item) {
            let found = true

            queryElements.forEach(categoryId => {
                let foundIndex = item.categories.findIndex(el => el['id'] == categoryId)
                found *= foundIndex > -1
            })

            return found
        }
    }
    
    return {
        ...toRefs(state),
        ...toComputed(compute),
        ...methods
    }
}

export default setup

File: lib/compostion/setup-articles.js

js
import setup from '../../lib/composition/setup-articles.js'

export default {
    name: 'Articles'
    setup,
    // Life Cycle Hooks
    mount () {}
}

File: vue/components/articles.vue

Mirgrate from Vue Component to Composite API

Before

html
<template>
    <div class="article-overview">
        <div class="main__inner">
            <header class="article-overview__header">
                <h1 class="article-overview__title"><slot name="title" /></h1>
                <span class="u-flex u-flex-cell">
                    <SelectOptions
                        class="u-flex-cell article-overview__select"
                        :options="mappedOptionsFromUniqueTagsFromList"
                        v-model="selected"
                        v-on:change="onChangeCategory"
                    />
                    <object-pulldown class="u-flex-cell filter-options" title="Filter">
                        <div class="u-flex-cell">
                            <tag-group
                                class="filter-type-item"
                                :query-items="queryItemsFromFilters"
                                :filtered-items="
                                    differenceQueryItemsWithUniqueTagsFromFilteredList
                                "
                                :on-remove-tag-from-filter="onRemoveFromFilters"
                                :on-add-tag-to-filter="onAddToFilters"
                            >
                            </tag-group>
                        </div>
                    </object-pulldown>
                </span>
            </header>
            <div class="article-overview__count">{{ filteredList.length }}</div>
            <div class="post-feed">
                <Card
                    class="post-card"
                    v-for="(item, index) in filteredList"
                    :key="item.id"
                    :item="item"
                    :class="{ 'card--masthead': !index }"
                />
            </div>
        </div>
    </div>
</template>

<script>
const uniq = require('lodash/fp/uniq'),
    uniqBy = require('lodash/uniqBy'),
    sortBy = require('lodash/sortBy'),
    differenceBy = require('lodash/differenceBy'),
    dayjs = require('dayjs')

const urlQueryToParams = require('../lib/urlQueryToParams'),
    changeUrlPushStateFromFilters = require('../lib/changeUrlPushStateFromFilters'),
    milliseconds = require('../lib/milliseconds')

import Card from './Card'
import SelectOptions from './SelectOptions'
import ObjectPulldown from './ObjectPulldown'
import TagGroup from './TagGroup'

export default {
    name: 'ArticleOverviewAlternative',
    components: {
        SelectOptions,
        ObjectPulldown,
        TagGroup,
        Card
    },
    props: {
        endpoint: String
    },
    data() {
        return {
            list: [],
            filters: []
        }
    },
    computed: {
        selected: {
            get() {
                return this.filters[0]
            },
            set(value) {
                if (value == undefined) {
                    this.filters = []
                    return
                }

                const { filters } = this
                const index = filters.indexOf(value)

                if (index > 0) {
                    filters.unshift(...filters.splice(index, 1))
                } else {
                    filters.splice(0, 1, value)
                }
            }
        },
        mappedOptionsFromUniqueTagsFromList() {
            const options = this.uniqueTagsFromList.map(item => ({
                value: item.id,
                text: item.name
            }))
            const label = {
                selected: true,
                // disabled: true,
                value: null,
                text: 'Choose Category'
            }

            return [label, ...options]
        },
        queryItemsFromFilters() {
            return this.filters.map(value => ({
                id: +value,
                name: (() => {
                    const found = this.uniqueTagsFromList.find(({ id }) => id == value)
                    return found ? found.name : value
                })()
            }))
        },
        uniqueTagsFromList() {
            const filters = this.list.reduce(
                (acc, item) => [...acc, ...item.categories],
                []
            )

            return sortBy(uniqBy(filters, 'id'), 'id')
        },
        filteredList() {
            const {
                filters: queryElements,
                list,
                foundByTags,
                onlyShowPastPublishedAtDate,
                sortByPublishedAt
            } = this

            if (!queryElements.length)
                return onlyShowPastPublishedAtDate(sortByPublishedAt(list))
            const filtered = list.filter(item => foundByTags(queryElements, item))

            return onlyShowPastPublishedAtDate(sortByPublishedAt(filtered))
        },
        uniqueTagsFromFilteredList() {
            const filters = this.filteredList.reduce(
                (acc, item) => [...acc, ...item.categories],
                []
            )

            return sortBy(uniqBy(filters, 'id'), 'id')
        },
        differenceQueryItemsWithUniqueTagsFromFilteredList() {
            const { uniqueTagsFromFilteredList, queryItemsFromFilters } = this
            return differenceBy(
                uniqueTagsFromFilteredList,
                queryItemsFromFilters,
                'id'
            )
        }
    },
    methods: {
        foundByTags(queryElements, item) {
            let found = true

            queryElements.forEach(categoryId => {
                let foundIndex = item.categories.findIndex(el => el['id'] == categoryId)
                found *= foundIndex > -1
            })

            return found
        },
        setFiltersFromQueryUrl() {
            const queryString = location.search
            const { filters } = urlQueryToParams(queryString)
            this.filters = Array.isArray(filters)
                ? filters.map(i => +i)
                : filters === (null || undefined)
                ? []
                : [+filters]
        },
        sortByPublishedAt(collection) {
            return collection.sort(
                (a, b) =>
                    milliseconds(b['published_at']) - milliseconds(a['published_at'])
            )
        },
        onlyShowPastPublishedAtDate(collection) {
            return collection.filter(
                item => milliseconds(item['published_at']) < dayjs().valueOf()
            )
        },
        // Buttons Events
        onChangeCategory() {
            const value = this.selected
            this.filters = value ? [value] : []
            changeUrlPushStateFromFilters({ filters: value })
        },
        onAddToFilters(categoryId) {
            const { filters } = this
            const foundIndex = filters.indexOf(categoryId)
            if (!(foundIndex > -1)) {
                filters.push(categoryId)
                changeUrlPushStateFromFilters({
                    filters
                })
            }
        },
        onRemoveFromFilters(categoryId) {
            console.log('onRemoveFromFilters:', { categoryId })
            const { filters } = this
            const foundIndex = filters.indexOf(+categoryId)
            console.log({ foundIndex })
            if (foundIndex > -1) {
                filters.splice(foundIndex, 1)
                changeUrlPushStateFromFilters({
                    filters
                })
            }
        },
        onPopstate(event) {
            const { filters } = event.state
            this.filters = filters || []
        }
    },
    // Life Cycle Hooks
    async beforeMount() {
        const response = await (await fetch(this.endpoint)).json()
        this.list = response
    },
    mounted() {
        window.onpopstate = this.onPopstate
        if (this.filters.length) {
            changeUrlPushStateFromFilters({
                filters: this.filters
            })
        } else {
            this.setFiltersFromQueryUrl()
        }
    }
}
</script>

File: js/vue/ArticleOverview.vue

After

js
import { computed } from '@vue/composition-api'

const toComputed = object => Object.entries(object).reduce((acc, [key, method]) => {
    acc[key] = computed(method)
    return acc
}, {})

Snippet: js/lib/composition/setup-articles.js

A snippet to easy convert your computed object to Vue Computed ones.

js
import { reactive, computed, toRefs } from '@vue/composition-api'

const uniq = require('lodash/fp/uniq'),
    uniqBy = require('lodash/uniqBy'),
    sortBy = require('lodash/sortBy'),
    differenceBy = require('lodash/differenceBy'),
    dayjs = require('dayjs')

const urlQueryToParams = require('../urlQueryToParams'),
    changeUrlPushStateFromFilters = require('../changeUrlPushStateFromFilters'),
    milliseconds = require('../milliseconds')

const toComputed = object => Object.entries(object).reduce((acc, [key, method]) => {
    acc[key] = computed(method)
    return acc
}, {})

const setup = props => {
    // data
    const state = reactive({
        list: [],
        filters: []
    })

    // computed
    const compute = {
        selected: {
            get() {
                return state.filters[0]
            },
            set(value) {
                // console.log({ value })
                if (value == undefined) {
                    state.filters = []
                    return
                }

                const { filters } = state
                const index = filters.indexOf(value)

                if (index > 0) {
                    filters.unshift(...filters.splice(index, 1))
                } else {
                    filters.splice(0, 1, value)
                }
            }
        },
        mappedOptionsFromUniqueTagsFromList() {
            const options = this.uniqueTagsFromList.map(item => ({
                value: item.id,
                text: item.name
            }))
            const label = {
                selected: true,
                // disabled: true,
                value: null,
                text: 'Choose Category'
            }

            return [label, ...options]
        },
        queryItemsFromFilters() {
            return state.filters.map(value => ({
                id: +value,
                name: (() => {
                    const found = this.uniqueTagsFromList.find(({ id }) => id == value)
                    return found ? found.name : value
                })()
            }))
        },
        uniqueTagsFromList() {
            const filters = state.list.reduce(
                (acc, item) => [...acc, ...item.categories],
                []
            )

            return sortBy(uniqBy(filters, 'id'), 'id')
        },
        filteredList() {
            const {
                filters: queryElements,
                list
            } = state
            const {
                foundByTags,
                onlyShowPastPublishedAtDate,
                sortByPublishedAt
            } = methods


            if (!queryElements.length)
                return onlyShowPastPublishedAtDate(sortByPublishedAt(list))
            const filtered = list.filter(item => foundByTags(queryElements, item))

            return onlyShowPastPublishedAtDate(sortByPublishedAt(filtered))
        },
        uniqueTagsFromFilteredList() {
            const filters = this.filteredList.reduce(
                (acc, item) => [...acc, ...item.categories],
                []
            )

            return sortBy(uniqBy(filters, 'id'), 'id')
        },
        differenceQueryItemsWithUniqueTagsFromFilteredList() {
            const { uniqueTagsFromFilteredList, queryItemsFromFilters } = this
            return differenceBy(
                uniqueTagsFromFilteredList,
                queryItemsFromFilters,
                'id'
            )
        }
    }

    // methods
    const methods = {
        foundByTags(queryElements, item) {
            let found = true

            queryElements.forEach(categoryId => {
                let foundIndex = item.categories.findIndex(el => el['id'] == categoryId)
                found *= foundIndex > -1
            })

            return found
        },
        setFiltersFromQueryUrl() {
            const queryString = location.search
            const { filters } = urlQueryToParams(queryString)
            state.filters = Array.isArray(filters)
                ? uniq(filters).map(i => +i)
                : filters === (null || undefined)
                    ? []
                    : [+filters]
        },
        sortByPublishedAt(collection) {
            return collection.sort(
                (a, b) =>
                    milliseconds(b['published_at']) - milliseconds(a['published_at'])
            )
        },
        onlyShowPastPublishedAtDate(collection) {
            return collection.filter(
                item => milliseconds(item['published_at']) < dayjs().valueOf()
            )
        },
        // Buttons Events
        onChangeCategory() {
            const { filters } = state
            changeUrlPushStateFromFilters({ filters })
        },
        onAddToFilters(categoryId) {
            const { filters } = state
            const foundIndex = filters.indexOf(categoryId)
            if (!(foundIndex > -1)) {
                filters.push(categoryId)
                changeUrlPushStateFromFilters({
                    filters
                })
            }
        },
        onRemoveFromFilters(categoryId) {
            console.log('onRemoveFromFilters:', { categoryId })
            const { filters } = state
            const foundIndex = filters.indexOf(+categoryId)
            console.log({ foundIndex })
            if (foundIndex > -1) {
                filters.splice(foundIndex, 1)
                changeUrlPushStateFromFilters({
                    filters
                })
            }
        },
        onPopstate(event) {
            const { filters } = event.state
            state.filters = filters || []
        }
    }

    // Spread Operator
    return {
        ...toRefs(state),
        ...toComputed(compute),
        ...methods
    }
}

export default setup

File: js/lib/composition/setup-articles.js

- Taking out data as **state**, computed as **compute** and methods and local dependencies.
- Correct reference this to **state** or **methods** or leave as this.

html
<template>
    <div class="article-overview">
        <div class="main__inner">
            <header class="article-overview__header">
                <h1 class="article-overview__title"><slot name="title" /></h1>
                <span class="u-flex u-flex-cell">
                    <SelectOptions
                        class="u-flex-cell article-overview__select"
                        :options="mappedOptionsFromUniqueTagsFromList"
                        v-model="selected"
                        v-on:change="onChangeCategory"
                    />
                    <object-pulldown class="u-flex-cell filter-options" title="Filter">
                        <div class="u-flex-cell">
                            <tag-group
                                class="filter-type-item"
                                :query-items="queryItemsFromFilters"
                                :filtered-items="
                                    differenceQueryItemsWithUniqueTagsFromFilteredList
                                "
                                :on-remove-tag-from-filter="onRemoveFromFilters"
                                :on-add-tag-to-filter="onAddToFilters"
                            >
                            </tag-group>
                        </div>
                    </object-pulldown>
                </span>
            </header>
            <div class="article-overview__count">{{ filteredList.length }}</div>
            <div class="post-feed">
                <Card
                    class="post-card"
                    v-for="(item, index) in filteredList"
                    :key="item.id"
                    :item="item"
                    :class="{ 'card--masthead': !index }"
                />
            </div>
        </div>
    </div>
</template>

<script>
import Card from './Card'
import SelectOptions from './SelectOptions'
import ObjectPulldown from './ObjectPulldown'
import TagGroup from './TagGroup'

import setup from '../lib/composition/setup-articles'

export default {
    name: 'ArticleOverviewAlternative',
    components: {
        SelectOptions,
        ObjectPulldown,
        TagGroup,
        Card
    },
    props: {
        endpoint: String
    },
    setup,
    // Life Cycle Hooks
    async beforeMount() {
        const response = await (await fetch(this.endpoint)).json()
        this.list = response
    },
    mounted() {
        window.onpopstate = this.onPopstate
        if (this.filters.length) {
            changeUrlPushStateFromFilters({
                filters: this.filters
            })
        } else {
            this.setFiltersFromQueryUrl()
        }
    }
}
</script>

File: js/vue/ArticleOverview.vue

- Importing setup from library setup-articles.
- Removing libraries and refactor.