Skip to content

Commit

Permalink
⚡ improvement(directive): Preserve directive content (#495) by @bpono…
Browse files Browse the repository at this point in the history
…marenko

* feature(directive): add .preserve modifier to v-t directive

Fixes #408

* feature(index): add new preserveDirectiveContent i18nOption

* improvement(directive): preserve directive content when preserveDirectiveContent i18nOption is set to true

Fixes #408

* docs(directive): add documentation about new .preserve modifier and update examples
  • Loading branch information
bponomarenko authored and kazupon committed Jan 2, 2019
1 parent aa20962 commit c29edba
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 4 deletions.
4 changes: 3 additions & 1 deletion decls/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ declare type I18nOptions = {
pluralizationRules?: {
[lang: string]: (choice: number, choicesLength: number) => number,
},
preserveDirectiveContent?: boolean,
};

declare type IntlAvailability = {
Expand Down Expand Up @@ -106,7 +107,8 @@ declare interface I18n {
n (value: number, ...args: any): NumberFormatResult,
pluralizationRules: {
[lang: string]: (choice: number, choicesLength: number) => number
}
},
preserveDirectiveContent: boolean
};

declare interface Formatter {
Expand Down
40 changes: 40 additions & 0 deletions examples/directive/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@
<p v-t="{ path: 'hello', locale: 'ja', args: { name: 'kazupon' } }"></p>
<p v-t="{ path: path, args: { name: nickName } }"></p>
</div>
<div id="in-transitions">
<p>
<transition name="fade">
<span v-if="toggle" v-t="'clean'" class="red-bg"></span>
</transition>
<transition name="fade">
<span v-if="toggle" v-t.preserve="'preserve'" class="red-bg"></span>
</transition>
</p>
<button @click="toggle = !toggle">Toggle</button>
</div>
<script>
// string syntax example
new Vue({
Expand Down Expand Up @@ -42,6 +53,35 @@
},
data: { path: 'hello' }
}).$mount('#object-syntax')

// use in transitions example
new Vue({
i18n: new VueI18n({
locale: 'en',
messages: {
en: {
clean: 'without preserve',
preserve: 'with preserve'
},
}
}),
data: { toggle: true }
}).$mount('#in-transitions')
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 1s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}

.red-bg {
background-color: red;
padding: 5px;
}
</style>
</body>
</html>
5 changes: 4 additions & 1 deletion src/directive.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ export function unbind (el: any, binding: Object, vnode: any, oldVNode: any): vo
return
}

el.textContent = ''
const i18n: any = vnode.context.$i18n || {}
if (!binding.modifiers.preserve && !i18n.preserveDirectiveContent) {
el.textContent = ''
}
el._vt = undefined
delete el['_vt']
el._locale = undefined
Expand Down
4 changes: 4 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export default class VueI18n {
pluralizationRules: {
[lang: string]: (choice: number, choicesLength: number) => number
}
preserveDirectiveContent: boolean

constructor (options: I18nOptions = {}) {
// Auto install if it is not done yet and `window` has `Vue`.
Expand Down Expand Up @@ -94,6 +95,9 @@ export default class VueI18n {
this._dataListeners = []

this.pluralizationRules = options.pluralizationRules || {}
this.preserveDirectiveContent = options.preserveDirectiveContent === undefined
? false
: !!options.preserveDirectiveContent

this._exist = (message: Object, key: Path): boolean => {
if (!message || !key) { return false }
Expand Down
1 change: 1 addition & 0 deletions src/mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default {
options.i18n.fallbackLocale = this.$root.$i18n.fallbackLocale
options.i18n.silentTranslationWarn = this.$root.$i18n.silentTranslationWarn
options.i18n.pluralizationRules = this.$root.$i18n.pluralizationRules
options.i18n.preserveDirectiveContent = this.$root.$i18n.preserveDirectiveContent
}

// init locale messages via custom blocks
Expand Down
75 changes: 75 additions & 0 deletions test/unit/directive.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,5 +199,80 @@ describe('custom directive', () => {
}).then(done)
})
})

