Easily migrate Vue 2 component to Composition API (setup)
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 changingthis
.
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
npm i -S @vue/composition-api
"@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.
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
toComputed.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
composition/setup-template.js
import setup from '../composition/setup-template.js'
export default {
name: 'TemplateComponent',
setup,
// Life Cycle Hooks
mount() {}
}
vue/TemplateComponent.vue
Things you should mind (for this example).
data
When you have data
in your Component.
export default {
data: () => ({
list: [],
filters: []
})
}
.vue
Change data
as state to:
import { reactive, toRefs } from '@vue/composition-api'
const setup = props => {
// data
const state = reactive({
list: [],
filters: []
})
return {
...toRefs(state)
}
}
export default setup
.js
Easy access with by simply:state.list
andstate.list.push('addMe')
.
// 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
I find it tedious to write it like this:const list = ref([])
, and accessing values doing thislist.value.push('something')
. This takes fun out of coding zen using Vue.
computed
When you have computed
in your Component.
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]
}
}
}
.vue
Change computed
as compute to:
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
.js
methods
When you have methods
in your Component
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
}
}
}
.vue
Change methods
to:
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
.js
Combined result as a module
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
lib/compostion/setup-articles.js
import setup from '../../lib/composition/setup-articles.js'
export default {
name: 'Articles'
setup,
// Life Cycle Hooks
mount () {}
}
vue/components/articles.vue
Mirgrate from Vue Component to Composite API
Before
<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>
js/vue/ArticleOverview.vue
After
import { computed } from '@vue/composition-api'
const toComputed = object => Object.entries(object).reduce((acc, [key, method]) => {
acc[key] = computed(method)
return acc
}, {})
js/lib/composition/setup-articles.js
A snippet to easy convert your computed object to Vue Computed ones.
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
js/lib/composition/setup-articles.js
- Taking outdata
asstate
,computed
ascompute
andmethods
and local dependencies.
- Correct referencethis
tostate
ormethods
or leave asthis
.
<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>
js/vue/ArticleOverview.vue
- Importingsetup
from librarysetup-articles
.
- Removing libraries and refactor.