Jump to content

Vue.js

From mediawiki.org

Vue.js is being evaluated as an official library for building UI within the MediaWiki ecosystem. It's currently experimental (based on v2.x) and not ready for wide use as there are a number of unknowns but more importantly, no well thought out conventions yet. This page documents best practices and other knowledge that surfaces during evaluation. In general, this documentation should be minimal because it defines only MediaWiki-specific pieces, and largely refers to the official documentation.

Shared components library Wikimedia Vue UI

As part of the Vue.js Search Widget Case Study and in course of interest by a number of teams in prototyping Vue.js based projects with goal of later in-production use, we've setup shared components library Wikimedia Vue UI (WVUI).

Set up

Two ways to parse Vue.js templates

Pre-built (bandwidth and client performant at the expense of extra initial set up)

For professional developers building high performance experiences, pre-compilation and minificiation is recommended. However, this requires extra per-repo configuration changes which are currently undocumented. This extra configuration will be eliminated or formalized and minimized if the deploy build step RFC succeeds.

On the fly compilation (less performant but easy to develop)

ResourceLoader has built-in support for parsing single-file components. This lets you write Vue.js with very little configuration in the context of a MediaWiki installation which may be valuable in some contexts as well as for developers uncomfortable with compilers. Please note the following limitations:

  • Scoped styles are unsupported.
  • What you write is what you ship--No ES6, TypeScript, or language compilation support. At time of writing, this means ES5 only.
  • Vue.js directive shorthands are unsupported @ for v-on and : for v-bind.
  • The version of Less used must match MediaWiki's version (very old). However, everything that works in normal MediaWiki styles works here (e.g., CSSJanus, relative URLs, imports, etc).
How to use on the fly compilation

You can add a .vue file to any package module as if it were a script file:

{
    "ResourceModules": {
        "ext.myExtension.myModule": {
            "packageFiles": [
                "myModule/init.js",
                "myModule/MyComponent.vue"
            ],
            "messages": [
                "myextension-mycomponent-header"
            ],
            "dependencies": [
                "vue"
            ]
        }
    }
}

The .vue file must contain a template and JavaScript code that exports a component registration object. It can also (optionally) contain styles:

<template>
    <div class="mw-my-component">
        <h2>{{ $i18n( 'myextension-mycomponent-header' ) }}</h2>
        <p><input type="number" v-model="a"> + <input type="number" v-model="b"> = {{ sum }}</p>
    </div>
</template>

<script>
module.exports = {
  data: function () {
      return {
          a: 23,
          b: 19
      };
  },
  computed: {
      sum: function () {
          return Number( this.a ) + Number( this.b );
      }
  }
};
</script>

<style lang="less">
.mw-my-component {
    input {
        width: 3em;
    }
}
</style>

You can then require() the .vue file just like any other script file. For example, you can use it as a root component as follows:

var Vue = require( 'vue' ),
    MyComponent = require( './MyComponent.vue' );

// eslint-disable-next-line no-new
new Vue( {
    el: '#my-component-placeholder',
    render: function ( h ) {
        return h( MyComponent );
    }
} );

Or you can use it as a nested component inside another component:

<template>
    <div class="parent-component">
        <h2>{{ $i18n( 'myextension-parent-component-header' ) }}</h2>
        <my-component />
    </div>
</template>

<script>
module.exports = {
    components: {
        'my-component': require( './MyComponent.vue' )
    }
};
</script>

Some modules have an init script as their main file (usually called init.js) that creates a new Vue instance to take over a DOM element on the page and replace it with a Vue component. Other modules just export one or more reusable components; their main file is either a .vue file with the component that's exported, or a script file that exports several components, e.g.:

module.exports = {
    ComponentOne: require( './ComponentOne.vue' ),
    ComponentTwo: require( './ComponentTwo.vue' )
    // etc.
};

The vue module in ResourceLoader

MediaWiki core comes with a copy of Vue (v2.x) and some small MediaWiki-specific plugins for Vue. These live in the 'vue' module. In code that sets up a new Vue instance, (i.e. calls new Vue( ... )), you should make the 'vue' module a dependency of your module, then obtain the Vue object using var Vue = require( 'vue' );. You do not have to call Vue.use() to register the MediaWiki-specific plugins; this is done for you.

The i18n plugin

The 'vue' module comes with a small i18n plugin that wraps MediaWiki's i18n system (mw.message), so you can use MediaWiki i18n messages in your templates. This plugin creates an $i18n() function that you can use inside templates, which is an alias for mw.message(). For plain text messages (most cases), you can simply use:

<p>{{ $i18n( 'message-key' ) }}</p>

You can pass parameters either variadically, or as an array:

<p>{{ $i18n( 'message-key', param1, param2 ) }}</p>
<!-- or: -->
<p>{{ $i18n( 'message-key' ).params( [ param1, param2 ] ) }}</p>

The $i18n() function returns a Message object, so you can use it in all the same ways that you can use mw.message() in "normal" JS code. Remember that all message keys you use have to be added to the "messages" array in the ResourceLoader module definition.

Parsed messages

One important limitation of $i18n() is that you can't use it for parsed messages that return HTML. This is because template interpolations ({{variable}} syntax) can only return plain text, and are always HTML-escaped:

<!-- This DOES NOT WORK: -->
<p>{{ $i18n( 'category-empty' ).parse() }}</p>
<!-- This renders as: -->
<p>&lt;em&gt;This category currently contains no pages or media.&lt;/em&gt;</p>

The code above displays <em>This category currently contains no pages or media.</em>, as plain text. To get Vue to accept raw HTML, you have to use the v-html directive. Because writing <p v-html="$i18n('category-empty').parse()"> is verbose and annoying, the i18n plugin provides a shortcut using the v-i18n-html directive, which you'll want to use instead unless you need to pass message parameters or reference the message name from a variable.

<p v-i18n-html:category-empty />
<!-- or: -->
<p v-html="$i18n('category-empty', 'param1', 'param2').parse()" />
<!-- Both of these render as: -->
<p><em>This category currently contains no pages or media.</em></p>

See the documentation comments in resources/src/vue/i18n.js for a more detailed explanation.

Parsed messages with parameters

The v-i18n-html directive does not currently support passing parameters to the message. Support for this may be added in the future. The recommended way to pass a parameter to a parsed message (or to do other complicated things) is to put the parsed message in a computed property. It's possible to do these kinds of things inline, but using a computed property is recommended for readability.

<template>
    <p v-html="namespaceProtectedMessage" />
</template>
<script>
module.exports = {
    computed: {
        namespaceProtectedMessage: function () {
            return mw.message( 'namespace-protected' )
                .params( [ namespaceName ] )
                .parse();
        }
    }
};
</script>

Examples

Skin

Extension

Skin + Extension sharing a Vue instance

Gadget

User script

External resources

Most important:

Additional

Training vendors

Other Vue.js orgs

GitLab

Testing

You should write tests! The current recommendation is to use Jest.

More information about how to test Vue components in a MediaWiki environment can be found in this guide.

Projects using Vue

Please list any MediaWiki projects that are utilizing Vue during the testing phase here.

See also