diff --git a/docs/docs/interactive-mip/data-binding/global-data-definition.md b/docs/docs/interactive-mip/data-binding/global-data-definition.md index 9f571343..780e3677 100644 --- a/docs/docs/interactive-mip/data-binding/global-data-definition.md +++ b/docs/docs/interactive-mip/data-binding/global-data-definition.md @@ -206,7 +206,7 @@ MIP 提供 SPA 的整站沉浸式的体验,如果要打造复杂的业务场 2. 如果要修改的数据字段仅为页面数据,将不影响共享数据 如:在 C 页面调用 - `MIP.setData({'name': 'name-c'})` + `MIP.setData({'name': 'new-c'})` 此时 C 页面可用的数据源为 ```json diff --git a/docs/docs/interactive-mip/data-binding/mip-bind.md b/docs/docs/interactive-mip/data-binding/mip-bind.md index 11c4a675..4f0cdcf3 100644 --- a/docs/docs/interactive-mip/data-binding/mip-bind.md +++ b/docs/docs/interactive-mip/data-binding/mip-bind.md @@ -247,13 +247,13 @@ export default { let selected = MIP.getData('selected') let index = selected.indexOf(newVal) - if (index) { + if (index !== -1) { selected.splice(index, 1) } else { - selected.push(index) + selected.push(newVal) } - MIP.setData('selectedStr', selected.join(',')) + MIP.setData({'selectedStr': selected.join(',')}) }) ``` diff --git a/packages/mip/src/components/index.js b/packages/mip/src/components/index.js index 0d16880a..c71d0244 100644 --- a/packages/mip/src/components/index.js +++ b/packages/mip/src/components/index.js @@ -12,6 +12,7 @@ import MipCarousel from './mip-carousel' import MipIframe from './mip-iframe' import MipPix from './mip-pix' import mipBindInit from './mip-bind/init' +import MipDataWatch from './mip-bind/mip-data-watch' import MipData from './mip-bind/mip-data' import MipShell from './mip-shell/index' import MipFixed from './mip-fixed/index' @@ -34,7 +35,7 @@ export default { registerElement('mip-video', MipVideo) registerElement('mip-fixed', MipFixed) mipBindInit() - // new MipBind() + registerElement('mip-data-watch', MipDataWatch) registerElement('mip-data', MipData) isMIPShellDisabled() || registerElement('mip-shell', MipShell) } diff --git a/packages/mip/src/components/mip-bind/binding-attr.js b/packages/mip/src/components/mip-bind/binding-attr.js index 980f9db9..6390cc16 100644 --- a/packages/mip/src/components/mip-bind/binding-attr.js +++ b/packages/mip/src/components/mip-bind/binding-attr.js @@ -28,12 +28,10 @@ export function bindingAttr (node, key, value, oldValue) { } let attr = key.slice(prefixLen) - let prop = typeof value === 'object' ? JSON.stringify(value) : value if (prop === oldValue) { return prop } - if (prop === '' || prop === undefined) { node.removeAttribute(attr) } else { @@ -44,7 +42,6 @@ export function bindingAttr (node, key, value, oldValue) { } else if (BOOLEAN_ATTRS.indexOf(attr) > -1) { node[attr] = !!prop } - return prop } diff --git a/packages/mip/src/components/mip-bind/binding-dom-watcher.js b/packages/mip/src/components/mip-bind/binding-dom-watcher.js new file mode 100644 index 00000000..f3144b75 --- /dev/null +++ b/packages/mip/src/components/mip-bind/binding-dom-watcher.js @@ -0,0 +1,140 @@ +/** + * @file dom-watcher.js + * @author clark-t (clarktanglei@163.com) + */ + +import { isElementNode } from '../../util/dom/dom' +import { + traverse +} from '../../util/fn' + +import { + isBindingAttr +} from './binding' + +class DOMWatcher { + constructor () { + this.doms = [] + this.watchers = [] + } + + watch (watcher) { + this.watchers.push(watcher) + } + + add (doms) { + for (let dom of doms) { + this.doms.push(dom) + } + } + + remove (doms) { + let tmps = doms.slice() + for (let i = this.doms.length - 1; i > -1; i--) { + for (let j = tmps.length - 1; j > -1; j--) { + if (this.doms[i].node === tmps[j].node) { + this.doms.splice(i, 1) + tmps.splice(j, 1) + break + } + } + } + } + + update ({add: domList}) { + let bindings = [] + + for (let dom of domList) { + uniqueMerge(bindings, queryBindings(dom)) + } + + let changed = diffBindingDOMs(this.doms, bindings) + + this.remove(changed.removed) + this.add(changed.add) + + for (let watcher of this.watchers) { + watcher(changed, this.doms) + } + } +} + +function uniqueMerge (oldList, newList) { + for (let i = 0; i < newList.length; i++) { + let len = oldList.length + let j = 0 + for (j = 0; j < len; j++) { + if (oldList[j].node === newList[i].node) { + break + } + } + if (j === len) { + oldList.push(newList[i]) + } + } + return oldList +} + +function createBindingNodeWrapper (node, attrs) { + let wrapper = { node, attrs } + if (attrs) { + wrapper.keys = Object.keys(attrs) + } + return wrapper +} + +function queryBindings (root) { + let results = [] + traverse(root, node => { + /* istanbul ignore if */ + if (!isElementNode(node)) { + return + } + let attrs = queryBindingAttrs(node) + attrs && results.push(createBindingNodeWrapper(node, attrs)) + if (node.children) { + return Array.from(node.children) + } + }) + return results +} + +function queryBindingAttrs (node) { + let attrs + for (let i = 0; i < node.attributes.length; i++) { + let attr = node.attributes[i] + if (!isBindingAttr(attr.name)) { + continue + } + attrs = attrs || {} + attrs[attr.name] = {expr: attr.value} + } + return attrs +} + +function diffBindingDOMs (storeList, newList) { + let output = { + removed: [], + add: newList.slice() + } + + for (let i = storeList.length - 1; i > -1; i--) { + let stored = storeList[i] + if (!document.contains(stored.node)) { + output.removed.push(stored) + continue + } + + for (let j = output.add.length - 1; j > -1; j--) { + if (stored.node === newList[j].node) { + output.add.splice(j, 1) + break + } + } + } + + return output +} + +export const instance = new DOMWatcher() + diff --git a/packages/mip/src/components/mip-bind/binding-value.js b/packages/mip/src/components/mip-bind/binding-value.js new file mode 100644 index 00000000..fdac9ebf --- /dev/null +++ b/packages/mip/src/components/mip-bind/binding-value.js @@ -0,0 +1,39 @@ +/** + * @file binding-value.js + * @author clark-t (clarktanglei@163.com) + */ + +import { throttle } from '../../util/fn' +import { createSetDataObject } from './util' + +export function addInputListener (add, store) { + const key = 'm-bind:value' + + const FORM_ELEMENTS = [ + 'INPUT', + 'TEXTAREA', + 'SELECT' + ] + + for (let info of add) { + let {node, attrs} = info + if (FORM_ELEMENTS.indexOf(node.tagName) === -1) { + // if (!FORM_ELEMENTS.includes(node.tagName)) { + continue + } + + let expression = attrs[key] && attrs[key].expr + + if (!expression) { + continue + } + + const properties = expression.split('.') + const inputThrottle = throttle(function (e) { + let obj = createSetDataObject(properties, e.target.value) + store.set(obj) + }, 100) + node.addEventListener('input', inputThrottle) + } +} + diff --git a/packages/mip/src/components/mip-bind/data-store.js b/packages/mip/src/components/mip-bind/data-store.js index 98782a02..5bf2304f 100644 --- a/packages/mip/src/components/mip-bind/data-store.js +++ b/packages/mip/src/components/mip-bind/data-store.js @@ -7,6 +7,7 @@ import DataWatcher from './data-watcher' import GlobalData from './global-data' import { merge, getProperty } from './util' import { isObject } from '../../util/fn' + export default class DataStore { constructor () { const storage = {} diff --git a/packages/mip/src/components/mip-bind/init.js b/packages/mip/src/components/mip-bind/init.js index 8af83d1c..cd23997d 100644 --- a/packages/mip/src/components/mip-bind/init.js +++ b/packages/mip/src/components/mip-bind/init.js @@ -1,168 +1,94 @@ /** * @file mip-bind init * @author clark-t (clarktanglei@163.com) - * @description 在现有 MIP-bind 的模式下,mip-data 只能通过唯一的 MIP.setData - * 进行数据修改,所以完全可以通过每次调用 MIP.setData - * 的时候进行新旧数据比对,然后触发各种事件事件监听、数据绑定等等就可以了 + * @description m-bind 机制初始化 */ -import {isElementNode} from '../../util/dom/dom' -import {traverse, throttle, noop} from '../../util/fn' -import { - createSetDataObject -} from './util' +import { def } from './util' import DataStore from './data-store' +import { applyBinding } from './binding' +import { instance as domWatcher } from './binding-dom-watcher' + import { - applyBinding, - isBindingAttr -} from './binding' + addInputListener +} from './binding-value' + import log from '../../util/log' const logger = log('MIP-bind') -export default function () { - let bindingDOMs = [] +export const DOM_CHANGE_EVENT = 'dom-change' +export default function () { const store = new DataStore() const getData = store.get.bind(store) const setData = store.set.bind(store) - const $set = data => { - let bindings = queryBindings(document.documentElement) - let {add} = diffBindingDOMs(bindingDOMs, bindings) - - if (bindingDOMs.length > 0 && add.length > 0) { - logger.warn(`请勿在动态创建的节点上使用 m-bind`) - } - addInputListener(add, store) - for (let item of add) { - bindingDOMs.push(item) - } - MIP.setData(data) - } const watch = store.watcher.watch.bind(store.watcher) - def(MIP, 'setData', () => setData) - def(MIP, '$set', () => $set) - def(MIP, 'getData', () => getData) - def(MIP, 'watch', () => watch) - - // MIP.setData = store.set.bind(store) - // MIP.getData = store.get.bind(store) - // MIP.watch = store.watcher.watch.bind(store.watcher) - - // @deprecated - window.mipDataPromises = [] - window.m = store.data - // 兼容原有逻辑 - // MIP.$set = MIP.setData - MIP.$update = store.global.broadcast.bind(store.global) - - store.watcher.watch(() => { - for (let info of bindingDOMs) { + const applyBindings = domInfos => { + for (let info of domInfos) { try { applyBinding(info, store.data) } catch (e) /* istanbul ignore next */ { logger.error(e) } } - }) - - MIP.$set(store.global.data) -} + } -function def (obj, name, getter, setter) { - Object.defineProperty(obj, name, { - get: getter, - set: typeof setter === 'function' ? setter : noop, - enumerable: true, - configurable: false + domWatcher.watch((changed) => { + addInputListener(changed.add, store) + applyBindings(changed.add) }) -} -function queryBindings (root) { - let results = [] - traverse(root, node => { - /* istanbul ignore if */ - if (!isElementNode(node)) { - return - } - let attrs = queryBindingAttrs(node) - attrs && results.push({node, attrs, keys: Object.keys(attrs) }) - if (node.children) { - return Array.from(node.children) - } + document.addEventListener(DOM_CHANGE_EVENT, e => { + let changeInfo = e.detail && e.detail[0] + changeInfo && + Array.isArray(changeInfo.add) && + domWatcher.update(changeInfo) }) - return results -} -function queryBindingAttrs (node) { - let attrs - for (let i = 0; i < node.attributes.length; i++) { - let attr = node.attributes[i] - if (!isBindingAttr(attr.name)) { - continue - } - attrs = attrs || {} - attrs[attr.name] = {expr: attr.value} - } - return attrs -} - -function diffBindingDOMs (storeList, newList) { - let output = { - // removed: [], - add: [] - } - - const storeLength = storeList.length - - for (let item of newList) { - let i - for (i = 0; i < storeLength; i++) { - if (item.node === storeList[i].node) { - break - } - } + store.watcher.watch(() => { + applyBindings(domWatcher.doms) + }) - if (i === storeLength) { - output.add.push(item) - } + const $set = data => { + domWatcher.update({ + add: [document.documentElement] + }) + setData(data) } - return output -} - -function addInputListener (nodeInfos, store) { - const key = 'm-bind:value' - - const FORM_ELEMENTS = [ - 'INPUT', - 'TEXTAREA', - 'SELECT' - ] - - for (let info of nodeInfos) { - let {node, attrs} = info - if (FORM_ELEMENTS.indexOf(node.tagName) === -1) { - // if (!FORM_ELEMENTS.includes(node.tagName)) { - continue - } + def(MIP, 'setData', setData) + def(MIP, '$set', $set) + def(MIP, 'getData', getData) + def(MIP, 'watch', watch) + + /** + * 用于判断页面上 mip-data 是否完全加载 + * + * @type {Array.} + * @deprecated + */ + window.mipDataPromises = [] - let expression = attrs[key] && attrs[key].expr + /** + * 全局 mip-data 存储对象,之前用于 on/bind 表达式获取数据,现已通过直接使用 + * store.data 变量的方式获取数据,为确保第三方组件使用 window.m 而暂时挂回 window + * + * @type {Object} + * @deprecated + */ + window.m = store.data - if (!expression) { - continue - } + /** + * MIP 全局数据通知机制 + * + * @type {Function} + * @deprecated + */ + MIP.$update = store.global.broadcast.bind(store.global) - const properties = expression.split('.') - const inputThrottle = throttle(function (e) { - let obj = createSetDataObject(properties, e.target.value) - store.set(obj) - }, 100) - node.addEventListener('input', inputThrottle) - } + // 设置初始化数据 + MIP.$set(store.global.data) } -// function addBindingListener (nodeInfos, store) { -// } - diff --git a/packages/mip/src/components/mip-bind/mip-data-watch.js b/packages/mip/src/components/mip-bind/mip-data-watch.js new file mode 100644 index 00000000..da36fb6b --- /dev/null +++ b/packages/mip/src/components/mip-bind/mip-data-watch.js @@ -0,0 +1,21 @@ +/** + * @file mip-data-watch.js + * @author clark-t (clarktanglei@163.com) + */ + +import CustomElement from '../../custom-element' +import viewer from '../../viewer' + +export default class MIPDataWatch extends CustomElement { + build () { + let key = this.element.getAttribute('watch') + + key && MIP.watch(key, (newValue, oldValue) => { + viewer.eventAction.execute('change', this.element, { oldValue, newValue }) + }) + } + + prerenderAllowed () { + return true + } +} diff --git a/packages/mip/src/components/mip-bind/mip-data.js b/packages/mip/src/components/mip-bind/mip-data.js index 29ba62bd..05113151 100644 --- a/packages/mip/src/components/mip-bind/mip-data.js +++ b/packages/mip/src/components/mip-bind/mip-data.js @@ -9,65 +9,98 @@ import CustomElement from '../../custom-element' import jsonParse from '../../util/json-parse' +import Deffered from '../../util/deferred' +import log from '../../util/log' +import { timeout } from './util' +const logger = log('MIP-data') -/* - * Remove promise from global mipDataPromises array - * @param {Promise} target promise need to be removed - */ -function dropPromise (target) { - let index = mipDataPromises.indexOf(target) - mipDataPromises.splice(index, ~index ? 1 : 0) -} +class MIPData extends CustomElement { + static get observedAttributes () { + return ['src'] + } + + handleSrcChange () { + this.fetch() + } -class MipData extends CustomElement { build () { - let src = this.element.getAttribute('src') + this.addEventAction('refresh', () => { + this.fetch() + }) + + if (this.props.src) { + // get remote data + this.fetch() + } else { + // get local data + this.sync() + } + } + + sync () { let ele = this.element.querySelector('script[type="application/json"]') - /* istanbul ignore if */ - if (src) { - this.getData(src) - } else if (ele) { + if (ele) { let data = ele.textContent.toString() if (data) { - MIP.$set(jsonParse(data)) + this.assign(jsonParse(data)) } } } + request (url) { + let { credentials, timeout: time } = this.props + // return method === 'jsonp' + // ? fetchJsonp(url, { timeout: time }) + // : + return Promise.race([ + fetch(url, { credentials }), + timeout(time) + ]).then(res => { + if (!res.ok) { + throw Error(`Fetch request failed: ${url}`) + } + return res.json() + }) + } + /* - * get initial data asynchronouslly + * get remote initial data asynchronouslly */ - getData (url) { - let stuckResolve - let stuckReject + async fetch () { + let url = this.props.src + + if (!url) { + return + } + + let {promise, resolve, reject} = new Deffered() + // only resolve/reject when sth truly comes to a result // such as only to resolve when res.json() done - let stuckPromise = new Promise(function (resolve, reject) { - stuckResolve = resolve - stuckReject = reject - }) - mipDataPromises.push(stuckPromise) - - fetch(url, {credentials: 'include'}) - .then(res => { - if (res.ok) { - res.json().then(data => { - MIP.$set(data) - dropPromise(stuckPromise) - stuckResolve() - }) - } else { - console.error('Fetch request failed!') - dropPromise(stuckPromise) - stuckReject() - } - }) - .catch(e => { - console.error(e) - dropPromise(stuckPromise) - stuckReject() - }) + mipDataPromises.push(promise) + let resolver = resolve + + try { + let data = await this.request(url) + this.assign(data) + } catch (e) { + logger.error(e) + resolver = reject + MIP.viewer.eventAction.execute('fetch-error', this.element, e) + } + + let index = mipDataPromises.indexOf(promise) + if (index > -1) { + mipDataPromises.splice(index, 1) + } + + resolver() + } + + assign (data) { + let {id, scope} = this.props + MIP.$set(id && scope ? {[id]: data} : data) } /* istanbul ignore next */ @@ -76,4 +109,31 @@ class MipData extends CustomElement { } } -export default MipData +MIPData.props = { + src: { + type: String, + default: '' + }, + credentials: { + type: String, + default: 'omit' + }, + // method: { + // type: String, + // default: 'fetch' + // }, + timeout: { + type: Number, + default: 5000 + }, + id: { + type: String, + default: '' + }, + scope: { + type: Boolean, + default: false + } +} + +export default MIPData diff --git a/packages/mip/src/components/mip-bind/util.js b/packages/mip/src/components/mip-bind/util.js index 419f1a3d..5b731be0 100644 --- a/packages/mip/src/components/mip-bind/util.js +++ b/packages/mip/src/components/mip-bind/util.js @@ -3,7 +3,17 @@ * @author clark-t (clarktanglei@163.com) */ -import {traverse, getType} from '../../util/fn' +import {traverse, getType, noop} from '../../util/fn' + +export function def (obj, name, getter) { + Object.defineProperty(obj, name, { + get: () => getter, + set: noop, + // set: typeof setter === 'function' ? setter : noop, + enumerable: true, + configurable: false + }) +} export function merge (oldVal, newVal, replace = true) { let change = [] @@ -62,3 +72,14 @@ export function getProperty (data, expr) { return result } +export function timeout (time, shouldResolve = false) { + return new Promise((resolve, reject) => { + let message = 'timeout' + setTimeout(() => { + shouldResolve + ? resolve(message) + : reject(new Error(message)) + }, time) + }) +} + diff --git a/packages/mip/src/components/mip-img.js b/packages/mip/src/components/mip-img.js index 9aa23fc1..76dd3f62 100644 --- a/packages/mip/src/components/mip-img.js +++ b/packages/mip/src/components/mip-img.js @@ -224,7 +224,7 @@ function bindPopup (element, img) { let popupImg = new Image() popupImg.setAttribute('src', current) popup.appendChild(popupImg) - + // 背景 fade in naboo.animate(popupBg, { opacity: 1 diff --git a/packages/mip/src/polyfill.js b/packages/mip/src/polyfill.js index 82689477..57490e13 100644 --- a/packages/mip/src/polyfill.js +++ b/packages/mip/src/polyfill.js @@ -6,6 +6,8 @@ import 'core-js/modules/es6.promise' // phantomjs 中会用到 symbol import 'core-js/modules/es6.symbol' +// import 'core-js/modules/es7.array.includes' +import 'core-js/modules/es6.array.find' // import 'core-js/modules/es6.array.from' // import 'core-js/modules/es6.map' // import 'core-js/modules/es6/set' diff --git a/packages/mip/src/util/event-action/visitor/basic.js b/packages/mip/src/util/event-action/visitor/basic.js index 463d4570..8b11ecd3 100644 --- a/packages/mip/src/util/event-action/visitor/basic.js +++ b/packages/mip/src/util/event-action/visitor/basic.js @@ -35,7 +35,7 @@ const visitor = { let left = path.traverse(node.left) let right = path.traverse(node.right) return function () { - return operation(left(), right()) + return operation(left, right) } }, diff --git a/packages/mip/src/util/event-action/whitelist/basic.js b/packages/mip/src/util/event-action/whitelist/basic.js index 2ed5112a..2d3a6ce3 100644 --- a/packages/mip/src/util/event-action/whitelist/basic.js +++ b/packages/mip/src/util/event-action/whitelist/basic.js @@ -7,21 +7,21 @@ */ export const BINARY_OPERATION = { - '+': (left, right) => left + right, - '-': (left, right) => left - right, - '*': (left, right) => left * right, - '/': (left, right) => left / right, - '%': (left, right) => left % right, - '>': (left, right) => left > right, - '<': (left, right) => left < right, - '>=': (left, right) => left >= right, - '<=': (left, right) => left <= right, - '==': (left, right) => left == right, - '===': (left, right) => left === right, - '!=': (left, right) => left != right, - '!==': (left, right) => left !== right, - '&&': (left, right) => left && right, - '||': (left, right) => left || right + '+': (left, right) => left() + right(), + '-': (left, right) => left() - right(), + '*': (left, right) => left() * right(), + '/': (left, right) => left() / right(), + '%': (left, right) => left() % right(), + '>': (left, right) => left() > right(), + '<': (left, right) => left() < right(), + '>=': (left, right) => left() >= right(), + '<=': (left, right) => left() <= right(), + '==': (left, right) => left() == right(), + '===': (left, right) => left() === right(), + '!=': (left, right) => left() != right(), + '!==': (left, right) => left() !== right(), + '&&': (left, right) => left() && right(), + '||': (left, right) => left() || right() } export const UNARY_OPERATION = { @@ -36,7 +36,9 @@ function instanceSort (...args) { } function instanceSplice (...args) { - return this.slice().splice(...args) + let arr = this.slice() + arr.splice(...args) + return arr } export const PROTOTYPE = { @@ -50,6 +52,8 @@ export const PROTOTYPE = { reduce: Array.prototype.reduce, slice: Array.prototype.slice, some: Array.prototype.some, + every: Array.prototype.every, + find: Array.prototype.find, // sort: Array.prototype.sort, // splice: Array.prototype.splice, sort: instanceSort, @@ -94,6 +98,7 @@ export const CUSTOM_FUNCTIONS = { keys: Object.keys, values: Object.values, + assign: Object.assign, // 兼容以前的 MIP event 逻辑 decodeURI: decodeURI, diff --git a/packages/mip/src/util/event-action/whitelist/mip-action.js b/packages/mip/src/util/event-action/whitelist/mip-action.js index 78ece8ac..746bc431 100644 --- a/packages/mip/src/util/event-action/whitelist/mip-action.js +++ b/packages/mip/src/util/event-action/whitelist/mip-action.js @@ -70,8 +70,8 @@ const ALLOWED_GLOBALS = ( const FALLBACK_PARSE_STORE = {} -function setDataParseFallback ({argumentText, options}) { - if (!FALLBACK_PARSE_STORE[argumentText]) { +function setDataParseFallback ({argumentText, options, deprecate}) { + if (deprecate && !FALLBACK_PARSE_STORE[argumentText]) { FALLBACK_PARSE_STORE[argumentText] = new Function('DOM', `with(this){return ${argumentText}}`) logger.warn('当前的 setData 参数存在不符合 MIP-bind 规范要求的地方,请及时进行修改:') logger.warn(argumentText) @@ -112,17 +112,30 @@ export default function mipAction ({property, argumentText, options}) { action() return } + if (property === 'setData' || property === '$set') { + let fn let arg + try { - let fn = parse(argumentText, 'ObjectLiteral') + fn = parse(argumentText, 'ObjectLiteral') arg = fn(options) } catch (e) { - arg = setDataParseFallback({argumentText, options}) + /* istanbul ignore if */ + if (fn) { + logger.error(e) + } + arg = setDataParseFallback({ + argumentText, + options, + deprecate: !fn + }) } + action(arg) return } + let fn = parse(argumentText, 'MIPActionArguments') let args = fn(options) action(args[0]) diff --git a/packages/mip/test/specs/components/mip-bind.spec.js b/packages/mip/test/specs/components/mip-bind.spec.js index 229a1dbf..cdc8cce2 100644 --- a/packages/mip/test/specs/components/mip-bind.spec.js +++ b/packages/mip/test/specs/components/mip-bind.spec.js @@ -7,6 +7,10 @@ /* globals describe, before, it, expect, MIP, after, sinon */ import MipData from 'src/components/mip-bind/mip-data' +import { timeout } from 'src/components/mip-bind/util' +import EventAction from 'src/util/event-action' + +const action = new EventAction() function sleep (time) { if (time == null) { @@ -15,6 +19,39 @@ function sleep (time) { return new Promise(resolve => setTimeout(resolve, time)) } +function getMipDataProps (props = {}) { + return Object.assign( + Object.keys(MipData.props).reduce((obj, key) => { + obj[key] = MipData.props[key].default + return obj + }, {}), + props + ) +} + +describe('mip-bind util', function () { + it('timeout reject', async () => { + let shouldError = false + try { + await timeout(0) + } catch (e) { + shouldError = true + } + + expect(shouldError).to.be.equal(true) + }) + + it('timeout resolve', async () => { + let shouldError = false + try { + await timeout(0, true) + } catch (e) { + shouldError = true + } + expect(shouldError).to.be.equal(false) + }) +}) + describe('mip-bind', function () { let eleText let eleBind @@ -51,7 +88,7 @@ describe('mip-bind', function () { dup body

-

test:1

+

test:1

@@ -75,6 +112,27 @@ describe('mip-bind', function () { } + + + + + + ` @@ -98,11 +156,16 @@ describe('mip-bind', function () { city: '广州' }, list: ['a', 'b', {item: 2}], - id: 1 + id: 1, + scopedData: { + a: 1, + b: 2 + }, + aa: 1, + bb: 2 }) expect(MIP.getData('global.data.name')).to.equal('level-1') - await sleep() expect(window.g).to.eql({ @@ -153,6 +216,7 @@ describe('mip-bind', function () { let mipData before(function () { mipData = new MipData() + mipData.props = getMipDataProps() let mipDataTag = document.createElement('mip-data') let script = document.createElement('script') script.setAttribute('type', 'application/json') @@ -166,7 +230,7 @@ describe('mip-bind', function () { }) it('should not combine wrong formatted data with m', function () { - expect(mipData.build.bind(mipData)).to.throw(/Content should be a valid JSON string!/) + expect(mipData.sync.bind(mipData)).to.throw(/Content should be a valid JSON string!/) expect(window.m.wrongFormatData).to.be.undefined }) }) @@ -183,12 +247,16 @@ describe('mip-bind', function () { } let fetchOrigin + let div before(function () { + div = document.createElement('div') + document.body.appendChild(div) fetchOrigin = window.fetch sinon.stub(window, 'fetch') }) after(function () { + document.body.removeChild(div) window.fetch = fetchOrigin }) @@ -199,12 +267,14 @@ describe('mip-bind', function () { ) ) - let mipData = new MipData() - let mipDataTag = document.createElement('mip-data') - mipDataTag.setAttribute('src', '/testData') - mipData.element = mipDataTag + div.innerHTML = '' + // let mipData = new MipData() + // mipData.props = getMipDataProps() + // let mipDataTag = document.createElement('mip-data') + // mipDataTag.setAttribute('src', '/testData') + // mipData.element = mipDataTag - mipData.build.bind(mipData)() + // mipData.build.bind(mipData)() expect(window.mipDataPromises.length).to.equal(1) Promise.all(window.mipDataPromises).then(function () { @@ -214,6 +284,48 @@ describe('mip-bind', function () { }) }) + it('should fetch async data when src is set and refresh', async function () { + window.fetch.returns( + Promise.resolve( + json({ testAttributeChangeForMIPData: 123 }, 200) + ) + ) + + div.innerHTML = ` + +
+ ` + let dataDom = document.getElementById('dynamic-mip-data') + MIP.util.customEmit(document, 'dom-change', { add: [dataDom] }) + expect(MIP.getData('testAttributeChangeForMIPData')).to.be.equal(undefined) + MIP.setData({ dynamicSrc: '/testData' }) + await sleep(100) + expect(MIP.getData('testAttributeChangeForMIPData')).to.be.equal(123) + MIP.setData({ testAttributeChangeForMIPData: 234 }) + expect(MIP.getData('testAttributeChangeForMIPData')).to.be.equal(234) + + window.fetch.returns( + Promise.resolve( + json({ testAttributeChangeForMIPData: 123 }, 200) + ) + ) + + action.execute('haha', document.getElementById('dynamic-mip-data-trigger')) + await sleep(100) + expect(MIP.getData('testAttributeChangeForMIPData')).to.be.equal(123) + + window.fetch.returns( + Promise.resolve( + json({ testAttributeChangeForMIPData: 678}, 200) + ) + ) + + await sleep(100) + MIP.setData({ dynamicSrc: undefined }) + await sleep(100) + expect(MIP.getData('testAttributeChangeForMIPData')).to.be.equal(123) + }) + it('should fetch async data 404', function (done) { window.fetch.returns( Promise.resolve( @@ -221,12 +333,15 @@ describe('mip-bind', function () { ) ) - let mipData = new MipData() - let mipDataTag = document.createElement('mip-data') - mipDataTag.setAttribute('src', '/testData') - mipData.element = mipDataTag + div.innerHTML = '' + + // let mipData = new MipData() + // mipData.props = getMipDataProps() + // let mipDataTag = document.createElement('mip-data') + // mipDataTag.setAttribute('src', '/testData') + // mipData.element = mipDataTag - mipData.build.bind(mipData)() + // mipData.build.bind(mipData)() expect(window.mipDataPromises.length).to.equal(1) Promise.all(window.mipDataPromises).catch(function () { @@ -242,13 +357,14 @@ describe('mip-bind', function () { json({status: 'failed'}, 200) ) ) - - let mipData = new MipData() - let mipDataTag = document.createElement('mip-data') - mipDataTag.setAttribute('src', '/testData') - mipData.element = mipDataTag - - mipData.build.bind(mipData)() + div.innerHTML = '' + // let mipData = new MipData() + // mipData.props = getMipDataProps() + // let mipDataTag = document.createElement('mip-data') + // mipDataTag.setAttribute('src', '/testData') + // mipData.element = mipDataTag + + // mipData.build.bind(mipData)() expect(window.mipDataPromises.length).to.equal(1) Promise.all(window.mipDataPromises).catch(function () { @@ -515,6 +631,10 @@ describe('mip-bind', function () { eles.push(createEle('p', ['class', '[classObject, [items[0].iconClass]]'], 'bind')) eles.push(createEle('p', ['style', 'styleGroup.aStyle'], 'bind')) eles.push(createEle('p', ['class', 'classGroup.aClass'], 'bind')) + let ele = createEle('p', ['style', `{fontSize: fontSize + 'px'}`], 'bind') + ele.setAttribute('style', 'color: red;') + eles.push(ele) + MIP.$set({ loading: false, iconClass: 'grey lighten1 white--text', @@ -558,12 +678,12 @@ describe('mip-bind', function () { expect(eles[2].getAttribute('class')).to.equal('class-text') expect(eles[3].getAttribute('class')).to.equal('m-error') // console.log(eles[4].getAttribute('class')) - expect(eles[4].getAttribute('class')).to.be.equal(null) + expect(eles[4].getAttribute('class')).to.be.oneOf([null, undefined, '']) expect(eles[11].getAttribute('class')).to.equal('grey lighten1 white--text') expect(eles[12].getAttribute('class')).to.equal('grey lighten1 white--text m-error') expect(eles[13].getAttribute('class')).to.equal('warning-class loading-class grey lighten1 white--text') expect(eles[15].getAttribute('class')).to.be.equal('class-a class-b') - + expect(eles[16].getAttribute('style')).to.be.equal('color:red;font-size:12.5px;') MIP.setData({ tab: 'test' }) @@ -604,7 +724,7 @@ describe('mip-bind', function () { expect(eles[11].getAttribute('class')).to.equal('nothing') expect(eles[12].getAttribute('class')).to.equal('m-error nothing') expect(eles[13].getAttribute('class')).to.equal('warning-class active-class nothing') - expect(eles[15].getAttribute('class')).to.be.equal('') + expect(eles[15].getAttribute('class')).to.be.oneOf(['', null, undefined]) }) it('should update style', async function () { @@ -642,7 +762,8 @@ describe('mip-bind', function () { .sort() .join(';') ) - expect(eles[14].getAttribute('style')).to.be.equal('') + expect(eles[14].getAttribute('style')).to.be.oneOf(['', null, undefined]) + expect(eles[16].getAttribute('style')).to.be.equal('color:red;font-size:12.4px;') }) after(function () { @@ -676,7 +797,10 @@ describe('mip-bind', function () { expect(MIP.getData('num')).to.equal('2') }) - it('should change input value with m-bind', function () { + it('should change input value with m-bind', async function () { + await sleep(100) + expect(MIP.getData('num')).to.be.equal('2') + expect(eles[1].value).to.be.equal('2') eles[1].value = 3 let event = document.createEvent('HTMLEvents') event.initEvent('input', true, true) @@ -734,6 +858,54 @@ describe('mip-bind', function () { } }) }) + + describe('dom change event', function () { + it('should bind text to addition dom', async function () { + MIP.setData({ + aDomChangedText: 'Hello World' + }) + await sleep() + let div = document.createElement('div') + div.setAttribute('m-text', 'aDomChangedText') + document.body.appendChild(div) + expect(div.innerText).to.be.oneOf(['', null, undefined]) + await sleep() + MIP.util.customEmit(document, 'dom-change', { + add: [div] + }) + await sleep() + expect(div.innerText).to.be.equal('Hello World') + await sleep() + MIP.setData({ + aDomChangedText: 'Hello MIP' + }) + await sleep() + expect(div.innerText).to.be.equal('Hello MIP') + document.body.removeChild(div) + }) + }) + + describe('mip-data-watch', function () { + it('should watch correctly', async function () { + expect(MIP.getData('testChangeData')).to.be.equal(undefined) + expect(MIP.getData('testChangeDataCopy')).to.be.equal(undefined) + let dom = document.createElement('mip-data-watch') + dom.setAttribute('watch', 'testChangeData') + dom.setAttribute('on', 'change:MIP.setData({ testChangeDataCopy: event.newValue })') + dom.setAttribute('layout', 'nodisplay') + document.body.appendChild(dom) + await sleep() + expect(MIP.getData('testChangeDataCopy')).to.be.equal(undefined) + MIP.setData({ + testChangeData: 'Hello World' + }) + await sleep() + expect(MIP.getData('testChangeDataCopy')).to.be.equal('Hello World') + expect(MIP.setData({ testChangeData: 10086 })) + await sleep() + expect(MIP.getData('testChangeDataCopy')).to.be.equal(10086) + }) + }) }) function createEle (tag, props, key) { diff --git a/packages/mip/test/specs/util/event-action.spec.js b/packages/mip/test/specs/util/event-action.spec.js index 5f8ec7c1..80946a3a 100644 --- a/packages/mip/test/specs/util/event-action.spec.js +++ b/packages/mip/test/specs/util/event-action.spec.js @@ -15,6 +15,14 @@ import rect from 'src/util/dom/rect' const action = new EventAction() + +function sleep (time) { + if (time == null) { + return new Promise(resolve => resolve()) + } + return new Promise(resolve => setTimeout(resolve, time)) +} + const el = document.createElement('div') const target = dom.create("") @@ -171,6 +179,27 @@ describe('Event Action', () => { expect(argument.args).to.be.eql({class: 'event'}) expect(target.classList.contains('event')).to.be.true }) + + it('should handle multiple events and actions', () => { + el.setAttribute('class', '') + el.setAttribute('on', `success: + test-event-action.toggleClass(class='a'), + test-event-action.toggleClass(class='b'); + fail: + test-event-action.toggleClass(class='c'), + test-event-action.toggleClass(class='d')`) + action.execute('fail', el, {}) + expect(target.getAttribute('class').split(' ')).to.have.include.members(['c', 'd']) + expect(target.getAttribute('class').split(' ')).to.not.have.include.members(['a', 'b']) + action.execute('success', el) + expect(target.getAttribute('class').split(' ')).to.have.include.members(['c', 'd', 'a', 'b']) + action.execute('fail', el) + expect(target.getAttribute('class').split(' ')).to.be.have.include.members(['a', 'b']) + expect(target.getAttribute('class').split(' ')).to.not.have.include.members(['c', 'd']) + action.execute('success', el) + expect((target.getAttribute('class') || '').split(' ')).to.not.have.include.members(['a', 'b', 'c', 'd']) + // expect(target.getAttribute('class')).to.be.oneOf(['', null, undefined]) + }) }) describe('mip action', () => { diff --git a/packages/mip/test/specs/util/parser/parse.spec.js b/packages/mip/test/specs/util/parser/parse.spec.js index c4ea5bd0..97ce3ed4 100644 --- a/packages/mip/test/specs/util/parser/parse.spec.js +++ b/packages/mip/test/specs/util/parser/parse.spec.js @@ -62,7 +62,7 @@ describe('parser', () => { array: [1, 2, 3, 4, 5] }} let result = fn(source) - expect(result).to.be.deep.equal([2, 3, 4]) + expect(result).to.be.deep.equal([1, 5]) expect(source.data.array.length).to.be.equal(5) })