Skip to content

Commit

Permalink
⭐ new: support slots syntax for component interpolation (#685) by @aa…
Browse files Browse the repository at this point in the history
…vondet

* 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
  • Loading branch information
aavondet authored and kazupon committed Aug 12, 2019
1 parent 999782a commit 71ca843
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
<head>
<meta charset="utf-8">
<title>component interpolation</title>
<script src="../../node_modules/vue/dist/vue.min.js"></script>
<script src="../../dist/vue-i18n.min.js"></script>
<script src="../../../node_modules/vue/dist/vue.min.js"></script>
<script src="../../../dist/vue-i18n.min.js"></script>
</head>
<body>
<div id="app">
Expand Down
55 changes: 55 additions & 0 deletions examples/interpolation/slots/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>component interpolation</title>
<script src="../../../node_modules/vue/dist/vue.min.js"></script>
<script src="../../../dist/vue-i18n.min.js"></script>
</head>
<body>
<div id="app">
<form>
<div class="usernam">
<label for="username">username:</label>
<input id="username" type="text" value="username">
</div>
<div class="email">
<label for="email">email:</label>
<input id="email" type="text" value="[email protected]">
</div>
<div class="agreement">
<input id="tos" type="checkbox">
<i18n path="term" tag="label" for="tos">
<a slot="tos" :href="url" target="_blank">{{ $t('tos') }}</a>
</i18n>
</div>
<input type="submit" value="submit">
</form>
</div>
<script>
var messages = {
en: {
tos: 'Term of Service',
term: 'I accept xxx {tos}.'
},
ja: {
tos: '利用規約',
term: '私は xxx の{tos}に同意します。'
}
}

Vue.use(VueI18n)

var i18n = new VueI18n({
locale: 'en',
messages: messages
})
new Vue({
i18n: i18n,
data: {
url: '/term'
}
}).$mount('#app')
</script>
</body>
</html>
102 changes: 59 additions & 43 deletions src/components/interpolation.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ export default {
functional: true,
props: {
tag: {
type: String,
default: 'span'
type: String
},
path: {
type: String,
Expand All @@ -21,58 +20,75 @@ export default {
type: [Array, Object]
}
},
render (h: Function, { props, data, children, parent }: Object) {
const i18n = parent.$i18n

children = (children || []).filter(child => {
return child.tag || (child.text = child.text.trim())
})

if (!i18n) {
render (h: Function, { data, parent, props, slots }: Object) {
const { $i18n } = parent
if (!$i18n) {
if (process.env.NODE_ENV !== 'production') {
warn('Cannot find VueI18n instance!')
}
return children
return
}

const path: Path = props.path
const locale: ?Locale = props.locale
const { path, locale, places } = props
const params = slots()
const children = $i18n.i(
path,
locale,
onlyHasDefaultPlace(params) || places
? useLegacyPlaces(params.default, places)
: params
)

const params: Object = {}
const places: Array<any> | Object = props.places || {}
const tag = props.tag || 'span'
return tag ? h(tag, data, children) : children
}
}

const hasPlaces: boolean = Array.isArray(places)
? places.length > 0
: Object.keys(places).length > 0
function onlyHasDefaultPlace (params) {
let prop
for (prop in params) {
if (prop !== 'default') { return false }
}
return Boolean(prop)
}

const everyPlace: boolean = children.every(child => {
if (child.data && child.data.attrs) {
const place = child.data.attrs.place
return (typeof place !== 'undefined') && place !== ''
}
})
function useLegacyPlaces (children, places) {
const params = places ? createParamsFromPlaces(places) : {}
if (!children) { return params }

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

if (Array.isArray(places)) {
places.forEach((el, i) => {
params[i] = el
})
} else {
Object.keys(places).forEach(key => {
params[key] = places[key]
})
}
return children.reduce(
everyPlace ? assignChildPlace : assignChildIndex,
params
)
}

function createParamsFromPlaces (places) {
if (process.env.NODE_ENV !== 'production') {
warn('`places` prop is deprecated in next majaor version. Please switch to Vue slots.')
}

children.forEach((child, i: number) => {
const key: string = everyPlace
? `${child.data.attrs.place}`
: `${i}`
params[key] = child
})
return Array.isArray(places)
? places.reduce(assignChildIndex, {})
: Object.assign({}, places)
}

return h(props.tag, data, i18n.i(path, locale, params))
function assignChildPlace (params, child) {
if (child.data && child.data.attrs && child.data.attrs.place) {
params[child.data.attrs.place] = child
}
return params
}

function assignChildIndex (params, child, index) {
params[index] = child
return params
}

function vnodeHasPlaceAttribute (vnode) {
return Boolean(vnode.data && vnode.data.attrs && vnode.data.attrs.place)
}
2 changes: 1 addition & 1 deletion test/e2e/test/interpolation.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module.exports = {
interpolation: function (browser) {
browser
.url('http://localhost:8080/examples/interpolation/')
.url('http://localhost:8080/examples/interpolation/slots')
.waitForElementVisible('#app', 1000)
.assert.containsText('label[for="tos"]', 'I accept xxx Term of Service.')
.end()
Expand Down
97 changes: 97 additions & 0 deletions test/unit/interpolation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const messages = {
primitive: 'one: {0}, two: {1}',
component: 'element: {0}, component: {1}',
mixed: 'text: {x}, component: {y}',
named: 'header: {header}, footer: {footer}',
link: '@:primitive',
term: 'I accept xxx {0}.',
tos: 'Term of service',
Expand Down Expand Up @@ -290,6 +291,102 @@ describe('component interpolation', () => {
})
})

describe('slot', () => {
describe('with default slot', () => {
it('should be interpolated', done => {
const el = document.createElement('div')
const vm = new Vue({
i18n,
render (h) {
return h('i18n', { props: { path: 'text' }, slot: '' }, [this._v('1')])
}
}).$mount(el)
nextTick(() => {
assert.strictEqual(vm.$el.textContent, 'one: 1')
}).then(done)
})
})

describe('with named slots ', () => {
it('should be interpolated', done => {
const el = document.createElement('div')
const vm = new Vue({
i18n,
render (h) {
return h('i18n', { props: { path: 'named' } }, [
h('template', { slot: 'header' }, [h('p', 'header')]),
h('template', { slot: 'footer' }, [h('p', 'footer')])
])
}
}).$mount(el)
nextTick(() => {
assert.strictEqual(
vm.$el.innerHTML,
'header: <p>header</p>, footer: <p>footer</p>'
)
}).then(done)
})
})

describe('primitive nodes', () => {
it('should be interpolated', done => {
const el = document.createElement('div')
const vm = new Vue({
i18n,
render (h) {
return h('i18n', { props: { path: 'primitive' } }, [
h('template', { slot: '0' }, ['1']),
h('template', { slot: '1' }, ['2'])
])
}
}).$mount(el)
nextTick(() => {
assert.strictEqual(vm.$el.innerHTML, 'one: 1, two: 2')
}).then(done)
})
})

describe('linked', () => {
it('should be interpolated', done => {
const el = document.createElement('div')
const vm = new Vue({
i18n,
render (h) {
return h('i18n', { props: { path: 'link' } }, [
h('template', { slot: '0' }, ['1']),
h('template', { slot: '1' }, ['2'])
])
}
}).$mount(el)
nextTick(() => {
assert.strictEqual(vm.$el.innerHTML, 'one: 1, two: 2')
}).then(done)
})
})

describe('included translation locale message', () => {
it('should be interpolated', done => {
const el = document.createElement('div')
const vm = new Vue({
i18n,
render (h) {
return h('i18n', { props: { path: 'term' } }, [
h('template', { slot: '0' }, [
h('a', { domProps: { href: '/term', textContent: this.$t('tos') } })
])
])
}
}).$mount(el)
nextTick(() => {
assert.strictEqual(
vm.$el.innerHTML,
'I accept xxx <a href=\"/term\">Term of service</a>.'
)
}).then(done)
})
})
})

describe('warnning in render', () => {
it('should be warned', () => {
const spy = sinon.spy(console, 'warn')
Expand Down
4 changes: 4 additions & 0 deletions vuepress/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,10 @@ The element `textContent` will be cleared by default when `v-t` directive is unb
* `tag {string}`: optional, default `span`
* `places {Array | Object}`: optional (7.2+)

:::danger Important!!
In next major version, `places` prop is deprecated. Please switch to slots syntax.
:::

#### Usage:

```html
Expand Down
Loading

0 comments on commit 71ca843

Please sign in to comment.