This idea was inspired by some of the discussion around T286835 (and related tasks like T289718).
As WVUI currently stands, we don't have a great way to deliver Vue.js-based UI components in performance-critical contexts, which has required the Web team to come up with various work-arounds (e.g. only load the WVUI Typeadhead Search component after the user interacts with the server-rendered search input in the article chrome). Many other reader-facing features (sticky header, related articles, etc) will offer similar challenges.
Our new shared library should offer better support for performance-critical / progressively-enhanced use cases where loading all of Vue.js and the full library is not practical. One way to do that would be to provide a "lite" subset of the UI that is powered by petite-vue.
Petite-vue is a relatively new distribution of Vue.js (it is built around Vue 3's reactivity module) that is optimized for progressive enhancement. The full library is around 6KB minified and gzipped. This is small enough that we could likely deliver it to the user immediately instead of relying on workarounds to delay loading, which would hopefully simplify things for many of us. Petite-vue "components" would re-use existing DOM elements, but a most of the template, logic, and styles for the "real" components could be re-used.
We don't necessarily need a "lite" version of every component, but we'd want to support many of the features the Web team is currently working on. In particular, I think we could re-implement the entire TypeaheadSearch element as a set of petite-vue components that progressively enhance – rather than replace – the server-rendered search markup on article pages. (Related: T291526)
The library's component roadmap could list which components need to exist in which versions, and the overall documentation for the shared library could provide developers with guidelines for when to use which type of component.
To help ensure consistency, all code for both sets of components should probably live in the same repository, even though we will want to provide different builds.
Example: "wvui-lite" button
Here's an example of how the WvuiButton could be re-implemented in petite-vue. In this example, the assumption is that markup is generated in PHP (probably using Mustache templates or similar) and then JS is delivered via ResourceLoader (assuming the client has JS enabled). This approach should make it straightforward to design a UI that is usable and complete without JS (or before it is finished loading).
/** * Button component. Accepts a user-provided function which is bound to the * click handler. */ export default function Button ( props ) { return { /** * @type string */ action: props.action, /** * @type string */ type: props.type, /** * The original WVUI button emits an event to the parent Vue component, but * it probably makes more sense to just pass down the function we want to use * in this scenario * @type Function */ onClick: props.onClick, /** * Computed property example * @return {Object.<string, boolean>} */ get rootClasses () { return { 'wvui-button--action-default': this.action === 'default', 'wvui-button--action-progressive': this.action === 'progressive', 'wvui-button--action-destructive': this.action === 'destructive', 'wvui-button--type-primary': this.type === 'primary', 'wvui-button--type-normal': this.type === 'normal', 'wvui-button--type-quiet': this.type === 'quiet', 'wvui-button--framed': this.type !== 'quiet' } } } }
<div id="app" v-scope> <!-- v-effect will execute the inline statement whenever it changes --> <div v-effect="$el.textContent = count">Enable JS to see the count</div> <!-- Button component uses the existing DOM element as its template. It's also possible to provide the template as a string or as an external <template> element. We could even setup a mustache partial that accepted parameters if we wanted to programmatically generate these buttons in PHP. --> <!-- props for the first button are set up in JS --> <button v-scope="Button( incrementButton )" class="wvui-button" :class="rootClasses" @click="onClick()" > Increase the count </button> <!-- props for the second button are provided inline in the template --> <button v-scope="Button( { action: 'destructive', onClick: decrement } )" class="wvui-button" :class="rootClasses" @click="onClick()" > Decrease the count </button>
import { createApp } from 'petite-vue'; import { Button } from './lib'; createApp( { // exposed to all expressions count: 0, increment () { this.count++; }, decrement () { this.count--; }, // Similar to component declaration; make the button "component" // available Button, // Props for individual component instances can be provided inline // inside a v-scope expression, or a property can be declared here: incrementButton: { action: 'progressive', onClick() { this.increment(); } } } ).mount();