#Vue.js-Search, must both avoid changing the on-focus loading strategy used by the existing implementation and download significantly more bytes (see T249306). The increase is due largely to a new dependency on the Vue.js framework itself without replacing many existing dependencies that are used at least in part elsewhere. As more projects migrate to Vue.js, this loading strategy may be changed (e.g., it may be more like Popups) but #Vue.js-Search is the first in the main namespace and has few options. As such, the wait time from when the user interacts with search to when they see typeahead results may be remarkably greater.
Both the new and existing search implementations allow typing to occur during this window of dependency load time, but the existing experience gives no indication that results will be presented. Given the greater bandwidth needs, the new experience should give responsive feedback to the user that their input has been received, that work is indeed happening, and promise that results will be presented as quickly as possible. Setting user expectations this way may improve the perception of performance, which is an important part of the overall experience, and may help derisk the new loading requirements.
This task is to create a slim placeholder shim animation that has no dependencies and can be bundled directly into the skins.vector.js. The following GIF demonstrates the idea of the new workflow but suggests little design or implementation detail:
The intent of the above GIF is to show the following workflow:
- On pageview, the shim is loaded is with all other Vector JavaScript and styles such as the sidebar. It is available to any other JavaScript immediately.
- On focus, WVUI, Vue.js, and any other dependencies needed are requested.
- On nonblank key input, if WVUI is unavailable, an initial loading state is presented within 200 milliseconds. In the GIF, it's a mini-placeholder area. The intent is to indicate that the input has been received by the machine and is currently being processed.
- After 200 milliseconds and within one second, if WVUI is unavailable, a second loading state is shown. In the GIF, it's a larger placeholder transition that sports the final size and background of the Vue.js search form. The intent is to optimistically indicate that further computation is occurring and the user can be assured that their request is in flight; results will be served soon.
- After five seconds, if WVUI is still unavailable, a final loading state is shown. In the GIF, an emphatic optimistic promise is made that the machine is working as hard as is possible to present results soon. It is hoped this will be a rare scenario.
The final design may differ dramatically. For example, the last two loading states may be omitted entirely.
On input clear or focus loss, the shim is dismissed (but the dependency network requests continue). If the user reengages the input, the animation starts over as if no prior loading has happened. However, the implementation may wish to check if previous network request failed.
An objective is to keep this shim minimal both for bandwidth and development costs so supporting additional states or logic should be scrutinized.
Once WVUI and its dependencies are fully loaded, the placeholder shim razzle-dazzle should be effectively discarded. If search is still focused, the new search form should be gracefully transitioned to by tweening from the current user interface state to the final search form state.
The implementation can live within WVUI if potentially useful to any search consumer or directly within Vector if not.
The shim shown in the GIF is made via CSS animations to avoid state management and improve simplicity. The JavaScript and CSS are embedded below for reference but are not representative of the design or technical quality wanted in the production version.
const input = /** @type {HTMLInputElement} */ (document.getElementById('searchInput')) input.addEventListener('input', updatePlaceholderState) const preLibPlaceholderParent = document.getElementById('wvui-typeahead-search'); const preLibPlaceholder = document.createElement('div') preLibPlaceholder.className = 'wvui-pre-lib-placeholder' const size = { start: {w: '256px', h: '24px'}, end: {w: '640px', h: '480px'} } const loadingClass = 'wvui-pre-lib-placeholder--loading'; function updatePlaceholderState() { if ((document.activeElement !== input || !input.value.trim())) { if (preLibPlaceholder.parentNode) { preLibPlaceholder.classList.remove(loadingClass) preLibPlaceholder.parentNode.removeChild(preLibPlaceholder) } } else { // Set the initial placeholder state. preLibPlaceholder.style.width = size.start.w; preLibPlaceholder.style.height = size.start.h; preLibPlaceholderParent.appendChild(preLibPlaceholder) // Force layout so the transition kicks in. preLibPlaceholder.scrollTop preLibPlaceholder.style.width = size.end.w; preLibPlaceholder.style.height = size.end.h; preLibPlaceholder.classList.add(loadingClass) } } document.addEventListener('focusout', updatePlaceholderState) document.addEventListener('focusin', onFocusInLoadLibrary) function onFocusInLoadLibrary() { // if (the the actual WVUI library, Vue.js, and any other dependencies needed have not been // loaded, request them) { setTimeout(() => { import('/vue.js ').then(() => { console.log('loaded') // Mount into the wvui-typeahead-search container // Unregister and delete the placeholder. It won't be needed again. }) }, Math.random() * 5000) updatePlaceholderState() } // else this function is unregistered and the WVUI Vue.js library is in control. } updatePlaceholderState()
.wvui-pre-lib-placeholder { position: absolute; border: 1px solid rgba( 0, 0, 0, .1 ); border-radius: 4px; padding: 4px; opacity: 0; background:#f8f9fa; box-shadow: 2px 2px 2px 0 rgba( 0, 0, 0, .1 ); transition: all 250ms 750ms, opacity 250ms ease-in, transform 100ms ease-in; } .wvui-pre-lib-placeholder--loading { opacity: 1; transform: translateY(4px); /* Delay four seconds then start the loading animation. The duration is three seconds which is a nice speed for the size. */ animation: wvui-pre-lib-placeholder--loading_animation 3s 4s linear infinite; } @keyframes wvui-pre-lib-placeholder--loading_animation { 0%, 100% { /* The background is part of the animation so that it's not shown until after the delay. */ background: #f8f9fa repeating-linear-gradient( -45deg, #f8f9fa, #f8f9fa 18px, #eaecf080 18px, #eaecf080 32px ) 0 / 200%; } 100% { background-position: 100%; } }
See the respository for the above code and GIF.
This is not a replacement for skeletal placeholder content, perhaps similar to T124811, that would be shown while search results themselves were pending.
The shim can't depend on Vue.js but WVUI can depend on the shim. For example, if the indicator was a generic spinner that should be shown while waiting for search results to come in, it could be a tightly scoped common chunk that is expected by other chunks in the WVUI library.
This task was made based on an understanding of Jean-Pierre Vincent's performance recommendations given in the 2020-06-05 training session, section 15, "user perception" arranged by @Gilles.
Acceptance criteria
- Performance marks are recorded for notable steps (e.g., focus / dependency loading, first loading state shown, second loading state shown, third loading state shown, dependencies loaded, search form shown, search results shown, and focus loss / dismissal). -- @nray has added a mark for dependency loading which is all we think we'll need for this out the gate.
- The shim is small enough to be bundled with every-pageview Vector JavaScript.
- The loading strategy adheres to T249306 (this task shouldn't change how code actually loads)
- No changes to user input before and after library loads
- Don't break UniversalLanguageSelector input tools (note: mw.config.get( 'wgULSImeSelectors' ))