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(() => {