describe('preserve content', () => {
it('should clear element content on destroy by default', done => {
const vm = createVM({
i18n,
data: () => ({ visible: true }),
render (h) {
// <p ref="text" v-t="'message.hello'"></p>
const directives = this.visible ? [{
name: 't', rawName: 'v-t', value: ('message.hello'), expression: "'message.hello'"
}] : []
return h('p', { ref: 'text', directives })
}
})

nextTick(() => {
assert.strictEqual(vm.$refs.text.textContent, messages.en.message.hello)

vm.visible = false
vm.$forceUpdate()
}).then(() => {
assert.strictEqual(vm.$refs.text.textContent, '')
}).then(done)
})

it('should not clear element content with "preserve" modifier', done => {
const vm = createVM({
i18n,
data: () => ({ visible: true }),
render (h) {
// <p ref="text" v-t.preserve="'message.hello'"></p>
const directives = this.visible ? [{
name: 't', rawName: 'v-t', value: ('message.hello'), expression: "'message.hello'", modifiers: { preserve: true }
}] : []
return h('p', { ref: 'text', directives })
}
})

nextTick(() => {
assert.strictEqual(vm.$refs.text.textContent, messages.en.message.hello)

vm.visible = false
vm.$forceUpdate()
}).then(() => {
assert.strictEqual(vm.$refs.text.textContent, messages.en.message.hello)
}).then(done)
})

it('should not clear element content when "preserveDirectiveContent" i18nOption is set to true', done => {
const vm = createVM({
i18n: new VueI18n({
locale: 'en',
messages,
preserveDirectiveContent: true
}),
data: () => ({ visible: true }),
render (h) {
// <p ref="text" v-t="'message.hello'"></p>
const directives = this.visible ? [{
name: 't', rawName: 'v-t', value: ('message.hello'), expression: "'message.hello'"
}] : []
return h('p', { ref: 'text', directives })
}
})

nextTick(() => {
assert.strictEqual(vm.$refs.text.textContent, messages.en.message.hello)

vm.visible = false
vm.$forceUpdate()
}).then(() => {
assert.strictEqual(vm.$refs.text.textContent, messages.en.message.hello)
}).then(done)
})
})
})
})
2 changes: 2 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ declare namespace VueI18n {
sync?: boolean;
silentTranslationWarn?: boolean;
pluralizationRules?: PluralizationRulesMap;
preserveDirectiveContent?: boolean;
}
}

Expand Down Expand Up @@ -129,6 +130,7 @@ export declare interface IVueI18n {
formatter: VueI18n.Formatter;
silentTranslationWarn: boolean;
pluralizationRules: VueI18n.PluralizationRulesMap;
preserveDirectiveContent: boolean;
}

