Skip to content
This repository was archived by the owner on Dec 31, 2024. It is now read-only.

Commit 71ca843

Browse files
aavondetkazupon
authored andcommittedAug 12, 2019
⭐ new: support slots syntax for component interpolation (#685) by @aavondet
* Vue slots syntax for interpolation w/ tests * 👕 refactor: slot component interpolation * 👕 refactor: component interpolation tests * 🍭 examples(interpolation): add slots usage example * 🆙 update(interpolation): tweak warning messages * 📝 docs(vuepress): update component interplation docs
1 parent 999782a commit 71ca843

File tree

7 files changed

+299
-48
lines changed

7 files changed

+299
-48
lines changed
 

‎examples/interpolation/index.html renamed to ‎examples/interpolation/places/index.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
<head>
44
<meta charset="utf-8">
55
<title>component interpolation</title>
6-
<script src="../../node_modules/vue/dist/vue.min.js"></script>
7-
<script src="../../dist/vue-i18n.min.js"></script>
6+
<script src="../../../node_modules/vue/dist/vue.min.js"></script>
7+
<script src="../../../dist/vue-i18n.min.js"></script>
88
</head>
99
<body>
1010
<div id="app">
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<title>component interpolation</title>
6+
<script src="../../../node_modules/vue/dist/vue.min.js"></script>
7+
<script src="../../../dist/vue-i18n.min.js"></script>
8+
</head>
9+
<body>
10+
<div id="app">
11+
<form>
12+
<div class="usernam">
13+
<label for="username">username:</label>
14+
<input id="username" type="text" value="username">
15+
</div>
16+
<div class="email">
17+
<label for="email">email:</label>
18+
<input id="email" type="text" value="foo@bar.com">
19+
</div>
20+
<div class="agreement">
21+
<input id="tos" type="checkbox">
22+
<i18n path="term" tag="label" for="tos">
23+
<a slot="tos" :href="url" target="_blank">{{ $t('tos') }}</a>
24+
</i18n>
25+
</div>
26+
<input type="submit" value="submit">
27+
</form>
28+
</div>
29+
<script>
30+
var messages = {
31+
en: {
32+
tos: 'Term of Service',
33+
term: 'I accept xxx {tos}.'
34+
},
35+
ja: {
36+
tos: '利用規約',
37+
term: '私は xxx の{tos}に同意します。'
38+
}
39+
}
40+
41+
Vue.use(VueI18n)
42+
43+
var i18n = new VueI18n({
44+
locale: 'en',
45+
messages: messages
46+
})
47+
new Vue({
48+
i18n: i18n,
49+
data: {
50+
url: '/term'
51+
}
52+
}).$mount('#app')
53+
</script>
54+
</body>
55+
</html>

‎src/components/interpolation.js

+59-43
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ export default {
77
functional: true,
88
props: {
99
tag: {
10-
type: String,
11-
default: 'span'
10+
type: String
1211
},
1312
path: {
1413
type: String,
@@ -21,58 +20,75 @@ export default {
2120
type: [Array, Object]
2221
}
2322
},
24-
render (h: Function, { props, data, children, parent }: Object) {
25-
const i18n = parent.$i18n
26-
27-
children = (children || []).filter(child => {
28-
return child.tag || (child.text = child.text.trim())
29-
})
30-
31-
if (!i18n) {
23+
render (h: Function, { data, parent, props, slots }: Object) {
24+
const { $i18n } = parent
25+
if (!$i18n) {
3226
if (process.env.NODE_ENV !== 'production') {
3327
warn('Cannot find VueI18n instance!')
3428
}
35-
return children
29+
return
3630
}
3731

38-
const path: Path = props.path
39-
const locale: ?Locale = props.locale
32+
const { path, locale, places } = props
33+
const params = slots()
34+
const children = $i18n.i(
35+
path,
36+
locale,
37+
onlyHasDefaultPlace(params) || places
38+
? useLegacyPlaces(params.default, places)
39+
: params
40+
)
4041

41-
const params: Object = {}
42-
const places: Array<any> | Object = props.places || {}
42+
const tag = props.tag || 'span'
43+
return tag ? h(tag, data, children) : children
44+
}
45+
}
4346

44-
const hasPlaces: boolean = Array.isArray(places)
45-
? places.length > 0
46-
: Object.keys(places).length > 0
47+
function onlyHasDefaultPlace (params) {
48+
let prop
49+
for (prop in params) {
50+
if (prop !== 'default') { return false }
51+
}
52+
return Boolean(prop)
53+
}
4754

48-
const everyPlace: boolean = children.every(child => {
49-
if (child.data && child.data.attrs) {
50-
const place = child.data.attrs.place
51-
return (typeof place !== 'undefined') && place !== ''
52-
}
53-
})
55+
function useLegacyPlaces (children, places) {
56+
const params = places ? createParamsFromPlaces(places) : {}
57+
if (!children) { return params }
5458

55-
if (process.env.NODE_ENV !== 'production' && hasPlaces && children.length > 0 && !everyPlace) {
56-
warn('If places prop is set, all child elements must have place prop set.')
57-
}
59+
const everyPlace = children.every(vnodeHasPlaceAttribute)
60+
if (process.env.NODE_ENV !== 'production' && everyPlace) {
61+
warn('`place` attribute is deprecated in next major version. Please switch to Vue slots.')
62+
}
5863

59-
if (Array.isArray(places)) {
60-
places.forEach((el, i) => {
61-
params[i] = el
62-
})
63-
} else {
64-
Object.keys(places).forEach(key => {
65-
params[key] = places[key]
66-
})
67-
}
64+
return children.reduce(
65+
everyPlace ? assignChildPlace : assignChildIndex,
66+
params
67+
)
68+
}
69+
70+
function createParamsFromPlaces (places) {
71+
if (process.env.NODE_ENV !== 'production') {
72+
warn('`places` prop is deprecated in next majaor version. Please switch to Vue slots.')
73+
}
6874

69-
children.forEach((child, i: number) => {
70-
const key: string = everyPlace
71-
? `${child.data.attrs.place}`
72-
: `${i}`
73-
params[key] = child
74-
})
75+
return Array.isArray(places)
76+
? places.reduce(assignChildIndex, {})
77+
: Object.assign({}, places)
78+
}
7579

76-
return h(props.tag, data, i18n.i(path, locale, params))
80+
function assignChildPlace (params, child) {
81+
if (child.data && child.data.attrs && child.data.attrs.place) {
82+
params[child.data.attrs.place] = child
7783
}
84+
return params
85+
}
86+
87+
function assignChildIndex (params, child, index) {
88+
params[index] = child
89+
return params
90+
}
91+
92+
function vnodeHasPlaceAttribute (vnode) {
93+
return Boolean(vnode.data && vnode.data.attrs && vnode.data.attrs.place)
7894
}

‎test/e2e/test/interpolation.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module.exports = {
22
interpolation: function (browser) {
33
browser
4-
.url('http://localhost:8080/examples/interpolation/')
4+
.url('http://localhost:8080/examples/interpolation/slots')
55
.waitForElementVisible('#app', 1000)
66
.assert.containsText('label[for="tos"]', 'I accept xxx Term of Service.')
77
.end()

‎test/unit/interpolation.test.js

+97
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const messages = {
66
primitive: 'one: {0}, two: {1}',
77
component: 'element: {0}, component: {1}',
88
mixed: 'text: {x}, component: {y}',
9+
named: 'header: {header}, footer: {footer}',
910
link: '@:primitive',
1011
term: 'I accept xxx {0}.',
1112
tos: 'Term of service',
@@ -290,6 +291,102 @@ describe('component interpolation', () => {
290291
})
291292
})
292293

294+
describe('slot', () => {
295+
describe('with default slot', () => {
296+
it('should be interpolated', done => {
297+
const el = document.createElement('div')
298+
const vm = new Vue({
299+
i18n,
300+
render (h) {
301+
return h('i18n', { props: { path: 'text' }, slot: '' }, [this._v('1')])
302+
}
303+
}).$mount(el)
304+
nextTick(() => {
305+
assert.strictEqual(vm.$el.textContent, 'one: 1')
306+
}).then(done)
307+
})
308+
})
309+
310+
describe('with named slots ', () => {
311+
it('should be interpolated', done => {
312+
const el = document.createElement('div')
313+
const vm = new Vue({
314+
i18n,
315+
render (h) {
316+
return h('i18n', { props: { path: 'named' } }, [
317+
h('template', { slot: 'header' }, [h('p', 'header')]),
318+
h('template', { slot: 'footer' }, [h('p', 'footer')])
319+
])
320+
}
321+
}).$mount(el)
322+
nextTick(() => {
323+
assert.strictEqual(
324+
vm.$el.innerHTML,
325+
'header: <p>header</p>, footer: <p>footer</p>'
326+
)
327+
}).then(done)
328+
})
329+
})
330+
331+
describe('primitive nodes', () => {
332+
it('should be interpolated', done => {
333+
const el = document.createElement('div')
334+
const vm = new Vue({
335+
i18n,
336+
render (h) {
337+
return h('i18n', { props: { path: 'primitive' } }, [
338+
h('template', { slot: '0' }, ['1']),
339+
h('template', { slot: '1' }, ['2'])
340+
])
341+
}
342+
}).$mount(el)
343+
nextTick(() => {
344+
assert.strictEqual(vm.$el.innerHTML, 'one: 1, two: 2')
345+
}).then(done)
346+
})
347+
})
348+
349+
describe('linked', () => {
350+
it('should be interpolated', done => {
351+
const el = document.createElement('div')
352+
const vm = new Vue({
353+
i18n,
354+
render (h) {
355+
return h('i18n', { props: { path: 'link' } }, [
356+
h('template', { slot: '0' }, ['1']),
357+
h('template', { slot: '1' }, ['2'])
358+
])
359+
}
360+
}).$mount(el)
361+
nextTick(() => {
362+
assert.strictEqual(vm.$el.innerHTML, 'one: 1, two: 2')
363+
}).then(done)
364+
})
365+
})
366+
367+
describe('included translation locale message', () => {
368+
it('should be interpolated', done => {
369+
const el = document.createElement('div')
370+
const vm = new Vue({
371+
i18n,
372+
render (h) {
373+
return h('i18n', { props: { path: 'term' } }, [
374+
h('template', { slot: '0' }, [
375+
h('a', { domProps: { href: '/term', textContent: this.$t('tos') } })
376+
])
377+
])
378+
}
379+
}).$mount(el)
380+
nextTick(() => {
381+
assert.strictEqual(
382+
vm.$el.innerHTML,
383+
'I accept xxx <a href=\"/term\">Term of service</a>.'
384+
)
385+
}).then(done)
386+
})
387+
})
388+
})
389+
293390
describe('warnning in render', () => {
294391
it('should be warned', () => {
295392
const spy = sinon.spy(console, 'warn')

‎vuepress/api/README.md

+4
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,10 @@ The element `textContent` will be cleared by default when `v-t` directive is unb
677677
* `tag {string}`: optional, default `span`
678678
* `places {Array | Object}`: optional (7.2+)
679679

680+
:::danger Important!!
681+
In next major version, `places` prop is deprecated. Please switch to slots syntax.
682+
:::
683+
680684
#### Usage:
681685

682686
```html

‎vuepress/guide/interpolation.md

+81-2
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ the following output:
8585
</div>
8686
```
8787

88-
About the above example, see the [example](https://github.com/kazupon/vue-i18n/tree/dev/examples/interpolation)
88+
About the above example, see the [example](https://github.com/kazupon/vue-i18n/tree/dev/examples/interpolation/places)
8989

9090
The children of `i18n` functional component is interpolated with locale message of `path` prop. In the above example,
9191
:::v-pre
@@ -95,11 +95,90 @@ is interpolated with `term` locale message.
9595

9696
In above example, the component interpolation follows the **list formatting**. The children of `i18n` functional component are interpolated by their orders of appearance.
9797

98-
## Advanced Usage
98+
## Slots syntax usage
99+
100+
:::tip Support Version
101+
:new: 8.14+
102+
:::
103+
104+
You use slots syntax with Named formatting then, It's more convenient. For example:
105+
106+
```html
107+
<div id="app">
108+
<!-- ... -->
109+
<i18n path="info" tag="p">
110+
<span slot="limit">{{ changeLimit }}</span>
111+
<a slot="action" :href="changeUrl">{{ $t('change') }}</a>
112+
</i18n>
113+
<!-- ... -->
114+
</div>
115+
```
116+
117+
```js
118+
const messages = {
119+
en: {
120+
info: 'You can {action} until {limit} minutes from departure.',
121+
change: 'change your flight',
122+
refund: 'refund the ticket'
123+
}
124+
}
125+
126+
const i18n = new VueI18n({
127+
locale: 'en',
128+
messages
129+
})
130+
131+
new Vue({
132+
i18n,
133+
data: {
134+
changeUrl: '/change',
135+
refundUrl: '/refund',
136+
changeLimit: 15,
137+
refundLimit: 30
138+
}
139+
}).$mount('#app')
140+
```
141+
142+
Outputs:
143+
144+
```html
145+
<div id="app">
146+
<!-- ... -->
147+
<p>
148+
You can <a href="/change">change your flight</a> until <span>15</span> minutes from departure.
149+
</p>
150+
<!-- ... -->
151+
</div>
152+
```
153+
154+
In Vue 2.6 and later,you can can use the following slots syntax in templates:
155+
156+
```html
157+
<div id="app">
158+
<!-- ... -->
159+
<i18n path="info" tag="p">
160+
<span v-slot:limit>{{ changeLimit }}</span>
161+
<a v-slot:action :href="changeUrl">{{ $t('change') }}</a>
162+
</i18n>
163+
<!-- ... -->
164+
</div>
165+
```
166+
167+
:::warning Limitation
168+
:warning: In `i18n` component, slots porps is not supported.
169+
:::
170+
171+
172+
## Places syntax usage
173+
174+
:::danger Important!!
175+
In next major version, `place` attribute, `places` prop is deprecated. Please switch to slots syntax.
176+
:::
99177

100178
:::tip Support Version
101179
:new: 7.2+
102180
:::
181+
103182
:::warning Notice
104183
:warning: In `i18n` component, text content consists of only white spaces will be omitted.
105184
:::

0 commit comments

Comments
 (0)
This repository has been archived.