@vueschool_io
vueschool.io
New Features and Migrating
Lead Instructor @ Vue School
Full Stack developer (10 years)
Husband and Father
* backported to Vue 2.7
Pinia Experiment Begins
Nov, 2019
RFC Repo Created
Jan 14, 2019
Vue 3 Codebase Goes Public
Jan 3, 2020
Vue 3 Soft Launch
(One Piece)
Sep 18, 2020
Vue 3 New Default
Jan 20, 2022
Evan Announced Dev on Vite.js
Jan 12, 2020
๐ฃ
Vue 3 Development Announced
Feb 2018
Volar
<script setup>
more!
Benefits and why upgrade
A one-second site speed improvement can increase mobile conversions by up to 27%.
* backported to Vue 2.7
โ
Stakeholders
ย ย ย ย Faster turn around times, less back and forth
โ
Site Visitors/Customers
ย ย ย ย Less bugs, more features faster
โ Developers
ย ย ย ย Better DX, Iterate faster, spend less time debugging
// MyInput.vue
<template>
<input
@input="$emit('input', $event.target.value)"
:value="value"
/>
</template>
<script>
export default {
props:{
value: String
}
}
</script>
Support v-model on a component by emitting input and accepting a value prop
v2
<MyInput v-model="name" />
// MyInput.vue
<template>
<input
@input="$emit('update:modelValue', $event.target.value)"
:value="modelValue"
/>
</template>
<script>
export default {
props:{
modelValue: String
}
}
</script>
Support v-model on a component by emitting update:modelValue and accepting a modelValue prop
v3
<MyInput v-model="name" />
// MyInput.vue
<template>
<input
@input="$emit('update:modelValue', $event.target.value)"
:value="modelValue"
/>
<input
@input="$emit('update:email', $event.target.value)"
:value="title"
/>
<input
@input="$emit('update:password', $event.target.value)"
:value="title"
/>
</template>
<script>
export default {
props:{
modelValue: String,
email: String,
password: String,
}
}
</script>
Provide argument to v-model to specify any prop/update:[prop]
v3
<MyInput
v-model="name"
v-model:email="email"
v-model:password="password"
/>
<MyComponent
:title="pageTitle"
@update:title="pageTitle = $event"
/>
<!-- Shortand for Above -->
<MyComponent :title.sync="pageTitle" />
Multiple v-models replace .sync
v2
<MyComponent
:title="pageTitle"
@update:title="pageTitle = $event"
/>
<!-- Shortand for Above -->
<MyComponent v-model:title="pageTitle" />
Multiple v-models replace .sync
v3
<MyComponent v-model.capitalize="myText" />
Vue 3 also allows us to create custom modifiers for v-model
<!-- MyComponent.vue-->
<script>
export default {
props: {
//...
modelModifiers: {
default: () => ({})
}
},
created() {
console.log(this.modelModifiers) // { capitalize: true }
}
//...
}
</script>
Check for them on the modelModifiers prop. Added modifiers will be a boolean true
<MyComponent v-model.capitalize="myText" />
<!-- MyComponent.vue-->
<script>
export default {
//...
methods: {
emitValue(e) {
let value = e.target.value
if (this.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
this.$emit('update:modelValue', value)
}
}
}
</script>
Then you can alter the emitted value based on the modifier
Vue.set(this.frameworks, index, 'Vue')
v2
this.frameworks[index] = "Vue"
v3
Setting a new array item
Vue.set(this.framework, 'name', 'Vue')
v2
this.framework.name = "Vue"
v3
Adding a new object property
Vue.delete(this.framework, 'caveats')
v2
delete this.framework.caveats
v3
Deleting an object property
Less Caveats === More Intuitive === Less Bugs
aka. Multiple Root Elements
v2
<template>
<div>...</div>
<div>...</div>
</template>
โ
If you did this...
aka. Multiple Root Elements
v2
<template>
<div>...</div>
<div>...</div>
</template>
You'd get an error like this!
aka. Multiple Root Elements
v2
<template>
<div>
<div>...</div>
<div>...</div>
</div>
</template>
And have to wrap everything with a div
(which sometimes causes styling issues)
aka. Multiple Root Elements
v3
<template>
<div>...</div>
<div>...</div>
</template>
It's no problem! ๐
aka. Multiple Root Elements
v3
<template>
<div>...</div>
<div v-bind="$attrs">...</div>
</template>
And you can specify where to put the fall-through attributes
v2
<template>
<label>
<input type="text" v-bind="$attrs" v-on="$listeners" />
</label>
</template>
<script>
export default {
inheritAttrs: false
}
</script>
Listeners separate from $attrs and if you wanted them to fall through you must remember to bind seperately
v3
//MyInput.vue
<template>
<label>
<input type="text" v-bind="$attrs" />
</label>
</template>
<script>
export default {
inheritAttrs: false
}
</script>
Listeners combined with $attrs. Defined as functions prefixed with on
<MyInput id='my-input' @close="..."/>
$attrs === {
id: 'my-input',
onClose(){
...
}
}
v2
// MyInput.vue
<template>
<label>
<input type="text" v-bind="$attrs" />
</label>
</template>
<script>
export default {
inheritAttrs: false
}
</script>
Class and Style separate from $attrs
Given this component definition...
v2
<MyInput id="my-id" class="my-class" />
Class and Style separate from $attrs
And used like this...
<label class="my-class">
<input type="text" id="my-id" />
</label>
Would render this HTML
v3
<MyInput id="my-id" class="my-class" />
Class and Style included with $attrs
And used like this...
<label>
<input type="text" id="my-id" class="my-class" />
</label>
Would render this HTML
โฐ 20 mins
<template functional>
<component
:is="`h${props.level}`"
v-bind="attrs"
v-on="listeners"
>
<slot></slot>
</component>
</template>
<script>
export default {
props: ["level"],
};
</script>
v2
In v2, functional components provided a more performant alternative when component state was not needed
No $ prefix
<template>
<component
:is="`h${props.level}`"
v-bind="$attrs"
>
<slot></slot>
</component>
</template>
<script>
export default {
props: ["level"],
};
</script>
v3
In v3, the performance difference for stateful components is negligible so functional SFC's are removed
$ prefix
No functional
keyword
<script>
// note that h is imported from vue
// instead of provided as an argument to the render function
// (another diff between v2 and v3)
import { h } from "vue";
// functional components is a render function
const DynamicHeading = (props, context) => {
return h(`h${props.level}`, context.attrs, context.slots);
};
DynamicHeading.props = ["level"];
export default DynamicHeading;
</script>
v3
Or you can define functional components as plain functions
ย
<template>
<h1>Bank Account Balance</h1>
<p>{{ accountBalance | currencyUSD }}</p>
</template>
<script>
export default {
//...
filters: {
currencyUSD(value) {
return '$' + value
}
}
}
</script>
v2
In v2, you could use filters to format data in the template
ย
Custom syntax that involves:
<template>
<h1>Bank Account Balance</h1>
<p>{{ accountInUSD }}</p>
</template>
<script>
export default {
//...
computed: {
accountInUSD() {
return '$' + this.accountBalance
}
}
}
</script>
v3
In v3, filters are removed. Just use a computed prop instead.
ย
const app = createApp(App)
app.config.globalProperties.$filters = {
currencyUSD(value) {
return '$' + value
}
}
v3
In v3, global filters can be replaced with globally defined methods
ย
<template>
<h1>Bank Account Balance</h1>
<p>{{ $filters.currencyUSD(accountBalance) }}</p>
</template>
v2
In Vue 2 you could use keycodes as modifiers for keyboard events
ย
<!-- keycode 75 === 'k' -->
<input v-on:keyup.75="doThing" />
v3
In Vue 3 you use the event's key value instead
<input v-on:keyup.k="doThing" />
<input v-on:keyup.k="doThing" />
v3
For multi-word key names you'll use the kebab case
<input v-on:keyup.arrow-down="doThing" />
v3
The keys for some punctuation marks can just be included literally.
(This excludes "
, '
, /
, =
, >
, and .
You can check these on the event in your handler)
<input v-on:keyup.,="commaPress" />
KeyboardEvent.keyCode is now deprecated in browsers and no longer recommended for use. Therefore Vue 3 removes them.
ย
<input v-on:keyup.k="doThing" />
v2
<template>
<div>
<my-button>Change logo</my-button>
</div>
</template>
<script>
import MyButton from './MyButton'
export default {
components: { MyButton },
mounted() {
console.log(this.$children[0]) // VueComponent (MyButton)
}
}
</script>
In Vue 2, you could access all a components child components via $children
ย
v3
In Vue 3, $children no longer exists. If you need access to a component via the parent you can use a template ref instead
ย
<template>
<div>
<my-button ref="button">Change logo</my-button>
</div>
</template>
<script>
import MyButton from './MyButton'
export default {
components: { MyButton },
mounted() {
console.log(this.$refs.button) // VueComponent (MyButton)
}
}
</script>
โฐ 15 mins
โฐ 15 mins
Skip
โฐ 10 mins
* backported to Vue 2.7
Code Organization
Logic Reuse
Improved
TypeScript Support
Full Workshop Dedicated to CAPI
โก๏ธ Super Quick Overview
new Vue({
data(){
return {
loading: false,
count: 0,
user: {}
}
},
computed: {
double () { return this.count * 2 },
fullname () {/* ... */}
},
methods: {
increment () { this.count++ },
fetchUser () {/* ... */}
}
})
import { ref, computed, watch } from 'vue'
const loading = ref(false)
const count = ref(0)
const user = reactive({})
new Vue({
data(){
return {
loading: false,
count: 0,
user: {}
}
},
computed: {
double () { return this.count * 2 },
fullname () {/* ... */}
},
methods: {
increment () { this.count++ },
fetchUser () {/* ... */}
}
})
Define reactive data with
ref
or
reactive
const double = computed(()=> count.value * 2)
const fullname = computed(()=>({ /* ... */ }))
new Vue({
data(){
return {
loading: false,
count: 0,
user: {}
}
},
computed: {
double () { return this.count * 2 },
fullname () {/* ... */}
},
methods: {
increment () { this.count++ },
fetchUser () {/* ... */}
}
})
Define derived data with
computed
const increment = ()=> count.value++
function fetchUser(){ /* ... */ }
new Vue({
data(){
return {
loading: false,
count: 0,
user: {}
}
},
computed: {
double () { return this.count * 2 },
fullname () {/* ... */}
},
methods: {
increment () { this.count++ },
fetchUser () {/* ... */}
}
})
Define methods as
functions
Full Workshop Dedicated to CAPI
You seem like a pretty advanced audience
Do you have any specific questions about the composition API?
Would you like to dive a little deeper into the Composition API?
(I can show you some examples in StackBlitz)
๐คซ
Don't tell anyone but I'll give you the content and exercises from our dedicated Composition API workshop too!
(copy and paste into chat)
Modal overlay without teleport usually relies on fixed positioning
<template>
<button @click="open = true">Open Modal</button>
<div v-if="open" class="modal">
<slot></slot>
<footer>
<button @click="open = false">Close</button>
</footer>
</div>
</template>
<script>
// ....
</script>
<style scoped>
.modal {
@apply fixed;
}
</style>
v2
Append markup to any arbitrary element no matter where the component lives
<template>
<button @click="open = true">Open Modal</button>
<Teleport to="body">
<div v-if="open" class="modal">
<slot></slot>
<footer>
<button @click="open = false">Close</button>
</footer>
</div>
</Teleport>
</template>
<script>
// ....
</script>
<style scoped>
.modal {
@apply absolute;
}
</style>
v3
`to` prop takes a CSS selector or an actual DOM node
<template>
<!-- string query selector-->
<Teleport to="body">
<!--or actual element-->
<Teleport :to="body">
</template>
<script>
export default {
data() {
return {
body: document.querySelector("body"),
};
},
};
</script>
can combine <Teleport> with <Transition>
<template>
<Teleport :to="body">
<Transition>
<div v-if="open" class="modal">
//...
</div>
</Transition>
</Teleport>
</template>
<style scoped>
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>
Can have multiple teleports active at one time.
<Teleport to="#modals">
<div>A</div>
</Teleport>
<Teleport to="#modals">
<div>B</div>
</Teleport>
<div id="modals">
<div>A</div>
<div>B</div>
</div>
Can dynamically disable. Useful to conditionally display inside component
<Teleport :disabled="isMobile">
...
</Teleport>
ย The teleport `to` target must already exist in the DOM when the component using <Teleport> is mounted.
โฐ 25 mins
โฐ 25 mins
โฐ 25 mins
and more!
In v2, inline styles were the only option for dynamic CSS rules
<template>
<div
class="swatch"
:style="{ backgroundColor: color }"
></div>
</template>
<script>
export default{
data(){
return {
color: "red"
}
}
}
</script>
v2
Re-use would mean re-declaring the inline style
<template>
<div
class="swatch"
:style="{ backgroundColor: color }"
></div>
<div
class="swatch"
:style="{ backgroundColor: color }"
></div>
</template>
<script>
export default{
data(){
return {
color: "red"
}
}
}
</script>
v2
In v3, you can bind CSS values directly in the style block
v3
<template>
<div>
<div class="swatch"></div>
<div class="swatch"></div>
</div>
</template>
<script>
export default{
data(){
return { color: "red" }
}
}
</script>
<style scoped>
.swatch{
background-color: v-bind(color);
}
</style>
Defines a hashed CSS custom property on the root component element
(or on each element if no root)
v3
<template>
<div>
<div class="swatch"></div>
<div class="swatch"></div>
</div>
</template>
<script>
export default{
data(){
return { color: "red" }
}
}
</script>
<style scoped>
.swatch{
background-color: v-bind(color);
}
</style>
Can use dot notation to target nested data (must use quotes)
v3
<template>
<div class="swatch"></div>
<div class="swatch"></div>
</template>
<script>
export default{
data(){
return {
colors:{ primary: "red" }
}
}
}
</script>
<style scoped>
.swatch{
background-color: v-bind('color.primary');
}
</style>
v-bind must the entire value
v3
<template>
<div class="swatch"></div>
<div class="swatch"></div>
</template>
<script>
export default{
data(){
return {
colors:{ primary: "red" },
width: 100
}
},
computed:{
widthInPixels(){ return this.width + 'px'}
}
}
</script>
<style scoped>
.swatch{
background-color: v-bind('color.primary');
width: v-bind(width)px; /* โ This won't work */
width: v-bind(widthInPixels);
}
</style>
v3
<style scoped>
/* Style child components */
.a :deep(.b) {}
/* Style slot content */
:slotted(div) {}
/* Quickly define a global rule */
:global(.red) {}
</style>
<style>
/* Same as :global in scoped tag above */
.red{}
</style>
โฐ 15 mins
Declare Component Events
Emits scattered throughout component
<!-- DataSender.vue -->
<template>
<div>
<button @click="sendData">Send Data</button>
</div>
</template>
<script>
export default {
methods: {
sendData() {
this.$emit('sending-start')
// send data to API
this.$emit('sending-complete')
}
}
}
</script>
v2
Declare all events with emits option
<!-- DataSender.vue -->
<template>
<div>
<button @click="sendData">Send Data</button>
</div>
</template>
<script>
export default {
emits:['sending-start', 'sending-complete'],
methods: {
sendData() {
this.$emit('sending-start')
// send data to API
this.$emit('sending-complete')
}
}
}
</script>
v3
Benefits of Emit Option
<script>
export default {
emits:['sending-start', 'sending-complete'],
//...
}
</script>
Important for devs and tooling
Benefits of Emit Option
Benefits of Emit Option
<MyButton @click.native="doThing" />
Benefits of Emit Option
Validate event payload
<!-- DataSender.vue -->
<!--...-->
<script>
export default {
// define as object to use validation
emits:{
// null is no validation
'sending-start': null,
// or provide function to validate
// return truthy for valid, falsy for invalid
'sending-complete'(payload){
return !!(typeof payload.duration === 'number' && payload.response);
}
},
methods: {
sendData() {
this.$emit('sending-start')
// send data to API
this.$emit('sending-complete', {
duration: 2,
// response: 'how are you'
})
}
}
}
</script>
v3
Validate event payload with TypeScript
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
emits: {
addBook(payload: { bookName: string }) {
// perform runtime validation
return payload.bookName.length > 0
}
},
methods: {
onSubmit() {
this.$emit('addBook', {
bookName: 123 // Type error!
})
this.$emit('non-declared-event') // Type error!
}
}
})
</script>
v3
โฐ 20 mins
โฐ 20 mins
Skip
Admittedly a little difficult to understand if you've never heard of it before
Orchestrate async dependencies
Without Suspense
With Suspense
With Suspense
ย
(with more control at component level)
AKA - eliminate popcorn loading
In Vue 2 you'd have local loading data in
the component
<!-- PostsList.vue -->
<script>
export default{
data(){
return {
loading: true,
posts: null
}
},
async created(){
const res = await fetch("https://myapi.com/posts")
this.posts = await res.json()
this.loading = false
}
}
</script>
<template>
<div>
<AppSpinner v-if="loading"/>
<div v-else>...</div>
</div>
</template>
v2
Somewhere higher in the higharchy would use the component and others like it
<!-- ParentComponent.vue -->
<template>
<div>
<PostsList />
<UsersList />
<CommentsList />
</div>
</template>
v2
Let's take the same component and
modify it for Vue 3
<!-- PostsList.vue -->
<script>
export default{
data(){
return {
loading: true,
posts: null
}
},
async created(){
const res = await fetch("https://myapi.com/posts")
this.posts = await res.json()
this.loading = false
}
}
</script>
<template>
<div>
<AppSpinner v-if="loading"/>
<div v-else>...</div>
</div>
</template>
v2
In Vue 3 you can get rid of all that loading state in the component
<!-- PostsList.vue -->
<script>
export default{
data(){
return {
posts: null
}
},
async created(){
const res = await fetch("https://myapi.com/posts")
this.posts = await res.json()
}
}
</script>
<template>
<div>
<div>...</div>
</div>
</template>
v3
Then return an async setup function
instead
<!-- PostsList.vue -->
<script>
export default{
async setup(){
const res = await fetch("https://myapi.com/posts")
const posts = await res.json()
return { posts }
},
}
</script>
<template>
<div>
<div>...</div>
</div>
</template>
v3
Don't worry too much about how setup works right now
With script setup you can use a top level await
<!-- PostsList.vue -->
<script setup>
const res = await fetch("https://myapi.com/posts")
const posts = await res.json()
</script>
<template>
<div>
<div>...</div>
</div>
</template>
v3
Don't worry too much about how setup works right now
Then in the parent you use suspense to show a
single loader once all promises have resolved
<!-- ParentComponent.vue -->
<template>
<div>
<Suspense>
<!-- Put all the async components
inside the default slot -->
<template #default>
<PostsList />
<UsersList />
<CommentsList />
</template>
<!-- And your loader
in the fallback slot -->
<template #fallback>
<AppSpinner/>
</template>
</Suspense>
</div>
</template>
v3
Also works with asyncComponents
(different chunk from main js bundle)
<script>
import { defineAsyncComponent } from 'vue'
export default {
components: {
AdminPage: defineAsyncComponent(() =>
import('./components/AdminPageComponent.vue')
)
}
}
</script>
<template>
<Suspense>
<template #default> <AdminPage /> </template>
<template #fallback> <AppSpinner/> </template>
</Suspense>
</template>
v3
Total control by mixing async setup and local loading state
<!-- ParentComponent.vue -->
<template>
<div>
<Suspense>
<template #default>
<!-- has async setup -->
<PostsList />
<!-- has async setup -->
<UsersList />
<!-- handles own loading state -->
<CommentsList />
</template>
<template #fallback>
<AppSpinner/>
</template>
</Suspense>
</div>
</template>
v3
Does it do error handling?
Suspense is technically an experimental feature
But Nuxt 3 uses in production... so I wouldn't
worry about Vue.js core making breaking changes
โฐ 20 mins
โฐ 5 mins
The Official Migration guide provides:
Example of Filter
Besides the breaking changes already seen throughout the workshop, there are a few more worthy of noting
Async compnent syntax changed
<!-- Vue 2 -->
<script>
export default{
components:{
MyAsyncComponent: () => import('./MyAsyncComponent.vue')
}
}
</script>
<!-- Vue 3 -->
<script>
import {defineAsyncComponent} from "vue"
export default{
components:{
MyAsyncComponent: defineAsyncComponent(
() => import('./MyAsyncComponent.vue')
)
}
}
</script>
Watch on Arrays
<script>
export default{
data(){
return {
food: ['Hamburger', 'Hotdog', 'Spaghetti', 'Taco']
}
},
watch:{
// Vue 2 - will fire when array replaced, order changed, item added, etc
// Vue 3 - will only fire when array is replaced
food(){
console.log('food changed')
},
// Vue 3 - must provide for handler to fire on order changed, item added, etc
food:{
handler(){
console.log('food changed')
},
deep: true
}
}
}
</script>
Global API Treeshaking
<!-- Vue 2 -->
<script>
import Vue from 'vue'
Vue.nextTick(() => {
// something DOM-related
})
</script>
<!-- Vue 3 -->
<script>
import { nextTick } from 'vue'
nextTick(() => {
// something DOM-related
})
</script>
Global API Treeshaking
<!-- Vue 2 -->
<script>
import Vue from 'vue'
Vue.nextTick(() => {
// something DOM-related
})
</script>
<!-- Vue 3 -->
<script>
import { nextTick } from 'vue'
nextTick(() => {
// something DOM-related
})
</script>
Also Vue.version
<!-- Vue 2 -->
<script>
import Vue from 'vue'
console.log(Vue.version)
</script>
<!-- Vue 3 -->
<script>
import { version } from 'vue'
console.log(version)
</script>
Custom Directives Hooks
// Vue 2
Vue.directive('highlight', {
bind(el, binding, vnode) {
el.style.background = binding.value
}
})
// Vue 3
const app = Vue.createApp({})
app.directive('highlight', {
beforeMount(el, binding, vnode) {
el.style.background = binding.value
}
})
custom directive hooks mirrors component lifecycle hooks
Custom Directives Hooks
updated
, so this is redundant. Please use updated
instead.All breaking changes can be found at:
ย
The Official Migration guide provides:
Vue - Official
๐ฅ
๐จ
๐จ
๐ฅ
Difficulty
๐ฉ Easy
๐จ Medium
๐ฅ Hard
๐ฉ
๐ฉ
ย
The Official Migration guide provides:
npm install vue@3.2.45
npm install @vue/compat
Step
1
Step
2
// vue.config.js (vue-cli)
module.exports = {
chainWebpack: config => {
config.resolve.alias.set('vue', '@vue/compat')
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
return {
...options,
compilerOptions: {
compatConfig: {
MODE: 2
}
}
}
})
}
}
Vue Docs also contains example configs for:
Vue CLI
Step
3
Step
3
Step
4
module.exports = {
//...
compilerOptions: {
compatConfig: {
MODE: 2,
MODE: 3
},
},
}
Means the Vue compiler should expect all compiler level code to be written as per Vue 3 specification
Step
4
module.exports = {
//...
compilerOptions: {
compatConfig: {
MODE: 2,
MODE: 3
},
},
}
Means the Vue compiler should expect all compiler level code to be written as per Vue 3 specification
More on compatConfig in a minute!
Step
5
Official dependencies will have their own migration guides. Others may or may not. (This is where things can get a little dicey, but the ecosystem has definitely progressed)
Step
6
IDs for warnings found here:
https://v3-migration.vuejs.org/migration-build.html#feature-reference
ID
Explanation
ย Link to docs
Step
6
// main.js
import { configureCompat } from 'vue'
// disable compat for certain features
configureCompat({
COMPONENT_V_MODEL: false,
FEATURE_ID_B: false
})
Step
6
export default {
compatConfig: {
COMPONENT_V_MODEL: false,
FEATURE_ID_B: false
}
// ...
}
Step
6
feature/vue-3-migration
and maybe a commit per feature ID?
Step
6
configureCompat({
COMPONENT_V_MODEL: false,
GLOBAL_MOUNT_CONTAINER: false,
GLOBAL_MOUNT: false,
GLOBAL_SET: false,
//...
})
Step
7
.v-enter
.v-enter-from
.v-leave
.v-leave-from
Step
7
configureCompat({
// ...
TRANSITION_CLASSES: false
})
Step
7
configureCompat({
MODE: 3
})
Step
8
// package.json
"@vue/compat": "^3.1.0",
// vue.config.js
module.exports = {
chainWebpack: (config) => {
config.resolve.alias.set("vue", "@vue/compat");
config.module
.rule("vue")
.use("vue-loader")
.tap((options) => {
return {
...options,
compilerOptions: {
compatConfig: {
MODE: 3,
},
},
};
});
},
};
import {
createApp,
configureCompat
} from "vue";
import App from "./App.vue";
configureCompat({
COMPONENT_V_MODEL: false,
GLOBAL_MOUNT_CONTAINER: false,
GLOBAL_MOUNT: false,
GLOBAL_SET: false,
//...
})
1
2
3
Do you accept this challenge? ๐ช
โฐ 30 mins
โฐ 15 minutes