blob: 77ccd9a5f96881fb1fb103a02ec4ffb15c36a398 [file] [log] [blame]
/*
* 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
* Directive Parser
*/
import { bind, typof } from '../util/index'
import Watcher from '../core/watcher'
import config from '../config'
const { nativeComponentMap } = config
const SETTERS = {
attr: 'setAttr',
style: 'setStyle',
event: 'addEvent'
}
/**
* apply the native component's options(specified by template.type)
* to the template
*/
export function applyNaitveComponentOptions (template) {
const { type } = template
const options = nativeComponentMap[type]
if (typeof options === 'object') {
for (const key in options) {
if (template[key] == null) {
template[key] = options[key]
}
else if (typof(template[key]) === 'object' &&
typof(options[key]) === 'object') {
for (const subkey in options[key]) {
if (template[key][subkey] == null) {
template[key][subkey] = options[key][subkey]
}
}
}
}
}
}
/**
* bind all id, attr, classnames, style, events to an element
*/
export function bindElement (vm, el, template) {
setId(vm, el, template.id, vm)
setAttr(vm, el, template.attr)
setClass(vm, el, template.classList)
setStyle(vm, el, template.style)
bindEvents(vm, el, template.events)
}
/**
* bind all props to sub vm and bind all style, events to the root element
* of the sub vm if it doesn't have a replaced multi-node fragment
*/
export function bindSubVm (vm, subVm, template, repeatItem) {
subVm = subVm || {}
template = template || {}
const options = subVm._options || {}
// bind props
let props = options.props
if (Array.isArray(props)) {
props = props.reduce((result, value) => {
result[value] = true
return result
}, {})
}
mergeProps(repeatItem, props, vm, subVm)
mergeProps(template.attr, props, vm, subVm)
}
/**
* merge class and styles from vm to sub vm.
*/
export function bindSubVmAfterInitialized (vm, subVm, template, target = {}) {
mergeClassStyle(template.classList, vm, subVm)
mergeStyle(template.style, vm, subVm)
// bind subVm to the target element
if (target.children) {
target.children[target.children.length - 1]._vm = subVm
}
else {
target._vm = subVm
}
}
/**
* Bind props from vm to sub vm and watch their updates.
*/
function mergeProps (target, props, vm, subVm) {
if (!target) {
return
}
for (const key in target) {
if (!props || props[key]) {
const value = target[key]
if (typeof value === 'function') {
const returnValue = watch(vm, value, function (v) {
subVm[key] = v
})
subVm[key] = returnValue
}
else {
subVm[key] = value
}
}
}
}
/**
* Bind style from vm to sub vm and watch their updates.
*/
function mergeStyle (target, vm, subVm) {
for (const key in target) {
const value = target[key]
if (typeof value === 'function') {
const returnValue = watch(vm, value, function (v) {
if (subVm._rootEl) {
subVm._rootEl.setStyle(key, v)
}
})
subVm._rootEl.setStyle(key, returnValue)
}
else {
if (subVm._rootEl) {
subVm._rootEl.setStyle(key, value)
}
}
}
}
/**
* Bind class & style from vm to sub vm and watch their updates.
*/
function mergeClassStyle (target, vm, subVm) {
const css = vm._options && vm._options.style || {}
/* istanbul ignore if */
if (!subVm._rootEl) {
return
}
const className = '@originalRootEl'
css[className] = subVm._rootEl.classStyle
function addClassName (list, name) {
if (typof(list) === 'array') {
list.unshift(name)
}
}
if (typeof target === 'function') {
const value = watch(vm, target, v => {
addClassName(v, className)
setClassStyle(subVm._rootEl, css, v)
})
addClassName(value, className)
setClassStyle(subVm._rootEl, css, value)
}
else if (target != null) {
addClassName(target, className)
setClassStyle(subVm._rootEl, css, target)
}
}
/**
* bind id to an element
* each id is unique in a whole vm
*/
export function setId (vm, el, id, target) {
const map = Object.create(null)
Object.defineProperties(map, {
vm: {
value: target,
writable: false,
configurable: false
},
el: {
get: () => el || target._rootEl,
configurable: false
}
})
if (typeof id === 'function') {
const handler = id
id = handler.call(vm)
if (id || id === 0) {
vm._ids[id] = map
}
watch(vm, handler, (newId) => {
if (newId) {
vm._ids[newId] = map
}
})
}
else if (id && typeof id === 'string') {
vm._ids[id] = map
}
}
/**
* bind attr to an element
*/
function setAttr (vm, el, attr) {
bindDir(vm, el, 'attr', attr)
}
function setClassStyle (el, css, classList) {
if (typeof classList === 'string') {
classList = classList.split(/\s+/)
}
classList.forEach((name, i) => {
classList.splice(i, 1, ...name.split(/\s+/))
})
const classStyle = {}
const length = classList.length
for (let i = 0; i < length; i++) {
const style = css[classList[i]]
if (style) {
Object.keys(style).forEach((key) => {
classStyle[key] = style[key]
})
}
}
el.setClassStyle(classStyle)
}
/**
* bind classnames to an element
*/
function setClass (vm, el, classList) {
if (typeof classList !== 'function' && !Array.isArray(classList)) {
return
}
if (Array.isArray(classList) && !classList.length) {
el.setClassStyle({})
return
}
const style = vm._options && vm._options.style || {}
if (typeof classList === 'function') {
const value = watch(vm, classList, v => {
setClassStyle(el, style, v)
})
setClassStyle(el, style, value)
}
else {
setClassStyle(el, style, classList)
}
}
/**
* bind style to an element
*/
function setStyle (vm, el, style) {
bindDir(vm, el, 'style', style)
}
/**
* add an event type and handler to an element and generate a dom update
*/
function setEvent (vm, el, type, handler) {
el.addEvent(type, bind(handler, vm))
}
/**
* add all events of an element
*/
function bindEvents (vm, el, events) {
if (!events) {
return
}
const keys = Object.keys(events)
let i = keys.length
while (i--) {
const key = keys[i]
let handler = events[key]
if (typeof handler === 'string') {
handler = vm[handler]
/* istanbul ignore if */
if (!handler) {
console.warn(`[JS Framework] The event handler "${handler}" is not defined.`)
}
}
setEvent(vm, el, key, handler)
}
}
/**
* set a series of members as a kind of an element
* for example: style, attr, ...
* if the value is a function then bind the data changes
*/
function bindDir (vm, el, name, data) {
if (!data) {
return
}
const keys = Object.keys(data)
let i = keys.length
while (i--) {
const key = keys[i]
const value = data[key]
if (typeof value === 'function') {
bindKey(vm, el, name, key, value)
}
else {
el[SETTERS[name]](key, value)
}
}
}
/**
* bind data changes to a certain key to a name series in an element
*/
function bindKey (vm, el, name, key, calc) {
const methodName = SETTERS[name]
// watch the calc, and returns a value by calc.call()
const value = watch(vm, calc, (value) => {
function handler () {
el[methodName](key, value)
}
const differ = vm && vm._app && vm._app.differ
if (differ) {
differ.append('element', el.depth || 0, el.ref, handler)
}
else {
handler()
}
})
el[methodName](key, value)
}
/**
* watch a calc function and callback if the calc value changes
*/
export function watch (vm, calc, callback) {
if (vm._static) {
return calc.call(vm, vm)
}
const watcher = new Watcher(vm, calc, function (value, oldValue) {
/* istanbul ignore if */
if (typeof value !== 'object' && value === oldValue) {
return
}
callback(value)
})
return watcher.value
}