• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements.  See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership.  The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License.  You may obtain a copy of the License at
9 *
10 *   http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied.  See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19
20/**
21 * @fileOverview
22 * ViewModel template parser & data-binding process
23 */
24
25import {
26  hasOwn,
27  Log,
28  removeItem
29} from '../../utils/index';
30import {
31  initData,
32  initComputed
33} from '../reactivity/state';
34import {
35  bindElement,
36  setClass,
37  setIdStyle,
38  setTagStyle,
39  setId,
40  bindSubVm,
41  bindSubVmAfterInitialized,
42  newWatch
43} from './directive';
44import {
45  createBlock,
46  createBody,
47  createElement,
48  attachTarget,
49  moveTarget,
50  removeTarget
51} from './domHelper';
52import {
53  bindPageLifeCycle
54} from './pageLife';
55import Vm from './index';
56import Element from '../../vdom/Element';
57import Comment from '../../vdom/Comment';
58import Node from '../../vdom/Node';
59import { VmOptions } from './vmOptions';
60
61export interface FragBlockInterface {
62  start: Comment;
63  end: Comment;
64  element?: Element;
65  blockId: number;
66  children?: any[];
67  data?: any[];
68  vms?: Vm[];
69  updateMark?: Node;
70  display?: boolean;
71  type?: string;
72  vm?: Vm;
73}
74
75export interface AttrInterface {
76  type: string;
77  value: () => void | string;
78  tid: number;
79  append: string;
80  slot: string;
81  name: string;
82  data: () => any | string;
83}
84
85export interface TemplateInterface {
86  type: string;
87  attr: Partial<AttrInterface>;
88  classList?: () => any | string[];
89  children?: TemplateInterface[];
90  events?: object;
91  repeat?: () => any | RepeatInterface;
92  shown?: () => any;
93  style?: Record<string, string>;
94  id?: () => any | string;
95  append?: string;
96  onBubbleEvents?: object;
97  onCaptureEvents?: object;
98  catchBubbleEvents?: object;
99  catchCaptureEvents?: object;
100}
101
102interface RepeatInterface {
103  exp: () => any;
104  key?: string;
105  value?: string;
106  tid?: number;
107}
108
109interface MetaInterface {
110  repeat: object;
111  shown: boolean;
112  type: string;
113}
114
115interface ConfigInterface {
116  latestValue: undefined | string | number;
117  recorded: boolean;
118}
119
120export function build(vm: Vm) {
121  const opt: any = vm.vmOptions || {};
122  const template: any = opt.template || {};
123  compile(vm, template, vm.parentEl);
124  Log.debug(`"OnReady" lifecycle in Vm(${vm.type}).`);
125  vm.$emit('hook:onReady');
126  if (vm.parent) {
127    vm.$emit('hook:onAttached');
128  }
129  vm.ready = true;
130}
131
132/**
133 * Compile the Virtual Dom.
134 * @param {Vm} vm - Vm object needs to be compiled.
135 * @param {TemplateInterface} target - Node need to be compiled. Structure of the label in the template.
136 * @param {FragBlockInterface | Element} dest - Parent Node's VM of current.
137 * @param {MetaInterface} [meta] - To transfer data.
138 */
139function compile(vm: Vm, target: TemplateInterface, dest: FragBlockInterface | Element, meta?: Partial<MetaInterface>): void {
140  const app: any = vm.app || {};
141  if (app.lastSignal === -1) {
142    return;
143  }
144  meta = meta || {};
145  if (targetIsSlot(target)) {
146    compileSlot(vm, target, dest as Element);
147    return;
148  }
149
150  if (targetNeedCheckRepeat(target, meta)) {
151    if (dest.type === 'document') {
152      Log.warn('The root element does\'t support `repeat` directive!');
153    } else {
154      compileRepeat(vm, target, dest as Element);
155    }
156    return;
157  }
158  if (targetNeedCheckShown(target, meta)) {
159    if (dest.type === 'document') {
160      Log.warn('The root element does\'t support `if` directive!');
161    } else {
162      compileShown(vm, target, dest, meta);
163    }
164    return;
165  }
166  const type = meta.type || target.type;
167  const component: VmOptions | null = targetIsComposed(vm, type);
168  if (component) {
169    compileCustomComponent(vm, component, target, dest, type, meta);
170    return;
171  }
172  if (targetIsBlock(target)) {
173    compileBlock(vm, target, dest);
174    return;
175  }
176  compileNativeComponent(vm, target, dest, type);
177}
178
179/**
180 * Check if target type is slot.
181 *
182 * @param  {object}  target
183 * @return {boolean}
184 */
185function targetIsSlot(target: TemplateInterface) {
186  return target.type === 'slot';
187}
188
189/**
190 * Check if target needs to compile by a list.
191 * @param {TemplateInterface} target - Node needs to be compiled. Structure of the label in the template.
192 * @param {MetaInterface} meta - To transfer data.
193 * @return {boolean} - True if target needs repeat. Otherwise return false.
194 */
195function targetNeedCheckRepeat(target: TemplateInterface, meta: Partial<MetaInterface>) {
196  return !hasOwn(meta, 'repeat') && target.repeat;
197}
198
199/**
200 * Check if target needs to compile by a 'if' or 'shown' value.
201 * @param {TemplateInterface} target - Node needs to be compiled. Structure of the label in the template.
202 * @param {MetaInterface} meta - To transfer data.
203 * @return {boolean} - True if target needs a 'shown' value. Otherwise return false.
204 */
205function targetNeedCheckShown(target: TemplateInterface, meta: Partial<MetaInterface>) {
206  return !hasOwn(meta, 'shown') && target.shown;
207}
208
209/**
210 * Check if this kind of component is composed.
211 * @param {Vm} vm - Vm object needs to be compiled.
212 * @param {string} type - Component type.
213 * @return {VmOptions} Component.
214 */
215function targetIsComposed(vm: Vm, type: string): VmOptions {
216  let component;
217  if (vm.app && vm.app.customComponentMap) {
218    component = vm.app.customComponentMap[type];
219  }
220  if (component) {
221    if (component.data && typeof component.data === 'object') {
222      if (!component.initObjectData) {
223        component.initObjectData = component.data;
224      }
225      component.data = Object.assign({}, component.initObjectData);
226    }
227  }
228  return component;
229}
230
231/**
232 * Compile a target with repeat directive.
233 * @param {Vm} vm - Vm object needs to be compiled.
234 * @param {TemplateInterface} target - Node needs to be compiled. Structure of the label in the template.
235 * @param {dest} dest - Node need to be appended.
236 */
237function compileSlot(vm: Vm, target: TemplateInterface, dest: Element): Element {
238  if (!vm.slotContext) {
239    // slot in root vm
240    return;
241  }
242
243  const slotDest = createBlock(vm, dest);
244
245  // reslove slot contentext
246  const namedContents = vm.slotContext.content;
247  const parentVm = vm.slotContext.parentVm;
248  const slotItem = { target, dest: slotDest };
249  const slotName = target.attr.name || 'default';
250
251  // acquire content by name
252  const namedContent = namedContents[slotName];
253  if (!namedContent) {
254    compileChildren(vm, slotItem.target, slotItem.dest);
255  } else {
256    compileChildren(parentVm, { children: namedContent }, slotItem.dest);
257  }
258}
259
260/**
261 * Compile a target with repeat directive.
262 * @param {Vm} vm - Vm object needs to be compiled.
263 * @param {TemplateInterface} target - Node needs to be compiled. Structure of the label in the template.
264 * @param {Element} dest - Parent Node's VM of current.
265 */
266function compileRepeat(vm: Vm, target: TemplateInterface, dest: Element): void {
267  const repeat = target.repeat;
268  let getter: any;
269  let key: any;
270  let value: any;
271  let trackBy: any;
272
273  if (isRepeat(repeat)) {
274    getter = repeat.exp;
275    key = repeat.key || '$idx';
276    value = repeat.value;
277    trackBy = repeat.tid;
278  } else {
279    getter = repeat;
280    key = '$idx';
281    value = '$item';
282    trackBy = target.attr && target.attr.tid;
283  }
284  if (typeof getter !== 'function') {
285    getter = function() {
286      return [];
287    };
288  }
289  const fragBlock: FragBlockInterface = createBlock(vm, dest);
290  fragBlock.children = [];
291  fragBlock.data = [];
292  fragBlock.vms = [];
293  bindRepeat(vm, target, fragBlock, { getter, key, value, trackBy });
294}
295
296/**
297 * Compile a target with 'if' directive.
298 * @param {Vm} vm - Vm object needs to be compiled.
299 * @param {TemplateInterface} target - Node needs to be compiled. Structure of the label in the template.
300 * @param {FragBlockInterface | Element} dest - Parent Node's VM of current.
301 * @param {MetaInterface} meta - To transfer data.
302 */
303function compileShown(
304  vm: Vm,
305  target: TemplateInterface,
306  dest: Element | FragBlockInterface,
307  meta: Partial<MetaInterface>
308): void {
309  const newMeta: Partial<MetaInterface> = { shown: true };
310  const fragBlock = createBlock(vm, dest);
311  if (isBlock(dest) && dest.children) {
312    dest.children.push(fragBlock);
313  }
314  if (hasOwn(meta, 'repeat')) {
315    newMeta.repeat = meta.repeat;
316  }
317  bindShown(vm, target, fragBlock, newMeta);
318}
319
320/**
321 * Support <block>.
322 * @param {TemplateInterface} target - Node needs to be compiled. Structure of the label in the template.
323 * @return {boolean} True if target supports bolck. Otherwise return false.
324 */
325function targetIsBlock(target: TemplateInterface): boolean {
326  return target.type === 'block';
327}
328
329/**
330 * If <block> create block and compile the children node.
331 * @param {Vm} vm - Vm object needs to be compiled.
332 * @param {TemplateInterface} target - Node needs to be compiled. Structure of the label in the template.
333 * @param {Element | FragBlockInterface} dest - Parent Node's VM of current.
334 */
335function compileBlock(vm: Vm, target: TemplateInterface, dest: Element | FragBlockInterface): void {
336  const block = createBlock(vm, dest);
337  if (isBlock(dest) && dest.children) {
338    dest.children.push(block);
339  }
340  const app: any = vm.app || {};
341  const children = target.children;
342  if (children && children.length) {
343    children.every((child) => {
344      compile(vm, child, block);
345      return app.lastSignal !== -1;
346    });
347  }
348}
349
350/**
351 * Compile a composed component.
352 * @param {Vm} vm - Vm object needs to be compiled.
353 * @param {VmOptions} component - Composed component.
354 * @param {TemplateInterface} target - Node needs to be compiled. Structure of the label in the template.
355 * @param {Element | FragBlockInterface} dest - Parent Node's VM of current.
356 * @param {string} type - Component Type.
357 * @param {MetaInterface} meta - To transfer data.
358 */
359function compileCustomComponent(
360  vm: Vm,
361  component: VmOptions,
362  target: TemplateInterface,
363  dest: Element | FragBlockInterface,
364  type: string,
365  meta: Partial<MetaInterface>
366): void {
367  const subVm = new Vm(
368    type,
369    component,
370    vm,
371    dest,
372    undefined,
373    {
374      'hook:_innerInit': function() {
375        // acquire slot content of context
376        const namedContents = {};
377        if (target.children) {
378          target.children.forEach(item => {
379            const slotName = item.attr.slot || 'default';
380            if (namedContents[slotName]) {
381              namedContents[slotName].push(item);
382            } else {
383              namedContents[slotName] = [item];
384            }
385          });
386        }
387        this.slotContext = { content: namedContents, parentVm: vm };
388        setId(vm, null, target.id, this);
389
390        // Bind template earlier because of lifecycle issues.
391        this.externalBinding = {
392          parent: vm,
393          template: target
394        };
395
396        // Bind props before init data.
397        bindSubVm(vm, this, target, meta.repeat);
398      }
399    });
400  bindSubVmAfterInitialized(vm, subVm, target, dest);
401}
402
403/**
404 * Reset the element style.
405 * @param {Vm} vm - Vm object needs to be compiled.
406 * @param {Element} element - To be reset.
407 */
408function resetElementStyle(vm: Vm, element: Element): void {
409  // Add judgment statements to avoid repeatedly calling 'setClass' function.
410  let len = 0;
411  if (element.children !== undefined) {
412    len = element.children.length;
413  }
414  const css = vm.css || {};
415  const mqArr = css['@MEDIA'];
416  for (let ii = 0; ii < len; ii++) {
417    const el = element.children[ii] as Element;
418    if (!el.isCustomComponent) {
419      resetElementStyle(vm, el);
420    }
421  }
422  if (element.type) {
423    setTagStyle(vm, element, element.type);
424  }
425  if (element.id) {
426    setIdStyle(vm, element, element.id);
427  }
428  if (element.classList && mqArr) {
429    for (let i = 0; i < element.classList.length; i++) {
430      for (let m = 0; m < mqArr.length; m++) {
431        const clsKey = '.' + element.classList[i];
432        if (hasOwn(mqArr[m], clsKey)) {
433          setClass(vm, element, element.classList);
434        }
435      }
436    }
437  }
438}
439
440/**
441 * <p>Generate element from template and attach to the dest if needed.</p>
442 * <p>The time to attach depends on whether the mode status is node or tree.</p>
443 * @param {Vm} vm - Vm object needs to be compiled.
444 * @param {TemplateInterface} template - Generate element from template.
445 * @param {FragBlockInterface | Element} dest - Parent Node's VM of current.
446 * @param {string} type - Vm type.
447 */
448function compileNativeComponent(vm: Vm, template: TemplateInterface, dest: FragBlockInterface | Element, type: string): void {
449  function handleViewSizeChanged(e) {
450    if (!vm.mediaStatus) {
451      vm.mediaStatus = {};
452    }
453    vm.mediaStatus.orientation = e.orientation;
454    vm.mediaStatus.width = e.width;
455    vm.mediaStatus.height = e.height;
456    vm.mediaStatus.resolution = e.resolution;
457    vm.mediaStatus['device-type'] = e.deviceType;
458    vm.mediaStatus['aspect-ratio'] = e.aspectRatio;
459    vm.mediaStatus['device-width'] = e.deviceWidth;
460    vm.mediaStatus['device-height'] = e.deviceHeight;
461    vm.mediaStatus['round-screen'] = e.roundScreen;
462    vm.mediaStatus['dark-mode'] = e.darkMode;
463    const css = vm.vmOptions && vm.vmOptions.style || {};
464    const mqArr = css['@MEDIA'];
465    if (!mqArr) {
466      return;
467    }
468    if (e.isInit && vm.init) {
469      return;
470    }
471    vm.init = true;
472    resetElementStyle(vm, e.currentTarget);
473    e.currentTarget.addEvent('show');
474  }
475
476  let element;
477  if (!isBlock(dest) && dest.ref === '_documentElement') {
478    // If its parent is documentElement then it's a body.
479    element = createBody(vm, type);
480  } else {
481    element = createElement(vm, type);
482    element.destroyHook = function() {
483      if (element.block !== undefined) {
484        removeTarget(element.block);
485      }
486      if (element.watchers !== undefined) {
487        element.watchers.forEach(function(watcher) {
488          watcher.teardown();
489        });
490        element.watchers = [];
491      }
492    };
493  }
494
495  if (!vm.rootEl) {
496    vm.rootEl = element;
497
498    // Bind event earlier because of lifecycle issues.
499    const binding: any = vm.externalBinding || {};
500    const target = binding.template;
501    const parentVm = binding.parent;
502    if (target && target.events && parentVm && element) {
503      for (const type in target.events) {
504        const handler = parentVm[target.events[type]];
505        if (handler) {
506          element.addEvent(type, handler.bind(parentVm));
507        }
508      }
509    }
510    // Page show hide life circle hook function.
511    bindPageLifeCycle(vm, element);
512    element.setCustomFlag();
513    element.customFlag = true;
514    vm.init = true;
515    element.addEvent('viewsizechanged', handleViewSizeChanged);
516  }
517
518  // Dest is parent element.
519  bindElement(vm, element, template, dest);
520  if (element.event && element.event['attached']) {
521    element.fireEvent('attached', {});
522  }
523
524  if (template.attr && template.attr.append) {
525    template.append = template.attr.append;
526  }
527  if (template.append) {
528    element.attr = element.attr || {};
529    element.attr.append = template.append;
530  }
531  let treeMode = template.append === 'tree';
532  const app: any = vm.app || {};
533
534  // Record the parent node of treeMode, used by class selector.
535  if (treeMode) {
536    if (!global.treeModeParentNode) {
537      global.treeModeParentNode = dest;
538    } else {
539      treeMode = false;
540    }
541  }
542  if (app.lastSignal !== -1 && !treeMode) {
543    app.lastSignal = attachTarget(element, dest);
544  }
545  if (app.lastSignal !== -1) {
546    compileChildren(vm, template, element);
547  }
548  if (app.lastSignal !== -1 && treeMode) {
549    delete global.treeModeParentNode;
550    app.lastSignal = attachTarget(element, dest);
551  }
552}
553
554/**
555 * Set all children to a certain parent element.
556 * @param {Vm} vm - Vm object needs to be compiled.
557 * @param {any} template - Generate element from template.
558 * @param {Element | FragBlockInterface} dest - Parent Node's VM of current.
559 * @return {void | boolean} If there is no children, return null. Return true if has node.
560 */
561function compileChildren(vm: Vm, template: any, dest: Element | FragBlockInterface): void | boolean {
562  const app: any = vm.app || {};
563  const children = template.children;
564  if (children && children.length) {
565    children.every((child) => {
566      compile(vm, child, dest);
567      return app.lastSignal !== -1;
568    });
569  }
570}
571
572/**
573 * Watch the list update and refresh the changes.
574 * @param {Vm} vm - Vm object need to be compiled.
575 * @param {TemplateInterface} target - Node needs to be compiled. Structure of the label in the template.
576 * @param {FragBlockInterface} fragBlock - {vms, data, children}
577 * @param {*} info - {getter, key, value, trackBy, oldStyle}
578 */
579function bindRepeat(vm: Vm, target: TemplateInterface, fragBlock: FragBlockInterface, info: any): void {
580  const vms = fragBlock.vms;
581  const children = fragBlock.children;
582  const { getter, trackBy } = info;
583  const keyName = info.key;
584  const valueName = info.value;
585
586  function compileItem(item: any, index: number, context: Vm) {
587    const mergedData = {};
588    mergedData[keyName] = index;
589    mergedData[valueName] = item;
590    const newContext = mergeContext(context, mergedData);
591    vms.push(newContext);
592    compile(newContext, target, fragBlock, { repeat: item });
593  }
594  const list = watchBlock(vm, fragBlock, getter, 'repeat',
595    (data) => {
596      Log.debug(`The 'repeat' item has changed ${data}.`);
597      if (!fragBlock || !data) {
598        return;
599      }
600      const oldChildren = children.slice();
601      const oldVms = vms.slice();
602      const oldData = fragBlock.data.slice();
603
604      // Collect all new refs track by.
605      const trackMap = {};
606      const reusedMap = {};
607      data.forEach((item, index) => {
608        const key = trackBy && item[trackBy] !== undefined ? item[trackBy] : index;
609        if (key === null || key === '') {
610          return;
611        }
612        trackMap[key] = item;
613      });
614
615      // Remove unused element foreach old item.
616      const reusedList: any[] = [];
617      oldData.forEach((item, index) => {
618        const key = trackBy && item[trackBy] !== undefined ? item[trackBy] : index;
619        if (hasOwn(trackMap, key)) {
620          reusedMap[key] = {
621            item, index, key,
622            target: oldChildren[index],
623            vm: oldVms[index]
624          };
625          reusedList.push(item);
626        } else {
627          removeTarget(oldChildren[index]);
628        }
629      });
630
631      // Create new element for each new item.
632      children.length = 0;
633      vms.length = 0;
634      fragBlock.data = data.slice();
635      fragBlock.updateMark = fragBlock.start;
636
637      data.forEach((item, index) => {
638        const key = trackBy && item[trackBy] !== undefined ? item[trackBy] : index;
639        const reused = reusedMap[key];
640        if (reused) {
641          if (reused.item === reusedList[0]) {
642            reusedList.shift();
643          } else {
644            removeItem(reusedList, reused.item);
645            moveTarget(reused.target, fragBlock.updateMark);
646          }
647          children.push(reused.target);
648          vms.push(reused.vm);
649          reused.vm[valueName] = item;
650
651          reused.vm[keyName] = index;
652          fragBlock.updateMark = reused.target;
653        } else {
654          compileItem(item, index, vm);
655        }
656      });
657      delete fragBlock.updateMark;
658    }
659  );
660  if (list && Array.isArray(list)) {
661    fragBlock.data = list.slice(0);
662    list.forEach((item, index) => {
663      compileItem(item, index, vm);
664    });
665  }
666}
667
668/**
669 * Watch the display update and add/remove the element.
670 * @param {Vm} vm - Vm object needs to be compiled.
671 * @param {TemplateInterface} target - Node needs to be compiled. Structure of the label in the template.
672 * @param {FragBlockInterface} fragBlock - {vms, data, children}
673 * @param {MetaInterface} meta - To transfer data.
674 */
675function bindShown(
676  vm: Vm,
677  target: TemplateInterface,
678  fragBlock: FragBlockInterface,
679  meta: Partial<MetaInterface>
680): void {
681  const display = watchBlock(vm, fragBlock, target.shown, 'shown',
682    (display) => {
683      Log.debug(`The 'if' item was changed ${display}.`);
684      if (!fragBlock || !!fragBlock.display === !!display) {
685        return;
686      }
687      fragBlock.display = !!display;
688      if (display) {
689        compile(vm, target, fragBlock, meta);
690      } else {
691        removeTarget(fragBlock, true);
692      }
693    }
694  );
695
696  fragBlock.display = !!display;
697  if (display) {
698    compile(vm, target, fragBlock, meta);
699  }
700}
701
702/**
703 * Watch calc changes and append certain type action to differ.
704 * @param {Vm} vm - Vm object needs to be compiled.
705 * @param {FragBlockInterface} fragBlock - {vms, data, children}
706 * @param {Function} calc - Function.
707 * @param {string} type - Vm type.
708 * @param {Function} handler - Function.
709 * @return {*} Init value of calc.
710 */
711function watchBlock(vm: Vm, fragBlock: FragBlockInterface, calc: Function, type: string, handler: Function): any {
712  const differ = vm && vm.app && vm.app.differ;
713  const config: Partial<ConfigInterface> = {};
714  const newWatcher = newWatch(vm, calc, (value) => {
715    config.latestValue = value;
716    if (differ && !config.recorded) {
717      differ.append(type, fragBlock.blockId.toString(), () => {
718        const latestValue = config.latestValue;
719        handler(latestValue);
720        config.recorded = false;
721        config.latestValue = undefined;
722      });
723    }
724    config.recorded = true;
725  });
726  fragBlock.end.watchers.push(newWatcher);
727  return newWatcher.value;
728}
729
730/**
731 * Clone a context and merge certain data.
732 * @param {Vm} context - Context value.
733 * @param {Object} mergedData - Certain data.
734 * @return {*} The new context.
735 */
736function mergeContext(context: Vm, mergedData: object): any {
737  const newContext = Object.create(context);
738  newContext._data = mergedData;
739  newContext._shareData = {};
740  initData(newContext);
741  initComputed(newContext);
742  newContext._realParent = context;
743  return newContext;
744}
745
746/**
747 * Check if it needs repeat.
748 * @param {Function | RepeatInterface} repeat - Repeat value.
749 * @return {boolean} - True if it needs repeat. Otherwise return false.
750 */
751function isRepeat(repeat: Function | RepeatInterface): repeat is RepeatInterface {
752  const newRepeat = <RepeatInterface>repeat;
753  return newRepeat.exp !== undefined;
754}
755
756/**
757 * Check if it is a block.
758 * @param {FragBlockInterface | Node} node - Node value.
759 * @return {boolean} - True if it is a block. Otherwise return false.
760 */
761export function isBlock(node: FragBlockInterface | Node): node is FragBlockInterface {
762  const newNode = <FragBlockInterface>node;
763  return newNode.blockId !== undefined;
764}
765