diff --git a/html5/runtime/init.js b/html5/runtime/init.js
index 1cba7ca180..f94b42b335 100644
--- a/html5/runtime/init.js
+++ b/html5/runtime/init.js
@@ -1,3 +1,5 @@
+import { registerElement } from './vdom/element-types'
+
let frameworks
const versionRegExp = /^\s*\/\/ *(\{[^}]*\}) *\r?\n/
@@ -59,6 +61,9 @@ const methods = {
*/
function genInit (methodName) {
methods[methodName] = function (...args) {
+ if (methodName === 'registerComponents') {
+ checkComponentMethods(args[0])
+ }
for (const name in frameworks) {
const framework = frameworks[name]
if (framework && framework[methodName]) {
@@ -68,6 +73,16 @@ function genInit (methodName) {
}
}
+function checkComponentMethods (components) {
+ if (Array.isArray(components)) {
+ components.forEach((name) => {
+ if (name && name.type && name.methods) {
+ registerElement(name.type, name.methods)
+ }
+ })
+ }
+}
+
/**
* Register methods which will be called for each instance.
* @param {string} methodName
diff --git a/html5/runtime/listener.js b/html5/runtime/listener.js
index 30255a218d..44f891335b 100644
--- a/html5/runtime/listener.js
+++ b/html5/runtime/listener.js
@@ -175,6 +175,22 @@ Object.assign(Listener.prototype, {
return this.addActions(createAction('removeEvent', [ref, type]))
},
+ /**
+ * Call a component method with args.
+ * @param {string} ref
+ * @param {string} type
+ * @param {string} method
+ * @param {array} args
+ * @return {undefined | number} the signal sent by native
+ */
+ callComponentMethod (ref, type, method, args) {
+ return this.addActions({
+ component: type,
+ method,
+ args: [ref, ...args]
+ })
+ },
+
/**
* Default handler.
* @param {object | array} actions
diff --git a/html5/runtime/vdom/element-types.js b/html5/runtime/vdom/element-types.js
new file mode 100644
index 0000000000..59a3bfccb9
--- /dev/null
+++ b/html5/runtime/vdom/element-types.js
@@ -0,0 +1,57 @@
+import { getListener } from './operation'
+
+let Element
+
+export function setElement (El) {
+ Element = El
+}
+
+/**
+ * A map which stores all type of elements.
+ * @type {Object}
+ */
+export const elementTypes = {}
+
+/**
+ * Register an extended element type with component methods.
+ * @param {string} type component type
+ * @param {array} methods a list of method names
+ */
+export function registerElement (type, methods) {
+ // Skip when no special component methods.
+ if (!methods || !methods.length) {
+ return
+ }
+
+ // Init constructor.
+ const XElement = function (props) {
+ Element.call(this, type, props, true)
+ }
+
+ // Init prototype.
+ XElement.prototype = Object.create(Element.prototype)
+ XElement.prototype.constructor = Element
+
+ // Add methods to prototype.
+ methods.forEach(methodName => {
+ XElement.prototype[methodName] = function (...args) {
+ const listener = getListener(this.docId)
+ if (listener) {
+ listener.callComponentMethod(this.ref, type, methodName, args)
+ }
+ }
+ })
+
+ // Add to element type map.
+ elementTypes[type] = XElement
+}
+
+/**
+ * Clear all element types. Only for testing.
+ */
+export function clearElementTypes () {
+ for (const type in elementTypes) {
+ delete elementTypes[type]
+ }
+}
+
diff --git a/html5/runtime/vdom/element.js b/html5/runtime/vdom/element.js
index 2af5054a19..9a706d0c3a 100644
--- a/html5/runtime/vdom/element.js
+++ b/html5/runtime/vdom/element.js
@@ -16,10 +16,18 @@ import {
moveIndex,
removeIndex
} from './operation'
+import {
+ elementTypes,
+ setElement
+} from './element-types'
const DEFAULT_TAG_NAME = 'div'
-export default function Element (type = DEFAULT_TAG_NAME, props) {
+export default function Element (type = DEFAULT_TAG_NAME, props, isExtended) {
+ const XElement = elementTypes[type]
+ if (XElement && !isExtended) {
+ return new XElement(props)
+ }
props = props || {}
this.nodeType = 1
this.nodeId = uniqueId()
@@ -41,6 +49,8 @@ function registerNode (docId, node) {
doc.nodeMap[node.nodeId] = node
}
+setElement(Element)
+
Object.assign(Element.prototype, {
/**
* Append a child node.
diff --git a/html5/runtime/vdom/index.js b/html5/runtime/vdom/index.js
index 67a85b4a9c..e2f0ad76fa 100644
--- a/html5/runtime/vdom/index.js
+++ b/html5/runtime/vdom/index.js
@@ -1,8 +1,14 @@
import Node from './node'
-import Comment from './comment'
import Element from './element'
+import Comment from './comment'
import Document from './document'
+export {
+ elementTypes,
+ registerElement,
+ clearElementTypes
+} from './element-types'
+
export {
Document,
Node,
diff --git a/html5/test/unit/vdom/index.js b/html5/test/unit/vdom/index.js
index 886d7e6dd2..2c396af9e1 100644
--- a/html5/test/unit/vdom/index.js
+++ b/html5/test/unit/vdom/index.js
@@ -10,7 +10,10 @@ global.callAddElement = function () {}
import {
Document,
Element,
- Comment
+ Comment,
+ elementTypes,
+ registerElement,
+ clearElementTypes
} from '../../../runtime/vdom'
global.callNative = function () {}
@@ -38,6 +41,49 @@ describe('document constructor', () => {
})
})
+describe('component methods management', () => {
+ before(() => {
+ registerElement('x', ['foo', 'bar'])
+ registerElement('y', [])
+ registerElement('z')
+ })
+
+ after(() => {
+ clearElementTypes()
+ })
+
+ it('has registered element types', () => {
+ expect(Object.keys(elementTypes)).eql(['x'])
+ })
+
+ it('will call component method', () => {
+ const spy = sinon.spy()
+ const doc = new Document('test', '', spy)
+ const x = new Element('x')
+ const y = new Element('y')
+ const z = new Element('z')
+ const n = new Element('n')
+ expect(x.foo).is.function
+ expect(x.bar).is.function
+ expect(x.baz).is.undefined
+ expect(y.foo).is.undefined
+ expect(z.foo).is.undefined
+ expect(n.foo).is.undefined
+
+ doc.createBody('r')
+ doc.documentElement.appendChild(doc.body)
+ doc.body.appendChild(x)
+ doc.body.appendChild(y)
+ doc.body.appendChild(z)
+ doc.body.appendChild(n)
+ expect(spy.args.length).eql(5)
+
+ x.foo(1, 2, 3)
+ expect(spy.args.length).eql(6)
+ expect(spy.args[5]).eql([[{ component: 'x', method: 'foo', args: [x.ref, 1, 2, 3] }]])
+ })
+})
+
describe('document methods', () => {
let doc
@@ -435,12 +481,12 @@ describe('complicated situations', () => {
doc = new Document('foo', '', spy)
doc.createBody('r')
doc.documentElement.appendChild(doc.body)
- el = new Element('bar', null, doc)
- el2 = new Element('baz', null, doc)
- el3 = new Element('qux', null, doc)
- c = new Comment('aaa', doc)
- c2 = new Comment('bbb', doc)
- c3 = new Comment('ccc', doc)
+ el = new Element('bar')
+ el2 = new Element('baz')
+ el3 = new Element('qux')
+ c = new Comment('aaa')
+ c2 = new Comment('bbb')
+ c3 = new Comment('ccc')
})
afterEach(() => {