Browse Source

feat(weex): support parse object literal in binding attrs and styles (#7291)

Hanks 8 years ago
parent
commit
ff8fcd2e2b

+ 2 - 0
package.json

@@ -59,6 +59,7 @@
   "devDependencies": {
     "@types/node": "^8.0.33",
     "@types/webpack": "^3.0.13",
+    "acorn": "^5.2.1",
     "babel-core": "^6.25.0",
     "babel-eslint": "^8.0.3",
     "babel-helper-vue-jsx-merge-props": "^2.0.2",
@@ -79,6 +80,7 @@
     "cz-conventional-changelog": "^2.0.0",
     "de-indent": "^1.0.2",
     "es6-promise": "^4.1.0",
+    "escodegen": "^1.8.1",
     "eslint": "^4.13.1",
     "eslint-loader": "^1.7.1",
     "eslint-plugin-flowtype": "^2.34.0",

+ 2 - 0
packages/weex-template-compiler/package.json

@@ -18,6 +18,8 @@
   },
   "homepage": "https://github.com/vuejs/vue/tree/dev/packages/weex-template-compiler#readme",
   "dependencies": {
+    "acorn": "^5.2.1",
+    "escodegen": "^1.8.1",
     "he": "^1.1.0"
   }
 }

+ 2 - 3
src/platforms/weex/compiler/modules/recycle-list/v-bind.js

@@ -1,6 +1,7 @@
 /* @flow */
 
 import { camelize } from 'shared/util'
+import { generateBinding } from 'weex/util/parser'
 import { bindRE } from 'compiler/parser/index'
 import { getAndRemoveAttr, addRawAttr } from 'compiler/helpers'
 
