Vue3 / Nuxt3 / Vuetify3 Migration Steps
The following notes are a 'checklist' for migrating a large Vue2 / Vuetify2 project to Vue3 / Nuxt3 / Vuetify3. It is NOT intended to be a step-by-step migration guide, they are rough notes our team used for migration
Our team decided to create a brand new Nuxt3 app, instead of tyring a 'bridge' or running Vue2/Vue3 side-by-side:
We also changed our store from Vuex to Pinia
We decided to use the experimental @vue/reactivity-transform. This provides the ability to use a ref in the script block without having to use .value everywhere
without reactivity-transform
let userName = ref<string>('')
userName.value = "John Doe"
with reactivity-transform
let userName = $ref<string>('')
userName = "John Doe"
to enable add the following to nuxt.config.ts
experimental: { reactivityTransform: true }
Props are defined using interfaces, this means you can access props without having to use props.myVar
without interface definition
const props = defineProps(['userName'])
console.log("NAME", props.userName)
with interface definition
interface Props { userName: string; }
const { userName = 'Joe Doe' } = defineProps<Props>();
console.log("NAME", userName)
Using reactivity-transform and defining props with interfaces keeps our resulting code very clean because you do not have to use myvar.value or props.myvar.
Copy each Vue2 file into a new Vue3 file using proper Nuxt folders:
components/UserInfoCard.vue > components/User/InfoCard.vue
Move the Script block above the Template block
Add into the script tag: <script setup lang="ts">
Search and remove "this."
Delete all imports statements, most are auto-imported by Nuxt
Remove from the copied Vue2 export block {}:
Migrate Props
interface Props { sid: string;}
const { sid } = defineProps< Props >();
Migrate Data
let var = $ref< string >()
Migrate Computed
mapGetters > const { var } = $(useSomething())
const var = $computed(()=>{})
Migrate Created
Migrate Watchers
watch(() => varToWatch,() => { functionToCall(); });
watch(() => varToWatch,() => { functionToCall(); }, { immediate: true });
watch(() => varToWatch,() => { functionToCall(); }, { deep: true, immediate: true });
watch([() => var1ToWatch, () => var2ToWatch],() => { functionToCall(); })
Migrate Methods
mapActions > const { function } = useStore()
copy functions directly out of method {} block
Change constants to enums
Paages
Add the Nuxt Page metadata
definePageMeta({key: (route) => route.fullPath, middleware: ['auth'], layout: 'page',});
Remove all Filters replace with functions or composables
<span>{{ updateDate | dateFormat}}</span>
> <span>{{ formatDate(updateDate)}}</span>
Change $refs
$refs
are no longer available for access to components in template as a ref
Include component with ref: <UserDialog ref="UserDialogRef" />
On UserDialog component expose function to open: defineExpose({showDialog})
In script of parent, create a ref: const UserDialogRef = $ref<InstanceType<typeof UserDialog> | null>(null);
from the parent call the exposed method: MyDialogRef.showDialog();
Change Icons
change fa-icon > v-icon icon="fa:fas fa-home"
check text colors green--text > color="green"
Change Text Typogrpahy
title > text-h5
caption > text-caption
success--text > text-success
warning--text > text-warning
error--text > text-error
white--text > text-white
v-list Changes
remove v-list-item-content > It is now the default slot on v-list-item
v-chip Changes
sizes are now size="small" not attributes
colors are now color="primary" not a class
default is a translucent background, need to use variant="flat"
v-select and v-autocomplete
@input= > @update:model-value=
item-text > item-title
v-tabs
v-tabs-items > v-window
v-tab-item > v-window-item
v-simple-table
v-simple-table > v-table
dense > density="compact"
v-virtual-scroll (Not available until Vuetify 3.1+)
v-textfield
filled > variant="plain"
append-icon="mdi-magnify" > prepend-inner-icon="fa:fas fa-search"
@input= > @update:model-value=
Activators (v-tooltip)
#activator="{ on }" > #activator="{ props }"
v-on="on" > v-bind="props"
Original Vue2 / Vuetify2 file
components/PersonSoftwareCard.vue
<template >
<BaseCard :title =" title" >
<template #body >
<LoadingSpinner :loading =" softwaresLoading" title =" Software" :size =" 30" >
<v-row >
<v-col cols =" 6" >
<div class =" title grey darken-2 pl-2" > Owned: </div >
<div v-if =" softwareOwned.length > 0" >
<v-virtual-scroll
:bench =" 10"
:items =" softwareOwned"
:item-height =" 30"
:height =" 200"
class =" scroller-blue"
>
<template #default =" { item , index } " >
<v-row
dense
no-gutters
style =" height : 30px "
:style =" AppService.getAltRowColor(index, '#121212')"
align =" center"
>
<v-col cols =" 8" class =" text-truncate" >
<span class =" ml-2 font-weight-bold" >{{ item.softwareName }}</span >
</v-col >
<v-col cols =" 1" >
{{ item.quantity | numFormat('0,0') }}
</v-col >
<v-col cols =" 3" class =" text-center" >
<SoftwareStatusLabel :software =" item" />
</v-col >
</v-row >
</template >
</v-virtual-scroll >
</div >
<div v-else >
<div class =" text-center headline my-5" >
There is no Software assigned to {{ userId }} as the Owned.
</div >
</div >
</v-col >
<v-col cols =" 6" >
<div class =" title grey darken-2 pl-2" > Subscribed: </div >
<div v-if =" softwareSubscribed.length > 0" >
<v-virtual-scroll :bench =" 10" :items =" softwareSubscribed" :item-height =" 30" :height =" 200" class =" scroller-blue" >
<template #default =" { item , index } " >
<v-row
dense
no-gutters
style =" height : 30px "
:style =" AppService.getAltRowColor(index, '#121212')"
align =" center"
>
<v-col cols =" 8" class =" text-truncate" >
<span class =" ml-2 font-weight-bold" >{{ item.softwareName }}</span >
</v-col >
<v-col cols =" 1" >
{{ item.quantity | numFormat('0,0') }}
</v-col >
<v-col cols =" 3" class =" text-center" >
<SoftwareStatusLabel :software =" item" />
</v-col >
</v-row >
</template >
</v-virtual-scroll >
</div >
<div v-else >
<div class =" text-center headline my-5" >
There is no Software assigned to {{ userId }} as the Subscribed.
</div >
</div >
</v-col >
</v-row >
</LoadingSpinner >
</template >
</BaseCard >
</template >
<script >
import { mapActions , mapGetters } from ' vuex'
import AppService from ' @/services/app.service.js'
import BaseCard from ' @/components/layout/BaseCard.vue'
import LoadingSpinner from ' @/components/common/LoadingSpinner.vue'
import SoftwareStatusLabel from ' @/components/software/SoftwareStatusLabel.vue'
export default {
name: ' PersonSoftwareCard' ,
components: {
BaseCard,
LoadingSpinner,
SoftwareStatusLabel,
},
props: {
userId: {
type: String ,
default: null ,
},
},
data () {
return {
AppService,
softwareOwned: [],
softwareSubscribed: [],
}
},
computed: {
... mapGetters ([' softwares' , ' softwaresLoading' , ' userIdAuth' ]),
title () {
return this .userId === this .userIdAuth ? ' My Assigned Software' : ` Software Assigned to: ${ this .userId } `
},
},
created () {
this .initialize ()
},
methods: {
... mapActions ([' getSoftwares' ]),
async initialize () {
if (this .softwares .length === 0 ) {
await this .getSoftwares ()
}
this .softwareOwned = this .softwares .filter ((s ) => {
if (s .ownedUserId ? ._id === this .userId ) return true
return false
})
this .softwareSubscribed = this .softwares .filter ((s ) => {
if (s .subscribeUserId ? ._id === this .userId ) return true
return false
})
},
},
}
< / script>
New Vue3 / Nuxt3 / Vuetify3 file
components/Person/SoftwareCard.vue
<script lang="ts" setup>
interface Props {
userId? : string ;
}
const { userId } = defineProps <Props >();
const NumSvc = useNumeral ();
let softwareOwned = $ref <any []>([]);
let softwareSubscribed = $ref <any []>([]);
const { userIdAuth } = useAuthStore ();
const { getAltRowColor } = useAppStore ();
const { getSoftwares, softwares, softwaresLoading } = useSoftwareStore ();
const title = computed (() => {
return userId === userIdAuth ? ' My Assigned Software' : ` Software Assigned to: ${userId } ` ;
});
onBeforeMount (async () => {
if (softwares .length === 0 ) {
await getSoftwares ();
}
softwareOwned = softwares .filter ((s ) => {
if (s .ownedUserId ?._id === userId ) return true ;
return false ;
});
softwareSubscribed = softwares .filter ((s ) => {
if (s .subscribeUserId ?._id === userId ) return true ;
return false ;
});
});
</script >
<template >
<BaseCard :title =" title" >
<template #body >
<LoadingSpinner :loading =" softwaresLoading" title =" Software" :size =" 30" >
<v-row >
<v-col cols =" 6" >
<div class =" text-h5 grey-darken-2 pl-2" >Owned:</div >
<div v-if =" softwareOwned.length > 0" >
<RecycleScroller
v-slot =" { item, index }"
:items =" softwareOwned"
:item-size =" 30"
class =" scroller-200"
key-field =" _id"
>
<v-row no-gutters style =" height : 30px " :style =" getAltRowColor(index, '#121212')" align =" center" >
<v-col cols =" 8" class =" text-truncate" >
<span class =" ml-2 font-weight-bold" >{{ item.softwareName }}</span >
</v-col >
<v-col cols =" 1" >
{{ NumSvc.numberFormat(item.quantity, '0,0') }}
</v-col >
<v-col cols =" 3" class =" text-center" >
<SoftwareStatusLabel :software =" item" />
</v-col >
</v-row >
</RecycleScroller >
</div >
<div v-else >
<div class =" text-center text-h5 my-5" >
There is no Software assigned to <b >{{ userId }}</b > as the Owned.
</div >
</div >
</v-col >
<v-col cols =" 6" >
<div class =" text-h5 grey-darken-2 pl-2" >Subscribed:</div >
<div v-if =" softwareSubscribed.length > 0" >
<RecycleScroller
v-slot =" { item, index }"
:items =" softwareSubscribed"
:item-size =" 30"
class =" scroller-200"
key-field =" _id"
>
<v-row no-gutters style =" height : 30px " :style =" getAltRowColor(index, '#121212')" align =" center" >
<v-col cols =" 8" class =" text-truncate" >
<span class =" ml-2 font-weight-bold" >{{ item.softwareName }}</span >
</v-col >
<v-col cols =" 1" >
{{ NumSvc.numberFormat(item.quantity, '0,0') }}
</v-col >
<v-col cols =" 3" class =" text-center" >
<SoftwareStatusLabel :software =" item" />
</v-col >
</v-row >
</RecycleScroller >
</div >
<div v-else >
<div class =" text-center text-h5 my-5" >
There is no Software assigned to <b >{{ userId }}</b > as the Subscribed.
</div >
</div >
</v-col >
</v-row >
</LoadingSpinner >
</template >
</BaseCard >
</template >