declare class VueI18n {
Expand Down
2 changes: 2 additions & 0 deletions types/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const i18n = new VueI18n({
fallbackRoot: false,
sync: true,
silentTranslationWarn: true,
preserveDirectiveContent: true,
});
i18n.messages[locale][key]; // $ExpectType LocaleMessage
i18n.dateTimeFormats[locale][key]; // $ExpectType DateTimeFormatOptions
Expand All @@ -72,6 +73,7 @@ i18n.fallbackLocale; // $ExpectType string
i18n.missing; // $ExpectType MissingHandler
i18n.formatter; // $ExpectType Formatter
i18n.silentTranslationWarn; // $ExpectType boolean
i18n.preserveDirectiveContent; // $ExpectType boolean
i18n.setLocaleMessage; // $ExpectType (locale: string, message: LocaleMessageObject) => void
i18n.getLocaleMessage; // $ExpectType (locale: string) => LocaleMessageObject
i18n.mergeLocaleMessage; // $ExpectType (locale: string, message: LocaleMessageObject) => void
Expand Down
35 changes: 33 additions & 2 deletions vuepress/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ Whether synchronize the root level locale to the component localization locale.

If `false`, regardless of the root level locale, localize for each component locale.

### silentTranslationWarn
#### silentTranslationWarn

> 6.1+
Expand All @@ -259,6 +259,16 @@ Whether suppress warnings outputted when localization fails.

If `true`, supress localization fail warnings.

#### preserveDirectiveContent

> 8.7+
* **Type:** `Boolean`

* **Default:** `false`

Whether `v-t` directive's element should preserve `textContent` after directive is unbinded.

### Properties

#### locale
Expand Down Expand Up @@ -331,14 +341,24 @@ The formatter that implemented with `Formatter` interface.

Whether suppress warnings outputted when localization fails.

#### preserveDirectiveContent

> 8.7+
* **Type:** `boolean`

* **Read/Write**

Whether `v-t` directive's element should preserve `textContent` after directive is unbinded.

### Methods

#### getChoiceIndex

* **Arguments:**
* `{number} choice`
* `{number} choicesLength`

* **Return:** `finalChoice {number}`

Get pluralization index for current pluralizing number and a given amount of choices. Can be overriden through prototype mutation:
Expand Down Expand Up @@ -519,13 +539,21 @@ This is the same as `$n` method of Vue instance method. More detail see [$n](#n)

* **Expects:** `string | Object`

* **Modifiers:**

* `.preserve`: (8.7.0+) preserves element `textContent` when directive is unbinded.

* **Details:**

Update the element `textContent` that localized with locale messages. You can use string syntax or object syntax. string syntax can be specified as a keypath of locale messages. If you can be used object syntax, you need to specify as the object key the following params:

* path: required, key of locale messages
* locale: optional, locale
* args: optional, for list or named formatting

::::tip NOTE
The element `textContent` will be cleared by default when `v-t` directive is unbinded. This might be undesirable situation when used inside [transitions](https://vuejs.org/v2/guide/transitions.html). To preserve `textContent` data after directive unbind use `.preserve` modifier or global [`preserveDirectiveContent` option](#preservedirectivecontent).
::::
* **Examples:**
```html
<!-- string syntax: literal -->
Expand All @@ -539,6 +567,9 @@ Update the element `textContent` that localized with locale messages. You can us

<!-- object syntax: binding via data or computed props -->
<p v-t="{ path: greeting, args: { name: fullName } }"></p>

<!-- with preserve modifier -->
<p v-t.preserve="'foo.bar'"></p>
```

* **See also:** [Custom directive localization](../guide/directive.md)
Expand Down
65 changes: 65 additions & 0 deletions vuepress/guide/directive.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,71 @@ Outputs:
</div>
```

## Use with transitions

:::tip Support Version
:new: 8.7+
:::

When `v-t` directive is applied to an element inside [`<transition>` component](https://vuejs.org/v2/api/#transition), you may notice that translated message will disappear during the transition. This behavior is related to the nature of the `<transition>` component implementation – all directives in disappearing element inside `<transition>` component will be destroyed **before transition starts**. This behavior may result in content flickering on short animations, but most noticable on long transitions.

To make sure directive content will stay un-touched during transition just add [`.preserve` modifier](../api/#v-t) to `v-t` directive defintion.

Javascript:

```js
new Vue({
i18n: new VueI18n({
locale: 'en',
messages: {
en: { preserve: 'with preserve' },
}
}),
data: { toggle: true }
}).$mount('#in-transitions')
```

Templates:

```html
<div id="in-transitions">
<transition name="fade">
<span v-if="toggle" v-t.preserve="'preserve'"></span>
</transition>
<button @click="toggle = !toggle">Toggle</button>
</div>
```

It is also possible to set global setting on `VueI18n` instance itself, which will have effect on all `v-t` directives without modifier.

Javascript:

```js
new Vue({
i18n: new VueI18n({
locale: 'en',
messages: {
en: { preserve: 'with preserve' },
},
preserveDirectiveContent: true
}),
data: { toggle: true }
}).$mount('#in-transitions')
```

Templates:

```html
<div id="in-transitions">
<transition name="fade">
<span v-if="toggle" v-t="'preserve'"></span>
</transition>
<button @click="toggle = !toggle">Toggle</button>
</div>
```

About the above examples, see the [example](https://github.com/kazupon/vue-i18n/tree/dev/examples/directive)

## `$t` vs `v-t`

### `$t`
Expand Down

0 comments on commit c29edba

Please sign in to comment.