Transitioning from the server-rendered HTML to the new Vue.js JavaScript experience is complicated and important. This initial render is called [[ https://ssr.vuejs.org/guide/hydration.html | client-side hydration ]]. This task tracks the work needed smoothly transition from server state to client state.
== Complications and concerns
- The server-rendered HTML is generated with PHP, not Vue.js.
- The searchSatisfaction client instrumentation loads on every pageview and [[ https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/extensions/WikimediaEvents/+/17ccf21/modules/ext.wikimediaEvents/searchSatisfaction.js#662 | alters the DOM ]].
- Per T249306, the search experience does not load until the user focus on the search form which means the user is likely to be actively typing their query when dependency loading completes. In other words, focus state, selection state, and input value must be preserved. If the DOM changes, the CSS transition styles cause the focus to flicker.
- Per T254695, we want the transition to be seamless and perceived as prompt.
- Other JavaScript, such as [[ https://www.mediawiki.org/wiki/Help:Extension:UniversalLanguageSelector/Input_methods | UniversalLanguageSelector's Input Methods ]] (also called "input tools") load by default on some wiki (on user input focus!) and bind to the inputs. Another example is the [[ https://he.wikipedia.org/wiki/%D7%9E%D7%93%D7%99%D7%94_%D7%95%D7%99%D7%A7%D7%99:Gadget-Dwim.js | DWIM gadget ]].
== Options
1. Render the whole search form. This provides a deeper Vue.js integration that would allow for rich interactivity in the form itself, not just the results. However, it also has all the concerns identified above.
1. Properly hydrate by adding `data-server-rendered` to the SSR form.
2. Manually pass state across.
2. Render the results only. Very similar to old search. There's not much to hydrate on the client.
3. Something else.
== Acceptance criteria
- [] No changes to loading strategy. Vue.js and dependencies are loaded on user input focus.
- [] No regressions to other integrations.
- [] The transition from server-rendered state to client-rendered state is seamless.
== References
- [[ https://ssr.vuejs.org | Vue.js server-side rendering guide ]]
- [[ https://vuejs.org/v2/guide/forms.html | Notes on rendering forms specifically ]]
- [[ https://github.com/vuejs/vue-hackernews-2.0/ | Evan You's SSR demo ]]
- [[ https://vuejs.org/v2/guide/ssr.html | SSR framework examples ]]
== Questions
- What server styles do we need to isolate in WVUI for sharing? They would need to load on every pageview.
== Developer notes
Option 1.a is the most desirable but must challenging. The Vue.js client renderer implementation expects exact synchronization with the server-renderer. Even a blank text node can cause runtime errors or hydration failures. For example:
```name=Client template,lang=html
<div id="app">
<div>foo</div>
</div>
```
```name=Server template (working),lang=html
<div id="app" data-server-rendered="true"><div>foo2</div></div>
```
```name=Server template (breaks client),lang=html
<div id="app" data-server-rendered="true"> <div>foo2</div></div>
```
In the second server template example, note the extra space after the opening `div` which breaks the client. Note also that the server-rendered outputs are minified. This is to help the WVUI consumer (Vector) avoid server-rendered differences by assuming a deterministic minified structure and is made with the following vue-loader configuration:
```name=vue-loader config,lang=js
// Process single-file components (SFCs). This matches loader extensions to the SFC
// language attributes.
{
test: /\.vue$/,
use: {
loader: 'vue-loader',
options: {
compilerOptions: {
// Any whitespace or comment differences between tags causes client
// hydration to fail. When whitespace is preserved, it is difficult as a
// client to anticipate where these occur as Vue.js is not always used
// to generate the HTML server-side. Remove all whitespace so clients
// can structure their elements deterministically.
whitespace: 'condense'
}
}
}
}
```
However, this structure also varies in development and production mode and I haven't figured out yet how to configure that. Since Vector does not use Vue.js on the backend for rendering, no variation is permitted. For example, whitespace differences can cause cryptic errors like `Uncaught DOMException: Node.appendChild: Cannot add children to a Text` or `Uncaught TypeError: nodeOps.tagName(...) is undefined` or just simple client hydration failures which are only reported in development mode:
```
Parent: <div class="wvui-input"> vue.runtime.esm.js:6426
Mismatching childNodes vs. VNodes: NodeList [ input#searchInput.wvui-input__input ] Array(3) [ {…}, {…}, {…} ] vue.runtime.esm.js:6427
[Vue warn]: The client-side rendered virtual DOM tree is not matching server-rendered content. This is likely caused by incorrect HTML markup, for example nesting block-level elements inside <p>, or missing <tbody>. Bailing hydration and performing full client-side render. vue.runtime.esm.js:619
```
In summary, the Vue.js client renderer seems to expect a Vue.js server renderer and has little tolerance. This does not work well with Vector's PHP implementation and the confounding factors identified above.
---
Option 1.b seems like the next best thing but the code is fragile and imperative. Worse, because the DOM is replaced, the focus state is lost momentarily and the CSS transitions cause a noticeable flicker. This could be worked around but will require more hacks.