Post
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
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.
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
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
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
datain your Component.
export default {
data: () => ({
list: [],
filters: []
})
}
Snippet: data of .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
Snippet: data as state of .js
Easy access with by simply:
state.listandstate.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
Tedious...
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
computedin 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]
}
}
}
Snippet: computed of .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
Snippet: computed as compute of .js
methods
When you have
methodsin 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
}
}
}
Snippet: methods of .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
Snippet: methods of .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
File: lib/compostion/setup-articles.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
<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
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.
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
dataas**state**,computedas**compute**andmethodsand local dependencies.
- Correct referencethisto**state**or**methods**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>
File: js/vue/ArticleOverview.vue
- Importing
setupfrom librarysetup-articles.
- Removing libraries and refactor.