aboutsummaryrefslogtreecommitdiffstats
path: root/resources/src/vue
diff options
context:
space:
mode:
authorRoan Kattouw <roan.kattouw@gmail.com>2023-01-30 15:17:34 -0800
committerRoan Kattouw <roan.kattouw@gmail.com>2023-02-08 12:13:23 -0800
commitc5d9e49b973d57072e0195a4c8de6d1c40da822f (patch)
tree11fd11ddcb44f1873f353f9c4065f0a3862ebb5e /resources/src/vue
parent9e8e8fd4086e485ca83a9f9ac3c1e1b7e39543e8 (diff)
downloadmediawikicore-c5d9e49b973d57072e0195a4c8de6d1c40da822f.tar.gz
mediawikicore-c5d9e49b973d57072e0195a4c8de6d1c40da822f.zip
vue i18n: Prevent unnecessary rerender in v-i18n-html
When the v-i18n-html directive is used in a component, the directive function is called every time anything changes in the component. Unfortunately this is necessary for detecting when the value of the directive changes, because Vue's directive API doesn't give us a better way to detect that. However, it does give us the old value. This change adds logic that should have been part of this directive from the start, comparing the old value to the new value and doing nothing if they're the same. This avoids unnecessary rerenders when an unrelated bit of component state changes. These rerenders were wasteful, and were also causing bugs like T327229. There's no binding.oldArg, so we can't detect changes in binding.arg. Those can only happen when a dynamic argument is used, which is already discouraged, so warn in the documentation that that isn't supported. Change-Id: I71f1cc0d08bfd02e1a9edb6cbfd849f10f929f3c
Diffstat (limited to 'resources/src/vue')
-rw-r--r--resources/src/vue/i18n.js73
1 files changed, 55 insertions, 18 deletions
diff --git a/resources/src/vue/i18n.js b/resources/src/vue/i18n.js
index 4e386d000472..7e2bb8be57e3 100644
--- a/resources/src/vue/i18n.js
+++ b/resources/src/vue/i18n.js
@@ -31,6 +31,29 @@ module.exports = {
return mw.message( key, ...parameters );
};
+ function renderI18nHtml( el, binding ) {
+ /* eslint-disable mediawiki/msg-doc */
+ let message;
+
+ if ( Array.isArray( binding.value ) ) {
+ if ( binding.arg === undefined ) {
+ // v-i18n-html="[ ...params ]" (error)
+ throw new Error( 'v-i18n-html used with parameter array but without message key' );
+ }
+ // v-i18n-html:messageKey="[ ...params ]"
+ message = mw.message( binding.arg ).params( binding.value );
+ } else if ( binding.value instanceof mw.Message ) {
+ // v-i18n-html="mw.message( '...' ).params( [ ... ] )"
+ message = binding.value;
+ } else {
+ // v-i18n-html:foo or v-i18n-html="'foo'"
+ message = mw.message( binding.arg || binding.value );
+ }
+ /* eslint-enable mediawiki/msg-doc */
+
+ el.innerHTML = message.parse();
+ }
+
/*
* Add a custom v-i18n-html directive. This is used to inject parsed i18n message contents.
*
@@ -62,27 +85,41 @@ module.exports = {
* styles (e.g. because the message key is dynamic, or contains unusual characters).
* Note that you can use mw.message() in computed properties, but in template attributes
* you have to use $i18n() instead as demonstrated above.
+ *
+ * WARNING: Do not use dynamic argument syntax, like <div v-i18n-html:[msgKeyVariable] />
+ * If you do this, the message will not update when msgKeyVariable changes, due to
+ * limitations in Vue's directives API. Instead, use the $i18n style described
+ * above if you need a dynamic message key.
*/
- app.directive( 'i18n-html', function ( el, binding ) {
- let message;
- /* eslint-disable mediawiki/msg-doc */
- if ( Array.isArray( binding.value ) ) {
- if ( binding.arg === undefined ) {
- // v-i18n-html="[ ...params ]" (error)
- throw new Error( 'v-i18n-html used with parameter array but without message key' );
+ app.directive( 'i18n-html', {
+ mounted: renderI18nHtml,
+ updated( el, binding ) {
+ // This function is invoked often, every time anything in the component changes.
+ // We don't want to rerender unnecessarily, because that's wasteful and can cause
+ // strange issues like T327229. For each possible type of binding.value, compare it
+ // to binding.oldValue, and abort if they're equal. This does not account for
+ // changes in binding.arg; we can't detect those, so there's a warning in the
+ // documentation above explaining that using a dynamic argument is not supported.
+
+ const areArraysEqual = ( arr1, arr2 ) =>
+ Array.isArray( arr1 ) && Array.isArray( arr2 ) &&
+ arr1.length === arr2.length &&
+ arr1.every( ( val, index ) => arr2[ index ] === val );
+ const areMessagesEqual = ( msg1, msg2 ) =>
+ msg1 instanceof mw.Message && msg2 instanceof mw.Message &&
+ msg1.key === msg2.key &&
+ areArraysEqual( msg1.parameters, msg2.parameters );
+
+ if (
+ binding.value === binding.oldValue ||
+ areArraysEqual( binding.value, binding.oldValue ) ||
+ areMessagesEqual( binding.value, binding.oldValue )
+ ) {
+ return;
}
- // v-i18n-html:messageKey="[ ...params ]"
- message = mw.message( binding.arg ).params( binding.value );
- } else if ( binding.value instanceof mw.Message ) {
- // v-i18n-html="mw.message( '...' ).params( [ ... ] )"
- message = binding.value;
- } else {
- // v-i18n-html:foo or v-i18n-html="'foo'"
- message = mw.message( binding.arg || binding.value );
- }
- /* eslint-enable mediawiki/msg-doc */
- el.innerHTML = message.parse();
+ renderI18nHtml( el, binding );
+ }
} );
}
};