Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/support html attributes #61

Merged
merged 10 commits into from
Aug 24, 2018
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,64 @@ const VComponent = Vue.component('hello-component', {
})
```

### Handling HTML attributes

Just like regular Vue components, you can pass HTML attributes from the parent Angular component to your Vue component.
Keep in mind that when you pass down literal strings, they must be surrounded by quotes, e.g. `data-value="'enabled'"`

```javascript
angular.module("app")
.directive("myCustomButton", createVueComponent => {
return createVueComponent(Vue.component("MyCustomButton", MyCustomButton))
})
```

```html
<my-custom-button
disabled="ctrl.isDisabled"
tabindex="3"
type="'submit'"
v-props-button-text="'Click me'" />
```

```vue
<template>
<!-- tabindex, type, and disabled will appear on the button element -->
<button>
{{ buttonText }}
</button>
</template>

<script>
export default {
name: "my-custom-button",
props: ["buttonText"],
}
</script>
```

Note that using `inheritAttrs: false` and binding `$attrs` to another element is also supported:

```vue
<template>
<div>
<!-- tabindex and type should appear on the button element instead of the parent div -->
<button v-bind="$attrs">
{{ buttonText }}
</button>
<span>Other elements</span>
</div>
</template>

<script>
export default {
inheritAttrs: false,
name: "my-custom-button",
props: ["buttonText"],
}
</script>
```

### The createVueComponent factory

The `createVueComponent` factory creates a reusable Angular directive which is bound to a specific Vue component.
Expand Down
45 changes: 45 additions & 0 deletions src/__tests__/create-vue-component.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Vue from 'vue'
import ngHtmlCompiler from './utils/ngHtmlCompiler'

import HelloComponent from './fixtures/HelloComponent'
import HelloWrappedComponent from './fixtures/HelloWrappedComponent'
import PersonsComponent from './fixtures/PersonsComponent'
import ButtonComponent from './fixtures/ButtonComponent'
import GreetingsComponent from './fixtures/GreetingsComponent'
Expand Down Expand Up @@ -54,6 +55,29 @@ describe('create-vue-component', () => {
)
expect(elem[0].innerHTML).toBe('<span>Hello John Doe</span>')
})

it('should render a vue component with original html attributes ', () => {
const elem = compileHTML(
`<hello
random="'hello'"
tabindex="1"
disabled
data-qa="'John'" />`
)
expect(elem[0].innerHTML).toBe('<span random="hello" tabindex="1" disabled="disabled" data-qa="John">Hello </span>')
})

it('should render a vue component with original html attributes on elements that bind $attrs ', () => {
$compileProvider.directive('helloWrapped', createVueComponent => createVueComponent(HelloWrappedComponent))
const elem = compileHTML(
`<hello-wrapped
random="'hello'"
tabindex="1"
disabled
data-qa="'John'" />`
)
expect(elem[0].innerHTML).toBe('<div><span random="hello" tabindex="1" disabled="disabled" data-qa="John">Hello </span></div>')
})
})

describe('update', () => {
Expand Down Expand Up @@ -144,6 +168,27 @@ describe('create-vue-component', () => {
done()
})
})

it('should re-render a vue component with attribute values change', (done) => {
const scope = $rootScope.$new()
scope.isDisabled = false
scope.tabindex = 0
scope.randomAttr = "enabled"
const elem = compileHTML(
`<hello random="randomAttr" tabindex="tabindex" disabled="isDisabled" />`,
scope
)
expect(elem[0].innerHTML).toBe('<span random="enabled" tabindex="0">Hello </span>')

scope.isDisabled = true
scope.tabindex = 1
scope.randomAttr = "disabled"
scope.$digest()
Vue.nextTick(() => {
expect(elem[0].innerHTML).toBe('<span random="disabled" tabindex="1" disabled="disabled">Hello </span>')
done()
})
})
})

describe('remove', () => {
Expand Down
13 changes: 13 additions & 0 deletions src/__tests__/fixtures/HelloWrappedComponent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Vue from 'vue'
import HelloComponent from './HelloComponent'

export default Vue.component('hello-wrapped-component', {
inheritAttrs: false,
render (h) {
return (
<div>
<HelloComponent {...{attrs: this.$attrs}}/>
</div>
)
}
})
51 changes: 51 additions & 0 deletions src/__tests__/vue-component.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Vue from 'vue'
import ngHtmlCompiler from './utils/ngHtmlCompiler'

import HelloComponent from './fixtures/HelloComponent'
import HelloWrappedComponent from './fixtures/HelloWrappedComponent'
import PersonsComponent from './fixtures/PersonsComponent'
import ButtonComponent from './fixtures/ButtonComponent'
import GreetingsComponent from './fixtures/GreetingsComponent'
Expand Down Expand Up @@ -55,6 +56,31 @@ describe('vue-component', () => {
)
expect(elem[0].innerHTML).toBe('<span>Hello John Doe</span>')
})

it('should render a vue component with original html attributes ', () => {
const elem = compileHTML(
`<vue-component
name="HelloComponent"
random="'hello'"
tabindex="1"
disabled
data-qa="'John'" />`
)
expect(elem[0].innerHTML).toBe('<span random="hello" tabindex="1" disabled="disabled" data-qa="John">Hello </span>')
})

it('should render a vue component with original html attributes on elements that bind $attrs ', () => {
$provide.value('HelloWrappedComponent', HelloWrappedComponent)
const elem = compileHTML(
`<vue-component
name="HelloWrappedComponent"
random="'hello'"
tabindex="1"
disabled
data-qa="'John'" />`
)
expect(elem[0].innerHTML).toBe('<div><span random="hello" tabindex="1" disabled="disabled" data-qa="John">Hello </span></div>')
})
})

describe('update', () => {
Expand Down Expand Up @@ -152,6 +178,31 @@ describe('vue-component', () => {
done()
})
})

it('should re-render a vue component with attribute values change', (done) => {
const scope = $rootScope.$new()
scope.isDisabled = false
scope.tabindex = 0
scope.randomAttr = "enabled"
const elem = compileHTML(
`<vue-component
name="HelloComponent"
random="randomAttr"
tabindex="tabindex"
disabled="isDisabled" />`,
scope
)
expect(elem[0].innerHTML).toBe('<span random="enabled" tabindex="0">Hello </span>')

scope.isDisabled = true
scope.tabindex = 1
scope.randomAttr = "disabled"
scope.$digest()
Vue.nextTick(() => {
expect(elem[0].innerHTML).toBe('<span random="disabled" tabindex="1" disabled="disabled">Hello </span>')
done()
})
})
})

describe('remove', () => {
Expand Down
14 changes: 10 additions & 4 deletions src/angular/ngVueLinker.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Vue from 'vue'
import getVueComponent from '../components/getVueComponent'
import getPropExprs from '../components/props/getExpressions'
import watchPropExprs from '../components/props/watchExpressions'
import evalPropValues from '../components/props/evaluateValues'
import evalValues from '../components/props/evaluateValues'
import evalPropEvents from '../components/props/evaluateEvents'
import evaluateDirectives from '../directives/evaluateDirectives'

Expand All @@ -14,7 +14,12 @@ export function ngVueLinker (componentName, jqElement, elAttributes, scope, $inj
const dataExprsMap = getPropExprs(elAttributes)
const Component = getVueComponent(componentName, $injector)
const directives = evaluateDirectives(elAttributes, scope) || []
const reactiveData = { _v: evalPropValues(dataExprsMap, scope) || {} }
const reactiveData = {
_v: {
props: evalValues(dataExprsMap.props || dataExprsMap.data, scope) || {},
attrs: evalValues(dataExprsMap.htmlAttributes, scope) || {}
}
}
const on = evalPropEvents(dataExprsMap, scope) || {}

const inQuirkMode = $ngVue ? $ngVue.inQuirkMode() : false
Expand All @@ -38,15 +43,16 @@ export function ngVueLinker (componentName, jqElement, elAttributes, scope, $inj
depth: elAttributes.watchDepth,
quirk: inQuirkMode
}
watchPropExprs(dataExprsMap, reactiveData, watchOptions, scope)
watchPropExprs(dataExprsMap, reactiveData, watchOptions, scope, 'props')
watchPropExprs(dataExprsMap, reactiveData, watchOptions, scope, 'attrs')

let vueInstance = new Vue({
name: 'NgVue',
el: jqElement[0],
data: reactiveData,
render (h) {
return (
<Component {...{ directives }} {...{ props: reactiveData._v, on }}>
<Component {...{ directives }} {...{ props: reactiveData._v.props, on, attrs: reactiveData._v.attrs }}>
{ <span ref="__slot__" /> }
</Component>
)
Expand Down
9 changes: 2 additions & 7 deletions src/components/props/evaluateValues.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import angular from 'angular'

/**
* @param dataExprsMap Object
* @param dataExprsMap.data Object|string|null
* @param dataExprsMap.props Object|string|null
* @param expr Object|string|null
* @param scope Object
* @returns {string|Object|null}
*/
export default function evaluateValues (dataExprsMap, scope) {
const key = dataExprsMap.props ? 'props' : 'data'
const expr = dataExprsMap[key]

export default function evaluateValues (expr, scope) {
if (!expr) {
return null
}
Expand Down
14 changes: 14 additions & 0 deletions src/components/props/extractHtmlAttributes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* @param attributes Object
* @returns {Array} Array of attributes that pass the filter
*/
export default function extractHtmlAttributes (attributes) {
// Filter out everything except for HTML attributes, e.g. tabindex, disabled
return Object.keys(attributes)
.filter(attr => {
const isSpecialAttr = /^(vProps|vData|vOn|vDirectives|watchDepth|ng)/i.test(attr)
const isJqliteProperty = attr[0] === '$'
// Vue does not consider class or style as an attr
return !isSpecialAttr && !isJqliteProperty && attr !== "class" && attr !== "style"
})
}
35 changes: 28 additions & 7 deletions src/components/props/getExpressions.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import angular from 'angular'
import extractExpressionName from './extractPropName'
import extractHtmlAttributes from './extractHtmlAttributes'

/**
* Extract the property/data expressions from the element attribute.
* Extract a subset of expressions from the element attributes, e.g. property/data, on, or htmlAttribute
*
* @param exprType 'props'|'data'|'on'
* @param exprType 'props'|'data'|'on'|'htmlAttributes'
* @param attributes Object
*
* @returns {Object|string|null}
Expand All @@ -24,17 +25,36 @@ export function extractExpressions (exprType, attributes) {
return objectExpr
}

const expressions = Object.keys(attributes)
.filter((attr) => objectPropExprRegExp.test(attr))
let expressions
if (exprType === 'htmlAttributes') {
expressions = extractHtmlAttributes(attributes)
} else {
expressions = Object.keys(attributes)
.filter((attr) => objectPropExprRegExp.test(attr))
}

if (expressions.length === 0) {
return null
}

const exprsMap = {/* name : expression */}
expressions.forEach((attrExprName) => {
const exprName = extractExpressionName(attrExprName, objectExprKey)
exprsMap[exprName] = attributes[attrExprName]
if (objectExprKey) {
const exprName = extractExpressionName(attrExprName, objectExprKey)
exprsMap[exprName] = attributes[attrExprName]
} else {
// Non-prefixed attributes, i.e. a regular HTML attribute
// Get original attribute name from $attr not stripped by Angular, e.g. data-qa and not qa
const attrName = attributes.$attr[attrExprName]
let attrValue
// Handle attributes with no value, e.g. <button disabled></button>
if (attributes[attrExprName] === '') {
attrValue = `'${attrExprName}'`
} else {
attrValue = attributes[attrExprName]
}
exprsMap[attrName] = attrValue
}
})

return exprsMap
Expand All @@ -48,6 +68,7 @@ export default function getExpressions (attributes) {
return {
data: extractExpressions('data', attributes),
props: extractExpressions('props', attributes),
events: extractExpressions('on', attributes)
events: extractExpressions('on', attributes),
htmlAttributes: extractExpressions('htmlAttributes', attributes)
}
}