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 data
in 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.list
and state.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 this list.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]
}
}
}
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 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
}
}
}
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 data
as state
, computed
as compute
and methods
and local dependencies. - Correct reference this
to state
or methods
or leave as this
. <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.