| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| /** |
| * @fileOverview |
| * ViewModel template parser & data-binding process |
| */ |
| |
| import { |
| extend, |
| isObject, |
| bind |
| } from '../util/index' |
| import { |
| initData, |
| initComputed |
| } from '../core/state' |
| import { |
| bindElement, |
| setId, |
| bindSubVm, |
| bindSubVmAfterInitialized, |
| applyNaitveComponentOptions, |
| watch |
| } from './directive' |
| import { |
| createBlock, |
| createBody, |
| createElement, |
| attachTarget, |
| moveTarget, |
| removeTarget |
| } from './dom-helper' |
| |
| /** |
| * build() |
| * compile(template, parentNode) |
| * if (type is content) create contentNode |
| * else if (dirs have v-for) foreach -> create context |
| * -> compile(templateWithoutFor, parentNode): diff(list) onchange |
| * else if (dirs have v-if) assert |
| * -> compile(templateWithoutIf, parentNode): toggle(shown) onchange |
| * else if (type is dynamic) |
| * -> compile(templateWithoutDynamicType, parentNode): watch(type) onchange |
| * else if (type is custom) |
| * addChildVm(vm, parentVm) |
| * build(externalDirs) |
| * foreach childNodes -> compile(childNode, template) |
| * else if (type is native) |
| * set(dirs): update(id/attr/style/class) onchange |
| * append(template, parentNode) |
| * foreach childNodes -> compile(childNode, template) |
| */ |
| export function build (vm) { |
| const opt = vm._options || {} |
| const template = opt.template || {} |
| |
| if (opt.replace) { |
| if (template.children && template.children.length === 1) { |
| compile(vm, template.children[0], vm._parentEl) |
| } |
| else { |
| compile(vm, template.children, vm._parentEl) |
| } |
| } |
| else { |
| compile(vm, template, vm._parentEl) |
| } |
| |
| console.debug(`[JS Framework] "ready" lifecycle in Vm(${vm._type})`) |
| vm.$emit('hook:ready') |
| vm._ready = true |
| } |
| |
| /** |
| * Generate elements by child or children and append to parent elements. |
| * Root element info would be merged if has. The first argument may be an array |
| * if the root element with options.replace has not only one child. |
| * |
| * @param {object|array} target |
| * @param {object} dest |
| * @param {object} meta |
| */ |
| function compile (vm, target, dest, meta) { |
| const app = vm._app || {} |
| |
| if (app.lastSignal === -1) { |
| return |
| } |
| |
| if (target.attr && target.attr.hasOwnProperty('static')) { |
| vm._static = true |
| } |
| |
| if (targetIsFragment(target)) { |
| compileFragment(vm, target, dest, meta) |
| return |
| } |
| meta = meta || {} |
| if (targetIsContent(target)) { |
| console.debug('[JS Framework] compile "content" block by', target) |
| vm._content = createBlock(vm, dest) |
| return |
| } |
| |
| if (targetNeedCheckRepeat(target, meta)) { |
| console.debug('[JS Framework] compile "repeat" logic by', target) |
| if (dest.type === 'document') { |
| console.warn('[JS Framework] The root element does\'t support `repeat` directive!') |
| } |
| else { |
| compileRepeat(vm, target, dest) |
| } |
| return |
| } |
| if (targetNeedCheckShown(target, meta)) { |
| console.debug('[JS Framework] compile "if" logic by', target) |
| if (dest.type === 'document') { |
| console.warn('[JS Framework] The root element does\'t support `if` directive!') |
| } |
| else { |
| compileShown(vm, target, dest, meta) |
| } |
| return |
| } |
| const typeGetter = meta.type || target.type |
| if (targetNeedCheckType(typeGetter, meta)) { |
| compileType(vm, target, dest, typeGetter, meta) |
| return |
| } |
| const type = typeGetter |
| const component = targetIsComposed(vm, target, type) |
| if (component) { |
| console.debug('[JS Framework] compile composed component by', target) |
| compileCustomComponent(vm, component, target, dest, type, meta) |
| return |
| } |
| console.debug('[JS Framework] compile native component by', target) |
| compileNativeComponent(vm, target, dest, type) |
| } |
| |
| /** |
| * Check if target is a fragment (an array). |
| * |
| * @param {object} target |
| * @return {boolean} |
| */ |
| function targetIsFragment (target) { |
| return Array.isArray(target) |
| } |
| |
| /** |
| * Check if target type is content/slot. |
| * |
| * @param {object} target |
| * @return {boolean} |
| */ |
| function targetIsContent (target) { |
| return target.type === 'content' || target.type === 'slot' |
| } |
| |
| /** |
| * Check if target need to compile by a list. |
| * |
| * @param {object} target |
| * @param {object} meta |
| * @return {boolean} |
| */ |
| function targetNeedCheckRepeat (target, meta) { |
| return !meta.hasOwnProperty('repeat') && target.repeat |
| } |
| |
| /** |
| * Check if target need to compile by a boolean value. |
| * |
| * @param {object} target |
| * @param {object} meta |
| * @return {boolean} |
| */ |
| function targetNeedCheckShown (target, meta) { |
| return !meta.hasOwnProperty('shown') && target.shown |
| } |
| |
| /** |
| * Check if target need to compile by a dynamic type. |
| * |
| * @param {string|function} typeGetter |
| * @param {object} meta |
| * @return {boolean} |
| */ |
| function targetNeedCheckType (typeGetter, meta) { |
| return (typeof typeGetter === 'function') && !meta.hasOwnProperty('type') |
| } |
| |
| /** |
| * Check if this kind of component is composed. |
| * |
| * @param {string} type |
| * @return {boolean} |
| */ |
| function targetIsComposed (vm, target, type) { |
| let component |
| if (vm._app && vm._app.customComponentMap) { |
| component = vm._app.customComponentMap[type] |
| } |
| if (vm._options && vm._options.components) { |
| component = vm._options.components[type] |
| } |
| if (target.component) { |
| component = component || {} |
| } |
| return component |
| } |
| |
| /** |
| * Compile a list of targets. |
| * |
| * @param {object} target |
| * @param {object} dest |
| * @param {object} meta |
| */ |
| function compileFragment (vm, target, dest, meta) { |
| const fragBlock = createBlock(vm, dest) |
| target.forEach((child) => { |
| compile(vm, child, fragBlock, meta) |
| }) |
| } |
| |
| /** |
| * Compile a target with repeat directive. |
| * |
| * @param {object} target |
| * @param {object} dest |
| */ |
| function compileRepeat (vm, target, dest) { |
| const repeat = target.repeat |
| const oldStyle = typeof repeat === 'function' |
| let getter = repeat.getter || repeat.expression || repeat |
| if (typeof getter !== 'function') { |
| getter = function () { return [] } |
| } |
| const key = repeat.key || '$index' |
| const value = repeat.value || '$value' |
| const trackBy = repeat.trackBy || target.trackBy || |
| (target.attr && target.attr.trackBy) |
| |
| const fragBlock = createBlock(vm, dest) |
| fragBlock.children = [] |
| fragBlock.data = [] |
| fragBlock.vms = [] |
| |
| bindRepeat(vm, target, fragBlock, { getter, key, value, trackBy, oldStyle }) |
| } |
| |
| /** |
| * Compile a target with if directive. |
| * |
| * @param {object} target |
| * @param {object} dest |
| * @param {object} meta |
| */ |
| function compileShown (vm, target, dest, meta) { |
| const newMeta = { shown: true } |
| const fragBlock = createBlock(vm, dest) |
| |
| if (dest.element && dest.children) { |
| dest.children.push(fragBlock) |
| } |
| |
| if (meta.repeat) { |
| newMeta.repeat = meta.repeat |
| } |
| |
| bindShown(vm, target, fragBlock, newMeta) |
| } |
| |
| /** |
| * Compile a target with dynamic component type. |
| * |
| * @param {object} target |
| * @param {object} dest |
| * @param {function} typeGetter |
| */ |
| function compileType (vm, target, dest, typeGetter, meta) { |
| const type = typeGetter.call(vm) |
| const newMeta = extend({ type }, meta) |
| const fragBlock = createBlock(vm, dest) |
| |
| if (dest.element && dest.children) { |
| dest.children.push(fragBlock) |
| } |
| |
| watch(vm, typeGetter, (value) => { |
| const newMeta = extend({ type: value }, meta) |
| removeTarget(vm, fragBlock, true) |
| compile(vm, target, fragBlock, newMeta) |
| }) |
| |
| compile(vm, target, fragBlock, newMeta) |
| } |
| |
| /** |
| * Compile a composed component. |
| * |
| * @param {object} target |
| * @param {object} dest |
| * @param {string} type |
| */ |
| function compileCustomComponent (vm, component, target, dest, type, meta) { |
| const Ctor = vm.constructor |
| const subVm = new Ctor(type, component, vm, dest, undefined, { |
| 'hook:init': function () { |
| if (vm._static) { |
| this._static = vm._static |
| } |
| setId(vm, null, target.id, this) |
| // bind template earlier because of lifecycle issues |
| this._externalBinding = { |
| parent: vm, |
| template: target |
| } |
| }, |
| 'hook:created': function () { |
| bindSubVm(vm, this, target, meta.repeat) |
| }, |
| 'hook:ready': function () { |
| if (this._content) { |
| compileChildren(vm, target, this._content) |
| } |
| } |
| }) |
| bindSubVmAfterInitialized(vm, subVm, target, dest) |
| } |
| |
| /** |
| * Generate element from template and attach to the dest if needed. |
| * The time to attach depends on whether the mode status is node or tree. |
| * |
| * @param {object} template |
| * @param {object} dest |
| * @param {string} type |
| */ |
| function compileNativeComponent (vm, template, dest, type) { |
| applyNaitveComponentOptions(template) |
| |
| let element |
| if (dest.ref === '_documentElement') { |
| // if its parent is documentElement then it's a body |
| console.debug(`[JS Framework] compile to create body for ${type}`) |
| element = createBody(vm, type) |
| } |
| else { |
| console.debug(`[JS Framework] compile to create element for ${type}`) |
| element = createElement(vm, type) |
| } |
| |
| if (!vm._rootEl) { |
| vm._rootEl = element |
| // bind event earlier because of lifecycle issues |
| const binding = vm._externalBinding || {} |
| const target = binding.template |
| const parentVm = binding.parent |
| if (target && target.events && parentVm && element) { |
| for (const type in target.events) { |
| const handler = parentVm[target.events[type]] |
| if (handler) { |
| element.addEvent(type, bind(handler, parentVm)) |
| } |
| } |
| } |
| } |
| |
| bindElement(vm, element, template) |
| |
| if (template.attr && template.attr.append) { // backward, append prop in attr |
| template.append = template.attr.append |
| } |
| |
| if (template.append) { // give the append attribute for ios adaptation |
| element.attr = element.attr || {} |
| element.attr.append = template.append |
| } |
| |
| const treeMode = template.append === 'tree' |
| const app = vm._app || {} |
| if (app.lastSignal !== -1 && !treeMode) { |
| console.debug('[JS Framework] compile to append single node for', element) |
| app.lastSignal = attachTarget(vm, element, dest) |
| } |
| if (app.lastSignal !== -1) { |
| compileChildren(vm, template, element) |
| } |
| if (app.lastSignal !== -1 && treeMode) { |
| console.debug('[JS Framework] compile to append whole tree for', element) |
| app.lastSignal = attachTarget(vm, element, dest) |
| } |
| } |
| |
| /** |
| * Set all children to a certain parent element. |
| * |
| * @param {object} template |
| * @param {object} dest |
| */ |
| function compileChildren (vm, template, dest) { |
| const app = vm._app || {} |
| const children = template.children |
| if (children && children.length) { |
| children.every((child) => { |
| compile(vm, child, dest) |
| return app.lastSignal !== -1 |
| }) |
| } |
| } |
| |
| /** |
| * Watch the list update and refresh the changes. |
| * |
| * @param {object} target |
| * @param {object} fragBlock {vms, data, children} |
| * @param {object} info {getter, key, value, trackBy, oldStyle} |
| */ |
| function bindRepeat (vm, target, fragBlock, info) { |
| const vms = fragBlock.vms |
| const children = fragBlock.children |
| const { getter, trackBy, oldStyle } = info |
| const keyName = info.key |
| const valueName = info.value |
| |
| function compileItem (item, index, context) { |
| let mergedData |
| if (oldStyle) { |
| mergedData = item |
| if (isObject(item)) { |
| mergedData[keyName] = index |
| if (!mergedData.hasOwnProperty('INDEX')) { |
| Object.defineProperty(mergedData, 'INDEX', { |
| value: () => { |
| console.warn('[JS Framework] "INDEX" in repeat is deprecated, ' + |
| 'please use "$index" instead') |
| } |
| }) |
| } |
| } |
| else { |
| console.warn('[JS Framework] Each list item must be an object in old-style repeat, ' |
| + 'please use `repeat={{v in list}}` instead.') |
| mergedData = {} |
| mergedData[keyName] = index |
| mergedData[valueName] = item |
| } |
| } |
| else { |
| mergedData = {} |
| mergedData[keyName] = index |
| mergedData[valueName] = item |
| } |
| const newContext = mergeContext(context, mergedData) |
| vms.push(newContext) |
| compile(newContext, target, fragBlock, { repeat: item }) |
| } |
| |
| const list = watchBlock(vm, fragBlock, getter, 'repeat', |
| (data) => { |
| console.debug('[JS Framework] the "repeat" item has changed', data) |
| if (!fragBlock || !data) { |
| return |
| } |
| |
| const oldChildren = children.slice() |
| const oldVms = vms.slice() |
| const oldData = fragBlock.data.slice() |
| // 1. collect all new refs track by |
| const trackMap = {} |
| const reusedMap = {} |
| data.forEach((item, index) => { |
| const key = trackBy ? item[trackBy] : (oldStyle ? item[keyName] : index) |
| /* istanbul ignore if */ |
| if (key == null || key === '') { |
| return |
| } |
| trackMap[key] = item |
| }) |
| |
| // 2. remove unused element foreach old item |
| const reusedList = [] |
| oldData.forEach((item, index) => { |
| const key = trackBy ? item[trackBy] : (oldStyle ? item[keyName] : index) |
| if (trackMap.hasOwnProperty(key)) { |
| reusedMap[key] = { |
| item, index, key, |
| target: oldChildren[index], |
| vm: oldVms[index] |
| } |
| reusedList.push(item) |
| } |
| else { |
| removeTarget(vm, oldChildren[index]) |
| } |
| }) |
| |
| // 3. create new element foreach new item |
| children.length = 0 |
| vms.length = 0 |
| fragBlock.data = data.slice() |
| fragBlock.updateMark = fragBlock.start |
| |
| data.forEach((item, index) => { |
| const key = trackBy ? item[trackBy] : (oldStyle ? item[keyName] : index) |
| const reused = reusedMap[key] |
| if (reused) { |
| if (reused.item === reusedList[0]) { |
| reusedList.shift() |
| } |
| else { |
| reusedList.$remove(reused.item) |
| moveTarget(vm, reused.target, fragBlock.updateMark, true) |
| } |
| children.push(reused.target) |
| vms.push(reused.vm) |
| if (oldStyle) { |
| reused.vm = item |
| } |
| else { |
| reused.vm[valueName] = item |
| } |
| reused.vm[keyName] = index |
| fragBlock.updateMark = reused.target |
| } |
| else { |
| compileItem(item, index, vm) |
| } |
| }) |
| |
| delete fragBlock.updateMark |
| } |
| ) |
| |
| fragBlock.data = list.slice(0) |
| list.forEach((item, index) => { |
| compileItem(item, index, vm) |
| }) |
| } |
| |
| /** |
| * Watch the display update and add/remove the element. |
| * |
| * @param {object} target |
| * @param {object} fragBlock |
| * @param {object} context |
| */ |
| function bindShown (vm, target, fragBlock, meta) { |
| const display = watchBlock(vm, fragBlock, target.shown, 'shown', |
| (display) => { |
| console.debug('[JS Framework] the "if" item was changed', display) |
| |
| if (!fragBlock || !!fragBlock.display === !!display) { |
| return |
| } |
| fragBlock.display = !!display |
| if (display) { |
| compile(vm, target, fragBlock, meta) |
| } |
| else { |
| removeTarget(vm, fragBlock, true) |
| } |
| } |
| ) |
| |
| fragBlock.display = !!display |
| if (display) { |
| compile(vm, target, fragBlock, meta) |
| } |
| } |
| |
| /** |
| * Watch calc value changes and append certain type action to differ. |
| * It is used for if or repeat data-binding generator. |
| * |
| * @param {object} fragBlock |
| * @param {function} calc |
| * @param {string} type |
| * @param {function} handler |
| * @return {any} init value of calc |
| */ |
| function watchBlock (vm, fragBlock, calc, type, handler) { |
| const differ = vm && vm._app && vm._app.differ |
| const config = {} |
| const depth = (fragBlock.element.depth || 0) + 1 |
| |
| return watch(vm, calc, (value) => { |
| config.latestValue = value |
| if (differ && !config.recorded) { |
| differ.append(type, depth, fragBlock.blockId, () => { |
| const latestValue = config.latestValue |
| handler(latestValue) |
| config.recorded = false |
| config.latestValue = undefined |
| }) |
| } |
| config.recorded = true |
| }) |
| } |
| |
| /** |
| * Clone a context and merge certain data. |
| * |
| * @param {object} mergedData |
| * @return {object} |
| */ |
| function mergeContext (context, mergedData) { |
| const newContext = Object.create(context) |
| newContext._data = mergedData |
| initData(newContext) |
| initComputed(newContext) |
| newContext._realParent = context |
| if (context._static) { |
| newContext._static = context._static |
| } |
| return newContext |
| } |