@@ -12,9 +13,7 @@ export function preTransformVBind (el: ASTElement, options: WeexCompilerOptions)
   for (const attr in el.attrsMap) {
     if (bindRE.test(attr)) {
       const name: string = parseAttrName(attr)
-      const value = {
-        '@binding': getAndRemoveAttr(el, attr)
-      }
+      const value = generateBinding(getAndRemoveAttr(el, attr))
       delete el.attrsMap[attr]
       addRawAttr(el, name, value)
     }

+ 7 - 3
src/platforms/weex/compiler/modules/style.js

@@ -1,6 +1,6 @@
 /* @flow */
 
-import { cached, camelize } from 'shared/util'
+import { cached, camelize, isPlainObject } from 'shared/util'
 import { parseText } from 'compiler/parser/text-parser'
 import {
   getAndRemoveAttr,
@@ -10,7 +10,7 @@ import {
 
 type StaticStyleResult = {
   dynamic: boolean,
-  styleResult: string
+  styleResult: string | Object | void
 };
 
 const normalize = cached(camelize)
@@ -27,12 +27,14 @@ function transformNode (el: ASTElement, options: CompilerOptions) {
     )
   }
   if (!dynamic && styleResult) {
+    // $flow-disable-line
     el.staticStyle = styleResult
   }
   const styleBinding = getBindingAttr(el, 'style', false /* getStatic */)
   if (styleBinding) {
     el.styleBinding = styleBinding
   } else if (dynamic) {
+    // $flow-disable-line
     el.styleBinding = styleResult
   }
 }
@@ -53,7 +55,7 @@ function parseStaticStyle (staticStyle: ?string, options: CompilerOptions): Stat
   // "width: 200px; height: {{y}}" -> {width: 200, height: y}
   let dynamic = false
   let styleResult = ''
-  if (staticStyle) {
+  if (typeof staticStyle === 'string') {
     const styleList = staticStyle.trim().split(';').map(style => {
       const result = style.trim().split(':')
       if (result.length !== 2) {
@@ -71,6 +73,8 @@ function parseStaticStyle (staticStyle: ?string, options: CompilerOptions): Stat
     if (styleList.length) {
       styleResult = '{' + styleList.join(',') + '}'
     }
+  } else if (isPlainObject(staticStyle)) {
+    styleResult = JSON.stringify(staticStyle) || ''
   }
   return { dynamic, styleResult }
 }

+ 60 - 0
src/platforms/weex/util/parser.js

@@ -0,0 +1,60 @@
+/* @flow */
+
+// import { warn } from 'core/util/index'
+
+// this will be preserved during build
+// $flow-disable-line
+const acorn = require('acorn') // $flow-disable-line
+const walk = require('acorn/dist/walk') // $flow-disable-line
+const escodegen = require('escodegen')
+
+export function nodeToBinding (node: Object): any {
+  switch (node.type) {
+    case 'Literal': return node.value
+    case 'Identifier':
+    case 'UnaryExpression':
+    case 'BinaryExpression':
+    case 'LogicalExpression':
+    case 'ConditionalExpression':
+    case 'MemberExpression': return { '@binding': escodegen.generate(node) }
+    case 'ArrayExpression': return node.elements.map(_ => nodeToBinding(_))
+    case 'ObjectExpression': {
+      const object = {}
+      node.properties.forEach(prop => {
+        if (!prop.key || prop.key.type !== 'Identifier') {
+          return
+        }
+        const key = escodegen.generate(prop.key)
+        const value = nodeToBinding(prop.value)
+        if (key && value) {
+          object[key] = value
+        }
+      })
+      return object
+    }
+    default: {
+      // warn(`Not support ${node.type}: "${escodegen.generate(node)}"`)
+      return ''
+    }
+  }
+}
+
+export function generateBinding (exp: ?string): any {
+  if (exp && typeof exp === 'string') {
+    let ast = null
+    try {
+      ast = acorn.parse(`(${exp})`)
+    } catch (e) {
+      // warn(`Failed to parse the expression: "${exp}"`)
+      return ''
+    }
+
+    let output = ''
+    walk.simple(ast, {
+      Expression (node) {
+        output = nodeToBinding(node)
+      }
+    })
+    return output
+  }
+}

+ 1 - 1
test/weex/cases/cases.spec.js

@@ -64,7 +64,7 @@ describe('Usage', () => {
     it('text node', createRenderTestCase('recycle-list/text-node'))
     it('attributes', createRenderTestCase('recycle-list/attrs'))
     // it('class name', createRenderTestCase('recycle-list/classname'))
-    // it('inline style', createRenderTestCase('recycle-list/inline-style'))
+    it('inline style', createRenderTestCase('recycle-list/inline-style'))
     it('v-if', createRenderTestCase('recycle-list/v-if'))
     it('v-else', createRenderTestCase('recycle-list/v-else'))
     it('v-else-if', createRenderTestCase('recycle-list/v-else-if'))

+ 105 - 0
test/weex/compiler/parser.spec.js

@@ -0,0 +1,105 @@
+import { generateBinding } from '../../../src/platforms/weex/util/parser'
+
+describe('expression parser', () => {
+  describe('generateBinding', () => {
+    it('primitive literal', () => {
+      expect(generateBinding('15')).toEqual(15)
+      expect(generateBinding('"xxx"')).toEqual('xxx')
+    })
+
+    it('identifiers', () => {
+      expect(generateBinding('x')).toEqual({ '@binding': 'x' })
+      expect(generateBinding('x.y')).toEqual({ '@binding': 'x.y' })
+      expect(generateBinding(`x.y['z']`)).toEqual({ '@binding': `x.y['z']` })
+    })
+
+    it('object literal', () => {
+      expect(generateBinding('{}')).toEqual({})
+      expect(generateBinding('{ abc: 25 }')).toEqual({ abc: 25 })
+      expect(generateBinding('{ abc: 25, def: "xxx" }')).toEqual({ abc: 25, def: 'xxx' })
+      expect(generateBinding('{ a: 3, b: { bb: "bb", bbb: { bbc: "BBC" } } }'))
+        .toEqual({ a: 3, b: { bb: 'bb', bbb: { bbc: 'BBC' }}})
+    })
+
+    it('array literal', () => {
+      expect(generateBinding('[]')).toEqual([])
+      expect(generateBinding('[{ abc: 25 }]')).toEqual([{ abc: 25 }])
+      expect(generateBinding('[{ abc: 25, def: ["xxx"] }]')).toEqual([{ abc: 25, def: ['xxx'] }])
+      expect(generateBinding('{ a: [3,16], b: [{ bb: ["aa","bb"], bbb: [{bbc:"BBC"}] }] }'))
+        .toEqual({ a: [3, 16], b: [{ bb: ['aa', 'bb'], bbb: [{ bbc: 'BBC' }] }] })
+    })
+
+    it('expressions', () => {
+      expect(generateBinding(`3 + 5`)).toEqual({ '@binding': `3 + 5` })
+      expect(generateBinding(`'x' + 2`)).toEqual({ '@binding': `'x' + 2` })
+      expect(generateBinding(`\`xx\` + 2`)).toEqual({ '@binding': `\`xx\` + 2` })
+      expect(generateBinding(`item.size * 23 + 'px'`)).toEqual({ '@binding': `item.size * 23 + 'px'` })
+    })
+
+    it('object bindings', () => {
+      expect(generateBinding(`{ color: textColor }`)).toEqual({
+        color: { '@binding': 'textColor' }
+      })
+      expect(generateBinding(`{ color: '#FF' + 66 * 100, fontSize: item.size }`)).toEqual({
+        color: { '@binding': `'#FF' + 66 * 100` },
+        fontSize: { '@binding': 'item.size' }
+      })
+      expect(generateBinding(`{
+        x: { xx: obj, xy: -2 + 5 },
+        y: {
+          yy: { yyy: obj.y || yy },
+          yz: typeof object.yz === 'string' ? object.yz : ''
+        }
+      }`)).toEqual({
+        x: { xx: { '@binding': 'obj' }, xy: { '@binding': '-2 + 5' }},
+        y: {
+          yy: { yyy: { '@binding': 'obj.y || yy' }},
+          yz: { '@binding': `typeof object.yz === 'string' ? object.yz : ''` }
+        }
+      })
+    })
+
+    it('array bindings', () => {
+      expect(generateBinding(`[textColor, 3 + 5, 'string']`)).toEqual([
+        { '@binding': 'textColor' },
+        { '@binding': '3 + 5' },
+        'string'
+      ])
+      expect(generateBinding(`[
+        { color: '#FF' + 66 * -100 },
+        item && item.style,
+        { fontSize: item.size | 0 }
+      ]`)).toEqual([
+        { color: { '@binding': `'#FF' + 66 * -100` }},
+        { '@binding': 'item && item.style' },
+        { fontSize: { '@binding': 'item.size | 0' }}
+      ])
+      expect(generateBinding(`[{
+        x: [{ xx: [fn instanceof Function ? 'function' : '' , 25] }],
+        y: {
+          yy: [{ yyy: [obj.yy.y, obj.y.yy] }],
+          yz: [object.yz, void 0]
+        }
+      }]`)).toEqual([{
+        x: [{ xx: [{ '@binding': `fn instanceof Function ? 'function' : ''` }, 25] }],
+        y: {
+          yy: [{ yyy: [{ '@binding': 'obj.yy.y' }, { '@binding': 'obj.y.yy' }] }],
+          yz: [{ '@binding': 'object.yz' }, { '@binding': 'void 0' }]
+        }
+      }])
+    })
+
+    it('unsupported bindings', () => {
+      expect(generateBinding('() => {}')).toEqual('')
+      expect(generateBinding('function(){}')).toEqual('')
+      expect(generateBinding('(function(){})()')).toEqual('')
+      expect(generateBinding('var abc = 35')).toEqual('')
+      expect(generateBinding('abc++')).toEqual('')
+      expect(generateBinding('x.y(0)')).toEqual('')
+      expect(generateBinding('class X {}')).toEqual('')
+      expect(generateBinding('if (typeof x == null) { 35 }')).toEqual('')
+      expect(generateBinding('while (x == null)')).toEqual('')
+      expect(generateBinding('new Function()')).toEqual('')
+    })
+  })
+})