Skip to content

Commit

Permalink
⭐ new(directives): support v-t custom directive (welcome back!)
Browse files Browse the repository at this point in the history
  • Loading branch information
kazupon committed Sep 10, 2017
1 parent 38ca0da commit af9a2e7
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 1 deletion.
68 changes: 68 additions & 0 deletions src/directive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* @flow */

import { warn, isPlainObject, looseEqual } from './util'

export function bind (el: any, binding: Object, vnode: any): void {
t(el, binding, vnode)
}

export function update (el: any, binding: Object, vnode: any, oldVNode: any): void {
if (looseEqual(binding.value, binding.oldValue)) { return }

t(el, binding, vnode)
}

function t (el: any, binding: Object, vnode: any): void {
const value: any = binding.value

const { path, locale, args } = parseValue(value)
if (!path && !locale && !args) {
warn('not support value type')
return
}

const vm: any = vnode.context
if (!vm) {
warn('not exist Vue instance in VNode context')
return
}

if (!vm.$i18n) {
warn('not exist VueI18n instance in Vue instance')
return
}

if (!path) {
warn('required `path` in v-t directive')
return
}

el._vt = el.textContent = vm.$i18n.t(path, ...makeParams(locale, args))
}

function parseValue (value: any): Object {
let path: ?string
let locale: ?Locale
let args: any

if (typeof value === 'string') {
path = value
} else if (isPlainObject(value)) {
path = value.path
locale = value.locale
args = value.args
}

return { path, locale, args }
}

function makeParams (locale: Locale, args: any): Array<any> {
const params: Array<any> = []

locale && params.push(locale)
if (args && (Array.isArray(args) || isPlainObject(args))) {
params.push(args)
}

return params
}
2 changes: 2 additions & 0 deletions src/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { warn } from './util'
import extend from './extend'
import mixin from './mixin'
import component from './component'
import { bind, update } from './directive'

export let Vue

Expand All @@ -28,6 +29,7 @@ export function install (_Vue) {

extend(Vue)
Vue.mixin(mixin)
Vue.directive('t', { bind, update })
Vue.component(component.name, component)

// use object-based merge strategy
Expand Down
35 changes: 34 additions & 1 deletion src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function warn (msg: string, err: ?Error): void {
}
}

export function isObject (obj: mixed): boolean {
export function isObject (obj: mixed): boolean %checks {
return obj !== null && typeof obj === 'object'
}

Expand Down Expand Up @@ -114,6 +114,39 @@ export function merge (target: Object): Object {
return output
}

export function looseEqual (a: any, b: any): boolean {
if (a === b) { return true }
const isObjectA: boolean = isObject(a)
const isObjectB: boolean = isObject(b)
if (isObjectA && isObjectB) {
try {
const isArrayA: boolean = Array.isArray(a)
const isArrayB: boolean = Array.isArray(b)
if (isArrayA && isArrayB) {
return a.length === b.length && a.every((e: any, i: number): boolean => {
return looseEqual(e, b[i])
})
} else if (!isArrayA && !isArrayB) {
const keysA: Array<string> = Object.keys(a)
const keysB: Array<string> = Object.keys(b)
return keysA.length === keysB.length && keysA.every((key: string): boolean => {
return looseEqual(a[key], b[key])
})
} else {
/* istanbul ignore next */
return false
}
} catch (e) {
/* istanbul ignore next */
return false
}
} else if (!isObjectA && !isObjectB) {
return String(a) === String(b)
} else {
return false
}
}

export const canUseDateTimeFormat: boolean =
typeof Intl !== 'undefined' && typeof Intl.DateTimeFormat !== 'undefined'

Expand Down
120 changes: 120 additions & 0 deletions test/unit/directive.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import messages from './fixture/index'

describe('custom directive', () => {
let i18n
beforeEach(() => {
i18n = new VueI18n({
locale: 'en',
messages
})
})

function createVM (options) {
const el = document.createElement('div')
return new Vue(options).$mount(el)
}

describe('v-t', () => {
describe('string literal', () => {
it('should be translated', done => {
const vm = createVM({
i18n,
render (h) {
// <p ref="text" v-t="'message.hello'"></p>
return h('p', { ref: 'text', directives: [{
name: 't', rawName: 'v-t', value: ('message.hello'), expression: "'message.hello'"
}] })
}
})
nextTick(() => {
assert.equal(vm.$refs.text.textContent, messages.en.message.hello)
assert.equal(vm.$refs.text._vt, messages.en.message.hello)
}).then(done)
})
})

describe('object', () => {
it('should be translated', done => {
const vm = createVM({
i18n,
data: {
msgPath: 'message.format.named'
},
render (h) {
// <p ref="text" v-t="{ path: msgPath, locale: 'ja', args: { name: 'kazupon' } }"></p>
return h('p', { ref: 'text', directives: [{
name: 't', rawName: 'v-t',
value: ({ path: this.msgPath, locale: 'ja', args: { name: 'kazupon' } }),
expression: "{ path: msgPath, locale: 'ja', args: { name: 'kazupon' } }"
}] })
}
})
nextTick(() => {
const expected = 'こんにちは kazupon, ごきげんいかが?'
assert.equal(vm.$refs.text.textContent, expected)
assert.equal(vm.$refs.text._vt, expected)
}).then(done)
})
})

describe('not support warning', () => {
it('should be warned', done => {
const spy = sinon.spy(console, 'warn')
createVM({
i18n,
render (h) {
// <p ref="text" v-t="[1]"></p>
return h('p', { ref: 'text', directives: [{
name: 't', rawName: 'v-t', value: ([1]), expression: '[1]'
}] })
}
})
nextTick(() => {
assert(spy.notCalled === false)
assert(spy.callCount === 1)
spy.restore()
}).then(done)
})
})

describe('path required warning', () => {
it('should be warned', done => {
const spy = sinon.spy(console, 'warn')
createVM({
i18n,
render (h) {
// <p ref="text" v-t="{ locale: 'ja', args: { name: 'kazupon' } }"></p>
return h('p', { ref: 'text', directives: [{
name: 't', rawName: 'v-t',
value: ({ locale: 'ja', args: { name: 'kazupon' } }),
expression: "{ locale: 'ja', args: { name: 'kazupon' } }"
}] })
}
})
nextTick(() => {
assert(spy.notCalled === false)
assert(spy.callCount === 1)
spy.restore()
}).then(done)
})
})

describe('VueI18n instance warning', () => {
it('should be warned', done => {
const spy = sinon.spy(console, 'warn')
createVM({
render (h) {
return h('p', { ref: 'text', directives: [{
name: 't', rawName: 'v-t', value: ('message.hello'), expression: "'message.hello'"
}] })
}
})
nextTick(() => {
assert(spy.notCalled === false)
assert(spy.callCount === 1)
spy.restore()
}).then(done)
})
})
})
})

0 comments on commit af9a2e7

Please sign in to comment.