Totara 19 Vue 3 migration guide

In Totara 19, we have shifted to the latest major version of the Vue framework, Vue 3.

For the most part, you should be able to keep writing components just like before. There are a few changes to be aware of. Most of these are covered by eslint.

General workflow

  • npm ci to make sure you have the correct version of all of the packages

  • Run eslint (npm run tui-style-check), fix the issues.

  • Build tui, fix any issues the compiler complains about.

  • Test everything works in the browser, while keeping the console open to see any runtime warnings.


Test the Tui/Vue front-end of the features/code you have built. Test that everything looks and works okay, and that there aren’t any errors or warnings in the browser console.

Try all possible states and interfaces – especially interactive things and things that make GraphQL requests.

Breaking changes

You can see the full list of breaking changes in the Vue documentation, but some of the most impactful are summarised below.

It may also be useful to familiarise yourself with the Vue 3 documentation.

It is also safe to assume that any core component overrides will need to be checked and updated to Vue 3. Changes to core components' public API are documented in upgrade.txt.

We don’t recommend attempting to replace vue with the the @vue/compat build -- in our experience, the compatibility layer is not reliable, and many options will need to be disabled as our codebase has been migrated to Vue 3 already.

Event handling (eslinted)

All component must now declare what events they emit, in an emits array on the component. This is enforced by eslint.

The .native modifier no longer exists, instead when attaching a listener (e.g. @click="..."), any event that is not declared in the emits field of the component will now also be attached as a native event listener to the root element of the component.

false attribute values

In Vue 2, false would remove the attribute. In Vue 3, false sets the attribute to the string "false"

If you want to remove the attribute, use null instead.

More info


Handling of whitespace inside templates has changed a little compared to Vue 2. The changes are:

  • Whitespace characters between elements that contain newlines are removed.

  • Consecutive whitespace characters in text nodes are condensed into a single space.

Render functions, vnodes, and h()

This has changed a lot between versions. If you’re using render functions, vnodes, or h(), expect to rework your code.

:key (eslinted)

  • When using with <template v-for>, the :key should be placed on the template tag, instead of the child

  • No longer necessary on v-if/else/else-if branches

v-bind object order

When using v-bind="someObject", individual props will now only take precedence if they come before the v-bind. More info

v-model and the input event (eslinted)

Plain v-model is no longer allowed, except on native HTML elements. Use v-model:value instead.

Avoid using the input event on components, prefer update:value (or update:something).

Slots (eslinted)

$scopedSlots no longer exists. Use $slots instead for all cases, it now behaves like $scopedSlots used to.


If your component has multiple root elements, or only renders a slot as its sole content (i.e. a renderless component), this.$el will point to an HTML comment instead of an actual element.

A workaround is to call extractSingleChild from tui/vue/vnode on the slot in the render function.


Watchers on arrays no longer fire if an item is added or removed from the array. You must pass deep: true as a watch option.

Jest tests

Some things that may catch you out in Jest tests:

  • propsData is now just props

  • scopedSlots is now just slots

  • mocks and stubs have now moved under the global field on mount/render

  • mockQueries works on both, but it is preferrable to put it under the global field to align with mocks

  • wrapper.emitted() now includes native events. It is recommended to just ask for the event you are looking for with e.g. wrapper.emitted('submit')

Lifecycle methods (eslinted)

  • The destroyed lifecycle option has been renamed to unmounted

  • The beforeDestroy lifecycle option has been renamed to beforeUnmount

Mutating props (eslinted)

This was deprecated in Vue 2 too, but is riskier in Vue 3. This will be automatically flagged by an eslint rule.

Instead of mutating the prop, copy it and change the copy. produce() from tui/immutable can help with this.


$children has been removed in Vue 3.

Vue namespaced methods

Vue.set and Vue.delete no longer exist -- they are no longer necessary with the proxy-based reactivity approach in Vue 3.

Other methods such as Vue.nextTick are now just import { nextTick } from 'vue';


Mixins should be avoided where possible, but there is a breaking change in Vue 3: data() on mixins is no longer merged with the component’s data().