<|start_filename|>test/akili.js<|end_filename|>
import { assert } from 'chai';
import Akili from '../src/akili.js';
import globals from '../src/globals.js';
import elements from './app/elements.js';
import ForCompile from './app/for-compile.js';

describe('akili.js', () => {
  describe('Akili', () => {
    describe('scope manipulations', () => {
      let name;
      let scope;
      
      before(() => {
        name = "fakeScope";
        scope = new Akili.Scope(name, document.createElement('div'));
      });

      describe('.addScope()', () => {
        it('should add new scope to the scopes list', () => {
          Akili.addScope(scope);
          assert.property(Akili.__scopes, name);
        });

        it('should not add scope with existent name', () => {
          assert.throws(() => {
            Akili.addScope(scope)
          });
        });
      });

      describe('.getScope()', () => {
        it('should return current scope', () => {
          assert.strictEqual(Akili.getScope(name), scope);
        });
      });
    });

    describe('.getAkiliParents()', () => {
      let parents;
      
      before(() => {
        parents = Akili.getAkiliParents(elements.sectionOneOne);
      });

      it('should return three parents', () => {
        assert.equal(parents.length, 3);
      });

      it('should be right order', () => {
        assert.strictEqual(parents[0], elements.sectionOne, 'check first parent');
        assert.strictEqual(parents[1], elements.app, 'check second parent');
        assert.strictEqual(parents[2], Akili.__root, 'check root element');
      });

      it('should return two parents', () => {
        parents = Akili.getAkiliParents(elements.sectionTwo);
        assert.equal(parents.length, 2);
      });
    });

    describe('.setTemplate()', () => {
      it('should return template with content replacement', () => {
        let el = document.createElement('div');

        el.innerHTML = "<b>World</b>";
        assert.equal(Akili.setTemplate(el, "<i>Hello</i>${this.__content}"), "<i>Hello</i><b>World</b>");
      });
    });

    describe('.createScopeName()', () => {
      it('should return string value', () => {
        assert.isString(Akili.createScopeName());
      });

      it('should be unique', () => {
        assert.isString(Akili.createScopeName());
      });
    });

    describe('.component()', () => {
      it('should add a component to the list', () => {
        Akili.component('for-compile', ForCompile);
        assert.property(Akili.__components, 'for-compile');
      });

      it('should get a component from the list', () => {        
        assert.strictEqual(Akili.component('for-compile'), ForCompile);
      });
    });

    describe('.removeComponent()', () => {
      it('should remove a component from the list', () => {
        Akili.removeComponent('for-compile');
        assert.notProperty(Akili.__components, 'for-compile');
      });
    });

    describe('.alias()', () => {
      it('should add an alias to the list', () => {
        Akili.alias('alias', 'component');
        assert.property(Akili.__aliases, 'alias');
      });

      it('should get an alias from the list', () => {        
        assert.strictEqual(Akili.alias('alias'), 'component');
      });
    });

    describe('.removeAlias()', () => {
      it('should remove an alias from the list', () => {
        Akili.removeAlias('alias');
        assert.notProperty(Akili.__aliases, 'alias');
      });
    });

    describe('.compile()', () => {
      it('should create component for the element', () => {
        Akili.component('for-compile', ForCompile);
        return Akili.compile(elements.forCompile).then(() => {
          assert.instanceOf(elements.forCompile.__akili, Akili.Component);
        });
      });

      it('should not recompile existing', () => {
        return Akili.compile(elements.app).then(() => {
          assert.isNotOk(elements.app.__akili.wasRecompiled);
        });
      });

      it('should recompile existing', () => {
        return Akili.compile(elements.app, { recompile: true, checkChanges: false }).then(() => {
          assert.equal(elements.sectionOneOne.innerHTML, '2', 'full recompiling');
        });        
      });
    });

    describe('.createCallbackIsolation()', () => {
      it('should be in the isolation', () => {
        let fn = callback => callback();
        fn = Akili.createCallbackIsolation(fn);
        fn(() => assert.isOk(Akili.__isolation, 'check before'));
        assert.isNotOk(Akili.__isolation, 'check after');
      });
    });

    describe('.prepareServerSideHtml()', () => {
      it('should return the right html', () => {
        let res = Akili.prepareServerSideHtml();
        assert.equal(Akili.__root.outerHTML, res);
      });
    }); 

    describe('.isolate()', () => {
      let component;

      before(() => {
        component = elements.sectionTwo.__akili;
      });

      it('should create isolation object', () => {
        Akili.isolate(() => {
          assert.isOk(Akili.__isolation);
        });
      });
    
      it('should be isolate', () => {
        Akili.isolate(() => {
          component.scope.html = '1';
          assert.equal(component.el.innerHTML, '0', 'check changing');
          delete component.scope.html;
          assert.equal(component.el.innerHTML, '0', 'check deleting');
          component.scope.html = '2';
          assert.equal(component.el.innerHTML, '0', 'check resetting');
        });

        assert.equal(component.el.innerHTML, '2', 'check a result');
      });

      it('should not be isolate', () => {
        component.scope.html = '3';
        assert.equal(component.el.innerHTML, '3');
      });
    });

    describe('tags', () => {
      let component;

      class TestTag extends Akili.Component {
        static template = '${ globalFn() }';
        
        created() {
          this.globalCounter = 0;
          globals.globalFn = () => this.globalCounter++;
          globals.globalVar = 1;
        }
      }

      before(() => {
        Akili.component('test-tag', TestTag);
        const el = document.createElement('test-tag');
        Akili.root.el.appendChild(el);
        return Akili.compile(el).then(() => {
          component = Akili.root.child('test-tag');
        });        
      });

      describe('check the current state', () => {
        it('should have called the global function once', () => {
          assert.equal(component.globalCounter, 1);
        });
      });

      describe('.hasTag()', () => {
        it('should return true', () => {          
          assert.ok(Akili.hasTag('globalFn'));
        });

        it('should return false', () => {          
          assert.notOk(Akili.hasTag('nonexistent'));
        });
      });

      describe('.getTag()', () => {
        it('should return an array', () => {    
          assert.isArray(Akili.getTag('globalFn'));
        });

        it('should return a null', () => {          
          assert.isNull(Akili.getTag('nonexistent'));
        });
      });

      describe('.triggerTag()', () => {
        it('should trigger the funcion', () => {
          Akili.triggerTag('globalFn');
          assert.equal(component.globalCounter, 2);
        });
      });

      describe('.removeTag()', () => {
        it('should remove the tag by node', () => {
          const obj = Akili.getTag('globalFn')[0];
          Akili.removeTag('globalFn', obj.node);
          assert.isFalse(Akili.hasTag('globalFn')); 
          Akili.__tags[obj.node.__name] = { globalFn: [obj] };
        });        

        it('should remove the tag', () => {
          delete globals.globalFn;
          assert.isFalse(Akili.hasTag('globalFn'));
        });
      });
    });
  });
});

<|start_filename|>src/services/router.js<|end_filename|>
import Akili from '../akili.js';
import utils from '../utils.js';
import request from './request.js';
import Route from '../components/route.js';

/**
 * Transition class.
 * The instance of this class includes the last actual router transition information.
 * 
 * {@link https://akilijs.com/docs/routing#docs_transition}
 */
export class Transition {
  constructor(url, previous = null) {  
    this.url = url;  
    this.previous = previous;
    this.path = null;
    this.routes = [];
    this.states = {};
    this.__cancelled = false;    
    this.__finished = false;
  }

  /**
   * Redirect to another state
   * 
   * @see router.state
   */
  redirect(state, params = {}, query = {}, hash = undefined, options = {}) {
    options = { ...this.path.options, ...options };
    this.cancel();   
    return router.state.call(router, state, params, query, hash, options);
  }

  /**
   * Reload the current state
   * 
   * @see router.state
   */
  reload(params = {}, query = {}, hash = undefined, options = {}) {
    options = { ...this.path.options, ...options };
    params = { ...this.path.params, ...params };
    query = { ...this.path.query, ...query };
    hash = hash === undefined? this.path.hash: hash;
    return this.redirect(this.path.state.name, params, query, hash, options);
  }

  /**
   * Set the current path
   * 
   * @param {object} path 
   */
  setPath(path) {
    path.parent = this.path || null;
    this.path = { params: {}, query: {}, ...path };
    this.routes.push(this.path);
    this.states[this.path.state.name] = this.path;
  }

  /**
   * Get a route by the state
   *  
   * @param {*} state 
   */
  getRoute(state) {
    for (let i = 0, l = this.routes.length; i < l; i++) {
      let route = this.routes[i];

      if (route.state === state) {
        return route;
      }
    }

    return null;
  }

  /**
   * Check if it has the state
   * 
   * @param {object} state 
   */
  hasState(state) {
    return !!this.states[state.name];
  }

  /**
   * Check the route is changed
   * 
   * @param {object} route
   */
  isRouteChanged(route) {
    if(!this.previous) {
      return true;
    }

    const state = route.state;

    if(!this.previous.hasState(state)) {
      return true;
    }

    const paramKeys = [];
    state.fullPattern.replace(router.__paramRegex, (m, f, v) => paramKeys.push(v));
    
    for(let key in state.params) {
      if(paramKeys.indexOf(key) == -1) {
        paramKeys.push(key);
      }
    }

    const queryKeys = Object.keys(state.query);
    const watchHash = state.hash !== undefined;    
    const prevRoute = this.previous.getRoute(state); 

    const prev = { 
      params: utils.includeKeys(prevRoute.params, paramKeys), 
      query: utils.includeKeys(prevRoute.query, queryKeys)
    };

    const current = {
      params: utils.includeKeys(route.params, paramKeys),
      query: utils.includeKeys(route.query, queryKeys)
    }

    if(watchHash) {
      prev.hash = prevRoute.hash;
      current.hash = route.hash;
    }

    return !utils.compare(prev, current);
  }

  /**
   * Cancel the current transition
   */
  cancel() {   
    this.__cancelled = true;  
    this.finish();  
  }

  /**
   * Finish the transition
   */
  finish() {
    this.__finished = true;
    delete this.previous;
    const index = router.__queue.indexOf(this);
    index >= 0 && router.__queue.splice(index, 1);
  }
}

/**
 * Akili router
 * 
 * {@link https://akilijs.com/docs/routing}
 */
const router = {};

router.baseUrl = "/";
router.states = [];
router.hashMode = true;
router.transition = null;
router.__info = {};
router.__queue = [];
router.__redirects = 0;
router.__init = false;
router.__paramRegex = /(\/?:([\w\d-]+))/g;
router.__routeSelector = c => c instanceof Route;

/**
 * Add a new state
 *
 * @param {string} name - state name. To set parents you can use dot, e.g. 'app.article.edit'
 * @param {string} pattern - url pattern like 'app/article/:id'
 * @param {object} [options]
 * @returns {router}
 */
router.add = function (name, pattern, options = {}) {
  if(typeof name == 'object') {
    options = name;
    pattern = options.pattern;
    name = options.state;
    delete options.pattern;
    delete options.state;
  }

  if(!name) {
    throw new Error(`Router state must have a name`);
  }

  const defaultOptions = {
    template: '',
    templateUrl: '',
    abstract: false,
    component: null,
    params: {},
    query: {},
    handler: () => {},
    options: {}
  };

  if(Akili.options.debug && options.component && (options.template || options.templateUrl)) {
    // eslint-disable-next-line no-console
    console.warn(`Router state "${name}" must only have a component or template option`);
  } 

  if (!options.template && !options.templateUrl && !options.component) {
    options.abstract = true;
  }

  if (this.has(name)) {
    throw new Error(`Router state "${name}" is already exists`)
  }

  this.setState(name, pattern, {...defaultOptions, ...options});
  return this;
};

/**
 * Remove the state
 *
 * @param {string} name
 */
router.remove = function(name) {
  for (let i = 0, l = this.states.length; i < l; i++) {
    if (this.states[i].name == name) {
      this.states.splice(i, 1);

      return;
    }
  }
};

/**
 * Check the state exists
 *
 * @param {string} name
 * @returns {boolean}
 */
router.has = function(name) {
  for (let i = 0, l = this.states.length; i < l; i++) {
    if (this.states[i].name == name) {
      return true;
    }
  }

  return false;
};

/**
 * Change the state
 *
 * @param {string} state
 * @param {object} [params] - params for state {id: 1} => '/app/:id' => '/app/1'
 * @param {object} [query] - query {x: 1} => '/app/?x=1'
 * @param {string} [hash]
 * @param {object} [options]
 */
router.state = function (state, params = {}, query = {}, hash = undefined, options = {}) {  
  typeof state !== 'object' && (state = this.getState(state));
  
  if (!state) {
    throw new Error(`Not found route state with name ${state}`);
  }

  options = { ...options };
  options.manual = true;
  ({ params, query, hash, options } = this.prepareStateArgs(state, params, query, hash, options, false));  
  let url = this.createStateUrl(state, params, query, hash, options);    
  this.isolate(() => this.setUrl(url));
  return this.changeState(options);
};

/**
 * Go back
 */
router.back = function() {
  return window.history.back.apply(window.history, arguments);
};

/**
 * Go to some position
 */
router.go = function() {
  return window.history.go.apply(window.history, arguments);
};

/**
 * Go forward
 */
router.forward = function() {
  return window.history.forward.apply(window.history, arguments);
};

/**
 * Change the state by the url
 *
 * @param {string} url
 * @param {object} [options]
 */
router.location = function (url, options = {}) {
  options = { ...options };
  options.manual = true;
  this.isolate(() => this.setUrl(url));  
  return this.changeState(options);
};

/**
 * Initialize the router. 
 * Must be called before Akili.init()
 *
 * @param {string} [defaultUrl]
 * @param {boolean} [hashMode=true]
 */
router.init = function (defaultUrl = '', hashMode = true) {
  let oldPushState = window.history.pushState;

  window.history.pushState = function() {
    let res = oldPushState.apply(this, arguments);
    // eslint-disable-next-line no-console
    router.changeState().catch(err => console.error(err));
    return res;
  };

  this.__onStateChangeHandler = () => {
    // eslint-disable-next-line no-console
    this.changeState().catch(err => console.error(err));
  };

  this.defaultUrl = defaultUrl;
  this.hashMode = hashMode;    

  this.states.sort((a, b) => {
    a = a.name.split('.').length;
    b = b.name.split('.').length;
    return a - b;
  });

  for (let i = 0, l = this.states.length; i < l; i++) {
    this.initState(this.states[i]);
  }

  if (!this.states.length && Akili.options.debug) {
    // eslint-disable-next-line no-console
    console.warn(`You didn't add any routes to the router`);
  }

  window.addEventListener('popstate', this.__onStateChangeHandler);
  this.__init = true;
};

/**
 * Initialize the state 
 * 
 * @param {object} state 
 */
router.initState = function (state) {
  let parents = [];
  state.children = [];
  parents = state.name.split('.');
  parents.pop();
  state.level = state.abstract? null: parents.length;

  if (parents.length) {
    let parentName = parents.join('.');
    let parent = this.getState(parentName);

    if (!parent) {
      throw new Error(`Not found parent route state "${parentName}" for "${state.name}"`)
    }

    if (state.level !== null && parent.abstract) {
      state.level--;
    }

    state.fullPattern = this.splitSlashes(parent.fullPattern + '/' + state.pattern);
    parent.children.push(state);
  }
  else {
    state.fullPattern = state.pattern;
  }
}

/**
 * Get the state by the name
 *
 * @param {string} name
 * @returns {object|null}
 */
router.getState = function (name) {
  for (let i = 0, l = this.states.length; i < l; i++) {
    let state = this.states[i];

    if (state.name == name) {
      return state;
    }
  }

  return null;
};

/**
 * Set the state
 *
 * @param {string} name
 * @param {string} pattern
 * @param {object} options
 */
router.setState = function (name, pattern, options = {}) {
  let state = {...options, name, pattern};
  this.states.push(state);
  this.__init && this.initState(state);
  return state;
};

/**
 * Remove the state by the name
 *
 * @param {string} name
 */
router.removeState = function (name) {
  for (let i = 0, l = this.states.length; i < l; i++) {
    let state = this.states[i];

    if (state.name == name) {
      this.states.splice(i, 1);

      return;
    }
  }
};

/**
 * Set the url
 *
 * @param {string} url
 */
router.setUrl = function (url) {
  this.hashMode? this.setHashUrl(url): this.setHistoryUrl(url);
};

/**
 * Set the url using the history
 *
 * @param url
 */
router.setHistoryUrl = function (url) {
  window.history.pushState(undefined, undefined, url);
};

/**
 * Set the url using the hash
 *
 * @param url
 */
router.setHashUrl = function (url) {
  window.location.hash = '#' + (url || '/');
};

/**
 * Replace the url
 *
 * @param {string} url
 */
router.replaceUrl = function (url) {
  this.hashMode? this.replaceHashUrl(url): this.replaceHistoryUrl(url);
};

/**
 * Replace the url using the history
 *
 * @param url
 */
router.replaceHistoryUrl = function (url) {
  window.history.replaceState(undefined, undefined, url);
};

/**
 * Replace the url using the hash
 *
 * @param url
 */
router.replaceHashUrl = function (url) {
  window.history.replaceState(undefined, undefined, '#' + (url || '/'));
};

/**
 * Get the url
 *
 * @returns {string}
 */
router.getUrl = function () {
  return this.hashMode ? this.getHashUrl() : this.getHistoryUrl();
};

/**
 * Get the url using the history
 *
 * @returns {string}
 */
router.getHistoryUrl = function () {
  return window.location.pathname + window.location.search + window.location.hash;
};

/**
 * Get the url using the hash
 *
 * @returns {string}
 */
router.getHashUrl = function () {
  return window.location.hash.replace(/^#/, '');
};

/**
 * Get the query params
 *
 * @returns {object}
 */
router.getUrlQuery = function() {
  return this.hashMode ? this.getHashUrlQuery() : this.getHistoryUrlQuery();
};

/**
 * Get the query params using the history
 *
 * @returns {object}
 */
router.getHistoryUrlQuery = function() {
  return request.paramsFromQuery(window.location.search.replace(/^\?/, ''));
};

/**
 * Get the query params using the hash
 *
 * @returns {object}
 */
router.getHashUrlQuery = function() {
  return request.paramsFromQuery((window.location.hash.split('?')[1] || ''));
};

/**
 * Create the url
 *
 * @param {string|Object} state
 * @param {object} [params]
 * @param {object} [query]
 * @param {string} [hash]
 * @param {object} [options]
 * @param {boolean} [prepare]
 */
router.createStateUrl = function (state, params = {}, query = {}, hash = undefined, options = {}, prepare = true) {
  typeof state !== 'object' && (state = this.getState(state));
  options = { ...options };
  
  if(prepare) {
    ({ params, query, hash, options } = this.prepareStateArgs(state, params, query, hash, options));
  }

  let url = state.fullPattern.replace(this.__paramRegex, (m, f, v) => '/' + (params[v] || ''));
  url = url.replace(/^\^/, '');
  url = this.splitSlashes(url);

  if (Object.keys(query).length) {
    url += '?' + request.paramsToQuery(query);
  }

  if (!this.hashMode && hash) {
    url += '#' + hash.replace('#', '');
  }

  return url;
};

/**
 * Prepare the state arguments
 * 
 * @param {string|Object} state
 * @param {object} [params]
 * @param {object} [query]
 * @param {string} [hash]
 * @param {object} [options]
 */
router.prepareStateArgs = function (state, params = {}, query = {}, hash = undefined, options = {}) {
  let args = { params, query, hash, options };

  for(let i = 0; i < 4^4; i++) {
    const paramsTemp = this.prepareStateParams(state, params, args);
    const queryTemp = this.prepareStateQuery(state, query, args);
    const hashTemp = this.prepareStateHash(state, hash, args);
    const optionsTemp = this.prepareStateOptions(state, options, args);

    if(hashTemp === null) {
      optionsTemp.emptyHash = null;
    }
    else if(hashTemp === '') {
      optionsTemp.emptyHash = '';
    }

    const newArgs = { params: paramsTemp, query: queryTemp, hash: hashTemp, options: optionsTemp };

    if(utils.compare(newArgs, args)) {
      break;
    }

    args = newArgs;
  }
    
  return args;
}

/**
 * Prepare the state params
 * 
 * @param {string|Object} state
 * @param {object} params
 * @param {object} [args]
 */
router.prepareStateParams = function(state, params, args) {
  typeof state !== 'object' && (state = this.getState(state));
  const states = state.name.split('.');
  params = Object.assign({}, params);

  for(let i = states.length - 1; i >= 0; i--) {
    const current = states.slice(0, states.length - i).join('.');
    params = this.createStateObjectArgs(params, this.getState(current).params, args);
  }

  return params;
}

/**
 * Prepare the state query
 * 
 * @param {string|Object} state
 * @param {object} query
 * @param {object} [args]
 */
router.prepareStateQuery = function(state, query, args) {  
  typeof state !== 'object' && (state = this.getState(state));
  const states = state.name.split('.');
  query = Object.assign({}, query);

  for(let i = states.length - 1; i >= 0; i--) {
    const current = states.slice(0, states.length - i).join('.');
    query = this.createStateObjectArgs(query, this.getState(current).query, args);
  }

  return query;
}

/**
 * Prepare the state options
 * 
 * @param {string|Object} state
 * @param {object} options
 * @param {object} [args]
 */
router.prepareStateOptions = function(state, options, args) {  
  typeof state !== 'object' && (state = this.getState(state));
  const states = state.name.split('.');
  options = Object.assign({}, options);

  for(let i = states.length - 1; i >= 0; i--) {
    const current = states.slice(0, states.length - i).join('.');
    options = this.createStateObjectArgs(options, this.getState(current).options, args);
  }

  return options;
}

/**
 * Create the state arguments
 * 
 * @param {object} current
 * @param {object} defaults
 * @param {object} [args]
 */
router.createStateObjectArgs = function (current, defaults, args = { params: {}, query: {}, options: {} }) {
  const all = Object.assign({}, current);

  for(let key in defaults) {
    let val = defaults[key];

    if(all[key] === null) {
      continue;
    }

    if(typeof val == 'function') {
      all[key] = val(args);
    }

    if(all[key] === undefined) {
      all[key] = val;
    }
  }

  for(let key in all) {
    if(all[key] === null) {
      delete all[key];
    }
  }

  return all; 
}

/**
 * Prepare the state hash
 * 
 * @param {string|Object} state
 * @param {string} hash
 * @param {object} [args]
 */
router.prepareStateHash = function(state, hash, args) {
  if(hash === null) {
    return hash;
  }

  typeof state !== 'object' && (state = this.getState(state));
  const states = state.name.split('.');

  for(let i = states.length - 1; i >= 0; i--) {
    const current = states.slice(0, states.length - i).join('.');
    hash = this.createStateHashArgs(hash, this.getState(current).hash, args);
  }
  
  return hash;
}

/**
 * Create the state hash arguments
 * 
 * @param {string|null} current
 * @param {string|null} defaults
 * @param {object} [args]
 */
router.createStateHashArgs = function (current, defaults, args = { params: {}, query: {} }) {
  if(current === null) {
    return current;
  }

  if(typeof defaults == 'function') {
    return defaults(args);
  }

  if(current === undefined) {
    return defaults;
  }

  return current;
}

/**
 * Remove all unnecessary slashes from the url
 *
 * @param {string} url
 * @returns {string}
 */
router.splitSlashes = function (url) {
  return url.replace(/[/]+/g, '/');
};

/**
 * Get the pattern info
 *
 * @param {string|Object} state
 * @param {string} url
 * @returns {object}
 */
router.getPatternContent = function (state, url) {
  typeof state !== 'object' && (state = this.getState(state));
  let keys = [];
  let params = {};
  url = url.split('?')[0];
  url = url.split('#')[0];
  let urlPattern = state.fullPattern.replace(this.__paramRegex, (m, f, v) => {
    keys.push(v);
    return '/?([^/]*)';
  });
  urlPattern = urlPattern.replace(/([^^/]+)[/]+$/, '$1');
  let regex = new RegExp(urlPattern, 'g');
  let isIncluded = url.match(regex);
  
  if (!isIncluded) {
    return null;
  }

  url.replace(regex, (m, ...args) => { 
    args = args.slice(0, args.length - 2);

    for(let i = 0, l = args.length; i < l; i++) {
      let v = args[i];
      v && (params[keys[i]] = v);
    }  
  });

  return { params };
};

/**
 * Check the state is active
 *
 * @param {string|Object} state
 * @param {boolean} includes
 * @returns {boolean}
 */
router.isActiveState = function(state, includes = false) {
  typeof state !== 'object' && (state = this.getState(state));
  let url = router.transition && !router.transition.__finished? router.transition.url: this.getUrl();
  url = this.splitSlashes('/' + url.split('?')[0] + '/');
  let urlPattern = state.fullPattern.replace(this.__paramRegex, '/?([^/]*)');
  urlPattern = urlPattern.replace(/^\^/, '').replace(/\$$/, '');
  let str = includes? urlPattern: this.splitSlashes('^/' + urlPattern + '/$');
  let regex = new RegExp(str);  
  return regex.test(url);
};

/**
 * Check the current url includes the state
 *
 * @param {string|Object} state
 * @returns {boolean}
 */
router.inActiveState = function(state) {
  return router.isActiveState(state, true);
};

/**
 * Get the route component by the level
 *
 * @param {number} level
 */
router.getRoute = function (level) {
  let i = 0;

  const find = el => {
    let route = el.child(this.__routeSelector);

    if (!route) {
      return null;
    }

    if (i == level) {
      return route;
    }

    i++;
    return find(route);
  };

  return find(Akili.root);
};

/**
 * Get the patterns array info
 *
 * @param {array} arr
 * @param {string} url
 * @returns {object|null}
 */
router.getArrayPatternContent = function (arr, url) {
  for (let i = 0, l = arr.length; i < l; i++) {
    let state = arr[i];
    let content = this.getPatternContent(state, url);

    if (!content) {
      continue;
    }

    return { state, ...content };
  }

  return null;
};

/**
 * Get the states by the level
 *
 * @param {number} level
 * @returns {Array}
 */
router.getStatesByLevel = function (level) {
  let states = [];

  for (let i = 0, l = this.states.length; i < l; i++) {
    let state = this.states[i];

    if (state.level < level) {
      continue;
    }
    else if (state.level > level) {
      break;
    }

    states.push(state);
  }

  return states;
};

/**
 * Reload the state
 * 
 * @see Transition.reload
 */
router.reload = function(...args) {
  if(!this.transition) {
    throw new Error('Not found an active transition to reload the state');
  }

  return this.transition.reload(...args);
}

/**
 * Isolate the function
 *
 * @param {function} fn
 * @returns {*}
 */
router.isolate = function (fn) { 
  this.__isolated = true;
  let res;

  try {
    res = fn();
  }
  catch(err) {
    this.__isolated = false;
    throw err;
  }

  this.__isolated = false
  return res;
}

/**
 * Change the state
 * 
 * @param {object} [options]
 */
router.changeState = function (options = {}) {    
  if(this.__isolated) {
    return Promise.resolve();
  }

  let url = this.getUrl();
  let transition = new Transition(url);  
  this.__queue.push(transition);
  
  return new Promise((resolve) => {
    const interval = setInterval(() => {
      if(this.__queue[0] === transition) {       
        clearInterval(interval);
        resolve();
      }
    });
  }).then(() => {   
    transition.previous = router.transition || null;
    router.transition = transition;
    window.dispatchEvent(new CustomEvent('state-change', { detail: transition }));   
    const emptyHash = options.emptyHash;
    
    if(!this.__redirects) {
      const docStyle = getComputedStyle(document.documentElement);
      this.__info.scrollPos = { left: window.scrollX, top: window.scrollY };      
      this.__info.docMinHeight = docStyle.minHeight;
      this.__info.docMinWidth = docStyle.minWidth;    
      document.documentElement.style.minHeight = document.documentElement.offsetHeight + 'px';
      document.documentElement.style.minWidth = document.documentElement.offsetWidth + 'px';
    }
   
    let params = {};
    let query = this.getUrlQuery();
    let hash = this.hashMode? '': window.location.hash.replace('#', '');
    let level = 0;

    const next = states => {
      if (!states.length) {
        return Promise.resolve();
      }

      let content = this.getArrayPatternContent(states, url);

      if (!content) {
        return Promise.resolve();
      }

      let state = content.state;
      transition.setPath({ state, component: route });
      params = { ...params, ...content.params };
      hash = hash || emptyHash;
      ({ params, query, hash, options } = this.prepareStateArgs(state, params, query, hash, options));  
      options.init = Akili.__init === null;
      hash = hash || '';
      let realUrl = this.createStateUrl(state, params, query, hash, options, false); 
      this.isolate(() => this.replaceUrl(realUrl)); 
      let route = state.abstract? null: this.getRoute(level);
      
      if (!route && !state.abstract) {      
        throw new Error (`Not found route component for state "${state.name}"`);
      }    

      transition.path.params = params;
      transition.path.query = query;
      transition.path.hash = hash;
      transition.path.url = realUrl;
      transition.path.options = options;
      !state.abstract && level++;
      let isDifferent = true;

      if(realUrl != url) {
        isDifferent = transition.isRouteChanged(transition.path);
      }
        
      let load = isDifferent && options.reload !== false;
      const prevData = transition.previous && transition.previous.path? transition.previous.path.data: undefined;

      return Promise.resolve(load? state.handler(transition): prevData).then(data => {
        transition.path.data = data;
        state.title && (document.title = typeof state.title == 'function'? state.title(transition): state.title);

        if (transition.__cancelled) {
          return;
        }

        if (state.abstract) {
          return;
        }

        const prevRoute = transition.previous && transition.previous.getRoute(state);
        (prevRoute && !prevRoute.loaded) && (load = true);
        return route.setTransition(transition, load).then(() => transition.path.loaded = true);
      }).then(() => {
        return next(state.children);
      });
    };

    return next(this.getStatesByLevel(0)).then(() => {
      if (!transition.routes.length) {
        if (this.__redirects) {
          throw new Error(`Wrong router default url "${this.defaultUrl}"`);
        }

        if (this.defaultUrl) {
          if (this.defaultUrl == this.getUrl()) {
            throw new Error(`Not found any routes`);
          }

          this.isolate(() => this.replaceUrl(this.defaultUrl));          
          this.__redirects++;
          transition.finish();
          return this.changeState(options);
        }
        
        if (Akili.options.debug) {
          // eslint-disable-next-line no-console
          console.warn(`Not found a default route. You can pass it in "router.init(defaultUrl)" function`);
        }
      }

      if(this.hashMode || !transition.path || !transition.path.hash) {
        window.scrollTo({ ...options.saveScrollPosition? this.__info.scrollPos: { top: 0, left: 0 } });
      }

      document.documentElement.style.minHeight = this.__info.docMinHeight;
      document.documentElement.style.minWidth = this.__info.docMinWidth;
      this.__redirects = 0;
      this.__info = {};
      window.dispatchEvent(new CustomEvent('state-changed', { detail: transition }));      
      transition.finish();
      return transition;
    });
  }).catch(err => {
    transition && transition.finish();
    throw err;
  })
};

/**
 * Deinitialize the router
 */
router.deinit = function () {
  window.removeEventListener('popstate', this.__onStateChangeHandler);
  this.__init = false;
};

router.Transition = Transition;
export default router;

<|start_filename|>test/app/store.js<|end_filename|>
import Akili from '../../src/akili.js';

export class FirstStore extends Akili.Component {
  static template = '${ this.val }';

  resolved() {
    this.store('val', 'val');
    this.scope.val = 1;
  }
}

export class SecondStore extends Akili.Component {
  static template = '${ this.val }';

  resolved() {
    this.store('val', 'val');
    this.scope.val = 2;
  }
}

<|start_filename|>src/components/image.js<|end_filename|>
import Url from './url.js';
import Akili from '../akili.js';

/**
 * Component for images.
 * 
 * {@link https://akilijs.com/docs/components#docs_image,_iframe,_embed,_audio,_video,_track,_source,_object}
 * 
 * @tag img
 * @see Url
 */
export default class Image extends Url {  
  static define() {
    Akili.component('img', this);
  }
}

<|start_filename|>test/services/router.js<|end_filename|>
import Akili from '../../src/akili';
import router, { Transition } from '../../src/services/router';
import Component from '../../src/component';

class RouterComponent extends Component {
  static templateUrl = "router-2.html";

  static define() {    
    Akili.component('router-component', this);
  }
}

describe('router.js', () => {
  describe('router', () => {
    function onStateChange(callback, event = 'popstate') {
      const fn = e => {
        callback(e);
        window.removeEventListener(event, fn);
      };
      window.addEventListener(event, fn);
    }

    let indexHref;
    let routeOptions;

    before(() => {      
      RouterComponent.define();
      indexHref = location.href;

      routeOptions = [
        {
          state: '1',
          pattern: '^/1',
          templateUrl: "router-1.html",
          title: 'Router title'
        },
        {
          state: '1.1',
          pattern: '/1-1',
          templateUrl: "router-1-1.html"
        },
        {
          state: '1.2',
          pattern: '/1-2',
          templateUrl: "router-1-2.html"
        },
        {
          params: { o: () => 1 },
          query: { m: () => 1 },
          hash: 's',
          state: '2',
          pattern: '^/2',
          options: { x: 2 },
          component: RouterComponent
        },
        {
          state: '3',
          pattern: '^/3',
          handler: transition => {
            if(transition.path.query.type == 1) {              
              transition.redirect('4');
            }
            else if(transition.path.query.type == 2) {
              transition.cancel();
            }
            else if(transition.path.query.type == 3) {
              transition.reload({}, { type: 4 });
            }
          }
        },
        {
          state: '4',
          pattern: '^/4',
        }
      ];
    });

    describe('.add()', () => {
      it('should add new state', () => {
        router.add('x', '/x/:id', { 
          params: { id: args => args.params.id || 4 }, 
          query: { z: args => args.query.z || 3 } 
        });
        routeOptions.forEach(route => router.add(route));
        assert.equal(router.states[0].name, 'x');
      });

      it('should not add an empty state', () => {
        assert.throws(() => router.add('', '/empty'));
      });      

      it('should not add an existent state', () => {
        assert.throws(() => router.add('x', '/x2'));
      });
    });

    describe('.init()', () => {
      it('should initialize router', () => {
        router.init('', false);
      });

      it('should not add state with wrong parent after the initializtion', () => {
        assert.throws(() => router.add('noparent.x', '/noparent'));
      });
    });

    describe('.has()', () => {
      it('should find existing state', () => {
        assert.isOk(router.has('x'));
      });

      it('should not find existing state', () => {
        assert.isNotOk(router.has('non-existent'));
      });
    });

    describe('.getState()', () => {
      it('should get the necessary state by name', () => {
        assert.equal(router.getState('x').name, 'x');
      });
    });

    describe('.setState()', () => {
      it('should set a new state', () => {
        router.setState('fake', '!fake!');
        assert.isOk(router.has('fake'));
      });
    });

    describe('.removeState()', () => {
      it('should remove state by name', () => {
        router.removeState('fake');
        assert.isNotOk(router.has('fake'));
      });
    });

    describe('.createStateUrl()', () => {
      it('should create right url by data', () => {
        let state = router.getState('x');
        let params = { id: 2, fix: 3 };
        let query = { x: 1, y: 2 };
        assert.equal(router.createStateUrl(state, params, query), '/x/2?x=1&y=2&z=3');
      });

      it('should create right url by data', () => {
        assert.equal(router.createStateUrl('x', {}, {}), '/x/4?z=3');
      });
    });

    describe('.splitSlashes()', () => {
      it('should remove repeated slashes', () => {
        let url = '/path//to///something';
        assert.equal(router.splitSlashes(url), '/path/to/something');
      });
    });

    describe('.getPatternContent()', () => {
      it('should get content', () => {
        let url = '/x/9';
        let content = router.getPatternContent(router.getState('x'), url);
        assert.equal(JSON.stringify(content.params), JSON.stringify({ id: "9" }));
      });

      it('should not get content', () => {
        let url = '/y/9';
        assert.isNull(router.getPatternContent(router.getState('x'), url));
      });
    });

    describe('.getStatesByLevel()', () => {
      it('should return 0 level', () => {
        assert.lengthOf(router.getStatesByLevel(0), 5);
      });

      it('should return 1 level', () => {
        assert.lengthOf(router.getStatesByLevel(1), 2);
      });
    });

    describe('.getArrayPatternContent()', () => {
      it('should return a necessary pattern content', () => {
        let url = '/1/1-2';
        let content = router.getArrayPatternContent(router.getStatesByLevel(1), url);
        assert.strictEqual(content.state, router.getState('1.2'));
      });
    });

    describe('.prepareStateArgs()', () => {
      it('should return the right args', () => {
        const result = router.prepareStateArgs('2', { o: 2 }, {}, null, { x: 3 });
        assert.equal(result.params.o, 1, 'check params');
        assert.equal(result.query.m, 1, 'check query');
        assert.equal(result.hash, null, 'check hash');
        assert.equal(result.options.x, 3, 'check options');
      });
    });

    describe('.state()', () => {
      it('should change the state', () => {
        return router.state('x', { id: 1 }, { z: 2 }).then(() => {
          assert.equal(location.pathname + location.search, '/x/1?z=2');
        });        
      });

      it('should not change the state with nonexistent state', () => {
        assert.throws(() => router.state('y'));
      });

      it('should has transition', () => {
        assert.instanceOf(router.transition, Transition);
      });
    });

    describe('.back()', () => {
      it('should go back', done => {
        onStateChange(() => {
          assert.equal(location.href, indexHref);
          done();
        });

        router.back();
      });
    });

    describe('.forward()', () => {
      it('should go forward', done => {
        onStateChange(() => {
          assert.equal(location.pathname + location.search, '/x/1?z=2');
          done();
        });

        router.forward();
      });
    });

    describe('.go()', () => {
      it('should go 1 step back', done => {
        onStateChange(() => {
          assert.equal(location.href, indexHref);
          done();
        });

        router.go(-1);
      });
    });

    describe('.remove()', () => {
      it('should remove a state', () => {
        router.remove('x');
        assert.isNotOk(router.has('x'));
      });
    });

    describe('.location()', () => {
      it('should change the location', () => {
        return router.location('/x/1?z=1').then(() => {
          assert.equal(location.pathname + location.search, '/x/1?z=1');
        });        
      });
    });

    describe('.isolate()', () => {
      it('should isolate the code', () => {
        router.isolate(() => window.history.pushState(null, '', `/f/a/k/e`));
        assert.equal(location.pathname + location.search, '/f/a/k/e');
      });
    });

    describe('Transition.prototype.redirect()', () => {
      it('should redirect', done => {
        onStateChange(() => {
          assert.equal(location.pathname + location.search, '/4');
          done();
        }, 'state-changed');

        router.state('3', {},  { type: 1 });       
      });
    });

    describe('Transition.prototype.reload()', () => {
      it('should reload', done => {
        onStateChange(() => {
          assert.equal(location.pathname + location.search, '/3?type=4');
          done();
        }, 'state-changed');

        router.state('3', {},  { type: 3 });       
      });
    });

    describe('.reload()', () => {
      it('should reload', done => {
        onStateChange(() => {          
          assert.equal(location.pathname + location.search, '/3?type=4&z=1');
          done();
        }, 'state-changed');

        router.reload({}, { z: 1 });       
      });
    });

    describe('wrong url', () => {
      it('should set the default url', done => {   
        router.defaultUrl = '/3';  

        onStateChange(() => {
          assert.equal(location.pathname + location.search, '/3');
          done();
        }, 'state-changed'); 

        window.history.pushState(null, '', `/f/a/k/e`);
      });
    });

    describe('enable hash mode', () => {
      before(() => {
        router.hashMode = true;
      });

      after(() => {
        router.hashMode = false;
        window.location.hash = '';
      });

      describe('.state()', () => {
        it('should change the state', () => { 
          return router.state('4').then(() => {
            assert.equal(location.hash, '#/4');
          });
        });
      });

      describe('.location()', () => {
        it('should change url', () => { 
          return router.state('3').then(() => {
            assert.equal(location.hash, '#/3');
          });
        });
      });
    });
  });
});

<|start_filename|>src/utils.js<|end_filename|>
import EventEmitter from './event-emitter.js'

/**
 * The object with a lot of useful functions
 * 
 * {@link https://akilijs.com/docs/utils}
 */
const utils = {};

/**
 * Create the class attribute from the object
 *
 * @example
 * // returns "red active"
 * utils.class({red: true, active: true, green: false});
 *
 * @param {object} obj
 * @returns {string}
 */
utils.class = function (obj) {
  if (!obj || typeof obj !== 'object') {
    return '';
  }

  let classes = [];
  let keys = Object.keys(obj);

  for (let i = 0, l = keys.length; i < l; i++) {
    let k = keys[i];
    let val = obj[k];
    val && classes.push(k);
  }

  return classes.join(' ');
};

/**
 * Create the style attribute from the object
 *
 * @example
 * // returns "color:red;width:10px"
 * utils.style({color: "red", background: false, width: "10px"});
 *
 * @param {object} obj
 * @returns {string}
 */
utils.style = function(obj) {
  if (!obj || typeof obj !== 'object') {
    return '';
  }

  let styles = [];
  let keys = Object.keys(obj);

  for (let i = 0, l = keys.length; i < l; i++) {
    let k = keys[i];
    let val = obj[k];
    val && styles.push(`${this.toDashCase(k)}:${val}`);
  }

  return styles.join(';');
};

/**
 * Split the string extended
 * 
 * @example
 * // returns ["Hello", "World"]
 * utils.split("Hello World", " ");
 * 
 * @example
 * // returns ['x = 5', ' y = "1;2;3"']
 * utils.split('x = 5; y = "1;2;3"', ";", ['"']);
 * 
 * @param {string|RegExp} str 
 * @param {string} [del]
 * @param {string[]} [exclude] 
 */
utils.split = function(str, del = '', exclude = []) {
  const exps = [];  
  let last = '';

  if(!del) {
    return str.split('');
  }
  else if(del instanceof RegExp) {
    return str.split(del);
  }
  else if(str.indexOf(del) == -1) {
    last = str;
  }
  else {
    const arr = str.split('');
    let open = '';

    for(let i = 0, l = arr.length; i < l; i++) {
      let val = arr[i];
      let index = exclude.indexOf(val);      

      if(index > -1 && (!open || open == val)) {
        !open? open = exclude[index]: open = '';
      }

      if(val == del && !open) {
        exps.push(last);
        last = '';
        continue;
      }

      last += val;
    }
  }

  last && exps.push(last);
  return exps;
}

/**
 * Filter the array
 *
 * @example
 * // returns [1, 11]
 * utils.filter([1, 2, 3, 11], '1');
 *
 * @example
 * // returns [{x: 1}, {x: 11}]
 * utils.filter([{x: 1}, {x: 2}, {x: 3}, {x: 11}], '1', 'x');
 * 
 * @example
 * // returns [{x: 1}, {x: 11}]
 * utils.filter([{x: 1}, {x: 2}, {x: 3}, {x: 11}], '1', ['x']);
 * 
 * @example
 * // returns [{x: 1}, {x: 11}]
 * utils.filter([{x: 1}, {x: 2}, {x: 3}, {x: 11}], '1', [['x']]);
 * 
 * @example
 * // returns [{x: {y: 1}}]
 * utils.filter([{x: {y: 1}}, {x: {y: 2}}], '1', [['x', 'y']]);
 * 
 * @example
 * // returns [{x:1, y: 2}, {x: 2, y:1}] 
 * utils.filter([{x: 1, y: 2}, {x: 3, y: 3}, {x: 2, y: 1}], '1', ['x', 'y']);
 * 
 * @example
 * // returns [{x:1, y: 2}, {x: 2, y:1}]
 * utils.filter([{x: 1, y: 2}, {x: 3, y: 3}, {x: 2, y: 1}], '1', [['x'], ['y']]);
 *
 * @param {Array} arr
 * @param {string|RegExp|function} handler - type of the filtering
 * @param {string[]|string} [keys] - filter by the keys
 * @returns {Array} 
 */
utils.filter = function (arr, handler, keys = null) {
  let res = [];

  if (keys && !Array.isArray(keys)) {
    keys = [keys];
  }

  if(typeof handler != 'function') {
    const str = handler? (handler + '').toLowerCase(): '';
    handler = val => (val? (val + '').toLowerCase(): '').match(this.escapeForRegExp(str));
  }

  for (let i = 0, l = arr.length; i < l; i++) {
    let item = arr[i];
    let filtered = false;

    if(!keys && handler(item)) {
      filtered = true;
    }
    else if(keys) {
      for(let k = 0, c = keys.length; k < c; k++) {
        let key = keys[k];
        key = Array.isArray(key)? key: [key];
        let val = key? this.getPropertyByKeys(key, item): item;
    
        if (handler(val)) {
          filtered = true;
          continue;
        }
      }
    }

    filtered && res.push(item);   
  }

  return res;
};

/**
 * Sort the array
 *
 * @example
 * // returns [1, 2, 3]
 * utils.sort([3, 2, 1], true);
 *
 * @example
 * // returns [3, 2, 1]
 * utils.sort([1, 2, 3], false);
 *
 * @example
 * // returns [{x: 1}, {x: 2}, {x: 3}]
 * utils.sort([{x: 3}, {x: 2}, {x: 1}], [['x']], [true]);
 * utils.sort([{x: 3}, {x: 2}, {x: 1}], ['x'], true);
 * utils.sort([{x: 3}, {x: 2}, {x: 1}], 'x');
 *
 * @example
 * // returns [{x: 3}, {x: 2}, {x: 1}]
 * utils.sort([{x: 1}, {x: 2}, {x: 3}], [['x']], [false]);
 * utils.sort([{x: 1}, {x: 2}, {x: 3}], ['x'], false);
 * utils.sort([{x: 1}, {x: 2}, {x: 3}], 'x', false);
 *
 * @example
 * // returns [{x: 1, y: 3}, {x: 2, y: 1}, {x:2, y: 2}]
 * utils.sort([{x: 2, y: 2}, {x: 2, y: 1}, {x: 2, y: 3}], [['x'], ['y']], [true, true]);
 * 
 * @example
 * // returns [{x: {y: 1}}, {x: {y: 2}}]
 * utils.sort([{x: {y: 2}}, {x: {y: 1}}], [['x', 'y']], [true]);
 *
 * @param {Array} arr
 * @param {boolean|Array[]|string[]|string} [keys]
 * @param {boolean|boolean[]} [order] - reverse or not
 * @returns {Array}
 */
utils.sort = function(arr, keys = true, order = []) {
  arr = arr.slice();

  if (keys === true) {
    keys = [];
    order = [true];
  }
  else if (keys === false) {
    keys = [];
    order = [false];
  }

  if (!Array.isArray(keys)) {
    keys = [keys];
  }

  if (!Array.isArray(order)) {
    order = [order];
  }

  let l = keys.length;

  arr.sort((a, b) => {
    let i = 0;

    const check = (a, b, reverse = false) => {
      if (a instanceof Date) {
        a = a.getTime();
      }

      if (b instanceof Date) {
        b = b.getTime();
      }

      if (a > b) {
        return reverse? -1: 1;
      }
      else if (a < b) {
        return reverse? 1: -1;
      }

      return 0;
    };

    const next = () => {
      if (i >= l) {
        return 0;
      }

      let key = keys[i];

      if (!Array.isArray(key)) {
        key = [key];
      }

      let aV = this.getPropertyByKeys(key, a);
      let bV = this.getPropertyByKeys(key, b);
      let res = check(aV, bV, order[i] === false);

      if (res !== 0) {
        return res;
      }

      i++;
      return next();
    };

    if (!l) {
      return check(a, b, order[i] === false);
    }

    return next();
  });

  return arr;
};

/**
 * Return a new object with the specified keys
 * 
 * @example
 * // returns {x: 1, z: 1}
 * utils.includeKeys({x: 1, y: 1, z: 1}, ['x', 'z']);
 * 
 * @param {object} obj
 * @param {string[]} keys
 */
utils.includeKeys = function(obj, keys) {
  let newObj = {};
  let objKeys = Object.keys(obj);

  for(let i = 0, l = objKeys.length; i < l; i++) {
    let key = objKeys[i];

    if(keys.indexOf(key) != -1) {
      newObj[key] = obj[key];
    }
  }

  return newObj;
}

/**
 * Return a new object without the specified keys
 * 
 * @example
 * // returns {x: 1, z: 1}
 * utils.includeKeys({x: 1, y: 1, z: 1}, ['y']);
 * 
 * @param {object} obj
 * @param {string[]} keys
 */
utils.excludeKeys = function(obj, keys) {
  let newObj = {};
  let objKeys = Object.keys(obj);

  for(let i = 0, l = objKeys.length; i < l; i++) {
    let key = objKeys[i];

    if(keys.indexOf(key) == -1) {
      newObj[key] = obj[key];
    }
  }

  return newObj;
}

/**
 * Check the value is the scope proxy object
 *
 * @param {*} val
 * @returns {boolean}
 */
utils.isScopeProxy = function(val) {
  return !!(val && typeof val == 'object' && val.__isProxy);
};

/**
 * Check the value is the store proxy object
 *
 * @param {*} val
 * @returns {boolean}
 */
utils.isStoreProxy = function(val) {
  return !!(val && typeof val == 'object' && val.__isProxy && !val.__component);
};

/**
 * Check the value is a plain object
 *
 * @param {*} obj
 * @returns {boolean}
 */
utils.isPlainObject = function(obj) {
  return !!(obj && typeof obj == 'object' && (obj.constructor == Object || obj.constructor == Array));
};

/**
 * Copy the value
 *
 * @param {*} value
 * @param {boolean} [options] 
 * @returns {*}
 */
utils.copy = function(value, options = {}) {
  if (typeof value != 'object' || !value) {
    return value;
  }

  options = { nested: true, enumerable: true, plain: false, ...options };

  const next = obj => {
    obj = this.isScopeProxy(obj) || this.isStoreProxy(obj)? obj.__target: obj; 

    if(options.plain && !this.isPlainObject(obj)) {
      return obj;
    }

    let keys = !options.enumerable? Object.getOwnPropertyNames(obj): Object.keys(obj);
    let newObj = Array.isArray(obj)? []: {}; 
    
    for (let i = 0, l = keys.length; i < l; i++) {
      let key = keys[i];
      let val = obj[key];
      val = val && typeof val == 'object' && options.nested? next(val): val;      
      
      if(!Object.prototype.propertyIsEnumerable.call(obj, key)) {
        Object.defineProperty(newObj, key, {
          ...Object.getOwnPropertyDescriptor(obj, key),
          value: val
        });

        continue;
      }

      newObj[key] = val;  
    }

    return newObj;
  }

  return next(value);
};

/**
 * Change the attribute value to the appropriate format
 *
 * @param {*} value
 * @returns {*}
 */
utils.makeAttributeValue = function(value) {
  if (value === false || value === null || value === undefined) {
    return '';
  }

  if (value instanceof EventEmitter) {
    return '[object Event]';
  }

  if (typeof value == 'function') {
    return '[object Function]';
  }

  if (typeof value == 'object') {
    return Object.prototype.toString.call(value);
  }

  return value + '';
};

/**
 * Compare two values
 *
 * @param {*} a
 * @param {*} b
 * @param {object} [options]
 * @returns {boolean}
 */
utils.compare = function (a, b, options = {}) {
  if ((a instanceof Date) && (b instanceof Date)) {
    return a.getTime() === b.getTime();
  }
  else if (typeof a == 'function' && typeof b == 'function') {
    return a.toString() === b.toString();
  }
  else if (typeof a == 'object' && typeof b == 'object') {
    if (a === null || b === null) {
      return a === b;
    }

    options = { enumerable: true, ignoreUndefined: true, ...options };

    const clearUndefined = val => {
      let obj = Array.isArray(val)? []: {};
      let keys = !options.enumerable? Object.getOwnPropertyNames(): Object.keys(val);

      for(let i = 0, l = keys.length; i < l; i++) {
        let key = keys[i];
        val[key] !== undefined && (obj[key] = val[key])
      }

      return obj;
    }

    if(options.ignoreUndefined) {
      a = clearUndefined(a);
      b = clearUndefined(b);
    }

    let aKeys = !options.enumerable? Object.getOwnPropertyNames(): Object.keys(a);
    let bKeys = !options.enumerable? Object.getOwnPropertyNames(): Object.keys(b);
    
    if (aKeys.length != bKeys.length) {
      return false;
    }

    a = this.isScopeProxy(a)? a.__target: a;
    b = this.isScopeProxy(b)? b.__target: b;

    for(let i = 0, l = aKeys.length; i < l; i++) {
      const key = aKeys[i];

      if (!this.compare(a[key], b[key])) {
        return false;
      }
    }

    return true;
  }

  return a === b;
};

/**
 * Escape the string to pass in RegExp
 * 
 * @param {string} str
 * @returns {string}
 */
utils.escapeForRegExp = function (str) {
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

/**
 * Create a variable hash
 * 
 * @param {*} source
 * @param {object} [options]
 * @param {boolean} [options.ignoreScopeHash]
 * @returns {string}
 */
utils.createHash = function(source, options = {}) {
  if(!options.ignoreScopeHash && this.isScopeProxy(source) && source.__hash) {
    return source.__hash;
  }

  const loop = obj => {
    if(!obj || typeof obj != 'object') {
      return String(obj);
    }    

    if(this.isScopeProxy(obj)) {
      if(obj !== source && obj.__hash) {
        return obj.__hash;
      }

      obj = obj.__target;
    }

    const keys = Object.keys(obj);
    const length = keys.length;
    let str = '';

    for(let i = 0; i < length; i++) {
      const key = keys[i];
      str += `"${key}"::"${loop(obj[key])}";;`;
    }

    return str;
  }

  source = loop(source);
  let hash = 0;

  for (let i = 0; i < source.length; i++) {
    let char = source.charCodeAt(i);
    hash = ((hash << 5) - hash) + char;
    hash = hash & hash;
  }

  return hash + '';
}

/**
 * Encode the html entities
 *
 * @example
 * // returns "you &amp me"
 * utils.encodeHtmlEntities('you & me');
 *
 * @param {string} html
 * @returns {string}
 */
utils.encodeHtmlEntities = function(html) {
  let el = document.createElement("div");
  let value;
  el.textContent = html;
  value = el.innerHTML;
  el.remove();
  el = null;
  return value;
};

/**
 * Decode the html entities
 *
 * @example
 * // returns "you & me"
 * utils.decodeHtmlEntities('you &amp me');
 *
 * @param {string} html
 * @returns {string}
 */
utils.decodeHtmlEntities = function(html) {
  let el = document.createElement("textarea");
  let value;
  el.innerHTML = html;
  value = el.value;
  el.remove();
  el = null;
  return value;
};

/**
 * Convert the string from a dash to a camel case
 *
 * @param {string} str
 * @returns {string}
 */
utils.toCamelCase = function(str) {
  return str.replace(/\W+(.)/g, (m, c) => c.toUpperCase());
};

/**
 * Capitalize the string
 *
 * @param {string} str
 * @returns {string}
 */
utils.capitalize = function(str) {
  return str[0].toUpperCase() + str.slice(1);
};

/**
 * Convert the string from a camel to a dash case
 *
 * @param {string} str
 * @returns {string}
 */
utils.toDashCase = function(str) {
  return str.replace(/([A-Z])/g, (m, c) => `-${c.toLowerCase()}`);
};

/**
 * Get the nested object property by array keys
 *
 * @example
 * // returns 5
 * utils.getPropertyByKeys(['x', 'y'], {x: {y: 5}});
 *
 * @param {string[]} keys
 * @param {object} object
 * @param {object} [options]
 * @param {boolean|array} [options.closest]
 * @returns {*}
 */
utils.getPropertyByKeys = function(keys, object, options = {}) {
  const breakCode = 'ERR_ARRAY_REDUCE_BREAK';
  let current;
  let length = keys.length;
  let i = 0;
  let endKeys = Array.isArray(options.closest)? options.closest: [];

  const breakReducer = () => {
    const err = new Error('Break the reducer');
    err.code = breakCode;
    throw err;
  }

  const fn = (o, k) => {
    i++;

    if (!o || typeof o != 'object') {
      breakReducer();
    }

    if (o[k] === undefined && options.closest) {
      current = endKeys.length? o: undefined;
      breakReducer();
    }

    endKeys.push(k);

    if (o[k] === undefined) {
      return {};
    }

    (i == length) && (current = o[k]);
    return o[k];
  }

  try {
    keys.reduce(fn, object);
  }
  catch(err) {
    if(err.code != breakCode) {
      throw err;
    }
  }
  
  return current;
};

/**
 * Check the object nested property existence
 *
 * @example
 * // returns true
 * utils.hasPropertyByKeys(['x', 'y'], {x: {y: 5}});
 *
 * @param {string[]} keys
 * @param {object} object
 * @returns {boolean}
 */
utils.hasPropertyByKeys = function(keys, object) {
  let has = false;
  let length = keys.length;
  let i = 0;
  keys.reduce((o, k) => {
    i++;

    if (!o || typeof o != 'object') {
      return !!o;
    }

    if (!Object.prototype.hasOwnProperty.call(o, k)) {
      return {};
    }

    (i == length) && (has = Object.prototype.hasOwnProperty.call(o, k));
    return o[k];
  }, object);
  return has;
};

/**
 * Check the object nested enumerable property existence
 *
 * @example
 * // returns true
 * utils.hasEnumerablePropertyByKeys(['x', 'y'], {x: {y: 5}});
 *
 * @param {string[]} keys
 * @param {object} object
 * @returns {boolean}
 */
utils.hasEnumerablePropertyByKeys = function(keys, object) {
  let has = false;
  let length = keys.length;
  let i = 0;
  keys.reduce((o, k) => {
    i++;

    if (!o || typeof o != 'object') {
      return !!o;
    }

    if (!Object.prototype.propertyIsEnumerable.call(o, k)) {
      return {};
    }

    (i == length) && (has = Object.prototype.hasOwnProperty.call(o, k));
    return o[k];
  }, object);
  return has;
};

/**
 * Set the object nested property value
 *
 * @example
 * // returns {y: 6}
 * utils.setPropertyByKeys(['x', 'y'], {x: {y: 5}}, (last, val) => last? 6: (val || {}));
 *
 * @param {string[]} keys
 * @param {object} object
 * @param {function} fn
 * @returns {*}
 */
utils.setPropertyByKeys = function(keys, object, fn) {
  let current = undefined;
  let length = keys.length;
  let i = 0;
  keys.reduce((o, k) => {
    i++;

    if (!o || typeof o != 'object') {
      return;
    }

    if (!Object.prototype.hasOwnProperty.call(o, k)) {
      o[k] = undefined;
    }

    o[k] = fn(i == length, o[k]);
    current = o;
    return o[k];
  }, object);
  return current;
};

/**
 * Delete the property from the object
 * You can cancel the deletion returning false in the callback function
 *
 * @example
 * // returns 5
 * utils.deletePropertyByKeys(['x', 'y'], {x: {y: 5}});
 * @example
 * // returns 5
 * utils.deletePropertyByKeys(['x', 'y'], {x: {y: 5}}, val => val != 5);
 *
 * @param {string[]} keys
 * @param {object} object
 * @param {function} [fn]
 * @returns {*}
 */
utils.deletePropertyByKeys = function(keys, object, fn) {
  let length = keys.length;
  let value;
  let i = 0;
  keys.reduce((o, k) => {
    i++;

    if (!o || typeof o != 'object') {
      return;
    }

    if (!Object.prototype.hasOwnProperty.call(o, k)) {
      return {};
    }

    if (i == length) {
      value = o[k];
      (!fn || fn(value)) && (delete o[k]);
      return value;
    }

    return o[k];
  }, object);
  return value;
};


/**
 * Get the enumerable property target object
 *
 * @param {object} target
 * @param {string[]} keys
 * @returns {object}
 */

utils.getEnumerablePropertyTarget = function(target, keys) {
  const check = obj => {
    if (this.hasEnumerablePropertyByKeys(keys, obj)) {
      return obj;
    }

    let proto = Object.getPrototypeOf(obj);

    if (!proto) {
      return null;
    }

    return check(proto);
  };

  return check(target);
};

/**
 * Get the own property target object
 *
 * @param {object} target
 * @param {string[]} keys
 * @returns {object}
 */
utils.getOwnPropertyTarget = function(target, keys) {
  const check = obj => {
    if (this.hasPropertyByKeys(keys, obj)) {
      return obj;
    }

    let proto = Object.getPrototypeOf(obj);

    if (!proto) {
      return null;
    }

    return check(proto);
  };

  return check(target);
};

/**
 * Generate a random string
 *
 * @param {number} length
 * @param {function} fn
 * @returns {*}
 */
utils.createRandomString = function(length = 16, fn = null) {
  let str = Math.random().toString(36).substring(2, length + 2);
  let now = Date.now();
  let val = '';  

  for (let i = 0, l = str.length; i < l; i++) {
    if (!(now % i)) {
      val += str[i].toUpperCase();
    }
    else {
      val += str[i];
    }
  }

  if (fn && fn(val)) {
    return this.createRandomString(length, fn);
  }

  return val;
};

/**
 * Create a function with the debounce
 *
 * @param {function} fn
 * @param {number} delay
 * @returns {fn}
 */
utils.debounce = function(fn, delay = 0) { 
  const wrapper = () => {
    clearTimeout(fn.__debounceTimeout);
    fn.__debounceTimeout = setTimeout(() => {
      fn();
      wrapper.removeDebounce();
    }, delay);
  };

  wrapper.removeDebounce = () => {
    clearTimeout(fn.__debounceTimeout);
    delete fn.__debounceTimeout;
  }

  return wrapper;
}

export default utils;

<|start_filename|>src/component.js<|end_filename|>
import Akili from './akili.js';
import Scope from './scope.js';
import request from './services/request.js';
import store from './services/store.js';
import globals from './globals.js';
import utils from './utils.js';

export const evaluationRegex = /\${(((?!\${).)*)}/;
export const evaluationRegexGlobal = new RegExp(evaluationRegex.source, "g");
export const systemAttributes = ['component', 'scope', 'no-ssr'];

/**
 * Base class from which all components are inherited
 */
export default class Component {
  static matches = '';
  static booleanAttributes = [];
  static events = [];
  static controlAttributes = false;
  static transparent = false;
  static template = '';
  static templateUrl = '';
  static scope = null;  
  static ssr = true;

  /**
   * Define the component
   */
  static define() {
    Akili.component('component', Component);
  }

  /**
   * Parse the expression
   *
   * @param {object} context
   * @param {string} expression
   * @param {object} [vars]
   */
  static parse(context, expression, variables = {}) {
    const keys = [];
    const vars = [];
    const exps = utils.split(expression, ';', ['"', "'", '`']);  
    exps[exps.length - 1] = `return ${exps[exps.length - 1]}`;

    for (let key in variables) {
      keys.push(key);
      vars.push(variables[key]);
    }
    
    return new Function(...keys, `${exps.join('; ')}`).apply(context, vars);
  }

  /** 
   * @param {Element} el
   * @param {object} [scope] 
   */
  constructor(el, scope = {}) {
    if (!(el instanceof Element)) {
      throw new Error(`You must pass an html element to the component constructor`);
    }

    this.__isCreated = false;
    this.__isMounted = false;
    this.__isCompiled = false;
    this.__isResolved = false;
    this.__isRemoved = false;
    this.__cancelled = false;
    this.__prevent = false;
    this.__bindings = {};
    this.__evaluatingEvent = null;
    this.__recompiling = null;
    this.__compiling = null;
    this.__disableProxy = {};
    this.__disableStoreKeys = {};
    this.__disableAttrKeys = {};
    this.__children = [];
    this.__parent = null;
    this.__parents = [];
    this.__content = '';
    this.__attrs = {};
    this.__attrLinks = {};
    this.__storeLinks = {};
    this.__attributeOf = null;
    this.__evaluationComponent = this;
    this.scope = scope;
    this.el = el;
    !this.constructor.ssr && window.AKILI_SSR && this.cancel();
  }

  /**
   * Create the compilation options 
   * 
   * @param {object} [options]
   * @protected
   */
  __createCompilationOptions(options = {}) {
    return { 
      checkChanges: false, 
      setEvents: true,
      setParents: true,
      setBooleanAttributes: true,
      defineAttributes: true,
      ...options 
    };
  }

  /**
   * Create the recompilation options 
   * 
   * @param {object} [options]
   * @protected
   */
  __createRecompilationOptions(options = {}) {
    return { 
      ...this.__createCompilationOptions(),
      setEvents: false,
      setParents: false,
      defineAttributes: false,
      ...options 
    };
  }
 
  /**
   * Recompile the component
   * 
   * @param {object} [options]
   * @protected
   */
  __recompile(options = {}) {
    this.__isMounted = false;
    this.__recompiling = true; 
    this.__evaluationComponent.__disableProxy = {};  
    this.__compiling = this.__createRecompilationOptions(options);    
    this.__compiling.setEvents && this.__setEvents();
    this.__compiling.setParents && this.__setParents();
    this.__compiling.setBooleanAttributes && this.__setBooleanAttributes();
    this.__compiling.defineAttributes && this.__defineAttributes();
  }

  /**
   * Create the component
   *
   * @protected
   */
  __create() {
    this.__compiling = this.__createCompilationOptions(); 
    this.__initialize();
    this.__setEvents();
    this.__setParents();
    this.__setBooleanAttributes();
    this.__defineAttributes(); 
    Akili.isolate(() => this.created(this.attrs)); 
    this.__isCreated = true;
  }

  /**
   * Compile the node
   *
   * @protected
   * @returns {Promise}
   */
  __compile() {
    let control = this.__controlAttributes || !this.__evaluationParent;
    let p = Promise.resolve();
    this.__attributeOf = control? this: this.__evaluationParent.__akili;

    if (!this.__recompiling || this.__compiling.newParent || this.__controlAttributes) {
      this.__interpolateAttributes(this.el, this.__attributeOf);
    }
    
    const interpolate = (children, parent) => {
      for (let i = 0, l = children.length; i < l; i++) {
        let child = children[i];

        if (child.nodeType == 3 && this.__initializeNode(child, parent)) {          
          this.__evaluateNode(child, this.__compiling? this.__compiling.checkChanges: false);  
        }
        else if (child.nodeType == 1 && !child.__akili) {
          this.__interpolateAttributes(child);
          interpolate(child.childNodes, child);
        }
      }
    };    

    interpolate(this.el.childNodes, this.el);
    this.__isMounted = true;
    let res;

    if (!this.__recompiling) {
      res = Akili.isolate(() => {
        this.attrs.onCompiled && this.attrs.onCompiled.trigger(undefined, { bubbles: false });
        return this.compiled();
      });

      if (this.constructor.templateUrl) {
        p = request.get(this.constructor.templateUrl, { cache: this.constructor.templateCache }).then(res => {
          this.el.innerHTML = this.__content;
          Akili.setTemplate(this.el, res.data);
          delete this.__content;          
          return Akili.compile(this.el, { recompile: true });
        });
      }
    }
    else {
      res = Akili.isolate(() => {
        this.attrs.onRecompiled && this.attrs.onRecompiled.trigger(undefined, { bubbles: false });
        this.recompiled();
      });
    }
    
    this.__recompiling = null;
    this.__compiling = null;

    return p.then(() => {
      this.__isCompiled = true;
      return res;
    });
  }

  /**
   * Resolve the component
   *
   * @protected
   * @returns {Promise}
   */
  __resolve() {
    if (this.__isResolved || this.__isRemoved) {
      return Promise.resolve();
    }
    
    this.attrs.onResolved && this.attrs.onResolved.trigger(undefined, { bubbles: false });

    return Promise.resolve(Akili.isolate(() => this.resolved())).then(res => {
      this.__isResolved = true;
      return res;
    });
  }

  /**
   * Initialize the component
   *
   * @protected
   */
  __initialize() {    
    let parent = Akili.getAkiliParents(this.el, false);     
    let Scope = this.constructor.scope || Akili.Scope;
    let scope;
    let isRoot = Akili.__root === this.el;
    this.el.__akili = this;

    if (parent) {
      scope = new Scope(this.el.getAttribute('scope') || Akili.createScopeName(), this.el, this);
    }
    else {
      scope = new Scope(isRoot? 'root': Akili.createScopeName(), this.el, this);
      isRoot && (Akili.root = this);
    }

    let __scope = scope;
    let _scope = Object.assign(scope, this.scope);
    let controlAttributes = this.constructor.controlAttributes;
    let events = this.constructor.events;

    if (this.constructor.template) {
      Akili.setTemplate(this.el, this.constructor.template);
    }

    if (this.constructor.templateUrl) {
      this.__content = this.el.innerHTML;
      this.el.innerHTML = '';
    }

    this.__scope = __scope;
    this.__events = events;
    this.__controlAttributes = controlAttributes;
    Akili.addScope(scope);    
    this.scope = this.__nestedObserve(_scope, []);
    return true;
  }

  /**
   * Set the boolean attributes
   *
   * @protected
   */
  __setBooleanAttributes() {
    this.booleanAttributes = [].concat(Akili.htmlBooleanAttributes, this.constructor.booleanAttributes);

    const setAttr = el => {
      for (let i = 0, attrs = el.attributes, l = attrs.length; i < l; i++) {
        let node = attrs[i];

        if (this.booleanAttributes.indexOf(node.nodeName) != -1) {
          if (el.hasAttribute(`boolean-${node.nodeName}`)) {
            continue;
          }

          el.setAttribute(`boolean-${node.nodeName}`, el.getAttribute(node.nodeName) || node.nodeName);
          el.removeAttribute(node.nodeName);
        }
      }

      for (let i = 0, l = el.children.length; i < l; i++) {
        let child = el.children[i];

        if (!child.__akili) {
          setAttr(child);
        }
      }
    };

    setAttr(this.el);
  }

  /**
   * Set the events
   *
   * @protected
   */
  __setEvents() {
    for (let i = 0, l = this.__events.length; i < l; i++) {
      let ev = this.__events[i];
      !/^on-/i.test(ev) && (ev = 'on-' + ev);

      if (!this.el.hasAttribute(ev)) {
        this.el.setAttribute(ev, '');
      }
    }
  }

  /**
   * Set the component parents
   *
   * @protected
   */
  __setParents() {
    let parents = Akili.getAkiliParents(this.el);

    if (!parents.length) {
      return;
    }

    let newParent = this.__parent !== parents[0];
    let evaluationParent = null;

    for (let i = 0, l = parents.length; i < l; i++) {
      let parent = parents[i];

      if (!parent.__akili.constructor.transparent) {
        evaluationParent = parent;
        break;
      }
    }

    this.__compiling.newParent = newParent;
    newParent && this.__detach();

    if (this.constructor.transparent) {
      this.__evaluationComponent = evaluationParent.__akili;
    }

    this.__evaluationParent = evaluationParent;
    this.__parent = parents[0];
    this.__parents = parents;
    this.scope.__parent = this.__evaluationParent.__akili.scope;
    !this.__recompiling && this.__parent.__akili.__addChild(this.el);
    Object.setPrototypeOf(this.scope, this.__parent.__akili.__scope);
  }

  /**
   * Add the child element to the list
   *
   * @param {Element} el
   * @protected
   */
  __addChild(el) {
    this.__children.push(el);
  }

  /**
   * Splice the child from the list
   *
   * @param {Element} el
   * @protected
   */
  __spliceChild(el) {
    for (let i = 0, l = this.__children.length; i < l; i++) {
      let child = this.__children[i];

      if (child === el) {
        this.__children.splice(i, 1);
        i--;
        l--;
      }
    }
  }

  /**
   * Get the parsed expression
   *
   * @param {string} expression
   * @returns {*}
   * @protected
   */
  __getParsedExpression(expression) {
    return expression;
  }

  /**
   * Check the need of the avaluation
   *
   * @param {Node} node
   * @returns {boolean}
   * @protected
   */
  __checkEvaluation(node) {
    if (!Object.keys(node.__properties).length) {
      return true;
    }

    for (let k in node.__properties) {
      if (!Object.prototype.hasOwnProperty.call(node.__properties, k)) {
        continue;
      }

      let prop = node.__properties[k];
      let value = utils.getPropertyByKeys(prop.keys, prop.component.__scope);

      if (!node.__component.__compareNodePropertyValue(prop, value)) {
        return true;
      }
    }

    return false;
  }

  /**
   * Check the node properties change
   *
   * @param {Node} node
   * @param {string[]} keys
   * @param {*} value
   * @returns {boolean}
   * @protected
   */
  __checkNodePropertyChange(node, keys, value) {
    let prop = this.__getNodeProperty(node, keys);
    
    if (!prop) {
      return true;
    }

    return !node.__component.__compareNodePropertyValue(prop, value);
  }

  /**
   * Compare the node property value
   * 
   * @param {object} prop 
   * @param {*} value 
   * @param {object} [options] 
   * @protected
   */
  __compareNodePropertyValue(prop, value, options = {}) {  
    return utils.compare(utils.createHash(value), prop.hash, options);
  }

  /**
   * Create an exception message
   * 
   * @param {Node} node 
   * @param {Error} err 
   * @protected
   */
  __createExceptionMessage(node, err) {
    let tagName = node.__component.el.tagName;
    let attrName = node.__component.el.getAttribute('component');
    let componentName = (attrName || tagName).toLowerCase();
    let elementName = node.__element.tagName.toLowerCase();
    let attributeName = (node instanceof window.Attr)? node.name.toLowerCase(): '';        
    let messages = [ err.message, node.__expression.trim() ];
    attributeName && messages.push(`[attribute ${attributeName}]`);
    messages = messages.concat([`[element ${elementName}]`, `[component ${componentName}]`]);
    return `Expression error: ` + messages.join('\n\tat ');
  }

  /**
   * Evaluate the node expression
   *
   * @param {Node} node
   * @returns {*}
   * @protected
   */
  __evaluate(node) {
    let counter = 0;
    let attributeValue;
    let expression;
    const evalComponent = node.__attributeOf || node.__component;

    if (node.__component.parents(com => com.__prevent).length) {
      return { res: node.__expression };
    }

    if (!(node instanceof window.Attr) && node.__component.__prevent) {
      return { res: node.__expression };
    }
    
    let res = node.__expression.replace(evaluationRegexGlobal, (m, d) => {     
      counter++;
      let evaluate;
      let parseValue = node.__component.__getParsedExpression(d);
      this.__finishEvaluationCycle();
      Akili.__evaluation = { node, list: [], component: evalComponent };
      
      try {        
        evaluate = this.constructor.parse(evalComponent.__evaluationComponent.scope, parseValue, { ...globals });
      }
      catch (err) {        
        throw this.__createExceptionMessage(node, err);
      }

      this.__finishEvaluationCycle();

      if (node instanceof window.Attr) {
        expression = m;
        attributeValue = evaluate;
        return utils.makeAttributeValue(evaluate);
      }

      if (typeof evaluate == 'object') {
        try {
          return JSON.stringify(evaluate);
        }
        catch(e) {
          return evaluate;
        }
      }

      return evaluate;
    });

    return { res, counter, expression, attributeValue };
  }

  /**
   * Finish the evaluation cycle
   *
   * @protected
   */
  __finishEvaluationCycle() {
    let evaluation = [];
    let existingBindings = {};
    let node;
    let component;
    
    if(Akili.__evaluation) {
      node = Akili.__evaluation.node;
      component = Akili.__evaluation.component;
      evaluation = Akili.__evaluation.list;
      Akili.__evaluation.list = null;
      Akili.__evaluation = null;
    }

    for (let i = evaluation.length - 1; i >= 0; i--) {
      const data = evaluation[i];

      if(data.component.__isRemoved) {
        continue;
      }

      const hash = data.component.__createKeysHash(data.keys);

      if (data.notBinding) {
        continue;
      }

      if (existingBindings[hash]) {
        continue;
      }

      const parentValue = utils.getPropertyByKeys(data.parents, data.component.__scope);
              
      if (
        utils.isScopeProxy(parentValue) && 
        data.component !== component.__evaluationComponent && 
        data.component === data.bindComponent
      ) {
        continue;
      }

      data.bindComponent.__bindAndSetProperty(node, data.keys, data.evaluated);
      existingBindings[hash] = true;
    }
  }

  /**
   * Bind the node and set the property
   *
   * @param {Node} node
   * @param {string[]} keys
   * @param {boolean} [evaluated]
   * @protected
   */
  __bindAndSetProperty(node, keys, evaluated) {
    let bind = this.__getBoundNode(keys, node);
    let value = utils.getPropertyByKeys(keys, this.__scope);

    if (!bind) {
      this.__bind(keys, { node, keys });
    }   

    this.__setNodeProperty(node, keys, value, evaluated);
  }

  /**
   * Get the keys evaluation components
   *
   * @param {string[]} keys
   * @returns {Component[]}
   * @protected
   */
  __getEvaluationComponents(keys) {
    let results = [];

    const loop = component => {
      if(component.__getBind(keys)) {
        results.push(component);
      }
      
      for(let i = 0; i < component.__children.length; i++) {       
        loop(component.__children[i].__akili);
      }
    }

    loop(this);
    return results.length? results: [this];
  }

  /**
   * Evaluate the component own keys
   *
   * @param {string[]} keys
   * @protected
   */
  __evaluateOwnKeys(keys) {
    const data = this.__getAllBinds(keys);
   
    for(let i = 0; i < data.length; i++) {         
      const res = data[i];

      if(!res) {
        continue;
      }

      const node = res.node;

      if(!res.node || !node.__initialized) {
        continue;
      }

      const value = utils.getPropertyByKeys(res.keys, this.__scope);

      if(!this.__checkNodePropertyChange(node, res.keys, value)) {
        continue;
      }

      this.__disableKeys(keys);        
      this.__evaluateNode(node);

      for (let key in node.__properties) {
        const prop = node.__properties[key];
        prop.component.__setNodeProperty(node, res.keys, value);
      }

      this.__enableKeys(keys);        
    }
  }
  
  /**
   * Evaluate all the tree
   *
   * @param {string[]} keys
   * @protected
   */
  __evaluateByKeys (keys) {
    keys = [...keys];
    
    const evaluate = () => {    
      const components = this.__getEvaluationComponents(keys);

      for(let i = 0; i < components.length; i++) {
        const component = components[i];
        component.__evaluateOwnKeys(keys);
      }
      
      keys.pop();
      keys.length && evaluate();  
    }
    
    evaluate();
  }

  /**
   * Evaluate the event expression
   *
   * @param {Node} node
   * @param {Element} el
   * @param {Event} e
   * @protected
   */
  __evaluateEvent(node, el, e) {
    let expression = evaluationRegex.exec(node.__expression);
    let evaluate;

    if (!expression) {
      return;
    }

    this.__evaluatingEvent = {
      el: el,
      component: this,
      event: e,
      node
    };

    this.__disableProxy = {};

    try {
      evaluate = this.constructor.parse(this.__evaluationComponent.scope, expression[1], { ...globals,  event: e });
    }
    catch (err) {        
      throw this.__createExceptionMessage(node, err);
    }
    
    this.__evaluatingEvent = null;    
    return evaluate;
  }

  /**
   * Evaluate the node expression with check
   *
   * @param {Node} node
   * @param {boolean} [check]
   * @protected
   */
  __evaluateNode(node, check = true) {
    const key = node instanceof window.Attr? 'value': 'nodeValue';  

    if (check? this.__checkEvaluation(node): true) { 
      const { res, attributeValue, expression, counter } = this.__evaluate(node);  
      node[key] != res && (node[key] = res);

      if (node instanceof window.Attr) {
        let value = res;
        let isBooleanAttribute = false;
  
        if (counter == 1 && expression && node.__expression == expression) {
          value = attributeValue;
        }
  
        const clearAttribute = node.nodeName.replace(/^boolean-(.+)/i, '$1');
        const camelAttribute = utils.toCamelCase(clearAttribute);
  
        if (clearAttribute != node.nodeName) {
          isBooleanAttribute = true;
          value = !!value;
        }          
  
        if (node.__attributeOn) {        
          const component = node.__attributeOn;
          value = component.__prepareAttributeIn(node, value);
          component.__disableAttributeSetter = true;
          component.attrs[camelAttribute] = value;
          component.__disableAttributeSetter = false;
  
          if (component.__isMounted) {
            component.__attrTriggerByName(camelAttribute, value);
          }
        }
        else if (isBooleanAttribute) {
          const element = node.__element;
          value? element.setAttribute(camelAttribute, 'true'): element.removeAttribute(camelAttribute);
        }
      }
    }

    return node[key];
  }

  /**
   * Prepare the attribute value for receiving
   * 
   * @param {Node} node
   * @param {*} value
   * @protected
   */
  __prepareAttributeIn(node, value) {    
    return utils.copy(value, { plain: true });
  }

  /**
   * Prepare the attribute value for sending
   * 
   * @param {Node} node
   * @param {*} value
   * @protected
   */
  __prepareAttributeOut(node, value) {    
    return utils.copy(value, { plain: true });
  }

  /**
   * Initialize the attributes
   *
   * @param {Node} node
   * @param {Element} el
   * @param {Component} attributeOf - if node is linked with parent scope
   * @protected
   */
  __initializeAttribute(node, el, attributeOf) {
    if (!node || systemAttributes.indexOf(node.nodeName) != -1) {
      return;
    }

    if(!this.__initializeNode(node, el)) {
      return;
    }

    let eventName = node.nodeName.replace(/^on-(.+)/i, '$1');    
    let component = attributeOf? attributeOf: this;

    if (node.__isEvent) {
      if (node.__event) {
        return;
      }

      const emitter = new Akili.EventEmitter(eventName, node, el, component);

      if (node.__hasBindings) {
        emitter.bind(e => component.__evaluateEvent(node, el, e));
      }

      node.__event = emitter;
      el.setAttribute(node.nodeName, utils.makeAttributeValue(emitter));

      if (attributeOf) {
        this.__disableAttributeSetter = true;
        this.attrs[utils.toCamelCase(node.nodeName)] = emitter;
        this.__disableAttributeSetter = false;
      }

      return;
    }

    if (attributeOf) {
      node.__attributeOn = this;
      node.__attributeOf = component;
    }
    
    let check = this.__compiling? this.__compiling.checkChanges: false;
    component.__evaluateNode(node, check);
  }

  /**
   * Initialize the node
   *
   * @param {Node} node
   * @param {Element} el
   * @returns {boolean}
   * @protected
   */
  __initializeNode(node, el) {
    if (node.__initialized) {
      return true;
    }

    const isAttr = node instanceof window.Attr;
    const val = node[isAttr? 'value': 'nodeValue'];
    const hasBindings = evaluationRegex.test(val.trim());
    const isBoolean = isAttr? /^boolean-/i.test(node.nodeName): false;
    const isEvent = isAttr? /^on-(.+)/i.test(node.nodeName): false;

    if(!el.__akili && !hasBindings && !isBoolean && !isEvent) {
      return false;
    }

    node.__name = this.__scope.__name + utils.createRandomString(16);
    node.__isEvent = isEvent;
    node.__hasBindings = hasBindings;
    node.__isBoolean = isBoolean;
    node.__expression = val;
    node.__properties = {};
    node.__attributeOf = null;
    node.__attributeOn = null;
    node.__event = null;    
    node.__initialized = true;
    node.__component = this;
    node.__element = el;
    return true;
  }

  /**
   * Deinitialize the node
   *
   * @param {Node} node
   * @param {object} [options]
   * @protected
   */
  __deinitializeNode(node, options = {}) {
    if(!node.__initialized) {
      return;
    }
    
    if (node.__event) {
      node.__event.remove();
    }

    if (node.__hasBindings && !options.saveBindings) { 
      this.__unbindByNodes(node);     
      this.__unbindParentsByNodes(node);    
    }
    
    Akili.removeTag(node);    
    delete node.__name;
    delete node.__hasBindings;
    delete node.__isBoolean;
    delete node.__isEvent;
    delete node.__expression;
    delete node.__properties;
    delete node.__attributeOf;
    delete node.__attributeOn;
    delete node.__event;    
    delete node.__initialized;
    delete node.__component;
    delete node.__element;    
  }

  /**
   * Interpolate the element attributes
   *
   * @param {Element} el
   * @param {Component} [attributeOf=null] - if the node has link with the parent scope
   * @protected
   */
  __interpolateAttributes(el, attributeOf = null) {
    for (let i = 0, attrs = el.attributes, l = attrs.length; i < l; i++) {
      this.__initializeAttribute(attrs[i], el, attributeOf);
    }
  }

  /**
   * Define the attributes as a proxy
   *
   * @protected
   */
  __defineAttributes() {
    const changeAttribute = (key, value, isDeleted = false) => {
      if (this.__disableAttributeSetter) {
        return;
      }

      let node = this.el.getAttributeNode(key);

      if (isDeleted) {
        node && this.__deinitializeNode(node);
        this.el.removeAttribute(key);
        return 
      }
      else if (!node) {
        this.el.setAttribute(key, value);
        node = this.el.getAttributeNode(key);
      }
      else {
        node.value = value;
        this.__deinitializeNode(node);
      }

      this.__initializeAttribute(node, this.el, this.__attributeOf);
    };

    this.attrs = new Proxy(this.__attrs, {
      get: (target, key) => {
        if (key == '__isProxy') {
          return true;
        }

        return target[key];
      },
      set: (target, key, value) => {
        let attrKey = utils.toDashCase(key);

        if (this.booleanAttributes.indexOf(attrKey) != -1) {
          value? this.el.setAttribute(attrKey, value): this.el.removeAttribute(attrKey);
          attrKey = `boolean-${attrKey}`;
        }
        
        target[key] = value;
        changeAttribute(attrKey, utils.makeAttributeValue(value));
        return true;
      },
      deleteProperty: (target, key, value) => {
        let attrKey = utils.toDashCase(key);
        changeAttribute(attrKey, utils.makeAttributeValue(value), true);
        delete target[key];
        return true;
      }
    });
  }

  /**
   * Convert the object to a proxy
   *
   * @param {object} obj
   * @param {string[]} parents
   * @returns {Proxy}
   * @protected
   */
  __observe(obj, parents) {
    return new Proxy(obj, {
      get: (target, key) => { 
        if (key == "__isProxy") {
          return true;
        }
        else if (key == "__target") {
          return obj;
        }
        else if (key == "__component") {
          return this;
        }
        else if (key == "__keys") {
          return parents;
        }
        else if (this.__isSystemKey(key)) {
          return target[key];
        }

        if (Akili.__evaluation && !this.__isRemoved) { 
          let keys = [].concat(parents, [key]);
          let notBinding = false;
          let evaluated = !utils.getOwnPropertyTarget(this.__scope, keys);
          let component = this;

          if (target instanceof Scope) {
            let realTarget = utils.getOwnPropertyTarget(target, [key]);
            realTarget && (realTarget instanceof Scope) && (component = realTarget.__component);
          } 

          if (Akili.__wrapping && keys.length > 1) {
            return target[key];
          }            

          if (key in target && !utils.getEnumerablePropertyTarget(target, [key])) {
            notBinding = true;
          }

          this.__bindNode(Akili.__evaluation.list, component, keys, parents, target[key], notBinding, evaluated);
          return target[key];
        }

        return target[key];
      },
      set: (target, key, value) => {           
        let keys = [...parents, key];

        if (this.__isRemoved || this.__isSystemKey(key) || this.__checkDisablement(keys)) {
          target[key] = value;
          this.__setScopeObjectHash(target, keys);
          return true;
        }

        if(this.__isCreated && value !== undefined && !Object.prototype.hasOwnProperty.call(target, key) && key in target) {          
          const tScope = utils.getEnumerablePropertyTarget(target, [key]);
          tScope.__component.scope[key] = value;
          return false;
        }

        if (typeof value === 'function') {
          value = Akili.wrapFunction(value);
        }

        target[key] = this.__nestedObserve(value, keys);
        
        if(!this.__isResolved) {
          this.__triggerStoreAndAttr(keys);
        }
       
        if (Akili.__isolation) { 
          this.__createIsolationObject(parents, key);          
          return true;
        }

        this.__setScopeObjectHash(target, keys);

        if(this.__isResolved) {
          this.__triggerStoreAndAttr(keys);
        }
        
        if (this.__isMounted) {                   
          this.__evaluateByKeys(keys);
        }

        return true;
      },
      deleteProperty: (target, key) => {
        const keys = [].concat(parents, [key]);

        if (this.__isRemoved || this.__isSystemKey(key) || this.__checkDisablement(keys)) {
          delete target[key];
          this.__setScopeObjectHash(target, keys);
          return true;
        }

        delete target[key];

        if(!this.__isResolved) {
          this.__triggerStoreAndAttr(keys);
        }
        
        if (Akili.__isolation) {
          this.__createIsolationObject(parents, key);
          return true;
        }

        if(this.__isResolved) {
          this.__triggerStoreAndAttr(keys);
        }

        this.__setScopeObjectHash(target, keys);
        this.__evaluateByKeys(keys);
        return true;
      }
    });
  }

  /**
   * Trigger the store and the attributes change
   * 
   * @param {string[]} keys 
   * @protected
   */
  __triggerStoreAndAttr(keys) {
    for(let i = 0, l = keys.length; i < l; i++) {
      const currentKeys = keys.slice(0, l - i);
      const currentKeyString = Akili.joinBindingKeys(currentKeys);
      const val = utils.getPropertyByKeys(currentKeys, this.__scope);

      if (this.__storeLinks[currentKeyString]) {  
        this.__storeTriggerByKeys(currentKeys, val);
      }

      if (this.__attrLinks[currentKeyString]) {
        this.__attrTriggerByKeys(currentKeys, val);
      }
    }
  }

  /**
   * Create the hash for the keys
   * 
   * @param {string[]} keys 
   * @returns {string}    
   * @protected
   */
  __createKeysHash(keys) {
    return `scope.${this.__scope.__name}.${Akili.joinBindingKeys(keys)}`;    
  }

  /**
   * Get the store of the disablement type
   * 
   * @param {string} type 
   * @returns {object}
   * @protected
   */
  __getDisablementTypeStore(type) {
    return ({
      proxy: '__disableProxy',
      store: '__disableStoreKeys',
      attr: '__disableAttrKeys'
    })[type];
  }

  /**
   * Disable the keys setter
   * 
   * @param {string[]} keys  
   * @param {string} [type]  
   * @protected
   */
  __disableKeys(keys, type = 'proxy') {    
    this[this.__getDisablementTypeStore(type)][keys? this.__createKeysHash(keys): '__all'] = true;
  }

  /**
   * Enable the keys setter
   * 
   * @param {string[]} keys 
   * @param {string} [type]   
   * @protected
   */
  __enableKeys(keys, type = 'proxy') {
    delete this[this.__getDisablementTypeStore(type)][keys? this.__createKeysHash(keys): '__all'];
  }

  /**
   * Check the keys setter disablemant
   * 
   * @param {string[]} keys 
   * @param {string} [type]   
   * @protected
   */
  __checkDisablement(keys, type = 'proxy') {
    const store = this.__getDisablementTypeStore(type);
    return this[store].__all? true: this[store][this.__createKeysHash(keys)];
  }

  /**
   * Save the value to the store by the keys
   * 
   * @param {string[]} keys 
   * @param {*} value    
   * @protected
   */
  __storeTriggerByKeys(keys, value) { 
    if (this.__checkDisablement(keys, 'store')) {      
      return;
    }

    const links = this.__storeLinks[Akili.joinBindingKeys(keys)];

    if (!links || !links.length) {
      return;
    }
    
    value = utils.copy(value, { plain: true });
    const p = [];

    for (let i = 0, l = links.length; i < l; i++) {
      const link = links[i];   
      
      if (!link.set) {
        continue;
      }

      p.push(this.__storeTriggerByName(link.name, value));      
    }
    
    return Promise.all(p);
  }

  /**
   * Save the value to the store by the name
   * 
   * @param {string} name 
   * @param {*} value 
   * @protected
   */
  __storeTriggerByName(name, value) {
    Akili.__disableStoreProxy = true;
    store[name] = value;
    Akili.__disableStoreProxy = false;
    let links = (Akili.__storeLinks[name] || []).concat(Akili.__storeLinks['*'] || []);

    if (!links || !links.length) {
      return;
    }

    links = utils.sort(links, ['date'], true);

    for (let i = 0, l = links.length; i < l; i++) {
      const link = links[i];
      const component = link.component;

      if (component === this) {
        continue;
      }
      
      if (link.fn) {
        Akili.isolate(() => link.fn.call(component, value, name));
        continue;
      }
      if (!link.get) {
        continue;
      }

      component.__disableKeys(link.keys, 'store');
      let current = utils.getPropertyByKeys(link.keys, component.__scope);
      !utils.compare(current, value) && component.scope.__set(link.keys, value);   
      component.__enableKeys(link.keys, 'store');
    }   
  }

  /**
   * Create the store link with the scope property
   * 
   * @param {string} name 
   * @param {string|string[]} keys
   * @param {obhect} [options]
   * @protected
   */
  __storeByKeys(name, keys, options = {}) {
    options = { get: true, set: true, ...options };

    if (!keys) {
      throw new Error(`Store link "${name}" must have the scope property name`);
    }

    if (!options.get && !options.set) {
      throw new Error(`Store link "${name}" must have at least "get" or "set" option as true`);
    }

    if (!Array.isArray(keys)) {
      keys = [keys];
    }
    
    this.__disableKeys(keys, 'store');
    (Object.prototype.hasOwnProperty.call(store, name) || !utils.hasPropertyByKeys(keys, this.__scope)) && this.scope.__set(keys, store[name]);
    this.__enableKeys(keys, 'store');
    let keyString = Akili.joinBindingKeys(keys);
    let info;

    if (!this.__storeLinks[keyString]) {
      this.__storeLinks[keyString] = [];
    }

    let arr = this.__storeLinks[keyString];
    
    for (let i = arr.length - 1; i >= 0; i--) {
      let res = arr[i];

      if (res.component === this && res.name == name && res.keyString == keyString) {
        arr[i] = { ...res, ...options, date: Date.now() };
        return;
      }
    }

    if (!Akili.__storeLinks[name]) {
      Akili.__storeLinks[name] = [];
    }

    info = { 
      ...options,
      component: this, 
      name, keys, 
      keyString, 
      date: Date.now()
    };
    this.__storeLinks[keyString].push(info);
    Akili.__storeLinks[name].push(info);
  }

  /**
   * Create the store link with the function
   * 
   * @param {string} name 
   * @param {function} fn   
   * @param {object} [options]
   * @protected
   * @returns {*}
   */
  __storeByFunction(name, fn, options = {}) { 
    let call = options.callOnStart === undefined? Object.prototype.hasOwnProperty.call(store.__target, name): options.callOnStart;

    if (!Akili.__storeLinks[name]) {
      Akili.__storeLinks[name] = [];
    }

    let links = Akili.__storeLinks[name];
    
    for (let i = links.length - 1; i >= 0; i--) {
      let res = links[i];

      if (res.component === this && res.name == name && res.fn === fn) {
        links[i] = { ...res, ...options, date: Date.now() };
        return;
      }
    }

    Akili.__storeLinks[name].push({ 
      ...options,
      component: this, 
      name, 
      fn,
      date: Date.now() 
    });

    if (name == '*' && options.callOnStart !== false) {
      let storeKeys = Object.keys(store.__target);
      let p = [];

      for (let i = 0, l = storeKeys.length ; i < l; i++) {
        let key = storeKeys[i];
        let val = utils.copy(store.__target[key], { plain: true });
        p.push(fn.call(this, val, key));
      }

      return Promise.all(p);
    }

    if (call) {
      return fn.call(this, utils.copy(store.__target[name], { plain: true }));
    }
  }

  /**
   * Remove the store link with the scope property
   * 
   * @param {string} name
   * @param {string|string[]} keys
   * @protected
   */
  __unstoreByKeys(name, keys) {
    if (!keys) {
      throw new Error(`You have to pass the scope property name for store link "${name}"`);
    }

    if (!Array.isArray(keys)) {
      keys = [keys];
    }

    let keyString = Akili.joinBindingKeys(keys);
    let arr = this.__storeLinks[keyString];

    if (!arr.length) {
      return;
    }
    
    for (let i = arr.length - 1; i >= 0; i--) {
      let res = arr[i];
      let links = Akili.__storeLinks[res.name];

      if (res.component !== this || res.name != name || res.keyString != keyString) {
        continue;
      }

      arr.splice(i, 1);

      if (!links || !links.length) {
        continue;
      }

      for (let k = links.length - 1; k >= 0; k--) {
        let link = links[k];

        if (link.component === this && link.keyString == keyString) {
          links.splice(k, 1);
        }
      }

      if (!links.length) {
        delete Akili.__storeLinks[name];
      }
    }

    if (!arr.length) {
      delete this.__storeLinks[keyString];
    }
  }

  /**
   * Remove the store link with the function
   * 
   * @param {string} name 
   * @param {function} fn 
   * @protected
   */
  __unstoreByFunction(name, fn) {
    if (!Akili.__storeLinks[name]) {
      return;
    }

    let links = Akili.__storeLinks[name];
    
    for (let i = links.length - 1; i >= 0; i--) {
      let res = links[i];

      if (res.component === this && res.name == name && res.fn === fn) {
        links.splice(i, 1);
        break;
      }
    }

    if (!links.length) {
      delete Akili.__storeLinks[name];
    }
  }

  /**
   * Trigger the attribute event by the keys
   * 
   * @param {string[]} keys 
   * @param {*} value    
   * @protected
   */
  __attrTriggerByKeys(keys, value) {
    if (this.__checkDisablement(keys, 'attr')) {
      return;
    }
    
    const links = this.__attrLinks[Akili.joinBindingKeys(keys)];
   
    if (!links || !links.length) {
      return;
    }
    
    return Akili.isolate(() => {
      for (let i = 0, l = links.length; i < l; i++) {
        const link = links[i];
        
        if (link.fn) {
          continue;
        }

        if (!link.set) {
          continue;
        }
        
        const ev = 'on' + utils.capitalize(link.name);
        this.attrs[ev] && this.attrs[ev].trigger(value, { bubbles: true });
      }
    });  
  }

  /**
   * Trigger the attribute event by the name
   * 
   * @param {string} name 
   * @param {*} value 
   * @protected
   */
  __attrTriggerByName(name, value) {
    let links = (this.__attrLinks[name] || []).concat(this.__attrLinks['*'] || []);
    
    if (!links || !links.length) {
      return;
    }  
    
    links = utils.sort(links, ['date'], true);
    
    for (let i = 0, l = links.length; i < l; i++) {
      const link = links[i];    

      if (link.fn) {
        Akili.isolate(() => link.fn.call(this, value, utils.toDashCase(name)));
        continue;
      }
      
      if(!link.get) {
        continue;
      }
      
      this.__disableKeys(link.keys, 'attr');
      let current = utils.getPropertyByKeys(link.keys, this.__scope);
      !utils.compare(current, value) && this.scope.__set(link.keys, value);
      this.__enableKeys(link.keys, 'attr');
    }
  }

  /**
   * Create the  attribute link with the scope property
   * 
   * @param {string} name 
   * @param {string|string[]} keys
   * @param {object} [options]
   * @protected
   */
  __attrByKeys(name, keys, options = {}) {   
    options = { get: true, set: true, ...options };
    name = utils.toCamelCase(name);

    if (!keys) {
      throw new Error(`Attribute link "${name}" must have the scope property name`);
    }

    if (!Array.isArray(keys)) {
      keys = [keys];
    }
    
    this.__disableKeys(keys, 'attr');
    (Object.prototype.hasOwnProperty.call(this.attrs, name) || !utils.hasPropertyByKeys(keys, this.__scope)) && this.scope.__set(keys, this.attrs[name]); 
    this.__enableKeys(keys, 'attr');
    let keyString = Akili.joinBindingKeys(keys);
    
    if (!this.__attrLinks[keyString]) {
      this.__attrLinks[keyString] = [];
    }

    let arr = this.__attrLinks[keyString];
    
    for (let i = arr.length - 1; i >= 0; i--) {
      let res = arr[i];

      if (res.name == name && res.keyString == keyString) {
        arr[i] = { ...res, ...options, date: Date.now() };
        return;
      }
    }

    this.__attrLinks[keyString].push({
      ...options, 
      name, 
      keys,
      keyString, 
      date: Date.now(),
      component: this 
    });
  }

  /**
   * Create the attribute link with the function
   * 
   * @param {string} name 
   * @param {function} fn
   * @param {object} [options]
   * @protected 
   * @returns {*}
   */
  __attrByFunction(name, fn, options = {}) {    
    name = utils.toCamelCase(name);
    let call = options.callOnStart === undefined? Object.prototype.hasOwnProperty.call(this.attrs, name): options.callOnStart;    

    if (!this.__attrLinks[name]) {
      this.__attrLinks[name] = [];
    }

    let links = this.__attrLinks[name];
    
    for (let i = links.length - 1; i >= 0; i--) {
      let res = links[i];

      if (res.name == name && res.fn === fn) {
        links[i] = { ...res, ...options, date: Date.now() };
        return;
      }
    }
    
    this.__attrLinks[name].push({ 
      ...options,
      name, 
      fn,
      date: Date.now(), 
      component: this 
    });

    if (name == '*' && options.callOnStart !== false) {
      let attrsKeys = Object.keys(this.__attrs).filter(k => !(this.__attrs[k] instanceof Akili.EventEmitter));
      let p = [];

      for (let i = 0, l = attrsKeys.length; i < l; i++) {
        let key = attrsKeys[i];
        let val = this.__attrs[key];
        p.push(fn.call(this, val, utils.toDashCase(key)));
      }

      return Promise.all(p);
    }
    
    if (call) {
      return fn.call(this, this.attrs[name]);
    }
  }

  /**
   * Remove the attribute link with the scope property
   * 
   * @param {string} name
   * @param {string|string[]} keys
   * @protected
   */
  __unattrByKeys(name, keys) {
    if (!keys) {
      throw new Error(`You have to pass the scope property name for attribute link "${name}"`);
    }

    if (!Array.isArray(keys)) {
      keys = [keys];
    }

    const keyString = Akili.joinBindingKeys(keys);
    const arr = this.__attrLinks[keyString];

    if (!arr.length) {
      return;
    }
    
    for (let i = arr.length - 1; i >= 0; i--) {
      let res = arr[i];

      if (res.name != name || res.keyString != keyString) {
        continue;
      }

      arr.splice(i, 1);
    }

    if (!arr.length) {
      delete this.__attrLinks[keyString];
    }
  }

  /**
   * Remove the attribute link with the function
   * 
   * @param {string} name 
   * @param {function} fn
   * @protected
   */
  __unattrByFunction(name, fn) {
    if (!this.__attrLinks[name]) {
      return;
    }

    const links = this.__attrLinks[name];
    
    for (let i = links.length - 1; i >= 0; i--) {
      let res = links[i];

      if (res.name == name && res.fn === fn) {
        links.splice(i, 1);
        break;
      }
    }

    if (!links.length) {
      delete this.__attrLinks[name];
    }
  }

  /**
   * Check the key is system
   *
   * @param {string} key
   * @returns {boolean}
   * @protected
   */
  __isSystemKey (key) {
    return (key.match && key.match('^_|#')) || key == 'constructor';
  }

  /**
   * Set the object hash
   *
   * @param {*} obj
   * @param {string[]} [keys]
   * @protected
   */
  __setScopeObjectHash (obj, keys) {
    if(!obj || typeof obj != 'object' || obj instanceof Scope) {
      return;
    }

    Object.defineProperty(obj, '__hash', {
      configurable: true,
      enumerable: false,
      value: utils.createHash(obj, { ignoreScopeHash: true })
    });
  
    if(!keys) {
      return;
    }

    keys = keys.slice(0, -1);
    this.__setScopeObjectHash(utils.getPropertyByKeys(keys, this.__scope), keys);
  }

  /**
   * Nested observing of the value
   *
   * @param {*} value
   * @param {string[]} [startKeys]
   * @protected
   */
  __nestedObserve (value, startKeys) {
    const observe = (value, parents) => {
      if (typeof value != 'object' || value === null) {
        return value;
      }

      if (!utils.isPlainObject(value) && !utils.isScopeProxy(value) && !(value instanceof Scope)) {
        return value;
      }

      let target = value;

      CHECK_PROXY: if (value.__isProxy) {
        target = value.__target;

        if (this.__disableProxyRedefining) {
          break CHECK_PROXY;
        }
        
        if (
          value.__component !== this ||
          Akili.joinBindingKeys(parents) != Akili.joinBindingKeys(value.__keys)
        ) {
          target = utils.copy(target, { nested: false });
          value = target;
        }
      }
      else if (!this.__disableProxyRedefining && !(value instanceof Scope)) {
        target = utils.copy(target, { nested: false } );          
        value = target;
      }

      const targetKeys = Object.keys(target);

      for (let i = 0, l = targetKeys.length; i < l; i++) {    
        const k = targetKeys[i];
        const val = target[k]; 
        target[k] = observe(val, [...parents, k]);
      }

      this.__setScopeObjectHash(target);

      if (!value.__isProxy) {
        return this.__observe(value, parents);
      }

      return value;
    };

    return observe(value, startKeys || []);
  }

  /**
   * Create an isolation object
   *
   * @param {string[]} parents
   * @param {string} key
   * @returns {*}
   * @protected
   */
  __createIsolationObject (parents, key) {
    const rootKeys = parents.length? [parents[0]]: [key];
    const rootKey = rootKeys[0];
    const keys = [...parents, key];
    const isolationHash = this.__createKeysHash(rootKeys);

    if (!Akili.__isolation[isolationHash]) {
      Akili.__isolation[isolationHash] = {
        updatedAt: Date.now(),
        component: this,
        rootKeys,
        rootKey,
        keys
      };
    }

    return Akili.__isolation[isolationHash];
  }

  /**
   * Prepare the node to the binding
   *
   * @param {object} bind - by default is component.__evaluation.list
   * @param {Component} bindComponent
   * @param {string[]} keys
   * @param {string[]} parents
   * @param {*} value
   * @param {boolean} [notBinding=false]
   * @param {boolean} [evaluated=false]
   * @protected
   */
  __bindNode(bind, bindComponent, keys, parents, value, notBinding = false, evaluated = false) {
    const parentKeysString = Akili.joinBindingKeys(parents);
    let component = this;

    if (bind.length && !notBinding) {
      let l = bind.length - 1;
      let data = bind[l];

      if (data.bindComponent === this && data.keysString == parentKeysString) {
        component = data.component;
      }
      
      if (data.keysString == parentKeysString && data.component === component) {
        bind.splice(l, 1);
      }
    }
    
    bind.push({
      component,
      bindComponent,
      keysString: Akili.joinBindingKeys(keys),
      parents,
      keys,
      value,
      evaluated,
      notBinding
    });
  }

  /**
   * Check the key is the system
   *
   * @param {string} key
   * @returns {boolean}
   * @protected
   */ 
  __isSystemBindingKey(key) {
    return key == '__data';
  }

  /**
   * Get a binding by the keys
   *
   * @param {string[]} keys
   * @returns {object|null}
   * @protected
   */
  __getBind(keys) {
    return utils.getPropertyByKeys(keys, this.__bindings) || null;
  }

  /**
   * Get all nested bindings by the keys
   * 
   * @param {string[]} keys 
   * @returns {array}
   */
  __getAllBinds(keys) {
    const root = this.__getBind(keys);

    if (!root) {
      return [];
    }

    let data = [];

    const collect = obj => {
      data = data.concat(obj.__data || []);

      for (let key in obj) {
        if (!Object.prototype.hasOwnProperty.call(obj, key) || key == '__data') {
          continue;
        }

        collect(obj[key]);
      }
    }

    collect(root);
    return data;
  }

  /**
   * Get a binding by the keys
   *
   * @param {string[]} keys
   * @param {Node} node
   * @returns {object|null}
   * @protected
   */
  __getBoundNode(keys, node) {
    const bind =  utils.getPropertyByKeys(keys, this.__bindings);

    if (!bind || !bind.__data || !bind.__data.length) {
      return null;
    }

    for (let i = 0, l = bind.__data.length; i < l; i++) {
      const data = bind.__data[i];

      if (data.node === node) {
        return data;
      }
    }

    return null;
  }

  /**
   * Set the node property
   *
   * @param {Node} node
   * @param {string[]} keys
   * @param {*} value
   * @param {boolean} [evaluated=false]
   * @returns {boolean}
   * @protected
   */
  __setNodeProperty(node, keys, value, evaluated = false) {
    const prop = this.__getNodeProperty(node, keys);    
    const hash = utils.createHash(value);

    if (prop) {
      let res = node.__component.__compareNodePropertyValue(prop, value);
      prop.value = value;
      prop.hash = hash;
      return !res;
    }
    
    node.__properties[this.__createKeysHash(keys)] = {
      component: this,
      value,
      hash,
      node,
      keys,
      evaluated  
    };

    return true;
  }

  /**
   * Get the node property
   *
   * @param {Node} node
   * @param {string[]} keys
   * @returns {object|null}
   * @protected
   */
  __getNodeProperty(node, keys) {
    return node.__properties[this.__createKeysHash(keys)] || null;
  }

  /**
   * Delete the node property
   *
   * @param {Node} node
   * @param {string[]} keys
   * @protected
   */
  __deleteNodeProperty(node, keys) {
    const hash = `scope.${this.__scope.__name}.${Akili.joinBindingKeys(keys)}`;
    delete node.__properties[hash];
  }

  /**
   * Map the nodes
   * 
   * @param {function} fn
   * @param {object} [options]
   * @protected
   */
  __mapNodes(fn, options = {}) {
    options = { rootAttrs: true, attrs: true, node: true, el: true, ...options };

    const find = el => {
      if(!el) {
        return;
      }

      if(options.attrs && (el !== this.el || options.rootAttrs)) {
        for (let k = 0, attrs = el.attributes, c = attrs.length; k < c; k++) {
          fn(attrs[k]);
        }
      }
      
      for (let i = el.childNodes.length - 1; i >= 0; i--) {
        const node = el.childNodes[i];

        if (node.nodeType == 3) {
          options.node && fn(node);
        }
        else if (node.nodeType == 1 && !node.__akili) {
          find(node);
          options.el && fn(node);          
        }
      }
    };
    
    find(this.el);
  }

  /**
   * Get all nodes
   * 
   * @returns {Node[]}
   * @protected 
   */
  __getAllNodes() {
    const nodes = [];
    this.__mapNodes(node => nodes.push(node));
    return nodes;
  }
  
  /**
   * Bind the data with the keys
   *
   * @param {string[]} keys
   * @param {object} data
   * @protected
   */
  __bind(keys, data) {
    utils.setPropertyByKeys(keys, this.__bindings, (last, value) => {
      let obj = { __data: [] };

      if (!last) {
        return value? value: obj;
      }

      if (typeof value == 'object') {
        obj = value;
      }

      if (!obj.__data) {
        obj.__data = [];
      }

      obj.__data.push(data);
      return obj;
    });
  }

  /**
   * Unbind the data by nodes
   *
   * @param {Node|Node[]} nodes
   * @protected
   */
  __unbindByNodes(nodes) {
    nodes instanceof Node && (nodes = [nodes]);

    const unbind = (obj, parent, key) => {
      const keys = Object.keys(obj);

      for (let j = 0, c = keys.length; j < c; j++) {
        const k = keys[j];
        
        if (k == '__data') {
          let data = obj[k] || [];
          let l = data.length;

          for (let i = 0; i < l; i++) {
            let bind = data[i];
            
            if (nodes.indexOf(bind.node) != -1) {
              delete bind.node;            
              data.splice(i, 1);
              i--;
              l--;
            }
          }

          if (!l) {
            delete obj[k];
          }
        }
        else if (!this.__isSystemBindingKey(k)) {
          unbind(obj[k], obj, k);
        }

        if(parent && !Object.keys(obj).length) {
          delete parent[key];
        }
      }
    };

    unbind(this.__bindings);
  }

  /**
   * Remove all parent bindings with the nodes
   * 
   * @param {Node|Node[]} nodes
   * @protected
   */
  __unbindParentsByNodes(nodes) {  
    for(let i = 0, l = this.__parents.length; i < l; i++) {
      const parent = this.__parents[i];      
      parent && parent.__akili && parent.__akili.__unbindByNodes(nodes);
    }
  }

  /**
   * Clear the links
   * 
   * @protected
   */
  __clearStoreLinks() {
    const links = Akili.__storeLinks;

    for (let key in links) {
      const arr = links[key];

      for (let i = arr.length - 1; i >= 0; i--) {
        const link = arr[i];
  
        if (link.component === this) {                 
          arr.splice(i, 1);      
        }
      }

      if (!arr.length) {
        delete links[key];
      }
    }
  }

  /**
   * Remove the component without the children removing
   *
   * @param {object} [options]
   * @returns {Node[]}
   * @protected
   */
  __remove(options = {}) {
    this.attrs.onRemoved && this.attrs.onRemoved.trigger(undefined, { bubbles: false }); 
    this.removed();
    const detachNodes = this.__detach({ saveBindings: true, deinitializeNodes: true });
    const nodes = [].concat(detachNodes, this.__empty({ saveBindings: true })); 
    this.__unbindByNodes(detachNodes);
    !options.saveBindings && this.__unbindParentsByNodes(nodes);    
    this.__clearStoreLinks();  
    this.__scope.__remove();
    delete this.__scope;
    delete this.scope; 
    this.el.remove();
    delete this.el.__akili;
    delete this.el;    
    delete this.__parent;
    delete this.__attributeOf;
    delete this.__evaluationComponent;
    delete this.__evaluationParent;   
    delete this.__parents;
    delete this.__bindings;
    delete this.__attrLinks;
    delete this.__storeLinks;
    delete this.__attrs;
    delete this.attrs;
    this.__isRemoved = true;
    return nodes;
  }

  /**
   * Remove all children
   *
   * @param {object} [options]
   * @returns {Node[]}
   * @protected
   */
  __removeChildren(options = {}) {
    let nodes = [];

    for (let i = 0; i < this.__children.length; i++) {
      let child = this.__children[i];
      nodes = nodes.concat(child.__akili.__remove(options));
      i--;
    }

    return nodes;
  }

  /**
   * Detach the component
   *
   * @param {object} [options]
   * @returns {object}
   * @protected
   */
  __detach(options = {}) {
    this.__parent && this.__parent.__akili.__spliceChild(this.el);
    const nodes = [];

    for (let i = 0, l = this.el.attributes.length; i < l; i++) {
      let node = this.el.attributes[i];

      if(node.__initialized) {
        nodes.push(node);
        options.deinitializeNodes && this.__deinitializeNode(node, { saveBindings: true });
      }
    }

    !options.saveBindings && this.__unbindParentsByNodes(nodes);
    return nodes;
  }

  /**
   * Clear the component html
   *
   * @param {object} [options]
   * @returns {Node[]}
   * @protected
   */
  __empty(options = {}) {    
    const nodes = this.__removeChildren({ saveBindings: true });    
    this.__mapNodes(node => {      
      if(node.nodeType == 1) {
        node.remove();
        return;
      }

      if(!node.__initialized) {
        return;
      }      
      
      this.__deinitializeNode(node, { saveBindings: true });
      nodes.push(node);
    }, { rootAttrs: false });

    this.__unbindByNodes(nodes);
    !options.saveBindings && this.__unbindParentsByNodes(nodes);
    this.el.innerHTML = '';
    return nodes;
  }

  /**
   * Get the parent components
   *
   * @param {string} [selector='']
   * @param {boolean} [findAll=true] - get array if true
   * @param {number|number[]} [levels=null]
   * @returns {Component|Component[]}
   * @protected
   */
  __getParent(selector = '', findAll = false, levels = null) {
    let arr = [];
    let level = 0;

    if (typeof levels != 'object') {
      levels = [levels];
    }

    const find = parent => {
      if (!parent) {
        return;
      }

      if (!levels || levels.indexOf(level) != -1) {
        if (!selector || parent.__akili.matches(selector)) {
          if (!findAll) {
            arr.push(parent.__akili);
            return;
          }

          arr.push(parent.__akili);
        }
      }

      level++;
      find(parent.__akili.__parent);
    };

    find(this.__parent);
    return findAll? arr: (arr[0] || null);
  }

  /**
   * Get the child components
   *
   * @param {string} [selector='']
   * @param {boolean} [findAll=true] - get array if true
   * @param {number|number[]} [levels=null]
   * @returns {Component|Component[]}
   * @protected
   */
  __getChildren(selector = '', findAll = true, levels = null) {
    let arr = [];
    let level = 0;

    if (typeof levels != 'object') {
      levels = [levels];
    }

    const find = children => {
      for (let i = 0, l = children.length; i < l; i++) {
        let child = children[i];

        if (!levels || levels.indexOf(level) != -1) {
          if (!selector || child.__akili.matches(selector)) {
            if (!findAll) {
              arr.push(child.__akili);
              return;
            }

            arr.push(child.__akili);
          }
        }
      }

      level++;

      for (let i = 0, l = children.length; i < l; i++) {
        find(children[i].__akili.__children);
      }
    };

    find(this.__children);
    return findAll? arr: (arr[0] || null);
  }

  /**
   * Get the nearest components
   *
   * @param {string} [selector='']
   * @param {boolean} [findAll=true] - get array if true
   * @param {boolean} [right=false] - from the right side if true
   * @returns {Component|Component[]}
   * @protected
   */
  __getNear(selector = '', findAll = true, right = false) {
    if (!this.__parent) {
      return null;
    }

    let levelElements = this.__parent.__akili.__children.slice();
    let arr = [];
    right && levelElements.reverse();

    for (let i = 0, l = levelElements.length; i < l; i++) {
      let el = levelElements[i];

      if (el === this.el) {
        break;
      }

      if (!selector || el.__akili.matches(selector)) {
        arr.push(el.__akili);
      }
    }

    arr.reverse();

    if (!findAll) {
      return arr.length? arr[0]: null;
    }

    return arr;
  }

  /**
   * Create the link with the store
   * 
   * @param {string} name 
   * @param {string|string[]|function} handler
   */
  store(name, handler) {
    if (!this.__isMounted) {
      throw new Error(`Method "store" must be called after the compilation. For example, in "compiled" method.`);
    }

    const args = [].slice.call(arguments);

    if (typeof name == 'function') {
      args.unshift('*');
      handler = name;
    }

    return typeof handler === 'function'? this.__storeByFunction(...args): this.__storeByKeys(...args);
  }

  /**
   * Create the link with the attribute
   * 
   * @param {string} name 
   * @param {string|string[]|function} handler
   */
  attr(name, handler) {
    if (!this.__isMounted) {
      throw new Error(`Method "attr" must be called after the compilation. For example, in "compiled" method.`);
    }

    const args = [].slice.call(arguments);

    if (typeof name == 'function') {
      args.unshift('*');
      handler = name;
    }

    return typeof handler === 'function'? this.__attrByFunction(...args): this.__attrByKeys(...args);
  }

  /**
   * Remove the store link
   * 
   * @param {string} name 
   * @param {string|string[]|function} handler
   */
  unstore(name, handler) {
    if (!this.__isMounted) {
      throw new Error(`Method "unstore" must be called after the compilation. For example, in "compiled" method.`);
    }

    const args = [].slice.call(arguments);

    if (typeof name == 'function') {
      args.unshift('*');
      handler = name;
    }

    return typeof handler === 'function'? this.__unstoreByFunction(...arguments): this.__unstoreByKeys(...arguments);
  }

  /**
   * Remove the attribute link
   * 
   * @param {string} name 
   * @param {string|string[]|function} handler
   */
  unattr(name, handler) {
    if (!this.__isMounted) {
      throw new Error(`Method "unattr" must be called after the compilation. For example, in "compiled" method.`);
    }

    const args = [].slice.call(arguments);

    if (typeof name == 'function') {
      args.unshift('*');
      handler = name;
    }

    return typeof handler === 'function'? this.__unattrByFunction(...arguments): this.__unattrByKeys(...arguments);
  }

  /**
   * Check the component using the selector
   *
   * @param {string|function} selector
   * @returns {boolean}
   */
  matches(selector) {
    if (typeof selector == 'function') {
      return selector(this);
    }

    return this.el.matches(selector);
  }

  /**
   * Get the closest parent component by the selector
   *
   * @param {string|function} [selector='']
   * @param {number|number[]} [levels=null]
   * @returns {Component}
   */
  parent(selector = '', levels = null) {
    return this.__getParent(selector, false, levels);
  }

  /**
   * Get an array of parent components by the selector
   *
   * @param {string|function} [selector='']
   * @param {number|number[]} [levels=null]
   * @returns {Component[]}
   */
  parents(selector = '', levels = null) {
    return this.__getParent(selector, true, levels);
  }

  /**
   * Get the closest child component by the selector
   *
   * @param {string|function} [selector='']
   * @param {number|number[]} [levels=null]
   * @returns {Component}
   */
  child(selector = '', levels = null) {
    return this.__getChildren(selector, false, levels);
  }

  /**
   * Get an array of child components by the selector
   *
   * @param {string|function} [selector='']
   * @param {number|number[]} [levels=null]
   * @returns {Component[]}
   */
  children(selector = '', levels = null) {
    return this.__getChildren(selector, true, levels);
  }

  /**
   * Get an array of components left from the current by the selector
   *
   * @param {string|function} [selector='']
   * @returns {Component[]}
   */
  before(selector = '') {
    return this.__getNear(selector, true, false);
  }

  /**
   * Get an array of components right from the current by the selector
   *
   * @param {string|function} [selector='']
   * @returns {Component[]}
   */
  after(selector = '') {
    return this.__getNear(selector, true, true);
  }

  /**
   * Get the closest leftcomponent by the selector
   *
   * @param {string|function} [selector='']
   * @returns {Component}
   */
  prev(selector = '') {
    return this.__getNear(selector, false, false);
  }

  /**
   * Get the closest right component by the selector
   *
   * @param {string|function} [selector='']
   * @returns {Component}
   */
  next(selector = '') {
    return this.__getNear(selector, false, true);
  }

  /**
   * Change the element parent
   *
   * @param {Element} parent
   */
  appendTo(parent) {   
    parent.appendChild(this.el);
    return Akili.compile(this.el, { recompile: { setParents: true, checkChanges: false } });
  }

  /**
   * Cancel the component compilation
   */
  cancel() {
    this.__cancelled = true;
  }

  /**
   * Compile the component but prevent the compilation inside
   */
  prevent() {
    this.__prevent = true;
  }

  /**
   * Clear the element content
   *
   * @returns {*}
   */
  empty() {
    return this.__empty.apply(this, arguments);
  }

  /**
   * Remove the element
   *
   * @returns {*}
   */
  remove() {
    return this.__remove.apply(this, arguments);
  }

  created() {}
  compiled() {}
  recompiled() {}
  resolved() {}
  removed() {}

  get transition() {
    return (this.__scope && this.__scope.__transition) || null;
  }

  get isCreated() {
    return this.__isCreated;
  }

  get isCompiled() {
    return this.__isCompiled;
  }

  get isResolved() {
    return this.__isResolved;
  }

  get isRemoved() {
    return this.__isRemoved;
  }
}

<|start_filename|>src/components/radio.js<|end_filename|>
import For, { Loop } from './for.js';
import Akili from '../akili.js';

/**
 * Component to work with radio group.
 * 
 * {@link https://akilijs.com/docs/components#docs_radio_group}
 * 
 * @tag radio
 * @selector radio[name]
 * @attr {string} name - name of the group
 * @attr {string|null} value - selected value
 * @attr {boolean} defaultRequired - default value is required or not
 * @attr [in] @see For
 * @message {string} radio - sent on value change
 */
export default class Radio extends For {
  static matches = '[name]';
  static events = ['radio'].concat(For.events);
  static booleanAttributes = ['default-required'].concat(For.booleanAttributes);

  static define() {
    Akili.component('radio', this);
    Akili.component('radio-button', this.RadioButton);
  }

  constructor(...args) {
    super(...args);
    this.iterable = this.el.hasAttribute('in');
  }

  created() {
    this.el.addEventListener('change', () => { 
      if(this._disableInternalEvents) {
        return;
      }
      
      let value = this.getRadioValue();

      if (value === this.prevValue) {
        return;
      }

      this.prevValue = value;

      if(this.attrs.value === undefined || this.__isResolved) {
        this.attrs.onRadio.trigger(value, { bubbles: true });
      }      
    });

    if(this.iterable) {
      return super.created.apply(this, arguments);
    }
  }

  compiled() {
    if(this.iterable) {
      const res = super.compiled.apply(this, arguments);
      this.unattr('in', this.draw);
      return res;
    }
  }

  resolved() {
    this.attr('value', this.setValue);
    this.attr('name', this.setNames);
    this.attr('defaultRequired', this.setDefaultRequired, { callOnStart: false });
    return this.attr('in', this.setIn, { callOnStart: false });
  }

  setIn(data) {
    return this.draw(data).then(this.redrawRadio.bind(this));
  }

  setDefaultRequired() {
    return this.setValue(this.prevValue);
  }

  redrawRadio() {
    this.setNames();    
    const value = this.isProperValue(this.prevValue)? this.prevValue: null;
    const children = this.children('input[type=radio]');
    let checkedValue = value;

    for (let i = 0, l = children.length; i < l; i++) {
      let radioEl = children[i].el;      
      let selection = !!radioEl.getAttribute('checked');
      
      if(value && radioEl.value !== value) {
        radioEl.checked = false;
        continue;
      } 

      if(value) {
        radioEl.checked = true;
        checkedValue = radioEl.value;
        continue;
      }      
      
      radioEl.checked = selection;
      selection && (checkedValue = radioEl.value);
    }
    
    this.setValue(checkedValue);
  }

  isProperValue(value) { 
    const children = this.children('input[type=radio]');

    for (let i = 0, l = children.length; i < l; i++) {
      if(children[i].el.value === value) {
        return true;
      }
    }

    return false;
  }

  setNames(name) {
    let children = this.children('input[type=radio]');

    for (let i = 0, l = children.length; i < l; i++) {
      children[i].el.setAttribute('name', name || this.attrs.name);
    }
  }

  setValue(value) { 
    if(!this.attrs.defaultRequired && value === this.prevValue) {
      return;
    }

    let children = this.children('input[type=radio]'); 

    if(this.attrs.defaultRequired && value === null && children.length) {
      value = children[0].el.value;
    }

    if (value === this.prevValue) {
      return;
    }
    
    this._disableInternalEvents = true;

    for (let i = 0, l = children.length; i < l; i++) {
      let radio = children[i];
      radio.setChecked(radio.el.value === value);
    }

    this._disableInternalEvents = false;
    this.prevValue = value;
    this.attrs.onRadio.trigger(value, { bubbles: true });    
  }

  getRadioValue() {
    let children = this.children('input[type=radio]');

    for (let i = 0, l = children.length; i < l; i++) {
      let radio = children[i];

      if (radio.el.checked) {
        return radio.el.value;
      }
    }

    return null;
  }
}

/**
 * Component to work with radio group item.
 * 
 * {@link https://akilijs.com/docs/components#docs_radio_group}
 * 
 * @tag radio-button
 * @attr {string} value - value
 */
export class RadioButton extends Loop {
  static template = '<label><input type="radio" value="${this.value}"/>${this.__content}</label>';

  constructor(...args) {
    super(...args);
  }

  compiled() {
    this.attr('value', 'value');
    return super.compiled.apply(this, arguments);
  }
}

Radio.RadioButton = RadioButton;

<|start_filename|>src/components/textarea.js<|end_filename|>
import Text from './text.js';
import Akili from '../akili.js';

/**
 * Component to work with textarea.
 * 
 * {@link https://akilijs.com/docs/components#docs_textarea}
 * 
 * @tag textarea
 * @attr @see Text
 * @message @see Text
 */
export default class Textarea extends Text {
  static define() {
    Akili.component('textarea', this);
  }
}

<|start_filename|>src/components/input.js<|end_filename|>
import Text from './text.js';
import Akili from '../akili.js';

/**
 * Component to work with input elements.
 * 
 * {@link https://akilijs.com/docs/components#docs_input}
 * {@link https://akilijs.com/docs/components#docs_checkbox_and_radio}
 * 
 * @tag input
 * @attr @see Text
 * @message @see Text
 */
export default class Input extends Text {
  static booleanAttributes = ['checked', 'multiple'].concat(Text.booleanAttributes);
  static events = ['change'].concat(Text.events);

  static define() {
    Akili.component('input', this);
  }

  constructor(...args) {
    super(...args);
    this.isCheckbox = this.el.type == 'checkbox';
    this.isRadio = this.el.type == 'radio';
  }

  compiled() {
    (this.isCheckbox || this.isRadio) && this.attr('checked', this.setChecked);    
    return super.compiled.apply(this, arguments);
  }  

  setChecked(value) {
    value = !!value;

    if(this.el.checked === value) {
      return;
    }
    
    this.el.checked = value;
    this.__isCompiled && this.attrs.onChange.dispatch(Event, { bubbles: true });
    this.isRadio && this.changeRadio();
  }

  setValue(value) {
    super.setValue.apply(this, arguments);
    this.isRadio && this.el[this.valueKey] != value && this.attrs.onChange.dispatch(Event, { bubbles: true });
  }

  changeRadio() {
    let name = this.el.getAttribute('name');
    let selector = `input[type=radio][name='${name}']:not([scope='${this.scope.__name}'])`;
    let children = this.parent().children(selector);

    for (let i = 0, l = children.length; i < l; i++) {
      let radio = children[i];
      radio.setChecked(radio.el.checked);
    }
  }
}

<|start_filename|>test/components/a.js<|end_filename|>
import { assert } from 'chai';
import A from '../../src/components/a.js';
import router from '../../src/services/router.js';

describe('a.js', () => {
  describe('A', () => {
    let component;
    let currentUrl;

    before(() => {
      component = new A(document.createElement('a'));
      component.__create();
      currentUrl = 'test/url';
    });

    describe('.setUrl()', () => {
      it('should set .url property', () => {
        component.setUrl(currentUrl);
        assert.equal(component.url, currentUrl);
      });
    });

    describe('.setState()', () => {
      it('should not set the undefined state', () => {
        assert.throws(() => component.setState('a-test'));
      });

      it('should set the defined state', () => {
        let state = 'a-test';
        router.add(state, '/a-test/:id');
        component.setState(state);
        assert.equal(component.state.name, state);
      });
    });

    describe('.setActivity()', () => {
      it('should set isActiveState and inActiveState to false', () => {
        component.setActivity();
        assert.isFalse(component.scope.isActiveState, 'check isActiveState');
        assert.isFalse(component.scope.inActiveState, 'check inActiveState');
      });
    });

    describe('.setParams()', () => {
      it('should not set wrong params', () => {
        assert.throws(() => component.setParams('isNotObject'));
      });

      it('should set right params', () => {
        let params = { id: 1 };
        component.setParams(params);
        assert.equal(JSON.stringify(component.params), JSON.stringify(params));
      });
    });

    describe('.setQuery()', () => {
      it('should not set wrong query', () => {
        assert.throws(() => component.setQuery('isNotObject'));
      });

      it('should set right query', () => {
        let query = { query: 1 };
        component.setQuery(query);
        assert.equal(JSON.stringify(component.query), JSON.stringify(query));
      });
    });

    describe('.setOptions()', () => {
      it('should not set wrong options', () => {
        assert.throws(() => component.setOptions('isNotObject'));
      });

      it('should set right options', () => {
        let options = { reload: true };
        component.setOptions(options);
        assert.equal(JSON.stringify(component.options), JSON.stringify(options));
      });
    });

    describe('.setHash()', () => {
      it('should not set wrong hash', () => {
        assert.throws(() => component.setHash({is: 'notString'}));
      });

      it('should set right options', () => {
        let hash = 'hash';
        component.setHash(hash);
        assert.equal(component.hash, hash);
      });
    });

    describe('.getState()', () => {
      it('should not get undefined state', () => {
        assert.throws(() => component.getState('undefined'));
      });

      it('should get defined state', () => {
        let state = 'a-test';
        assert.equal(component.getState(state).name, state);
      });
    });

    describe('.resetHref()', () => {
      it('should set the current url', () => {
        component.resetHref();
        assert.equal(component.attrs.href, currentUrl);
      });

      it('should set the real url', () => {
        delete component.url;
        component.resetHref();
        assert.equal(component.attrs.href, '/a-test/1?query=1#hash');
      });
    });

    describe('.remove()', () => {
      it('should remove the component', () => {
        component.remove();
      });
    });
  });
});


<|start_filename|>src/globals.js<|end_filename|>
import Akili from './akili.js';

const obj = {};

/**
 * Globals is for using the custom variables in html
 * 
 * {@link https://akilijs.com/docs/scope#docs_globals}
 */
const globals = new Proxy(obj, {
  get: (target, key) => {
    if(key == '__target') {
      return obj;
    }
    
    return target[key];
  },
  set: (target, key, value) => {
    target[key] = Akili.wrap(value, { tag: key });
    return true;
  },
  deleteProperty: (target, key) => {
    Akili.removeTag(key);
    delete target[key];
    return true;
  }
});

export default globals;

<|start_filename|>test/app/home.js<|end_filename|>
import Component from '../../src/component.js';

export default class Home extends Component {
  constructor(...args) {
    super(...args);

    this.el.setAttribute('hidden', 'hidden');
    this.el.setAttribute('parent', '${ this.homeAttr }');
    this.el.setAttribute('parent-two', '${ this.homeAttrTwo }');
    this.el.setAttribute('on-action', '${ this.onHomeAction() }');

    this.el.innerHTML = `
      <b>\${this.html}</b>
      <b>\${this.htmlTwo}</b>
      <p>\${this.parentTwo}</p>
      <x>\${this.data[0].title}</x> 
      <y>\${this.data[1]}</y> 
      <z>\${this.data[2]}</z>
      <f>\${this.data; ++this.a}</f>
      <s>\${this.data[0]; ++this.b}</s>
      <e on-click="\${this.onEvent()}"></e>
    `;

    this.scope.html = 'Hello World!';
    this.scope.htmlTwo = 'Hi!';
    this.scope.a = 0;
    this.scope.b = 0;

    this.scope.data = [
      { title: 'value1' },
      { title: 'value2' },
      { title: 'value3' }
    ];

    this.scope.onEvent = () => {
      this.onEventCount++;
    };

    this.onEventCount = 0;
  }
  
  created() {
    this.wasCreated = true;
  }

  compiled() {
    this.wasCompiled = true;
    this.scope.parentTwo = this.attrs.parentTwo;
  }

  resolved() {
    this.wasResolved = true;
  }

  removed() {
    this.wasRemoved = true;
  }
}

<|start_filename|>test/services/request.js<|end_filename|>
import request, { Request } from '../../src/services/request';

let oldXMLHttpRequestOpen = XMLHttpRequest.prototype.open;

Object.defineProperty(XMLHttpRequest.prototype, 'response', {
  writable: true
});

Object.defineProperty(XMLHttpRequest.prototype, 'status', {
  writable: true
});

XMLHttpRequest.prototype.open = function(method, url) {
  this.requestURL = url;
  this.requestMethod = method;
  return oldXMLHttpRequestOpen.apply(this, arguments);
};

XMLHttpRequest.prototype.send = function() {
  const end = (err, status = 200) => {
    this.status = status;

    if(err) {
      this.response = 'error';
      this.status = 500;
      this.onerror(new Error(`Wrong test url "${this.requestURL}"`));
      return;
    }

    this.onprogress && this.onprogress();
    this.onload();
  }

  if(this.requestMethod == 'GET' && this.requestURL == 'include.html') {
    this.response = '${this.__content}1';
  }
  else if(this.requestMethod == 'GET' && this.requestURL == 'router-1.html') {
    this.response = '<x>1</x>:<route></route>';
  }
  else if(this.requestMethod == 'GET' && this.requestURL == 'router-1-1.html') {
    this.response = '<y>1-1</y>:<route></route>';
  }
  else if(this.requestMethod == 'GET' && this.requestURL == 'router-1-2.html') {
    this.response = '<z>1-2</z>';
  }
  else if(this.requestMethod == 'GET' && this.requestURL == 'router-2.html') {
    this.response = '<w>2</w>';
  }
  else if(
    this.requestURL == 'ping' || 
    this.requestURL == 'ping?x=1' ||
    this.requestURL == 'form' ||
    (this.requestMethod == 'GET' && this.requestURL == 'get') ||
    (this.requestMethod == 'POST' && this.requestURL == 'post') ||
    (this.requestMethod == 'PUT' && this.requestURL == 'put') ||
    (this.requestMethod == 'DELETE' && this.requestURL == 'delete') ||
    (this.requestMethod == 'PATCH' && this.requestURL == 'patch') ||
    (this.requestMethod == 'HEAD' && this.requestURL == 'head')
  ) {
    this.response = 'ok';
  }
  else if(this.requestURL == 'json') {
    this.response = { success: true };
  }
  else if(this.requestURL == 'status404') {
    return end(null, 404);
  }

  end(!this.response);  
};

describe('request.js', () => {
  describe('request', () => {
    let instance;

    before(() => {
      instance = new Request('/');
    });

    describe('.addInstance()', () => {
      it('should add new instance', () => {
        request.addInstance('new', instance);
        assert.equal(request.__instances['new'], instance);
      });
    });

    describe('.use', () => {
      it('should get an instance', () => {
        assert.equal(request.use.new, instance);
      });
    });

    describe('.removeInstance()', () => {
      it('should remove an instance', () => {
        request.removeInstance('new');
        assert.isNotOk(request.__instances['new']);
      });
    });
  });

  describe('Request', () => {
    describe('.query()', () => {
      it('should make query request', () => {
        return request.query({ url: 'ping'}).then(res => {
          assert.equal(res.data, 'ok');
        });
      });

      it('should get the bad response', done => {
        request.query({ url: 'non-existent'}).then(() => {
          done(new Error('must be an error response'))
        }).catch(() => done());
      });

      it('should handle bad status pattern', done => {
        request.query({ url: 'status404'}).then(() => {
          done(new Error('must be an error response'))
        }).catch(() => done());
      });      

      it('should make GET request', () => {
        return request.get('get').then(res => {
          assert.equal(res.data, 'ok');
        });
      });

      it('should make POST request', () => {
        return request.post('post').then(res => {
          assert.equal(res.data, 'ok');
        });
      });

      it('should make PUT request', () => {
        return request.put('put').then(res => {
          assert.equal(res.data, 'ok');
        });
      });

      it('should make DELETE request', () => {
        return request.delete('delete').then(res => {
          assert.equal(res.data, 'ok');
        });
      });

      it('should make PATCH request', () => {
        return request.patch('patch').then(res => {
          assert.equal(res.data, 'ok');
        });
      });

      it('should make HEAD request', () => {
        return request.head('head').then(res => {
          assert.equal(res.data, 'ok');
        });
      });

      it('should make JSON request', () => {
        return request.get('json', { 
            json: { 
              id: 1 
            },
            onProgress: () => {}
          }).then(res => {
          assert.equal(JSON.stringify(res.data), JSON.stringify({ success: true }));
        });
      });

      it('should make request with form', () => {
        return request.get('form', { 
          form: { 
            id: 1, 
            file: new Blob([1, 0]),
            date: new Date()
          } 
        }).then(res => {
          assert.equal(res.data, 'ok');
        });
      });

      it('should set "responseType" option', () => {
        return request.get('ping', { responseType: 'json' }).then(res => {
          assert.equal(res.responseType, 'json');
        });
      });

      it('should set "withCredentials" option', () => {
        return request.get('ping', { withCredentials: true }).then(res => {
          assert.isOk(res.withCredentials);
        });
      });

      it('should create cache', () => {
        return request.get('ping', { cache: 1000 }).then(res => {
          return request.get('ping', { cache: true }).then(result => {
            assert.equal(JSON.stringify(res), JSON.stringify(result));
          });
        });
      });

      it('should create cache', () => {
        let length = Object.keys(request.__cache).length;

        return request.get('ping', { cache: 1000, params: { x: 1 } }).then(() => {
          assert.lengthOf(Object.keys(request.__cache), length + 1);
        });
      });

      it('should remove cache', () => {
        let length = Object.keys(request.__cache).length;
        let hash = Object.keys(request.__cache)[0];
        request.removeCache(hash);        
        assert.lengthOf(Object.keys(request.__cache), length - 1);
      });
    });

    describe('.paramsToQuery()', () => {
      it('should create query string', () => {
        let obj = {
          x: 5,
          y: false,
          z: [1, 2],
          a: {x: 1}
        };

        assert.equal(request.paramsToQuery(obj), 'x=5&y=false&z[0]=1&z[1]=2&a[x]=1');
      });
    });

    describe('.paramsFromQuery()', () => {
      it('should create object from query string', () => {
        let query = 'x=5&y=&z=1&z=2';

        let obj = {
          x: '5',
          y: '',
          z: ['1', '2']
        };

        assert.equal(JSON.stringify(request.paramsFromQuery(query)), JSON.stringify(obj));
      });

      it('should be an empty object', () => {
        let obj = request.paramsFromQuery('');
        assert.isObject(obj, 'check type');
        assert.isNotOk(Object.keys(obj).length, 'check empty or not');
      })
    });

    describe('.createFormData()', () => {
      it('should create FormData', () => {
        let obj = {
          x: 5,
          y: false,
          z: [1, 2],
          a: {x: 1}
        };

        let formData = request.createFormData(obj);
        let output = {};

        formData.forEach((val, key) => {
          output[key] = val;
        });

        assert.equal(output.x, '5', 'check x');
        assert.equal(output['z[0]'], '1', 'check z[0]');
        assert.equal(output['z[1]'], '2', 'check z[1]');
        assert.equal(output['a[x]'], '1', 'check a[x1]');
      });
    });
  });
});

<|start_filename|>test/app/for-compile.js<|end_filename|>
import Component from '../../src/component.js';

export default class ForCompile extends Component {}


<|start_filename|>webpack.config.js<|end_filename|>
const webpack = require('webpack');
const TerserPlugin = require('terser-webpack-plugin');
const ESLintPlugin = require('eslint-webpack-plugin');
const path = require('path');
const pack = require('./package.json');

let plugins = [];
let isProd = process.env.NODE_ENV == 'production';

let banner = `Akili is a javascript framework\n
const Akili = makeItEasy(js + html);\n
@version ${pack.version}
@author <NAME> <<EMAIL>>
{@link http://akilijs.com}
{@link https://github.com/ortexx/akili}`;

plugins.push(new webpack.BannerPlugin({
  banner: banner.trim()
}));

plugins.push(new ESLintPlugin());

let config = {
  mode: isProd? 'production': 'development',
  performance: { hints: false },
  watch: !isProd,
  devtool: isProd? false: "inline-source-map",
  entry: "./src/akili.js",
  output: {
    path: path.join(__dirname, "/dist"),
    filename: "akili.js",
    library: 'Akili',
    libraryExport: "default",
    libraryTarget: 'umd'
  },
  optimization: {
    minimizer: [
      new TerserPlugin({
        extractComments: false
      })
    ]
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  },
  plugins
};

module.exports = config;

<|start_filename|>src/services/request.js<|end_filename|>
import utils from '../utils.js';
import Akili from '../akili.js';

/**
 * Class to manage requests.
 * 
 * {@link https://akilijs.com/docs/requests}
 */
export class Request {
  __cache = {};

  /**
   * @param {string} baseUrl
   * @param {object} [defaults] 
   */
  constructor(baseUrl, defaults) {
    this.baseUrl = baseUrl? (baseUrl.replace(/\/$/, '') + '/'): '';

    this.defaults = {
      statusErrorPattern: /^[^23]/,
      cache: false,
      ...(defaults || {})
    };
  }

  /**
   * Send the request
   *
   * @param {object} options
   * @returns {Promise}
   */
  query(options) {
    return new Promise((resolve, reject) => {
      options = {...this.defaults, ...(options || {})}; 
      options.url = this.baseUrl? `${this.baseUrl}${options.url.replace(/^\//, '')}`: options.url;  
      options.method = options.method || 'GET';      
      let xhr = new XMLHttpRequest();      
      const before = this.transformBefore(xhr, options);
      xhr = before.xhr;
      options = before.options;
      (typeof options.onStart == 'function') && options.onStart(xhr);  
      options.headers = options.headers || {};
      
      if (options.json) {
        options.json !== true && (options.body = JSON.stringify(options.json));
        options.headers['content-type'] = 'application/json';
        options.responseType = options.responseType || 'json';
      }
      else if (options.form) {
        options.body = this.createFormData(options.form);
        options.headers['content-type'] = 'multipart/form-data';
      }

      if (options.params) {
        let str = this.paramsToQuery(options.params);
        str && (options.url += "?" + str);
      }
           
      let hash = null;
      let cache = typeof options.cache == 'function'? options.cache(options): options.cache;
      (!window.AKILI_SSR && !Akili.__init) && (cache = true);

      if(options.method.toUpperCase() == 'GET' && (!options.body || typeof options.body == 'string')) {
        hash = this.createCacheHash({ 
          url: options.url,
          method: options.method,
          user: options.user, 
          password: <PASSWORD>, 
          body: options.body 
        });        
      }
      
      if(cache && hash) {        
        let _cache = this.getCache(hash);
        let now = new Date().getTime();

        if(_cache && (cache === true || now - _cache.createdAt <= cache)) {
          return resolve(this.transformAfter(_cache.result));
        }
      }

      xhr.open(options.method, options.url, true, options.user, options.password);

      if (Object.prototype.hasOwnProperty.call(options, 'timeout')) {
        xhr.timeout = options.timeout;
      }

      if (options.responseType) {
        xhr.responseType = options.responseType;
      }

      if (options.withCredentials) {
        xhr.withCredentials = options.withCredentials;
      }

      const headerKeys = Object.keys(options.headers);

      for (let i = 0, l = headerKeys.length; i < l; i++) {
        let k = headerKeys[i];
        xhr.setRequestHeader(k, options.headers[k]);
      }

      if (typeof options.onProgress == 'function') {
        xhr.onprogress = event => options.onProgress(event, xhr);
      }

      xhr.onload = () => {
        const result = {
          response: xhr.response,
          status: xhr.status,
          readyState: xhr.readyState,
          statusText: xhr.statusText,
          withCredentials: xhr.withCredentials,
          responseType: xhr.responseType,
          responseText: xhr.responseType == 'text'? xhr.responseText: '',
          responseXML: xhr.responseType == 'document'? xhr.responseXML: '',
          responseURL: xhr.responseURL,
          timeout: xhr.timeout,
          headers: this.getHeaders(xhr),         
        };

        const response = this.transformAfter(result);

        if (options.statusErrorPattern && (xhr.status + '').match(options.statusErrorPattern)) {
          let err = new Error(`Request to "${options.url}" returns failure status code ${xhr.status}`);
          err.response = response;
          return reject(err);
        } 

        (cache || window.AKILI_SSR) && hash && this.createCache(hash, result);
        resolve(response);
      };
      xhr.ontimeout = () => reject(new Error(`Request to "${options.url}" timed out`));
      xhr.onerror = reject;
      xhr.send(options.body);
    });
  }

  /**
   * Get the headers of an XMLHttpRequest instance
   * 
   * @param {XMLHttpRequest}
   */
  getHeaders = function(xhr) {
    const headers = {};
    const str = xhr.getAllResponseHeaders();  
    const arr = str.split('\u000d\u000a');

    for (let i = 0, l = arr.length; i < l; i++) {
      let line = arr[i];
      let index = line.indexOf('\u003a\u0020');

      if (index > 0) {
        let key = line.substring(0, index);
        let val = line.substring(index + 2);
        headers[key] = val;
      }
    }

    return headers;
  }

  /**
   * Get the cache
   * 
   * @param {string} hash
   * @returns {object}
   */
  getCache(hash) {
    return this.__cache[hash] || null;
  }

  /**
   * Create the cache
   * 
   * @param {string} hash
   * @param {object} result
   */
  createCache(hash, result) {
    this.__cache[hash] = { result, createdAt: new Date().getTime() };
  }

  /**
   * Remove the cache
   * 
   * @param {string} hash
   */
  removeCache(hash) {
    delete this.__cache[hash];
  }

  /**
   * Create a hash for saving in the cache
   * 
   * @param {object} data
   * @returns {string}
   */
  createCacheHash(data) {
    return utils.createHash(data);
  }

  /**
   * Convert the object to a query string
   *
   * @param {object} obj
   * @returns {string}
   */
  paramsToQuery(obj) {
    const sep = '&';
    const eq = '=';

    if (!obj || typeof obj !== 'object') {
      return '';
    }

    return Object.keys(obj).map(k => {
      let ks = encodeURIComponent(k);

      if (Array.isArray(obj[k])) {
        ks = encodeURIComponent(k);
        return obj[k].map((v, i) => ks + `[${i}]` + eq + encodeURIComponent(v)).join(sep);
      }
      else if (obj[k] && typeof obj[k] === 'object') {
        let opt = [];
        Object.keys(obj[k]).map(key => opt.push(ks + `[${key}]` + eq + encodeURIComponent(obj[k][key])));
        return opt.join(sep);
      }
      else {
        return ks + eq + encodeURIComponent(obj[k]);
      }
    }).join(sep);
  }

  /**
   * Convert the query string to an object
   *
   * @param {string} str
   * @returns {object}
   */
  paramsFromQuery(str) {
    const query = {};
    const amps = str.split('&');

    for (let i = 0, l = amps.length; i <l; i++) {
      const eqs =  amps[i].split('=');
      const key = decodeURIComponent(eqs[0]);
      const val = decodeURIComponent(eqs[1]);

      if (!key) {
        continue;
      }

      if (Array.isArray(query[key])) {
        query[key].push(val);
      }
      else if (query[key]) {
        query[key] = [query[key], val];
      }
      else {
        query[key] = val;
      }
    }

    return query;
  }

  /**
   * Create multipart form data from the object
   *
   * @param {object} obj
   * @param {FormData} [data]
   * @param {string} [namespace]
   * @returns {FormData}
   */
  createFormData(obj, data = null, namespace = '') {
    const fd = data || new FormData();

    for (let k in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, k) && obj[k]) {
        const key = namespace? namespace + '[' + k + ']': k;

        if (obj[k] instanceof Date) {
          fd.append(key, obj[k].toISOString());
        }
        else if (utils.isPlainObject(obj[k])) {
          this.createFormData(obj[k], fd, key);
        }
        else {
          fd.append(key, obj[k]);
        }
      }
    }

    return fd;
  }

  /**
   * Transform the data before the request
   *
   * @param {XMLHttpRequest} xhr
   * @param {object} options
   * @returns {object}
   */
  transformBefore(xhr, options) {
    return { xhr, options }
  }

  /**
   * Transform the data after the request
   *
   * @param {object} result
   * @returns {object}
   */
  transformAfter(result) {
    return {
      ...result,
      data: result.response,
    };
  }

  /**
   *  Make a GET request
   *
   * @param {string} url
   * @param {object} options
   * @returns {Promise}
   */
  get(url, options = {}) {
    options.url = url;
    options.method = 'GET';
    return this.query(options);
  }

  /**
   * Makea a DELETE request
   *
   * @param {string} url
   * @param {object} options
   * @returns {Promise}
   */
  delete(url, options = {}) {
    options.url = url;
    options.method = 'DELETE';
    return this.query(options);
  }

  /**
   * Make a POST request
   *
   * @param {string} url
   * @param {object} options
   * @returns {Promise}
   */
  post(url, options = {}) {
    options.url = url;
    options.method = 'POST';
    return this.query(options);
  }

  /**
   * Make a PUT request
   *
   * @param {string} url
   * @param {object} options
   * @returns {Promise}
   */
  put(url, options = {}) {
    options.url = url;
    options.method = 'PUT';
    return this.query(options);
  }

  /**
   *  Make a PATCH request
   *
   * @param {string} url
   * @param {object} options
   * @returns {Promise}
   */
  patch(url, options = {}) {
    options.url = url;
    options.method = 'PATCH';
    return this.query(options);
  }

  /**
   * Make a HEAD request
   *
   * @param {string} url
   * @param {object} options
   * @returns {Promise}
   */
  head(url, options = {}) {
    options.url = url;
    options.method = 'HEAD';
    return this.query(options);
  }
}

const request = new Request();
request.__instances = {};
request.__cache = {};

/**
 * Add a new instance
 *
 * @param {string} name
 * @param {Request} instance
 */
request.addInstance = function (name, instance) {
  this.__instances[name] = instance;
};

/**
 * Remove the instance
 *
 * @param {string} name
 */
request.removeInstance = function (name) {
  delete this.__instances[name];
};

Object.defineProperty(request, 'use', {
  get: () => {
    return request.__instances;
  }
});

request.Request = Request;
export default request;

<|start_filename|>src/event-emitter.js<|end_filename|>
import utils from './utils';

/**
 * Class to work with events.
 * 
 * {@link https://akilijs.com/docs/events}
 */
export default class EventEmitter {
  constructor(name, node, el, component) {
    this.name = name;
    this.node = node;
    this.el = el;
    this.component = component;
  }

  /**
   * Trigger the event
   *
   * @param {*} data
   * @param {object} [options]
   * @param {boolean} [force]
   * @returns {Promise}
   */
  trigger(data, options = {}, force = false) {
    if(this.__removed) {
      return;
    }

    if (force || !this.inEvaluating()) {
      data = utils.copy(data, { plain: true });
      this.el.dispatchEvent(new CustomEvent(this.name, this.prepareOptions({ detail: data,  ...options })));
    }   
  }

  /**
   * Dispatch the event
   *
   * @param {Event} _Event - class of the event
   * @param {object} [options]
   * @param {boolean} [force]
   * @returns {Promise}
   */
  dispatch(_Event, options = {}, force = false) {
    if(this.__removed) {
      return;
    }

    if (force || !this.inEvaluating()) {
      this.el.dispatchEvent(new _Event(this.name, this.prepareOptions(options)));
    }
  }

  /**
   * Prepere the event options
   * 
   * @param {object} [options] 
   */
  prepareOptions(options = {}) {
    options = { bubbles: true, ...options }; 
    this.node.__component.__prepareAttributeOut(this.node, options.detail);
    return options;
  }

  /**
   * Check the event callback doesn't run recursively
   *
   * @returns {boolean}
   */
  inEvaluating() {
    let data = this.component.__evaluatingEvent;

    if (!data) {
      return false;
    }

    return data.el === this.el && data.event.type === this.name && data.component === this.component;
  }

  /**
   * Bind the function
   *
   * @param {function} fn
   */
  bind(fn) {
    this.el.addEventListener(this.name, this.fn = fn);
  }

  /**
   * Unbind the function
   */
  unbind() {
    this.fn && this.el.removeEventListener(this.name, this.fn);
  }

  /**
   * Remove the emitter
   */
  remove() {
    this.unbind();
    delete this.name;
    delete this.node;
    delete this.component
    delete this.el;
    this.__removed = true;
  }
}

<|start_filename|>src/components/object.js<|end_filename|>
import Url from './url.js';
import Akili from '../akili.js';

/**
 * Component for object elements.
 * 
 * {@link https://akilijs.com/docs/components#docs_image,_iframe,_embed,_audio,_video,_track,_source,_object}
 * 
 * @tag object
 * @see Url
 */
export default class Objects extends Url {  
  constructor(...args) {
    super(...args);

    this.urlAttribute = 'data';
  }

  static define() {
    Akili.component('object', this);
  }
}

<|start_filename|>src/components/if.js<|end_filename|>
import Component from '../component.js';
import Akili from '../akili.js';

/**
 * Component for conditional statements.
 * 
 * {@link https://akilijs.com/docs/components#docs_conditional_statements}
 * 
 * @tag if
 * @selector if[is]
 * @attr {boolean} is - show the element content or not
 * @attr {boolean} recreate - delete the content and recreate or just show/hide 
 */
export default class If extends Component {
  static transparent = true; 
  static matches = '[is]';
  static booleanAttributes = ['recreate'];

  static define() {
    Akili.component('if', this);
    Akili.component('else-if', this.ElseIf);
    Akili.component('else', this.Else);
  }

  constructor(...args) {
    super(...args);
    this.html = this.el.innerHTML;
    this.el.innerHTML = '';
    this.state = false;
    this.active = false;
    this.recreate = false;
    this.rendered = false;
    this.display = getComputedStyle(this.el).display;
  }

  compiled() {
    this.attr('recreate', this.setRecreation);
    return this.attr('is', this.setIs);
  }

  setIs(val) {
    this.state = !!val;
    return this.setState();
  }

  setActivity(active) {
    this.active = active;
  }

  setRecreation(recreate) {
    this.recreate = recreate;
  }

  setState() {
    let res = this.rendering();
    let next = this.el.nextElementSibling;
    let result;

    if (!next || !next.matches('else-if,else')) {
      return;
    }

    next.__akili.setActivity(this.active || this.state);
    next.__akili.setRecreation(this.recreate);
    result = next.__akili.setState();    
    return Promise.resolve(res).then(() => result);
  }

  rendering() {
    let res = Promise.resolve();

    if (this.state && !this.active) {
      this.el.style.setProperty('display', this.display, 'important');

      if (this.recreate || !this.rendered) {
        res = this.render();
      }
    }
    else {
      if (this.recreate) {
        this.empty();
      }
      else if (!this.rendered) {
        res = this.render();
      }

      this.el.style.setProperty('display', 'none', 'important');
    }

    return res;
  }

  render() {
    let res;
    this.empty();
    this.el.innerHTML = this.html;   
    res = Akili.compile(this.el, { recompile: true });
    this.rendered = true;
    return res;
  }
}

/**
 * Component for conditional statements.
 * 
 * {@link https://akilijs.com/docs/components#docs_conditional_statements}
 * 
 * @tag else-if
 * @selector else-if[is]
 * @attr {boolean} is @see If
 */
export class ElseIf extends If {
  constructor(...args) {
    super(...args);

    this.active = true;
  }
}

/**
 * Component for conditional statements.
 * 
 * {@link https://akilijs.com/docs/components#docs_conditional_statements}
 * 
 * @tag else
 */
export class Else extends ElseIf {
  static matches = '';

  constructor(...args) {
    super(...args);

    this.el.setAttribute('is', 'true');
  }
}

If.ElseIf = ElseIf;
If.Else = Else;

<|start_filename|>src/scope.js<|end_filename|>
import Akili from './akili.js';
import utils from './utils.js';

/**
 * Scope class
 * 
 * {@link https://akilijs.com/docs/scope}
 *
 * All properties starting with __ are not monitored.
 * You can use them for internal manipulations.
 */
export default class Scope {
  constructor(name, el, component) {
    this.__name = name;
    this.__el = el;
    this.__component = component;
    el.setAttribute('scope', name);
  }

  /**
   * Set the scope value
   * 
   * @param {string|string[]} keys
   * @param {*} value
   * @param {object} [options] 
   * @param {boolean} [options.saveProxy] 
   * @param {boolean} [options.silent]
   * @param {boolean} [options.target]
   * @protected
   */
  __set(keys, value, options = {}) {
    if (!Array.isArray(keys)) {
      keys = [keys];
    }
    
    Akili.unisolate(() => {
      options.saveProxy && (this.__component.__disableProxyRedefining = true);
      options.silent && this.__component.__disableKeys();
      utils.setPropertyByKeys(keys, options.target? this.__target: this, (last, val) => {
        if (!last) {
          return val || {};
        }

        return value;
      });
      options.silent && this.__component.__enableKeys();
      options.saveProxy && (this.__component.__disableProxyRedefining = false);
    });
  }

  /**
   * Remove the scope
   * 
   * @protected
   */
  __remove() {
    Akili.removeScope(this.__name);

    for(let key in this) {
      if(!Object.prototype.hasOwnProperty.call(this, key)) {
        continue;
      }

      delete this[key];
    }
  }
}

<|start_filename|>src/components/content.js<|end_filename|>
import Text from './text.js';
import Akili from '../akili.js';

/**
 * Component for elements with the contenteditable attribute.
 * 
 * {@link https://akilijs.com/docs/components#docs_contenteditable}
 * 
 * @tag content
 * @selector content,[contenteditable]
 * @attr @see Text
 * @attr {boolean} [editable] - editable or not
 */
export default class Content extends Text {
  static define() {
    Akili.component('content', this);
    Akili.alias('[contenteditable]', 'content');
  }

  constructor(...args) {
    super(...args);

    if (this.el.hasAttribute('editable')) {
      this.el.setAttribute('contenteditable', this.el.getAttribute('editable'));
      this.el.removeAttribute('editable');
    }

    if (getComputedStyle(this.el).display == 'inline') {
      this.el.style.display = 'block';
    }

    this.valueKey = 'innerHTML';
  }

  walkTextNodes(el, fn){
    const nodes = el.childNodes;

    for (let i = 0, l = nodes.length; i < l; i++) {
      let node = nodes[i];
      node.nodeType == 3? fn(node): this.walkTextNodes(node, fn);
    }
  }

  getElementTextLength() {
    let length = 0;
    this.walkTextNodes(this.el, node => length += node.nodeValue.length);
    return length;
  }

  setElementFocusOn(pos) {
    const nodes = [];
    let node;
    const max = this.getElementTextLength();
    pos > max && (pos = max); 
    
    if(pos <= 0) {
      return this.el.focus();
    }
    
    this.walkTextNodes(this.el, node => nodes.push(node));    

    for(let i = 0; i < nodes.length; i++) {
      node = nodes[i];
      
      if (pos > nodes[i].nodeValue.length && nodes[i + 1]) {
        pos -= nodes[i].nodeValue.length;
        continue;
      }
        
      node = nodes[i];
      break;
    }

    const range = document.createRange();
    const sel = window.getSelection();
    range.setStart(node, pos);
    range.setEnd(node, pos);
    sel.removeAllRanges();
    sel.addRange(range);
    this.el.focus();
  }
}

<|start_filename|>test/app/cancel.js<|end_filename|>
import Component from '../../src/component.js';

export default class Cancel extends Component {
  constructor(...args) {
    super(...args);
    this.cancel();
  }
}

<|start_filename|>test/services/store.js<|end_filename|>
import { assert } from 'chai';
import Akili from '../../src/akili.js';
import store from '../../src/services/store.js';
import elements from '../app/elements.js';

describe('store.js', () => {
  let handler;
  
  class TestStore extends Akili.Component {
    created() {
      this.checkStore? this.store('test', 'test'): this.unstore('test', 'test');     
    }
  } 

  before(() => {
    store.test = 'start';
    store.handler = 'start';
    handler = val => elements.sectionOne.__akili.scope.linkHandler = val;
  });
  
  describe('Component.prototype.store()', () => {
    it('should throw an error', () => {        
      let testStore = new TestStore(document.createElement('test-store')); 
      testStore.checkStore = true;
      assert.throws(() => elements.sectionOne.__akili.store('test'), '', 'nonexistent handler');
      assert.throws(() => testStore.created(), '', 'before the compilation');
    });

    it('should create the link by a scope property', () => {        
      store.test = 'start';  
      elements.sectionOne.__akili.store('test', ['test']);
      elements.sectionOne.__akili.store('test', 'test');
      elements.sectionTwo.__akili.store('test', ['tasty']);        
      assert.equal(elements.sectionOne.__akili.__storeLinks['test'][0].keyString, 'test', 'check the component.__storeLinks');
      assert.equal(Akili.__storeLinks['test'][1].keyString, 'tasty', 'check Akili.__storeLinks');
      assert.equal(elements.sectionOne.__akili.scope.test, 'start', 'check the value for sectionOne');
      assert.equal(elements.sectionTwo.__akili.scope.tasty, 'start', 'check the value for sectionTwo');
    });

    it('should change the scope property for sectionTwo', () => {
      elements.sectionOne.__akili.scope.test = 'link';  
      assert.equal(elements.sectionTwo.__akili.scope.tasty, 'link');
    });

    it('should change the scope property for sectionOne', () => {
      elements.sectionTwo.__akili.scope.tasty = 'nolink'; 
      assert.equal(elements.sectionOne.__akili.scope.test, 'nolink');
    });

    it('should create the link by a function', () => { 
      elements.sectionOne.__akili.store('handler', handler);
      elements.sectionOne.__akili.store('handler', handler);      
      assert.strictEqual(Akili.__storeLinks['handler'][0].fn, handler, 'check Akili.__storeLinks');
      assert.equal(elements.sectionOne.__akili.scope.linkHandler, 'start', 'check the value');
    }); 
    
    it('should create the link with all store properties by a function', () => { 
      elements.sectionOne.__akili.store(handler);       
      assert.strictEqual(Akili.__storeLinks['*'][0].fn, handler, 'check Akili.__storeLinks');
      store.test = '*';  
      assert.equal(elements.sectionOne.__akili.scope.linkHandler, '*', 'check the value');    
    });  
  });   

  describe('store property changing', () => {
    it('should not delete because of disablement', () => {
      Akili.__disableStoreProxy = true;
      store.test = 'start'; 
      assert.notEqual(elements.sectionOne.__akili.scope.test, 'start');
    });

    it('should set sectionOne scope value to undefined', () => {
      Akili.__disableStoreProxy = false;
      store.test = 'start';  
      assert.equal(elements.sectionOne.__akili.scope.test, 'start');
    });

    it('should not change the value if isolation is', () => {
      Akili.isolate(() => {
        store.test = 'change'; 
        assert.notEqual(elements.sectionOne.__akili.scope.test, 'change');
      });     
    });

    it('should change the value after the isolation', () => {
      assert.equal(elements.sectionOne.__akili.scope.test, 'change');
    });

    it('should change the nested property', () => {
      Akili.__disableStoreProxy = true;
      store.test = {};
      Akili.__disableStoreProxy = false;
      store.test.nested = true;
      assert.isTrue(elements.sectionOne.__akili.scope.test.nested);
    });

    it('should change the existed nested property', () => {
      store.test = { nested: { nested: true } };
      assert.isTrue(elements.sectionOne.__akili.scope.test.nested.nested);
    });

    it('should change the compound value with the simple one', () => {
      store.test = 'end'; 
      assert.equal(elements.sectionOne.__akili.scope.test, 'end');
    });

    it('should change sectionOne scope value by handler', () => {
      store.handler = 'end'; 
      assert.equal(elements.sectionOne.__akili.scope.linkHandler, 'end');
    });
  }); 

  describe('delete store property', () => {
    it('should not delete because of disablement', () => {
      Akili.__disableStoreProxy = true;
      delete store.test; 
      assert.equal(elements.sectionOne.__akili.scope.test, 'end');
    });

    it('should set sectionOne scope value to undefined', () => {
      Akili.__disableStoreProxy = false;
      delete store.test;   
      assert.isUndefined(elements.sectionOne.__akili.scope.test);
    });

    it('should not change the value if isolation is', () => {
      store.test = 'end';
      Akili.isolate(() => {
        delete store.test;
        assert.equal(elements.sectionOne.__akili.scope.test, 'end');
      });     
    });

    it('should change the value after the isolation', () => {
      assert.isUndefined(elements.sectionOne.__akili.scope.test);
    });
  });

  describe('Component.prototype.unstore()', () => {
    it('should throw an error', () => {        
      let testStore = new TestStore(document.createElement('test-store'));  
      assert.throws(() => elements.sectionOne.__akili.unstore('test'), '', 'nonexistent handler');
      assert.throws(() => testStore.created(), '', 'before the compilation');
    });

    it('should remove all links', () => {
      elements.sectionOne.__akili.unstore('test', 'test');
      elements.sectionTwo.__akili.unstore('test', 'tasty');
      elements.sectionOne.__akili.unstore('handler', handler);
      elements.sectionOne.__akili.unstore(handler);
      assert.doesNotHaveAllKeys(elements.sectionOne.__akili.__storeLinks, ['test', '*'], 'check component.__storeLinks for sectionOne');
      assert.doesNotHaveAllKeys(elements.sectionTwo.__akili.__storeLinks, ['tasty'], 'check component.__storeLinks for sectionTwo');
      assert.doesNotHaveAllKeys(Akili.__storeLinks, ['test', 'handler'], 'check Akili.__storeLinks for sectionOne');
      assert.doesNotHaveAllKeys(Akili.__storeLinks, ['tasty'], 'check Akili.__storeLinks for sectionTwo');
    });
  });  

  describe('Check lifecycle', () => {
    it('should be synchronized', () => {
      const firstStore = elements.all.querySelector('first-store');
      const secondStore = elements.all.querySelector('second-store');
      assert.equal(firstStore.innerHTML, '1', 'first store');
      assert.equal(secondStore.innerHTML, '1', 'second store');    
    });
  });  
});

<|start_filename|>test/app/abstract.js<|end_filename|>
import Component from '../../src/component.js';

export default class Abstract extends Component {  
  static transparent = true;
}

<|start_filename|>test/components/include.js<|end_filename|>
import { assert } from 'chai';
import Include from '../../src/components/include.js';

describe('include.js', () => {
  describe('Include', () => {
    let component;

    before(() => {
      component = new Include(document.createElement('include'));
      component.__create();
      return component.__compile();
    });

    describe('.setCache()', () => {
      it('should set cache', () => {
        let cache = 1000;
        component.setCache(cache);
        assert.equal(component.cache, cache);
      });
    });

    describe('.getTemplate()', () => {
      it('should not set wrong template', done => {
        component.setTemplate('undefined').then(() => {
          done(new Error('Must throw an exception'));
        }).catch(() => done());
      });

      it('should set the right template', () => {
        return component.setTemplate('include.html');
      });
    });
  });
});


<|start_filename|>test/app/end.js<|end_filename|>
import Akili from '../../src/akili';
import store from '../../src/services/store';
import globals from '../../src/services/store';

describe('Application deinitialization', () => {
  it('should prepare request cache', () => {
    let cache = Akili.prepareServerSideRequestCache();
    assert.isTrue(Object.keys(cache.__main).length > 0);
  });

  it('should be deinitialized', () => {
    Akili.deinit();
    assert.isNull(Akili.__init, 'check init');
    assert.isNull(Akili.__root, 'check the root');
  });

  it('should be destroyed', () => {
    Akili.destroy();
    assert.isUndefined(window.Akili, 'check window.Akili');
    assert.lengthOf(Object.keys(store.__target), 0, 'check the store');
    assert.lengthOf(Object.keys(globals.__target), 0, 'check the globals');
  });
});


<|start_filename|>test/app/ssr.js<|end_filename|>
import Component from '../../src/component.js';

export default class Ssr extends Component {
  static ssr = false;
}

<|start_filename|>src/components/a.js<|end_filename|>
import Component from '../component.js';
import router from '../services/router.js';
import Akili from '../akili.js';

/**
 * Component for links.
 * It works with router as well. 
 * 
 * {@link https://akilijs.com/docs/components#docs_links}
 * 
 * @tag a
 * @selector a[state]:not([url]),a[url]:not([state])
 * @attr {string} [url] - url to go
 * @attr {string} [state] - state to go {@link https://akilijs.com/docs/routing}
 * @attr {object} [params] - params for the state {@link https://akilijs.com/docs/routing#docs_state_params}
 * @attr {object} [query] - query for the state {@link https://akilijs.com/docs/routing#docs_state_query}
 * @attr {string} [hash] - hash for the state {@link https://akilijs.com/docs/routing#docs_state_hash}
 * @attr {object} [options] - options for the state {@link https://akilijs.com/docs/routing#docs_router.location}
 * @scope {boolean} isActiveState - the state is active or not 
 * @scope {boolean} inActiveState - the state is part of the active state or not
 */
export default class A extends Component {
  static matches = '[state]:not([url]),[url]:not([state])';
  static controlAttributes = true;

  static define() {
    Akili.component('a', this);
  }

  constructor(...args) {
    super(...args);

    this.state = null;
    this.params = {};
    this.query = {};
    this.options = {};
    this.isUrl = this.el.hasAttribute('url');
  }

  compiled() {
    this.el.addEventListener('click', e => {
      e.preventDefault();

      if (this.isUrl) {
        router.location(this.attrs.url, this.options);
        return;
      }

      router.state(this.state.name, this.params, this.query, this.hash,  this.options);
    });

    this.onStateChanged = () => this.state && this.setActivity();
    window.addEventListener('state-changed', this.onStateChanged);

    this.attr('state', this.setState);
    this.attr('params', this.setParams);
    this.attr('query', this.setQuery);
    this.attr('hash', this.setHash);
    this.attr('options', this.setOptions);
    this.attr('url', this.setUrl);
    this.attr('state', this.resetHref, { callOnStart: false });
    this.attr('params', this.resetHref, { callOnStart: false });
    this.attr('query', this.resetHref, { callOnStart: false });
    this.attr('hash', this.resetHref, { callOnStart: false });
    this.attr('url', this.resetHref, { callOnStart: false });
    this.resetHref();
  }

  removed() {
    window.removeEventListener('state-changed', this.onStateChanged);
  }

  setUrl(url) {
    this.url = url;
  }

  setState(name) {
    this.state = this.getState(name);
    this.setActivity();
  }

  setActivity() {
    this.scope.isActiveState = router.isActiveState(this.state);
    this.scope.inActiveState = router.inActiveState(this.state);
  }

  setParams(params) {
    if (typeof params != 'object' || !params) {
      throw new Error(`Router state params must be an object`);
    }

    this.params = params;
  }

  setQuery(query) {
    if (typeof query != 'object' || !query) {
      throw new Error(`Router state query must be an object`);
    }

    this.query = query;
  }

  setHash(hash) {
    if (typeof hash != 'string' && hash !== undefined && hash !== null) {
      throw new Error(`Router state hash must be a string|null`);
    }

    this.hash = hash;
  }

  setOptions(options) {
    if (typeof options != 'object' || !options) {
      throw new Error(`Router state options must be an object`);
    }

    this.options = options;
  }

  getState(name) {
    let state = router.getState(name);

    if (!state) {
      throw new Error(`Not found router state with name "${name}"`);
    }

    return state;
  }

  resetHref() {
    if (this.url) {
      this.attrs.href = this.url;
    }
    else {
      this.attrs.href = router.createStateUrl(this.state, this.params, this.query, this.hash, this.options);
    }
  }
}

<|start_filename|>src/components/style.js<|end_filename|>
import Akili from '../akili.js';
import Component from '../component.js';

/**
 * Component for a style element.
 */
export default class Style extends Component {
  static define() {
    Akili.component('style', this);
  }

  constructor(...args) {
    super(...args);
    this.prevent();
  }
}

<|start_filename|>src/components/audio.js<|end_filename|>
import Url from './url.js';
import Akili from '../akili.js';

/**
 * Component for audio elements.
 * 
 * {@link https://akilijs.com/docs/components#docs_image,_iframe,_embed,_audio,_video,_track,_source,_object}
 * 
 * @tag audio
 * @see Url
 */
export default class Audio extends Url {  
  static define() {
    Akili.component('audio', this);
  }
}

<|start_filename|>test/app/all.js<|end_filename|>
import Component from '../../src/component.js';

export default class All extends Component {
  constructor(...args) {
    super(...args);

    this.el.innerHTML += `
      <ssr>\${ this.x }</ssr>
    `;

    this.el.innerHTML += `
      <script>\${ this.undefinedValue.x }</script>
      <style>\${ this.undefinedValue.x }</style>
      <prevent>
        <if is="true"></if>
        <input/>        
      </prevent>
    `;

    this.el.innerHTML += `
      <cancel></cancel>
      <prevent>
        <if is="true"></if>
        <input/>        
      </prevent>
    `;

    this.el.innerHTML += `
      <img 
        url="\${ this.imageUrl }"
        loading="\${ this.imageLoading }"
        hidden-error="true"
      />
    `;

    this.el.innerHTML += `
      <a 
        url="/"
        on-compiled="\${ this.compiled = true }"
        on-recompiled="\${ this.recompiled = true }"
        on-resolved="\${ this.resolved = true }"
        on-removed="\${ this.__component.checkRemovedEvent = true }"
      >
        location link
      </a>
    `;

    this.el.innerHTML += `
      <attr 
        test="\${this.cAttr}" 
        on-test="\${this.cAttrEvent = event.detail}"
      ></attr>
    `;

    this.el.innerHTML += `
      <first-store></first-store>
      <second-store></second-store>
    `;

    this.el.innerHTML += `
      <object url="\${this.cObjectValue}"></object>
    `;

    this.el.innerHTML += `
      <if recreate is="\${this.cIfData === 0}">0</if>
      <else-if is="\${this.cIfData === 1}">1</else-if>
      <else-if is="\${this.cIfData === 2}">2</else-if>
      <else-if is="\${this.cIfData === 3}">3</else-if>
      <else>4</else>
    `;
    
    this.el.innerHTML += `
      <for in="\${this.cForData}">\${this.loopKey}=\${this.loopValue}</for>
      <for in="\${ {x: 1, y: 2} }">
        <div component="loop" in="\${[1]}">
          \${this.loopKey}=\${this.loopValue}
        </div>
      </for>
      <ul in="\${this.cForData}"><li>\${this.loopKey}=\${this.loopValue}</li></ul>
    `;

    this.el.innerHTML += `
      <input 
        value="\${this.cInputValue}" 
        focus="\${this.cInputFocus}"
        debounce="100"
        on-debounce="\${this.cInputValue = event.target.value}"
      />
      <textarea 
        value="\${this.cTextareaValue}" 
        on-input="\${this.cTextareaValue = event.target.value}"></textarea>
      <content 
        editable="true"
        focus="true"
        value="\${this.cContentValue}" 
        on-input="\${this.cContentValue = event.target.innerHTML}"></content>
      <div contenteditable>text</div>
      <input type="checkbox" checked="\${this.cCheckboxChecker}"/>
    `;

    this.el.innerHTML += `
      <select
        multiple="\${this.cSelectMultiple}"
        value="\${this.cSelectValue}"
        in="\${this.cSelectData}"
        chunks="2"
        on-change="\${this.cSelectValue = event.target.content}"
      >
       <option selected="\${this.loopKey == 2}">\${this.loopValue}</option>
      </select>
    `;

    this.el.innerHTML += `
      <radio
        name="radio"
        value="\${this.cRadioValue}"
        on-radio="\${this.cRadioValue = event.detail}"
      >        
        <input type="radio" value="radio-1"/>
        <radio-button value="radio-2"></radio-button>
      </radio>
      <radio
        name="radio2"
        in="\${this.cForData}"
        chunks="2"
      >
        <radio-button value="\${ this.loopValue }"></radio-button>
      </radio>
    `
  }
}


<|start_filename|>src/components/select.js<|end_filename|>
import For, { Loop } from './for.js';
import utils from '../utils.js';
import Akili from '../akili.js';

/**
 * Component to work with select.
 * 
 * {@link https://akilijs.com/docs/components#docs_select}
 * 
 * @tag select  
 * @attr {string} value - actual value
 * @attr [in] @see For
 */
export default class Select extends For {
  static matches = '';
  static booleanAttributes = ['multiple'].concat(For.booleanAttributes);
  static events = ['change'].concat(For.events);

  static define() {
    Akili.component('select', this);
    Akili.component('option', this.Option);
  }

  constructor(...args) {
    super(...args);
    this.iteratorTagName = 'option';
    this.iterable = this.el.hasAttribute('in');
  }

  created() {
    this.el.addEventListener('change', () => this.el.content = this.getContent());

    if(this.iterable) {
      return super.created.apply(this, arguments);
    }
  }

  compiled() {    
    this.iterable && (super.compiled.apply(this, arguments));
    this.attr('multiple', this.setMultiple, { callOnStart: false });
    this.attr('value', this.setValue);
    this.unattr('in', this.draw);
    return this.attr('in', this.setIn);
  }

  setIn(data) {
    return this.draw(data).then(this.redrawSelect.bind(this));
  }

  redrawSelect() {
    const vals = this.removeUnproperValues(this.el.content);
    const content = vals.length? vals: null;
    let selected = [];
    
    for (let i = this.el.options.length - 1; i >= 0; i--) {
      let option = this.el.options[i];
      let selection = !!option.getAttribute('selected');

      if(content && !content.includes(option.value)) {
        option.selected = false;
        continue;
      } 

      if(content) {
        option.selected = true;
        selected.push(option.value);
        continue;
      }

      option.selected = selection;
      selection && selected.push(option.value);
    }

    this.changeValue(this.formatValue(selected));
  }

  setMultiple() {    
    this.changeValue(this.formatValue(utils.copy(this.el.content)));
  }

  setValue(value) {
    if(value === undefined && !this.__isCompiled) {
      return;
    }

    const properValue = this.formatValue(value);
    this.changeValue(properValue, !utils.compare(properValue, value));
  }

  createIteratorElement() {
    let el = super.createIteratorElement.apply(this, arguments);

    if (!el.hasAttribute('value')) {
      el.setAttribute('value', this.html.trim());
    }

    return el;
  }

  getContent() {    
    if (!this.attrs.multiple) {
      return this.el.value;
    }

    let content = [];

    for (let i = 0, l = this.el.options.length; i < l; i++) {
      let option = this.el.options[i];

      if (option.selected) {
        content.push(option.value);
      }
    }

    return content;
  }

  formatValue(value) {
    if (this.attrs.multiple) {
      !Array.isArray(value) && (value = [value]);
      value = this.removeUnproperValues(value);
      return value;
    }

    Array.isArray(value) && (value = value[0]);  
    return this.removeUnproperValues(value).length? value: this.getDefaultValue();
  }

  removeUnproperValues(values) {
    !Array.isArray(values)? values = [values]: values.slice();
    const hash = {};

    for (let i = 0, l = this.el.options.length; i < l; i++) {
      hash[this.el.options[i].value] = true;
    }

    for (let i = values.length - 1; i >= 0; i--) {
      if(!hash[values[i]]) {
        values.splice(i, 1);
      }
    }

    return values;
  }

  redefine() {
    if(!this.el.querySelector('[selected]')) {
      this.el.value = this.getDefaultValue();
    }

    this.changeValue(this.getContent());
  }

  getDefaultValue() {
    const firstEl = this.el.options[0];
    return this.attrs.multiple? []: (firstEl? firstEl.value: undefined);
  }

  changeValue(value, force = false) {
    if (!utils.compare(this.el.content, value)) {
      if (Array.isArray(value)) {
        this.el.value = value[value.length - 1];
  
        for (let i = 0, l = this.el.options.length; i < l; i++) {
          let option = this.el.options[i];  
          option.selected = value.indexOf(option.value) != -1;
        }
      }
      else {
        this.el.value = value;
  
        for (let i = 0, l = this.el.options.length; i < l; i++) {
          let option = this.el.options[i];  
          option.selected = option.value == value;
        }
      }
  
      this.el.content = value;      
    }
    else {
      if(!force) {
        return;
      }
    }
    
    this.attrs.onChange.dispatch(Event, { bubbles: true });
  }
}

export class Option extends Loop {
  static booleanAttributes = ['selected'];

  constructor(...args) {
    super(...args);
    this.iterable = this.el.parentNode.__akili.iterable;    

    if (!this.iterable) {
      this.__cancelled = false;
    }
  }

  __getParsedExpression(expression) {
    return utils.decodeHtmlEntities(expression);
  }

  compiled() { 
    this.attr('selected', this.setSelected, { callOnStart: false });

    if(this.iterable) {
      super.compiled.apply(this, arguments);
    }
  }

  setSelected(value) {
    this.el.selected = value;
    this.for.redefine();
  }
}

Select.Option = Option;

<|start_filename|>src/services/store.js<|end_filename|>
import Akili from '../akili.js';
import utils from '../utils.js';

/**
 * Store allows you to save and share data.
 * 
 * {@link https://akilijs.com/docs/store}
 */
const store = wrap({}, []);

function createProxy (obj, parents) {
  return new Proxy(obj, {
    get: (target, key) => {
      if (key == '__target') {
        return obj;
      }
      else if (key == "__keys") {
        return parents;
      }
      else if(key == '__isProxy') {
        return true;
      }
  
      return target[key];
    },
    set: (target, key, value) => {      
      const keys = [...parents, key];      
      const rootKey = keys[0];
      target[key] = wrap(value, keys);  

      if(Akili.__disableStoreProxy) {
        return true;
      }       

      if (Akili.__isolation) { 
        createIsolationObject(parents, key);
        return true;
      }

      const copy = utils.copy(store[rootKey], { plain: true });  
      Akili.root && Akili.root.__storeTriggerByName(rootKey, copy);
      return true;
    },
    deleteProperty: (target, key) => {      
      if(Akili.__disableStoreProxy) {
        delete target[key];
        return true;
      }  

      const keys = [...parents, key];
      const rootKey = keys[0];
      const value = rootKey === key? undefined: store[rootKey];

      if (Akili.__isolation) {
        createIsolationObject(parents, key);
        delete target[key];
        return true;
      }

      const copy = utils.copy(value, { plain: true });
      Akili.root && Akili.root.__storeTriggerByName(rootKey, copy);
      delete target[key];
      return true;
    }
  });
}

function createIsolationObject (parents, key) {
  const rootKeys = parents.length? [parents[0]]: [key];
  const rootKey = rootKeys[0];
  const keys = [...parents, key];
  const isolationHash = `store.${ rootKey }`;
  Akili.__isolation[isolationHash] = { updatedAt: Date.now(), rootKey, rootKeys, keys };
}

function wrap (value, keys) {
  if(typeof value != 'object' || value === null) {
    return value;
  }

  if(!utils.isPlainObject(value) && !utils.isStoreProxy(value) && value !== store) {
    return value;
  }

  let target = value;

  if(value.__isProxy) {
    target = value.__target;

    if (
      !utils.isStoreProxy(value) ||
      Akili.joinBindingKeys(keys) != Akili.joinBindingKeys(value.__keys)
    ) {
      target = utils.copy(target, { nested: false });
      value = target;
    }
  }

  const targetKeys = Object.keys(value);

  for (let i = 0, l = targetKeys.length; i < l; i++) {
    const k = targetKeys[i];
    const val = target[k];
    target[k] = wrap(val, [...keys, k]);
  }

  if (!value.__isProxy) {
    return createProxy(value, keys);
  }

  return value;
}

export default store;

<|start_filename|>test/app/elements.js<|end_filename|>
const elements = {};

export default elements;



<|start_filename|>src/components/for.js<|end_filename|>
import Component from '../component.js';
import Akili from '../akili.js';
import utils from '../utils.js';

/**
 * Component for loops.
 * 
 * {@link https://akilijs.com/docs/components#docs_loops}
 * 
 * @tag for
 * @selector for[in],ol[in],ul[in],thead[in],tbody[in],tfoot[in]
 * @attr {object|array} in - data to create the loop
 * @message {object|array} out - sent on the data change
 */
export default class For extends Component {
  static matches = '[in]';
  static events = ['out'];

  static define() {
    Akili.component('for', this);
    Akili.component('ol', this);
    Akili.component('ul', this);
    Akili.component('thead', this);
    Akili.component('tbody', this);
    Akili.component('tfoot', this);
    Akili.component('tr', this.Loop);
    Akili.component('loop', this.Loop);
  }

  constructor(...args) {
    super(...args);    
    this.iterators = [];
    this.iteratorEl = null;
    this.reset();
  }

  __prepareAttributeIn(node, value) {  
    if((node instanceof window.Attr) && node.name == 'in') {
      return value;
    }

    return super.__prepareAttributeIn.apply(this, arguments);
  }

  __compareNodePropertyValue(prop, value) {
    const node = prop.node;

    if((node instanceof window.Attr) && node.name == 'in' && prop.value !== value) {
      return false;
    }
    
    return super.__compareNodePropertyValue.apply(this, arguments);
  }

  __stepIterationChunk(index) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        this.drawChunk(index)
          .then(resolve)
          .catch(reject)
      });
    });
  }

  __removeExcessIterators() {
    const length = Object.keys(this.data).length;

    for (let i = length, l = this.iterators.length; i < l; i++) {
      let iterator = this.iterators[i];
      iterator.__remove();
      this.iterators.splice(i, 1);
      l--;
      i--;
    }
  }

  __completeDrawing() {
    if(this.__isRemoved) {
      return;
    }
    
    this.reset();
    this.attrs.onOut.trigger(this.data, { bubbles: true });
  }

  __createIterator() {
    let el;

    for (let i = 0, l = this.el.children.length; i < l; i++) {
      let child = this.el.children[i];

      if (child.getAttribute('component') == 'loop') {
        el = child;
        break;
      }

      el = child;
    }

    if (!el) {
      el = document.createElement('loop');      
      el.innerHTML = this.el.innerHTML; 
      this.el.innerHTML = '';
      this.el.appendChild(el);
    }

    let componentName = el.getAttribute('component');

    if (componentName != 'loop') {
      let component = Akili.component(componentName || el.tagName);

      if (!component) {
        el.setAttribute('component', 'loop');
      }
      else if (!(component.prototype instanceof For)) {
        let mask = document.createElement('loop');
        mask.appendChild(el);
        el = mask;
      }
    }

    this.html = el.innerHTML;
    this.iteratorEl = this.__createIteratorElement(el.outerHTML);
    el.remove();
  }

  __createIteratorElement(html) {
    let el = document.createElement('template');
    el.innerHTML = html;
    return el.content.firstChild;
  }

  created() {    
    this.__createIterator();
  }

  compiled() {
    return this.attr('in', this.draw);
  }

  removed() {
    super.removed.apply(this, arguments);
    this.iteratorEl && this.iteratorEl.remove();
    delete this.html;
    delete this.iterators;
    delete this.iteratorEl; 
    delete this.__iterator;
    delete this.__value;
  }  

  loop(key, value, index) {
    let promise = Promise.resolve();
    this.__index = index;
    this.__key = key;
    this.__value = value;    
    this.__hash = utils.createHash(value);

    if(this.iterators.length > index) {
      let iterator = this.iterators[index];        
      iterator.setIndex(this.__index === iterator.index);     
      iterator.setKey(this.__key === iterator.key);
      iterator.setValue(utils.compare(this.__hash, iterator.hash));
      return { promise, iterator };
    }
    
    let el = this.iteratorEl.cloneNode();
    el.innerHTML = this.html;
    this.el.appendChild(el);
    promise = Akili.compile(el); 
    this.iterators.push(el.__akili);
    return { promise, iterator: el.__akili};
  }

  drawChunk(index) {
    const keys = Object.keys(this.data);
    const length = keys.length;

    if(index == length) {
      this.__removeExcessIterators();
      return Promise.resolve(this.__completeDrawing());
    }

    const promises = [];    
    let chunks = +(this.attrs.chunks || length);
    let size = index + chunks;      
    size > length && (size = length);  
    let finisher = this.__stepIterationChunk.bind(this);   

    for (let i = index; i < size; i++) {
      const key = keys[i];
      const n = i + 1;
      const res = this.loop(key, this.data[key], i);
      res.iterator.iterate(i);
      promises.push(res.promise);      

      if(n != size) {
        continue;
      }

      if(n == length) {
        this.__removeExcessIterators();
        finisher = this.__completeDrawing.bind(this);
      }

      return Promise.all(promises).then(() => finisher(n));
    }
  }

  draw(data) {
    if (typeof data != 'object' || data === null) {
      if (Akili.options.debug) {
        // eslint-disable-next-line no-console
        console.warn(`"For" component "in" attribute value type must be an object/array`);        
      }

      data = [];
    }

    this.data = data;
    const children = [].slice.call(this.el.children); 
    this.iterators.sort((a, b) => children.indexOf(a.el) - children.indexOf(b.el));
    this.__children.sort((a, b) => children.indexOf(a) - children.indexOf(b));    
    return this.drawChunk(0);
  }

  reset() {
    this.__iterator = null;
    this.__index = 0;
    this.__key = '';
    this.__value = null;
    this.__hash = '';
  }  
}

/**
 * Component for loops.
 * 
 * {@link https://akilijs.com/docs/components#docs_loops}
 * 
 * @tag loop
 * @selector loop,tr
 * @attr @see For
 * @scope {*} loopValue - data item's value of the current iteration 
 * @scope {string|number} loopKey - key of the iteration
 * @scope {number} loopIndex - index of the iteration
 */
export class Loop extends For {
  static matches = '';
  static controlAttributes = true;

  constructor(...args) {
    super(...args);

    this.for = null;
    this.html = this.el.innerHTML;
    this.isFor = this.el.hasAttribute('in');

    if (!this.isFor && !(this.el.parentNode.__akili instanceof For)) {
      this.cancel();
    }
  }

  created() {
    this.__parent.__akili.__iterator = this;
    this.for = this.__parent.__akili;
    this.setIndex(true);
    this.setKey(true);
    this.setValue(true);

    if(this.isFor) {
      return super.created.apply(this, arguments);
    }
  }

  compiled() {
    if(this.isFor) {
      return super.compiled.apply(this, arguments);
    }
  }

  removed() {
    super.removed.apply(this, arguments);
    delete this.for;
    delete this.value;
  }

  setIndex(target) {
    this.index = this.for.__index;
    this.scope.__set('loopIndex', this.index, { target });
  }

  setKey(target) {
    this.key = this.for.__key;
    this.scope.__set('loopKey', this.key, { target });
  }

  setValue(target) {
    this.value = this.for.__value;
    this.scope.__set('loopValue', this.value, { target, saveProxy: true });
    this.hash = this.for.__hash;
  }

  iterate() {}
}

For.Loop = Loop;

<|start_filename|>test/app/attribute.js<|end_filename|>
import Component from '../../src/component.js';

export default class Attribute extends Component {}

<|start_filename|>test/app/index.js<|end_filename|>
import { assert } from 'chai';
import Akili from '../../src/akili.js';
import App from './app';
import { SectionOne, SectionTwo, SectionOneOne, SectionEmpty } from './sections';
import { FirstStore, SecondStore } from './store';
import Abstract from './abstract';
import Attribute from './attribute';
import Prevent from './prevent';
import Ssr from './ssr';
import Cancel from './cancel';
import All from './all';
import elements from './elements';

elements.root = document.createElement('root');
elements.home = document.createElement('home');
elements.app = document.createElement('app');
elements.abstract = document.createElement('abstract');
elements.sectionOne = document.createElement('section-one');
elements.sectionOneOne = document.createElement('section-one-one');
elements.sectionTwo = document.createElement('section-two');
elements.sectionEmpty = document.createElement('section-empty');
elements.all = document.createElement('all');
elements.forCompile = document.createElement('for-compile');
document.body.appendChild(elements.root);

Akili.component('attr', Attribute);
Akili.component('first-store', FirstStore);
Akili.component('second-store', SecondStore);
Akili.component('cancel', Cancel);
Akili.component('prevent', Prevent);
Akili.component('ssr', Ssr);
Akili.component('app', App);
Akili.component('abstract', Abstract);
Akili.component('section-one', SectionOne);
Akili.component('section-two', SectionTwo);
Akili.component('section-one-one', SectionOneOne);
Akili.component('section-empty', SectionEmpty);
Akili.component('all', All);

Akili.options.debug = false;

describe('Application initialization', () => {
  it('should not be initialized', () => {
    assert.throws(() => Akili.init(document.documentElement), '',  'by html element');
    assert.throws(() => Akili.init({}), '',  'by non-element');
  }); 

  it('should be initialized on the client side', () => {
    window.AKILI_SERVER = { 
      html: elements.root.outerHTML, 
      requestCache: { 
        __main: {
          undefined: { result: {}, date: Date.now() }
        }
      }, 
    };  
    
    window.AKILI_SSR = true;

    return Akili.init(elements.root).then(() => {
      elements.root.appendChild(elements.app);
      elements.root.appendChild(elements.sectionEmpty);
      elements.root.appendChild(elements.all);
      elements.app.appendChild(elements.abstract);
      elements.app.appendChild(elements.sectionOne);
      elements.app.appendChild(elements.sectionTwo);
      elements.sectionOne.appendChild(elements.sectionOneOne);      
      return Akili.compile(elements.root);
    });
  });  
});


<|start_filename|>test/app/sections.js<|end_filename|>
import Component from '../../src/component.js';

export class SectionEmpty extends Component {}
export class SectionOne extends Component {}

export class SectionOneOne extends Component {
  constructor(...args) {
    super(...args);

    this.el.innerHTML = '${this.counter += 1}';
    this.scope.counter = 0;
  }
}

export class SectionTwo extends Component {
  constructor(...args) {
    super(...args);

    this.el.innerHTML = '${this.html}';
    this.scope.html = '0';
  }
}

<|start_filename|>test/app/prevent.js<|end_filename|>
import Component from '../../src/component.js';

export default class Prevent extends Component {
  constructor(...args) {
    super(...args);

    this.prevent();
  }
}

<|start_filename|>src/components/video.js<|end_filename|>
import Url from './url.js';
import Akili from '../akili.js';

/**
 * Component for video elements.
 * 
 * {@link https://akilijs.com/docs/components#docs_image,_iframe,_embed,_audio,_video,_track,_source,_object}
 * 
 * @tag video
 * @see Url
 */
export default class Video extends Url {  
  static define() {
    Akili.component('video', this);
  }
}

<|start_filename|>test/component.js<|end_filename|>
import { assert } from 'chai';
import Akili from '../src/akili.js';
import utils from '../src/utils.js';
import Home from './app/home.js';
import elements from './app/elements.js';

elements.abstract.appendChild(elements.home);

describe('component.js', () => {
  describe('Component', () => {
    let component;
    let parentComponent;

    describe('.__constructor()', () => {
      it('should throw an error', () => {
        assert.throws(() => new Home());
      });

      it('should create an instance', () => {
        component = new Home(elements.home);
        assert.instanceOf(component, Home);
      });
    });

    describe('.__create()', () => {
      before(() => {
        component.__create();
        parentComponent = component.__evaluationParent.__akili;
      });

      it('should call created function', () => {
        assert.isOk(component.wasCreated);
      });

      it('should return true', () => {
        assert.isTrue(component.isCreated);
      });

      describe('.__initialize()', () => {
        it('should has proxy scope', () => {
          assert.isOk(component.scope.__isProxy);
        });

        it('should has right scope proto', () => {
          assert.strictEqual(Object.getPrototypeOf(component.__scope), elements.abstract.__akili.__scope);
        });
      });

      describe('.__setParents()', () => {
        it('should has parent', () => {
          assert.strictEqual(component.__parent, elements.abstract, 'check nearest parent');
        });

        it('should has parents tree', () => {
          assert.isArray(component.__parents, 'check parents existent');
          assert.strictEqual(component.__parents[0], elements.abstract, 'check first parent');
          assert.strictEqual(component.__parents[1], elements.app, 'check second parent');
          assert.strictEqual(component.__parents[2], Akili.__root, 'check root parent');
        });

        it('should has evaluate parent', () => {
          assert.strictEqual(component.__evaluationParent, elements.app);
        });
      });
    });

    describe('children manipulation', () => {
      let el;

      before(() => {
        el = document.createElement('div');
      });
      
      describe('.__addChild()', () => {
        it('should add new child to the children list', () => {
          component.__addChild(el);
          assert.strictEqual(component.__children[component.__children.length - 1], el);
        });        
      });

      describe('.__spliceChild()', () => {
        it('should splice element from the children list', () => {
          component.__spliceChild(el);
          assert.isOk(component.__children.indexOf(el) == -1);
        });
      });
    });

    describe('.__compile()', () => {
      before(() => {
        return component.__compile();
      });

      it('should call compiled function', () => {
        assert.isOk(component.wasCompiled);
      });

      it('should return true', () => {
        assert.isTrue(component.isCompiled);
      });

      it('should create scope html binding', () => {
        assert.equal(component.__bindings.html.__data.length, 1);
      });

      it('should create scope data binding', () => {
        assert.equal(component.__bindings.data.__data.length, 1);
      });

      it('should create scope data[0] binding', () => {
        assert.equal(component.__bindings.data[0].__data.length, 1);
      });

      it('should create scope data[0].title binding', () => {
        assert.equal(component.__bindings.data[0].title.__data.length, 1);
      });

      it('should create scope data[1] binding', () => {
        assert.equal(component.__bindings.data[1].__data.length, 1);
      });

      it('should create scope data[2] binding', () => {
        assert.equal(component.__bindings.data[2].__data.length, 1);
      });

      it('should set scope a = 1', () => {
        assert.equal(component.__scope.a, 1);
      });

      it('should set scope b = 1', () => {
        assert.equal(component.__scope.b, 1);
      });

      it('should create evaluate parent scope htmlAttr binding', () => {
        assert.equal(parentComponent.__bindings.homeAttr.__data.length, 1);
      });

      it('should evaluate scope html value', () => {
        assert.equal(elements.home.querySelector('b').innerHTML, component.scope.html);
      });

      it('should create component attribute "parent"', () => {
        assert.equal(component.attrs.parent, component.__evaluationParent.__akili.scope.homeAttr);
      });

      it('should create component attribute "parentTwo"', () => {
        assert.equal(component.attrs.parentTwo, component.__evaluationParent.__akili.scope.homeAttrTwo);
      });

      it('should create component attribute "onAction"', () => {
        assert.instanceOf(component.attrs.onAction, Akili.EventEmitter);
      });

      it('should create component attribute "hidden"', () => {
        assert.isOk(component.attrs.hidden);
      });

      it('should create element attribute "boolean-hidden"', () => {
        assert.isOk(component.el.hasAttribute('boolean-hidden'));
      });

      it('should set <p> element', () => {
        assert.equal(elements.home.querySelector('p').innerHTML, parentComponent.scope.homeAttrTwo);
      });
    });

    describe('.__resolve()', () => {
      before(() => {
        return component.__resolve();
      });

      it('should call created function', () => {
        assert.isOk(component.wasResolved);
      });

      it('should return true', () => {
        assert.isTrue(component.isResolved);
      });
    });

    describe('node property changing manipulations', () => {
      let node;

      before(() => {
        node = component.__bindings.html.__data[0].node;
      });

      describe('.__checkEvaluation()', () => {
        it('should be false', () => {
          assert.isNotOk(component.__checkEvaluation(node));
        });

        it('should be true', () => {
          component.__scope.html = 'Goodbye World';
          assert.isOk(component.__checkEvaluation(node));
        });
      });

      describe('.__checkNodePropertyChange()', () => {
        it('should be false', () => {
          assert.isNotOk(component.__checkNodePropertyChange(node, ['html'], 'Hello World!'));
        });

        it('should be true', () => {
          assert.isOk(component.__checkNodePropertyChange(node, ['html'], 'new value'));
        });
      });

      describe('.__evaluateNode()', () => {
        it('should change node html', () => {
          component.__disableKeys();
          component.scope.html = 'New World';
          component.__enableKeys();
          component.__evaluateNode(node);
          assert.equal(node.nodeValue, 'New World');
        });
      });

      describe('.__evaluate()', () => {
        let node;

        before(() => {
          node = parentComponent.__bindings.homeAttr.__data[0].node;
          component.__disableKeys();
          parentComponent.scope.homeAttr = 'New value';
          component.__enableKeys();
        });

        it('should return right value', () => {
          assert.equal(parentComponent.__evaluate(node).res, 'New value');
        });

        it('should change component attrs value', () => {
          assert.equal(component.attrs.parent, 'New value');
        });
      });
    });

    describe('.__evaluateByKeys()', () => {
      before(() => {
        component.__disableKeys();
        component.scope.data.splice(2, 1);
        component.__enableKeys();
        component.__evaluateByKeys(['data'], component.scope.data);
      });

      it('should change <z> element to undefined', () => {
        assert.equal(elements.home.querySelector('z').innerHTML, 'undefined');
      });

      it('should increment <f> element', () => {
        assert.equal(elements.home.querySelector('f').innerHTML, '2');
      });

      it('should not increment <s> element', () => {
        assert.equal(elements.home.querySelector('s').innerHTML, '1');
      });

      it('should not change <x> element', () => {
        assert.equal(elements.home.querySelector('x').innerHTML, component.__scope.data[0].title);
      });

      it('should not change <y> element', () => {
        assert.equal(elements.home.querySelector('y').innerHTML, JSON.stringify(component.__scope.data[1]));
      });
    });    

    describe('.__evaluateEvent()', () => {
      let el;

      before(() => {
        el = elements.home.querySelector('e');
        el.click();
      });

      it('should call onEvent function', () => {
        assert.equal(component.onEventCount, 1);
      });
    });  

    describe('component attributes manipulation', () => {
      it('should change attribute', () => {
        component.attrs.parent = "Parent value";
        assert.equal(elements.home.getAttribute('parent'), "Parent value", 'check element attribute');
        assert.isUndefined(parentComponent.__bindings.homeAttr, 'check parent binding');
      });

      it('should change attribute with an expression', () => {
        component.attrs.parent = "${ this.newParentBinding }";
        assert.isUndefined(component.attrs.parent, 'check component attribute value');
        assert.isOk(parentComponent.__bindings.newParentBinding, 'check parent binding');
      });

      it('should delete attribute', () => {
        delete component.attrs.parent;
        assert.isNotOk(elements.home.hasAttribute('parent'));
      });
    });

    describe('set nonexistend nested scope value()', () => {
      it('should create parent object for the key', () => {   
        component.scope.__set(['unex', 'key'], 1)    
        assert.isObject(component.scope.unex, 'check an object');
        assert.equal(component.scope.unex.key, 1, 'check the key');
      });
    });

    describe('.__observe()', () => {
      it('should set proxy to the object', () => {
        let obj = {};
        obj = component.__observe(obj, []);
        assert.isOk(obj.__isProxy);
      });
    });

    describe('.__nestedObserve()', () => {
      it('should set nested proxy', () => {
        let obj = {
          x: {
            y: {}
          }
        };

        obj = component.__nestedObserve(obj, []);
        assert.isOk(obj.__isProxy, 'check obj');
        assert.isOk(obj.__isProxy, 'check obj.x');
        assert.isOk(obj.__isProxy, 'check obj.y');
      });
    });

    describe('.__createIsolationObject()', () => {
      let obj;

      before(() => {
        Akili.__isolation = {};
      });

      after(() => {
        Akili.__isolation = null;
      });

      it('should return the correct object', () => {
        obj = component.__createIsolationObject(['data', '0'], 'title');
        assert.equal(obj.component, component, 'check component');
        assert.equal(obj.keys[0], 'data', 'check keys');
      });
    });

    describe('.__bindNode()', () => {
      let list;

      before(() => {
        list = []
      });

      it('should create a correct object', () => {
        let keys = ['data'];
        let parents = [];
        let obj;
        component.__bindNode(list, component, keys, parents, 1);
        obj = list[0];
        assert.equal(obj.component, component, 'check component');
        assert.equal(obj.keysString, Akili.joinBindingKeys(keys), 'check keysString');
        assert.equal(Akili.joinBindingKeys(obj.keys), Akili.joinBindingKeys(keys), 'check keys');
        assert.equal(Akili.joinBindingKeys(obj.parents), Akili.joinBindingKeys(parents), 'check parents');
        assert.equal(obj.value, 1, 'check value');
        assert.equal(obj.notBinding, false, 'check notBinding');
      });

      it('should create only 2 bindings', () => {
        component.__bindNode(list, component, ['data'], [], 1);
        component.__bindNode(list, component,  ['data', '0'], ['data'], 1);
        component.__bindNode(list, component,  ['data', '0', 'title'], ['data', '0'], 1);

        assert.equal(list.length, 2, 'check count');
        assert.equal(list[0].keysString, 'data', 'check content one');
        assert.equal(list[1].keysString, Akili.joinBindingKeys(['data', '0', 'title']), 'check content two');
      });

      it('should create 3 new bindings', () => {
        component.__bindNode(list, component, ['x'], [], 1);
        component.__bindNode(list, component, ['y'], [], 1, true);
        component.__bindNode(list, component,  ['x', '0'], ['x'], 1);
        assert.equal(list.length, 5);
      });
    });

    describe('.__getBind()', () => {
      it('should get a correct binding', () => {
        let obj = component.__getBind(['data']);
        assert.strictEqual(obj, component.__bindings.data);
      });

      it('should get null', () => {
        let obj = component.__getBind(['fakeData']);
        assert.isNull(obj);
      });
    });

    describe('.__getBoundNode()', () => {
      it('should get a binding', () => {
        let obj = component.__getBoundNode(['data'], component.__bindings.data.__data[0].node);
        assert.strictEqual(obj, component.__bindings.data.__data[0]);
      });

      it('should get a binding', () => {
        let obj = component.__getBoundNode(['data'], 'fake');
        assert.isNull(obj);
      });
    });

    describe('bindings manipulation', () => {
      let node;
      let keys;

      before(() => {
        keys = ['new'];
        node = component.__bindings.html.__data[0].node;
      });

      describe('.__bind()', () => {
        it('should create a binding', () => {
          let data = { node }
          component.__bind(keys, data);
          assert.strictEqual(data, component.__bindings.new.__data[0]);
        });
      });

      describe('.__setNodeProperty()', () => {
        it('should set a node property', () => {
          let obj;
          let _keys;
          component.__setNodeProperty(node, keys, 1);
          _keys = Object.keys(node.__properties);
          obj = node.__properties[_keys[_keys.length - 1]];
          assert.equal(obj.component, component, 'check component');
          assert.equal(obj.keys.join('.'), keys.join('.'), 'check keys');
          assert.equal(obj.value, 1, 'check value');
          assert.equal(obj.hash, utils.createHash(obj.value), 'check hash');
        });
      });

      describe('.__getNodeProperty()', () => {
        it('should get a node property', () => {
          let obj = component.__getNodeProperty(node, keys);
          assert.equal(obj.component, component, 'check component');
          assert.equal(obj.keys.join('.'), keys.join('.'), 'check keys');
          assert.equal(obj.value, 1, 'check value');
          assert.equal(obj.hash, utils.createHash(obj.value), 'check hash');
        });
      });

      describe('.__deleteNodeProperty()', () => {
        it('should delete a node property', () => {
          let _keys = Object.keys(node.__properties);
          let prevHash = _keys[_keys.length - 1];
          let prevProperty = node.__properties[prevHash];
          let property;
          component.__deleteNodeProperty(node, keys);
          _keys = Object.keys(node.__properties);
          property = node.__properties[_keys[_keys.length - 1]];
          assert.notStrictEqual(property, prevProperty);
          node.__properties[prevHash] = prevProperty;
        });
      });

      describe('.__unbindByNodes()', () => {
        it('should unbind by nodes', () => {
          const node = component.__bindings.htmlTwo.__data[0].node;
          component.__unbindByNodes([node]);
          assert.isNotOk(component.__bindings.htmlTwo);
        });
      });
    });

    describe('.transition()', () => {
      it('should return null', () => {       
        assert.isNull(component.transition);
      });

      it('should return the right scope transition', () => {      
        component.__scope.__transition = {};
        assert.strictEqual(component.transition, component.__scope.__transition);
        delete component.__scope.__transition;
      });
    });

    describe('.appendTo()', () => {
      it('should change parent element', () => {
        component.appendTo(elements.sectionEmpty);
        assert.strictEqual(component.__parent.__akili.el, elements.sectionEmpty);
        parentComponent = component.__parent.__akili;
      });

      it('should has new evaluation parent', () => {
        assert.strictEqual(component.__evaluationParent, elements.sectionEmpty);
      });

      it('should has new scope proto', () => {
        assert.strictEqual(Object.getPrototypeOf(component.__scope), parentComponent.__scope);
      });

      it('should not change <p> element to undefined', () => {
        assert.equal(elements.home.querySelector('p').innerHTML, component.__scope.parentTwo);
      });

      it('should unbind old parent scope "htmlAttrTwo"', () => {
        assert.isNotOk(elements.app.__akili.__bindings.homeAttrTwo);
      });

      it('should bind new parent scope "htmlAttrTwo"', () => {
        assert.isOk(parentComponent.__bindings.homeAttrTwo);
      });

      it('should increment onEvent function counter', () => {
        elements.home.querySelector('e').click();
        assert.equal(component.onEventCount, 2);
      });
    });

    describe('.parent()', () => {
      it('should return nearest parent', () => {
        assert.strictEqual(component.parent().el, elements.sectionEmpty);
      });

      it('should return by selector', () => {
        assert.strictEqual(component.parent('[scope=root]').el, Akili.__root);
      });

      it('should return null', () => {
        assert.isNull(component.parent('non-existent'));
      });
    });

    describe('.parents()', () => {
      it('should return all parents list', () => {
        let parents = component.parents();
        assert.strictEqual(parents.length, 2, 'check count');
        assert.strictEqual(parents[0].el, elements.sectionEmpty, 'check nearest');
        assert.strictEqual(parents[1].el, Akili.__root, 'check root');
      });

      it('should return only one parent in the list', () => {
        assert.strictEqual(component.parents('[scope=root]')[0].el, Akili.__root);
      });

      it('should return empty array', () => {
        let parents = component.parents('non-existent');
        assert.isOk(Array.isArray && !parents.length);
      });
    });

    describe('.child()', () => {
      it('should return nearest child', () => {
        assert.strictEqual(Akili.root.child().el, elements.app);
      });

      it('should return by selector', () => {
        assert.strictEqual(Akili.root.child('section-one').el, elements.sectionOne);
      });

      it('should return null', () => {
        assert.isNull(component.child('non-existent'));
      });
    });

    describe('.children()', () => {
      it('should return all children list', () => {
        let children = elements.app.__akili.children();
        assert.strictEqual(children.length, 4, 'check count');
        assert.strictEqual(children[0].el, elements.abstract, 'check first');
        assert.strictEqual(children[1].el, elements.sectionOne, 'check second');
        assert.strictEqual(children[2].el, elements.sectionTwo, 'check third');
        assert.strictEqual(children[3].el, elements.sectionOneOne, 'check fourth');
      });

      it('should return only one child in the list', () => {
        assert.strictEqual(elements.app.__akili.children('abstract')[0].el,  elements.abstract);
      });

      it('should return empty array', () => {
        let children = component.children('non-existent');
        assert.isOk(Array.isArray && !children.length);
      });
    });

    describe('.prev()', () => {
      it('should return nearest previous element component', () => {
        assert.strictEqual(elements.sectionTwo.__akili.prev().el, elements.sectionOne);
      });

      it('should return by selector', () => {
        assert.strictEqual(elements.sectionTwo.__akili.prev('abstract').el, elements.abstract);
      });

      it('should return null', () => {
        assert.isNull(component.prev('non-existent'));
      });
    });

    describe('.before()', () => {
      it('should return all element components list from the left', () => {
        let before = elements.sectionTwo.__akili.before();
        assert.strictEqual(before.length, 2, 'check count');
        assert.strictEqual(before[0].el, elements.sectionOne, 'check nearest');
        assert.strictEqual(before[1].el, elements.abstract, 'check the next');
      });

      it('should return only one component in the list', () => {
        assert.strictEqual(elements.sectionTwo.__akili.before('abstract')[0].el, elements.abstract);
      });

      it('should return empty array', () => {
        let before = component.before('non-existent');
        assert.isOk(Array.isArray && !before.length);
      });
    });

    describe('.next()', () => {
      it('should return nearest next element component', () => {
        assert.strictEqual(elements.sectionOne.__akili.next().el, elements.sectionTwo);
      });

      it('should return by selector', () => {
        assert.strictEqual(elements.abstract.__akili.next('section-two').el, elements.sectionTwo);
      });

      it('should return null', () => {
        assert.isNull(component.next('non-existent'));
      });
    });

    describe('.after()', () => {
      it('should return all element components list from the right', () => {
        let after = elements.abstract.__akili.after();
        assert.strictEqual(after.length, 2, 'check count');
        assert.strictEqual(after[0].el, elements.sectionOne, 'check nearest');
        assert.strictEqual(after[1].el, elements.sectionTwo, 'check the next');
      });

      it('should return only one component in the list', () => {
        assert.strictEqual(elements.abstract.__akili.after('section-two')[0].el, elements.sectionTwo);
      });

      it('should return empty array', () => {
        let after = component.after('non-existent');

        assert.isOk(Array.isArray && !after.length);
      });
    });

    describe('attributes manipulation', () => {
      let all, attr, fn;
      class TestAttr extends Akili.Component {
        created() {
          this.checkAttr? this.attr('test', 'test'): this.unattr('test', 'test');     
        }
      } 

      before(() => {
        fn = val => attr.scope.handlerTest = val;
        all = Akili.root.child('all');
        attr = all.child('attr');      
      });

      describe('.attr()', () => {
        it('should throw an error', () => {       
          let testAttr = new TestAttr(document.createElement('test-attr')); 
          testAttr.checkAttr = true;
          assert.throws(() => testAttr.created(), '', 'before the compilation');   
          assert.throws(() => attr.attr('test'));
        });

        it('should create the link by a scope property', () => {  
          attr.attr('test', 'test');
          attr.attr('test', 'test');             
          assert.equal(attr.__attrLinks['test'][0].keyString, 'test');
        });
  
        it('should create the link by a function', () => {  
          attr.attr('test', fn);
          attr.attr('test', fn);         
          assert.strictEqual(attr.__attrLinks['test'][1].fn, fn);
        });

        it('should create the link with all attrs by a function', () => { 
          attr.attr(fn);      
          assert.strictEqual(attr.__attrLinks['*'][0].fn, fn);
          all.scope.cAttr = '*';          
          assert.equal(attr.scope.handlerTest, '*', 'check the value');
        });  
        
        it('should have the necessary scope value', () => {  
          all.scope.cAttr = 'link';             
          assert.equal(attr.scope.test, 'link', 'check by a scope');
          assert.equal(attr.scope.handlerTest, 'link', 'check by a function');
        });
  
        it('should change the parent scope value', () => {  
          attr.scope.test = 'linked';  
          assert.equal(all.scope.cAttrEvent, 'linked');
        });
      });  
    
      describe('.unattr()', () => {
        it('should throw an error', () => {   
          let testAttr = new TestAttr(document.createElement('test-attr')); 
          assert.throws(() => testAttr.created(), '', 'before the compilation');        
          assert.throws(() => attr.unattr('test'));
        });

        it('should remove all links', () => {
          attr.unattr('test', 'test');
          attr.unattr('test', fn);
          attr.unattr(fn);
          assert.doesNotHaveAllKeys(attr.__attrLinks, ['test', '*']);
        });
      }); 
    });

    describe('call the evaluation error', () => {
      it('should be an error', () => {
        assert.throws(() => {
          component.__scope.data = false;
          Akili.compile(component.el, { recompile: true });
        });
      });
    });

    describe('.empty()', () => {
      before(() => {
        component.empty();
      });

      it('should clear html', () => {
        assert.isNotOk(component.el.innerHTML);
      });

      it('should remove bindings', () => {
        assert.isNotOk(Object.keys(component.__bindings).length);
      });
    });

    describe('.remove()', () => {
      let el;

      before(() => {
        el = component.el;
        component.__storeByFunction('store', () => {});
        component.remove();
      });

      it('should call removed function', () => {
        assert.isOk(component.wasRemoved);
      });

      it('should return true', () => {
        assert.isTrue(component.isRemoved);
      });

      it('should be deleted from the parent element', () => {
        assert.isNotOk(parentComponent.el.querySelector('home'));
      });

      it('should remove parent bindings', () => {
        assert.isNotOk(Object.keys(parentComponent.__bindings).length);
      });

      it('should remove all store links', () => {
        assert.doesNotHaveAllKeys(Akili.__storeLinks, ['store']);
      });

      it('should remove all children', () => {
        assert.isEmpty(el.innerHTML);
      });
    });
    
    
    describe('.cancel()', () => {
      it('should not be compiled', () => { 
        assert.isNull(Akili.root.child('cancel'));
      });

      it('should not be compiled because of the ssr options', () => { 
        assert.isNull(Akili.root.child('ssr'));
      });
    });  

    describe('.prevent()', () => {
      let prevent, all;

      before(() => {
        all = Akili.root.child('all');
        prevent = all.child('prevent');      
      });

      it('should prevent children compilation', () => {       
        assert.lengthOf(prevent.children(), 0);
      });
    });
  });
});

<|start_filename|>src/components/text.js<|end_filename|>
import Component from '../component.js';
import utils from '../utils.js';

/**
 * Base component to work with text elements.
 * 
 * {@link https://akilijs.com/docs/components#docs_input}
 * {@link https://akilijs.com/docs/components#docs_textarea}
 * {@link https://akilijs.com/docs/components#docs_contenteditable}
 * 
 * @attr {string} value - actual value
 * @attr {boolean} focus - set the focus or not
 * @attr {number} debounce - debounce delay
 * @message {void} debounce - sent on the debounce {@link https://akilijs.com/docs/components#docs_input}
 */
export default class Text extends Component {
  constructor(...args) {
    super(...args);

    this.valueKey = 'value';
    this.debounceInterval = 500;
  }

  created() {
    if(this.el.hasAttribute('on-debounce')) {
      this.el.addEventListener('input', utils.debounce(() => {
        this.attrs.onDebounce.trigger(undefined, { bubbles: true });
      }, this.debounceInterval));
    }
  }

  compiled() {
    this.attr('focus', this.setFocus);
    this.attr('value', this.setValue);
    this.attr('debounce', this.setDebounce);
  }

  setDebounce(interval) {
    this.debounceInterval = +interval;
  }

  setFocus(value) {
    value? this.setElementFocus(): this.setElementBlur();
  }

  setValue(value) {
    (this.el[this.valueKey] !== value) && (this.el[this.valueKey] = value === 0? '0': (value || ''));
  }

  setElementFocus() {
    return this.setElementFocusOn(this.getElementTextLength());
  }

  setElementFocusOn(pos) {
    if(document.activeElement !== this.el) {
      this.el.focus();
    }  

    this.el.selectionStart = this.el.selectionEnd = pos;
  }

  setElementBlur() {
    this.el.blur();
  }

  getElementTextLength() {
    return this.el.value.length;
  }
}

<|start_filename|>src/components/url.js<|end_filename|>
import Component from '../component.js';

/**
 * Component for elements which have async attribute content.
 * 
 * {@link https://akilijs.com/docs/components#docs_image,_iframe,_embed,_audio,_video,_track,_source,_object}
 * 
 * @tag url
 * @selector [url],[urlset]
 * @attr {string} [url] - source link
 * @attr {string} [urlset] - source link set
 * @attr {boolean} [hidden-error] - hide the failed request picture
 * @attr {string} [loading] - loading type: "lazy", "viewport"
 */
export default class Url extends Component {
  static matches = '[url],[urlset]';
  static booleanAttributes = ['hidden-error'];

  constructor(...args) {
    super(...args);
    this.urlAttribute = 'src';
    this.handlerAttribute = this.el.hasAttribute('urlset')? 'urlset': 'url';
    this.elOpacity = getComputedStyle(this.el).opacity;
  }

  created() {
    this.viewport = false;
    this.cancelable = false;
    this.observer = null;
    this.finished = false;
    this.cancelling = false;
    this.onLoadListener = this.onLoad.bind(this);
    this.onErrorListener = this.onError.bind(this);
    this.el.addEventListener('load', this.onLoadListener);   
    this.el.addEventListener('error', this.onErrorListener);
    this.createUrlAttribute();
  }

  compiled() {
    this.attr('loading', this.setLoading);
    this.attr(this.handlerAttribute, this.setUrl);
  }

  removed() {
    this.observer && this.observer.disconnect();
  }

  createUrlAttribute() {
    if(this.handlerAttribute == 'urlset') {
      this.urlAttribute + 'set';
    }
  }

  onLoad() {
    if(this.isRemoved) {
      return;
    }

    this.attrs.hiddenError && (this.el.style.opacity = this.elOpacity);
    this.finished = true;
    this.cancelling = false;
  }

  onError() {
    if(this.isRemoved) {
      return;
    }
    
    this.attrs.hiddenError && (this.el.style.opacity = 0);
    !this.cancelling && (this.finished = true);
  }

  intersectionHandler(entries) {
    const entry = entries[0];
    this.isIntersecting = entry.isIntersecting;

    if(!entry.isIntersecting && !this.finished) {
      this.cancelling = true;
      this.attrs[this.urlAttribute] = '';
      return;
    }

    if(!this.finished) {
      this.cancelling = false;
      this.attrs[this.urlAttribute] = this.attrs[this.handlerAttribute];
    }
  }

  setLoading(value) {
    this.viewport = value == 'viewport';
    
    if(!this.viewport) {
      this.observer && this.observer.disconnect();
      delete this.isIntersecting;
      return;
    }

    this.observer = new IntersectionObserver(this.intersectionHandler.bind(this));
    this.observer.observe(this.el);
  }

  setUrl(url) {
    this.finished = false;
    this.cancelling = false;
    
    if(this.viewport && !this.isIntersecting) {
      return;
    }

    this.attrs[this.urlAttribute] = url;
  }
}

<|start_filename|>test/main.js<|end_filename|>
import './app/index.js';
import './services/request.js';
import './services/router.js';
import './services/store.js';
import './utils.js';
import './akili.js';
import './event-emitter.js';
import './component.js';
import './components.js';
import './components/a.js';
import './components/include.js';
import './app/end.js';

<|start_filename|>src/akili.js<|end_filename|>
/**
 * Javascript framework
 *
 * const Akili = makeItEasy(js + html);
 *
 * @author <NAME> <<EMAIL>>
 * @link http://akilijs.com
 */

import Component from './component.js';
import If from './components/if.js';
import For from './components/for.js';
import Select from './components/select.js';
import Input from './components/input.js';
import Radio from './components/radio.js';
import Text from './components/text.js';
import Textarea from './components/textarea.js';
import Content from './components/content.js';
import Include from './components/include.js';
import Iframe from './components/iframe.js';
import Image from './components/image.js';
import Embed from './components/embed.js';
import Audio from './components/audio.js';
import Video from './components/video.js';
import Track from './components/track.js';
import Source from './components/source.js';
import Objects from './components/object.js';
import Route from './components/route.js';
import Url from './components/url.js';
import A from './components/a.js';
import Script from './components/script.js';
import Style from './components/style.js';
import Scope from './scope.js';
import EventEmitter from './event-emitter.js';
import request from './services/request.js';
import router from './services/router.js';
import store from './services/store.js';
import utils from './utils.js';
import globals from './globals.js';

/**
 * The framework main object
 * 
 * {@link https://akilijs.com/docs/getting-started}
 */
const Akili = {};

Akili.options = {
  debug: true
};

Akili.__init = null;
Akili.__root = null;
Akili.__components = {};
Akili.__aliases = {};
Akili.__scopes = {};
Akili.__storeLinks = {};
Akili.__window = {};
Akili.__tags = {};
Akili.__isolation = null;
Akili.__evaluation = null;
Akili.__wrapping = false;
Akili.__disableStoreProxy = false;
Akili.__rootOuterHTML = '';
Akili.__onError = () => Akili.triggerInit(false);
Akili.htmlBooleanAttributes = [ 'disabled', 'contenteditable', 'hidden' ];
Akili.components = {};
Akili.services = {};
Akili.Component = Component;
Akili.EventEmitter = EventEmitter;
Akili.Scope = Scope;
Akili.utils = utils;
Akili.globals = globals;
Akili.components.A = A;
Akili.components.Audio = Audio;
Akili.components.Content = Content;
Akili.components.For = For;
Akili.components.Embed = Embed;
Akili.components.If = If;
Akili.components.Include = Include;
Akili.components.Input = Input;
Akili.components.Iframe = Iframe;
Akili.components.Image = Image;
Akili.components.Object = Objects;
Akili.components.Radio = Radio;
Akili.components.Route = Route;
Akili.components.Select = Select;
Akili.components.Source = Source;
Akili.components.Text = Text;
Akili.components.Textarea = Textarea;
Akili.components.Track = Track;
Akili.components.Url = Url;
Akili.components.Video = Video;
Akili.components.Script = Script;
Akili.components.Style = Style;
Akili.services.request = request;
Akili.services.router = router;
Akili.services.store = store;

/**
 * Define the default components
 */
Akili.define = function () {
  A.define();
  Audio.define();
  Content.define();
  Component.define();
  Embed.define();
  For.define();
  Include.define();
  Iframe.define();
  Image.define();
  Input.define();
  If.define();
  Objects.define();
  Radio.define();
  Route.define();
  Select.define();
  Source.define();
  Textarea.define();
  Track.define();
  Video.define();
  Script.define();
  Style.define();
};

/**
 * Clear the global context
 */
Akili.clearGlobals = function () {
  for(let key in globals) {
    delete globals[key];
  }

  for (let key in this.__window.Element.prototype) {
    Element.prototype[key] = this.__window.Element.prototype[key];
  }

  for (let key in this.__window.Array.prototype) {
    Array.prototype[key] = this.__window.Array.prototype[key];
  }

  for(let key in this.options.globals) {
    this.options.globals[key] = this.unwrap(this.options.globals[key]);
  }

  window.setTimeout = this.__window.setTimeout;
  window.setInterval = this.__window.setInterval;
  window.Promise = this.__window.Promise;
  window.removeEventListener('error', this.__onError);
};

/**
 * Join the binding keys
 *
 * @param {string[]} keys binding keys
 */
Akili.joinBindingKeys = function (keys) {
  return keys.map(el => el.toString()).join('.');
};

/**
 * Add the scope
 *
 * @param scope
 */
Akili.addScope = function (scope) {
  if (this.__scopes[scope.__name]) {
    throw new Error(`Scope name ${scope.__name} already exists`);
  }

  this.__scopes[scope.__name] = scope;
};

/**
 * Get the scope
 *
 * @param {string} name - scope name
 * @returns {Scope}
 */
Akili.getScope = function (name) {
  return this.__scopes[name];
};

/**
 * Remove the scope
 *
 * @param {string} name - scope name
 */
Akili.removeScope = function (name) {
  delete this.__scopes[name];
};

/**
 * Get all elements that is wrapped in the Akili component
 *
 * @param {Element} el
 * @param {boolean} [tree=true] - return array of the parents if true, closest parent if false
 * @returns {Array|Element|null}
 */
Akili.getAkiliParents = function (el, tree = true) {
  let arr = [];

  function check(node) {
    if (!node.parentNode) {
      return;
    }

    if (node.parentNode.__akili) {
      arr.push(node.parentNode);

      if (!tree) {
        return;
      }
    }

    check(node.parentNode);
  }

  check(el);
  return tree? arr: arr[0];
};

/**
 * Create the template
 *
 * @example
 * // returns "<i>Hello</i><b>World</b>"
 * el.innerHTML = "<b>World</b>";
 * Akili.setTemplate(el, "<i>Hello</i>${this.__content}");
 *
 * @param {Element} el
 * @param {string} template
 * @returns {string}
 */
Akili.setTemplate = function (el, template) {
  template = template.replace(/\${(((?!\${)\s*this\.__content\s*)*)}/, el.innerHTML);
  el.innerHTML = template;
  return el.innerHTML;
};

/**
 * Generate the unique scope name
 *
 * @returns {string}
 */
Akili.createScopeName = function () {
  return utils.createRandomString(16, str => !!this.__scopes[str]);
};

/**
 * Isolate the scope changes
 *
 * @param {function} fn
 * @returns {*}
 */
Akili.isolate = function (fn) { 
  let isolation = this.__isolation;
  !isolation && (this.__isolation = {});
  let res;

  try {
    res = fn();

    if(isolation) {
      return res;
    }
  }
  catch(err) {
    this.__isolation = null;
    throw err;
  }
  
  let props = [];

  for (let k in this.__isolation) {
    props.push(this.__isolation[k]);
  }

  this.__isolation = null;

  for (let i = 0, l = props.length; i < l; i++) {    
    const prop = props[i];

    if(!prop.component) {
      const value = utils.copy(utils.getPropertyByKeys(prop.rootKeys, store.__target), { plain: true });
      this.root && this.root.__storeTriggerByName(prop.rootKey, value);
      continue;
    }

    if(prop.component.__isRemoved) {
      continue;
    }
    
    prop.component.scope[prop.rootKey] = utils.getPropertyByKeys(prop.rootKeys, prop.component.__scope);
  }

  return res;
};

/**
 * Stop the evaluation inside the function
 *
 * @param {function} fn
 * @returns {*}
 */
Akili.unevaluate = function (fn) {
  let evaluation = this.__evaluation;  
  this.__evaluation = null;
  let res;

  try {
    res = fn();
  }
  catch(err) {
    this.__evaluation = evaluation;
    throw err;
  }

  this.__evaluation = evaluation; 
  return res;
};

/**
 * Evaluate only the root properties
 *
 * @param {function} fn
 * @returns {*}
 */
Akili.wrapping = function (fn) {
  let wrapping = this.__wrapping;
  !wrapping && (this.__wrapping = true);
  let res;

  try {
    res = this.isolate(fn);

    if(wrapping) {
      return res;
    }
  }
  catch(err) {
    this.__wrapping = false;
    throw err;
  }

  this.__wrapping = false; 
  return res;   
}

/**
 * Stop the isolation inside the function
 *
 * @param {function} fn
 * @returns {*}
 */
Akili.unisolate = function (fn) {
  let isolation = this.__isolation;
  this.__isolation = null;
  let res;

  try {
    res = fn();
  }
  catch(err) {
    this.__isolation = isolation;
    throw err;
  }

  this.__isolation = isolation;
  return res;
};

/**
 * Initialize the element
 *
 * @param {Element} el
 * @param {object} [options={}]
 * @returns {*}
 */
Akili.initialize = function (el, options = {}) {
  let recompile = options.recompile;
  let component = el.__akili;

  if (component) {
    if (recompile) {
      component.__recompile(recompile === true? {}: recompile);
      return component;
    }

    return;
  }

  let isRoot = el === this.__root;
  let componentName = utils.toDashCase(el.getAttribute('component') || el.tagName.toLowerCase());
  let _Component = this.__components[componentName];

  CHECK_ALIASES: if (!_Component) {
    let selectors = Object.keys(this.__aliases);

    if (!selectors.length) {
      break CHECK_ALIASES;
    }

    let selectorAll = selectors.join(',');

    if (!el.matches(selectorAll)) {
      break CHECK_ALIASES;
    }

    for (let i = 0, l = selectors.length; i < l; i++) {
      let selector = selectors[i];

      if (el.matches(selector)) {
        _Component = this.__components[this.__aliases[selector]];
        break;
      }
    }
  }

  if (!_Component && !isRoot) {
    return;
  }

  if (!_Component) {
    _Component = this.Component;
  }

  if (_Component.matches && !el.matches(_Component.matches)) {
    return;
  }

  component = new _Component(el, {});  
  
  if (component.__cancelled) {
    return;
  }

  if(Akili.getAkiliParents(el).find(p => p.__akili.__prevent)) {
    return;
  }

  component.__create();
  return component;
};

/**
 * Compile the element
 *
 * @param {Element} root
 * @param {object} [options]
 * @returns {Promise}
 */
Akili.compile = function (root, options = { recompile: false }) {  
  let elements = [];

  if(window.AKILI_SSR) {
    const arr = root.querySelectorAll('[no-ssr]');

    for(let i = 0, l = arr.length; i < l; i++) {
      arr[i].remove();
    }
  }

  const nestedInitializing = el => {
    let component = this.initialize(el, options);
    let children = el.children;
    component && elements.push(component);

    for (let i = 0, l = children.length; i < l; i++) {
      let child = children[i];
      nestedInitializing(child);
    }
  };
  
  nestedInitializing(root);
  let p = [];

  for (let i = 0, l = elements.length; i < l; i++) {
    let component = elements[i];    
    p.push(component.__compile());    
  }

  return Promise.all(p).then(() => {
    let r = [];

    for (let i = elements.length - 1; i >= 0; i--) {
      let component = elements[i];
      r.push(component.__resolve());
    }

    return Promise.all(r);
  });
};

/**
 * Register the component.
 * Or get it if the function is not passed
 *
 * @param {string} name
 * @param {Component} [fn]
 */
Akili.component = function (name, fn) {
  name = name.toLowerCase();

  if (!fn) {
    return this.__components[name] || null;
  }
  
  if (this.__components[name] && Akili.options.debug) {  
    // eslint-disable-next-line no-console
    console.warn(`Component ${name} already was added`);
  }

  this.__components[name] = fn;
};

/**
 * Remove the component
 *
 * @param {string} name
 */
Akili.removeComponent = function (name) {
  delete this.__components[name];
};

/**
 * Register the selector alias.
 * Or get it if the component name is not passed
 *
 * @param {string} selector - DOM selector
 * @param {string} [componentName]
 */
Akili.alias = function (selector, componentName = '') {
  componentName = componentName.toLowerCase();

  if (!componentName) {
    return this.__aliases[selector] || null;
  }

  if (this.__aliases[selector] && Akili.options.debug) {
    // eslint-disable-next-line no-console
    console.warn(`Alias with selector ${selector} already was added`);
  }

  this.__aliases[selector] = componentName;
};

/**
 * Remove the selector alias
 *
 * @param {string} selector
 */
Akili.removeAlias = function (selector) {
  delete this.__aliases[selector];
};

/**
 * Isolate the array prototype functions
 */
Akili.isolateArrayPrototype = function () {
  this.__window.Array = { prototype: {} };
  let keys = Object.getOwnPropertyNames(Array.prototype);

  for (let i = 0, l = keys.length; i < l; i++) {
    let key = keys[i];
    let old = Array.prototype[key];

    if (typeof old != 'function' || key == 'constructor') {
      continue;
    }

    this.__window.Array.prototype[key] = old;

    Array.prototype[key] = function () {
      return Akili.unevaluate(() => {
        if (!this.__isProxy) {
          return old.apply(this, arguments);
        }
        
        return Akili.isolate(() => old.apply(this, arguments));
      });
    };
  }
};

/**
 * Isolate the window functions
 */
Akili.isolateWindowFunctions = function () {
  this.__window.setTimeout = setTimeout;
  this.__window.setInterval = setInterval;
  this.__window.Promise = window.Promise;
  window.setTimeout = this.createCallbackIsolation(window.setTimeout, 0);
  window.setInterval = this.createCallbackIsolation(window.setInterval, 0);
  
  if(!window.AKILI_SSR) {
    window.Promise.constructor = this.createCallbackIsolation(window.Promise.constructor);
    window.Promise.prototype.then = this.createCallbackIsolation(window.Promise.prototype.then, [0, 'last']);
    window.Promise.prototype.catch = this.createCallbackIsolation(window.Promise.prototype.catch);
  }  
};

/**
 * Isolate the globals
 */
Akili.isolateGlobals = function () {  
  globals.__target.utils = this.wrap(utils); 
  this.isolateEvents();
  this.isolateArrayPrototype();
  this.isolateWindowFunctions();
};

/**
 * Isolate the event listeners
 */
Akili.isolateEvents = function () {
  this.__window.Element = { prototype: {} };

  if(window.AKILI_SSR) {
    return;
  }
  
  this.__window.Element.prototype.addEventListener = Element.prototype.addEventListener;
  this.__window.Element.prototype.removeEventListener = Element.prototype.removeEventListener;
  this.__window.Element.prototype.remove = Element.prototype.remove;

  Element.prototype.remove = function () {
    delete this.__akiliListeners;
    return Akili.__window.Element.prototype.remove.apply(this, arguments);
  };

  Element.prototype.addEventListener = function (name, fn) {
    let args = [].slice.call(arguments);

    if (!this.__akiliListeners) {
      this.__akiliListeners = {};
    }

    if (!this.__akiliListeners[name]) {
      this.__akiliListeners[name] = [];
    }
    
    if(typeof fn === 'function') {
      args[1] = function () {
        return Akili.isolate(() => fn.apply(this, arguments));
      };
    }    

    this.__akiliListeners[name].push({
      link: fn,
      fn: args[1]
    });

    return Akili.__window.Element.prototype.addEventListener.apply(this, args);
  };

  Element.prototype.removeEventListener = function (name, fn) {
    let args = [].slice.call(arguments);

    if (!this.__akiliListeners) {
      this.__akiliListeners = {};
    }

    if (!this.__akiliListeners[name]) {
      this.__akiliListeners[name] = [];
    }

    for (let i = 0, l = this.__akiliListeners[name].length; i < l; i++) {
      let listener = this.__akiliListeners[name][i];

      if (listener.link === fn) {
        this.__akiliListeners[name].splice(i, 1);
        args[1] = listener.fn;
        i--;
        l--;
        break;
      }
    }

    if (!this.__akiliListeners[name].length) {
      delete this.__akiliListeners[name];
    }

    return Akili.__window.Element.prototype.removeEventListener.apply(this, args);
  };
};

/**
 * Wrap the function callback with the isolation context
 *
 * @param {function} fn
 * @param {number|string|number[]|string[]} [pos="last"]
 * @returns {function}
 */
Akili.createCallbackIsolation = function (fn, pos = 'last') {
  return function () {
    let args = [].slice.call(arguments);
    !Array.isArray(pos) && (pos = [pos]);
    
    for(let i = 0, l = pos.length; i < l; i++) {
      let num = pos[i];
      let index = num;
      let callback = args[num];

      if(num == 'last') {
        index = args.length - 1;
        callback = args[index];
      }

      if(typeof callback != 'function') {
        continue;
      }

      if(!callback.__isolated) {
        args[index] = function () {
          return Akili.isolate(() => callback.apply(callback, arguments));
        };

        Object.defineProperty(args[index], '__isolated', {
          enumerable: false,
          value: callback
        });
      }
    }

    return fn.apply(this, args);
  };
};

/**
 * Wrap the object/function to isolate and unevaluate data
 *
 * @param {object|function} obj
 * @param {object} [options] 
 */
Akili.wrap = function (obj, options = {}) {
  let current = obj;

  if(typeof obj == 'function') {
    obj = this.wrapFunction(obj, options);

    if(obj === current) {
      return obj;
    }
  }
  else if(!obj || typeof obj != 'object' || Array.isArray(obj)) {
    return obj;
  }

  let keys = Object.getOwnPropertyNames(obj);

  for(let k = 0, c = keys.length; k < c; k++) {
    let key = keys[k];
    let descriptor = Object.getOwnPropertyDescriptor(obj, key);

    if(!descriptor.configurable || !descriptor.writable) {
      continue;
    }

    if(options.reverse) {
      Object.defineProperty(obj, key, {...descriptor, value: obj[key].__akili || obj[key] });
      continue;
    }
    
    Object.defineProperty(obj, key, {...descriptor, value: this.wrap(obj[key], options) });
  } 
  
  return obj;
};

/**
 * Unwrap the object/function
 *
 * @param {object|function} obj
 */
Akili.unwrap = function (obj) {
  return this.wrap(obj, { reverse: true });
}

/**
 * Isolate the function
 *
 * @param {function} fn
 * @param {object} [options] 
 * @returns {function}
 */
Akili.wrapFunction = function (fn, options = {}) {
  if (fn.__akili) {
    return fn;
  }

  const akiliWrappedFunction = function () {
    if(options.tag && Akili.__evaluation) {
      Akili.addTag(options.tag, Akili.__evaluation.node);
    }

    return Akili.wrapping(() => fn.apply(this, arguments));
  };

  let keys = Object.keys(fn);
  akiliWrappedFunction.prototype = fn.prototype;

  for(let i = 0, l = keys.length; i < l; i++) {
    let key = keys[i];
    akiliWrappedFunction[key] = fn[key];
  }

  Object.defineProperty(akiliWrappedFunction, '__akili', {
    enumerable: false,
    value: fn
  });

  return akiliWrappedFunction;
};

/**
 * Add the tag
 * 
 * @param {string} tag
 * @param {Node} node
 */
Akili.addTag = function (tag, node) {
  if(this.hasTag(tag, node)) {
    return;
  }

  if(!this.__tags[node.__name]) {
    this.__tags[node.__name] = {};    
  }

  if(!this.__tags[node.__name][tag]) {
    this.__tags[node.__name][tag] = [];    
  }

  this.__tags[node.__name][tag].push({ node });
};

/**
 * Get the tag
 * 
 * @param {string} tag
 * @param {Node} [node]
 * @returns {array|null}
 */
Akili.getTag = function(tag, node) {
  if(!node){
    for(let key in this.__tags) {
      for(let k in this.__tags[key]) {
        if(k == tag) {         
          return this.__tags[key][k];
        }
      }
    }

    return null;
  }

  if(this.__tags[node.__name] && this.__tags[node.__name][tag]) {
    return this.__tags[node.__name][tag];    
  }

  return null;
};

/**
 * Check the tag exists
 * 
 * @param {string} tag
 * @param {Node} [node]
 * @returns {boolean}
 */
Akili.hasTag = function(tag, node) {
  return !!this.getTag(tag, node);
};

/**
 * Remove the tag
 * 
 * @param {string} [tag]
 * @param {Node} [node]
 */
Akili.removeTag = function (tag, node) {
  if(typeof tag == 'object') {
    node = tag;
    tag = undefined;
  }

  if(!tag) { 
    delete this.__tags[node.__name];
    return;
  }

  if(!node) {
    for(let key in this.__tags) {
      for(let k in this.__tags[key]) {
        if(k == tag) {
          delete this.__tags[key][k]
        }
      }
  
      if(!Object.keys(this.__tags[key]).length) {
        delete this.__tags[key];
      }
    }

    return;    
  }

  for(let key in this.__tags[node.__name]) {
    if(key == tag) {
      delete this.__tags[node.__name][key];
    }
  }

  if(!Object.keys(this.__tags[node.__name]).length) {
    delete this.__tags[node.__name];
  }
};

/**
 * Evaluate the tag expressions
 * 
 * @param {string} tag
 */
Akili.triggerTag = function (tag) {
  for(let key in this.__tags) {
    for(let k in this.__tags[key]) {
      if(k == tag) {
        const arr = this.__tags[key][k];

        for(let i = 0, l = arr.length; i < l; i++) {
          const obj = arr[i];
          obj.node.__component.__evaluateNode(obj.node, false);
        }
      }
    }
  }
};

/**
 * Handle the errors
 */
Akili.handleErrors = function () {
  window.addEventListener('error', this.__onError);
};

/**
 * Trigger the initialization status
 *
 * @param {boolean} status
 */
Akili.triggerInit = function (status) {
  Akili.__init = status;
  window.dispatchEvent(new CustomEvent('akili-init', { detail: status }));
};

/**
 * Initialize the application
 *
 * @param {Element} [root]
 * @returns {Promise}
 */
Akili.init = function (root) { 
  root = root || document.body;
  this.__rootOuterHTML = root.outerHTML;

  if(!(root instanceof Element)) {
    throw new Error(`Root element must be an html element`);
  }

  if(root === document.documentElement) {
    throw new Error(`"html" can't be the root element`);
  }

  this.__root = root;
  
  if(window.AKILI_SERVER) {    
    Akili.initServerSideHtml(window.AKILI_SERVER.html);
    Akili.initServerSideRequestCache(window.AKILI_SERVER.requestCache);
  }
  else {
    window.AKILI_CLIENT = {
      html: this.prepareServerSideHtml()
    }
  }
  
  return this.compile(this.__root).then(() => {
    if (router.__init) {
      return router.changeState();
    }
  }).then(() => {    
    window.AKILI_CLIENT && (window.AKILI_CLIENT.requestCache = this.prepareServerSideRequestCache());
    this.triggerInit(true);
  }).catch(err => {
    this.triggerInit(false);
    throw err;
  });
};

/**
 * Deinitialize the application
 */
Akili.deinit = function () { 
  this.__root && (this.__root.outerHTML = this.__rootOuterHTML);

  for(let key in this.__scopes) {
    const component = this.__scopes[key].__component;
    component && component.remove();
  }

  this.__rootOuterHTML = '';
  this.__init = null;
  this.__root = null;
  this.__scopes = {};
  this.__storeLinks = {};
  this.__tags = {};
};

/**
 * Destroy the framework
 */
Akili.destroy = function () {
  this.deinit();  
  this.clearGlobals();
  let storeKeys = Object.keys(store.__target);
  
  for(let i = 0, l = storeKeys.length; i < l; i++) {
    delete store.__target[storeKeys[i]];
  }

  router.__init && router.deinit();
  delete window.AKILI_SERVER;
  delete window.AKILI_CLIENT;
  delete window.AKILI_SSR;

  for(let key in Akili) {
    delete Akili[key];
  }

  delete window.Akili;
}

/**
 * Initialize the SSR html
 * 
 * @param {string} html
 */
Akili.initServerSideHtml = function (html) {
  for (let i = this.__root.attributes.length - 1; i >= 0; i--){
    this.__root.removeAttribute(this.__root.attributes[i].name);
  }

  let parser = new DOMParser();
  let doc = parser.parseFromString(html, "text/html");
  let el = doc.querySelector(this.__root === document.body? 'body': 'body > *');
  this.__root.innerHTML = el.innerHTML;

  for (let i = el.attributes.length - 1; i >= 0; i--) {
    let attr = el.attributes[i];
    this.__root.setAttribute(attr.name, attr.value);
  }  
}

/**
 * Initialize the SSR cache
 * 
 * @param {object} obj
 */
Akili.initServerSideRequestCache = function (obj) {
  const init = (instance, obj) => {
    for(let key in obj) {
      instance.__cache[key] = obj[key];
    }
  }
  
  for (let key in obj) {
    let instance = key === '__main'? request: request.__instances[key];
    init(instance, obj[key]);
  }
}

/**
 * Prepare the SSR html
 */
Akili.prepareServerSideHtml = function () {
  return this.__root.outerHTML;
}

/**
 * Prepare the SSR cache
 */
Akili.prepareServerSideRequestCache = function () {
  let cache = { __main: request.__cache };

  for(let key in request.__instances) {
    cache[key] = request.__instances[key].__cache;
  }

  return cache;
}

export default Akili;
window.Akili = Akili;
Akili.define();
Akili.handleErrors();
Akili.isolateGlobals();

<|start_filename|>test/utils.js<|end_filename|>
import { assert } from 'chai';
import elements from './app/elements.js';
import utils from '../src/utils.js';
import Akili from '../src/akili.js';
import store from '../src/services/store.js';

describe('utils.js', () => {
  describe('utils', () => {
    describe('.isScopeProxy()', () => {
      it('should return true', () => {
        assert.isOk(utils.isScopeProxy(elements.app.__akili.__scope.proxy));
      });

      it('should return false', () => {
        assert.isNotOk(utils.isScopeProxy([]), 'check array');
        assert.isNotOk(utils.isScopeProxy({}), 'check object');
        assert.isNotOk(utils.isScopeProxy(() => {}), 'check function');
        assert.isNotOk(utils.isScopeProxy(1), 'check number');
        assert.isNotOk(utils.isScopeProxy("1"), 'check string');
        assert.isNotOk(utils.isScopeProxy(true), 'check boolean');
      });
    });

    describe('.isStoreProxy()', () => {
      it('should return true', () => {
        assert.isOk(utils.isStoreProxy(store));
      });

      it('should return false', () => {
        assert.isNotOk(utils.isStoreProxy([]), 'check array');
        assert.isNotOk(utils.isStoreProxy({}), 'check object');
        assert.isNotOk(utils.isStoreProxy(() => {}), 'check function');
        assert.isNotOk(utils.isStoreProxy(1), 'check number');
        assert.isNotOk(utils.isStoreProxy("1"), 'check string');
        assert.isNotOk(utils.isStoreProxy(true), 'check boolean');
      });
    });

    describe('.isPlainObject()', () => {
      it('should return true', () => {
        assert.isOk(utils.isPlainObject({}));
        assert.isOk(utils.isPlainObject([]));
      });

      it('should return false', () => {
        assert.isNotOk(utils.isPlainObject(() => {}), 'check function');
        assert.isNotOk(utils.isPlainObject(utils.Component), 'check Component');
        assert.isNotOk(utils.isPlainObject(1), 'check number');
        assert.isNotOk(utils.isPlainObject("1"), 'check string');
        assert.isNotOk(utils.isPlainObject(true), 'check boolean');
      });
    });

    describe('.split()', () => {
      let opts = [
        { val: 'string', res: ["s", "t", "r", "i", "n", "g"] },
        { val: 'Hello World', res: ["Hello", "World"], del: /\s+/ },
        { val: 'Hello World', res: ["Hello", "World"], del: ' ' },
        { 
          val: `x = 5; y = "1;2;3"; z = '4;5;6'`, 
          res: [`x = 5`, ` y = "1;2;3"`, ` z = '4;5;6'`], 
          del: ';', 
          exclude: ["'", '"'] 
        }
      ];
    
      opts.forEach(opt => {
        assert.equal(JSON.stringify(utils.split(opt.val, opt.del, opt.exclude)), JSON.stringify(opt.res))
      });
    });

    describe('.copy()', () => {
      it('should be equal but difference', () => {
        let obj = { key: 1, obj: utils.Component };
        let arr = ['1', '2'];
        assert.equal(JSON.stringify(utils.copy(obj)), JSON.stringify(obj), 'check object');
        assert.equal(JSON.stringify(utils.copy(arr)), JSON.stringify(arr), 'check array');
        assert.equal(utils.copy(1), 1, 'check primitive');
        assert.notStrictEqual(utils.copy(obj), obj, 'check difference');
      });

      it('should copy only root level', () => {
        let obj = { key: 1, obj: {} };
        assert.strictEqual(utils.copy(obj, { nested: false }).obj, obj.obj);
      });

      it('should not copy non-enumerable values', () => {
        let obj = { key: 1 };
        Object.defineProperty(obj, 'nonen', { enumerable: false, value: true });
        let copy = utils.copy(obj);
        assert.lengthOf(Object.getOwnPropertyNames(copy), 1);
      });

      it('should not change non-plain object', () => {
        let obj = { x: new Error('test') };
        let copy = utils.copy(obj, { plain: true });
        assert.instanceOf(copy.x, Error);
      });

      it('should copy non-enumerable values', () => {
        let obj = { key: 1 };
        Object.defineProperty(obj, 'nonen', { enumerable: false, value: true });
        let copy = utils.copy(obj, { enumerable: false });
        assert.lengthOf(Object.getOwnPropertyNames(copy), 2);
      });
    });

    describe('.includeKeys()', () => {
      it('should include the specified keys', () => {
        let obj = {x: 1, y: 1, z: 1};
        let res = {x: 1, y: 1};
        assert.equal(JSON.stringify(utils.includeKeys(obj, ['x', 'y'])), JSON.stringify(res));
      });
    });

    describe('.excludeKeys()', () => {
      it('should exclude the specified keys', () => {
        let obj = {x: 1, y: 1, z: 1};
        let res = {x: 1, z: 1};
        assert.equal(JSON.stringify(utils.excludeKeys(obj, ['y'])), JSON.stringify(res));
      });
    });

    describe('.makeAttributeValue()', () => {
      it('should be appropriate', () => {
        assert.equal(utils.makeAttributeValue([]), '[object Array]',  'check array');
        assert.equal(utils.makeAttributeValue({}), '[object Object]',  'check object');
        assert.equal(utils.makeAttributeValue(() => {}), '[object Function]',  'check function');
        assert.equal(utils.makeAttributeValue(1), '1', 'check number');
        assert.equal(utils.makeAttributeValue('1'), '1', 'check string');
        assert.equal(utils.makeAttributeValue(true), 'true', 'check boolean');
        assert.equal(utils.makeAttributeValue(null), '', 'check null');
        assert.equal(utils.makeAttributeValue(undefined), '', 'check undefined');
      });
    });

    describe('.compare()', () => {
      it('should be equal', () => {
        assert.isOk(utils.compare(1, 1), 'check number');
        assert.isOk(utils.compare('1', '1'), 'check string');
        assert.isOk(utils.compare(undefined, undefined), 'check undefined');
        assert.isOk(utils.compare(null, null), 'check null');
        // eslint-disable-next-line no-unused-vars
        assert.isOk(utils.compare(function x(a) {}, function x(a) {}), 'check function');
        assert.isOk(utils.compare(new Date(), new Date()), 'check date');
        assert.isOk(utils.compare(['1', 2, null], ['1', 2, null]), 'check array');
        assert.isOk(utils.compare({x: 1, y: 2}, {y: 2, x: 1}), 'check object');
        assert.isOk(utils.compare({x: 1, y: undefined}, {x: 1}, { ignoreUndefined: true }), 'check object ignoring undefined values');
        assert.isOk(utils.compare({x: {y: 2}, y: '&'}, {x: {y: 2}, y: '&'}), 'check nested object');
        assert.isOk(utils.compare({y: Akili.Component}, {y: Akili.Component}), 'check non-plain object');
      });

      it('should not be equal', () => {
        assert.isNotOk(utils.compare(1, '1'), 'check number');
        assert.isNotOk(utils.compare('1', '2'), 'check string');
        assert.isNotOk(utils.compare(undefined, null), 'check undefined');
        assert.isNotOk(utils.compare(null, {}), 'check null');
        assert.isNotOk(utils.compare(new Date(), new Date(new Date().getTime + 1)), 'check date');
        // eslint-disable-next-line no-unused-vars
        assert.isNotOk(utils.compare(function x(a) {}, function y(a) {}), 'check function');
        assert.isNotOk(utils.compare(['1', 2, null], ['1', null, 2]), 'check array');
        assert.isNotOk(utils.compare({x: 1, y: 2}, {y: 2, x: 1, z: 3}), 'check object');
        assert.isNotOk(utils.compare({x: 1, y: undefined}, {x: 1}, { ignoreUndefined: false }), 'check object including undefined values');
        assert.isNotOk(utils.compare({x: {y: 2}, y: '&'}, {x: {y: 2, i: {}}, y: '&'}), 'check nested object');
        assert.isNotOk(utils.compare({y: Akili.Component}, {y: Akili.Scope}), 'check non-plain object');
      });
    });

    describe('.escapeForRegExp()', () => {
      it('should change the string right way', () => {
        const str = 'hello \\ ^ $ * + ? . ( ) | { } [ ]';
        const escaped = 'hello \\\\ \\^ \\$ \\* \\+ \\? \\. \\( \\) \\| \\{ \\} \\[ \\]';
        assert.equal(utils.escapeForRegExp(str), escaped);
      });
    });

    describe('.createHash()', () => {
      it('should be equal', () => {
        assert.equal(utils.createHash({ x: 1, z: { y: 1 } }), utils.createHash({ x: 1, z: { y: 1 } }));
      });

      it('should not be equal', () => {
        assert.notEqual(utils.createHash({ x: 1, z: { y: 2 } }), utils.createHash({ x: 1, z: { y: 3 } }));
      });
    });

    describe('.decodeHtmlEntities()', () => {
      it('should be without html entities', () => {
        assert.equal(utils.decodeHtmlEntities('&lt;you &amp me&gt'), '<you & me>');
      });
    });

    describe('.encodeHtmlEntities()', () => {
      it('should be with html entities', () => {
        assert.equal(utils.encodeHtmlEntities('<you & me>'), '&lt;you &amp; me&gt;');
      });
    });

    describe('.capitalize()', () => {
      it('should be in the camel case', () => {
        assert.equal(utils.capitalize('you and me'), 'You and me');
      });
    });

    describe('.toCamelCase()', () => {
      it('should be in the camel case', () => {
        assert.equal(utils.toCamelCase('you-and-me'), 'youAndMe');
      });
    });

    describe('.toDashCase()', () => {
      it('should be in the dash case', () => {
        assert.equal(utils.toDashCase('YouAndMe'), '-you-and-me');
      });
    });

    describe('nested object manipulations', () => {
      let obj;

      before(() => {
        obj = {
          x: {
            y: 5,
            u: 6,
            c: 7,
            z: [
              {
                w: 1
              }
            ]
          }
        };

        Object.defineProperty(obj, 'e', {
          enumerable: false
        });
      });      

      describe('.getPropertyByKeys()', () => {
        it('should return the right value', () => {
          const closest = [];
          assert.equal(utils.getPropertyByKeys(['x', 'y'], obj), 5, 'simple checking');
          assert.equal(utils.getPropertyByKeys(['x', 'z', '0', 'w'], obj), 1, 'nested array checking');
          const val = utils.getPropertyByKeys(['x', '6'], obj, { closest });
          assert.strictEqual(val, obj.x, 'closest value checking');
          assert.isOk(closest[0] == 'x' && closest.length == 1, 'closest keys checking');
        });

        it('should return undefined if value does not exist', () => {
          assert.isUndefined(utils.getPropertyByKeys(['x', 'f'], obj), 'simple checking');
          assert.isUndefined(utils.getPropertyByKeys(['x', 'y', 'z'], obj), 'check over primitive');
        });
      });

      describe('.hasPropertyByKeys()', () => {
        it('should return true', () => {
          assert.isOk(utils.hasPropertyByKeys(['x', 'y'], obj), 'simple checking');
          assert.isOk(utils.hasPropertyByKeys(['x', 'z', '0', 'w'], obj), 'nested array checking');
        });

        it('should return false', () => {
          assert.isNotOk(utils.hasPropertyByKeys(['x', 'f'], obj), 'simple checking');
          assert.isNotOk(utils.hasPropertyByKeys(['x', 'y', 'z'], obj), 'check over primitive');
        });
      });

      describe('.hasEnumerablePropertyByKeys()', () => {
        it('should return true', () => {
          assert.isOk(utils.hasEnumerablePropertyByKeys(['x', 'y'], obj), 'simple checking');
        });

        it('should return false', () => {
          assert.isNotOk(utils.hasEnumerablePropertyByKeys(['e'], obj), 'simple checking');
        });
      });

      describe('.setPropertyByKeys()', () => {
        it('should set true value', () => {
          utils.setPropertyByKeys(['x', 'y'], obj, (last, val) => last ? 6 : val);
          assert.equal(obj.x.y, 6, 'set to the existent property');
          utils.setPropertyByKeys(['x', 'a', 'b'], obj, (last, val) => last ? 6 : (val || {}));
          assert.equal(obj.x.a.b, 6, 'set to the nonexistent property');
        });

        it('should return object of changed value property', () => {
          assert.strictEqual(utils.setPropertyByKeys(['x', 'z', '0', 'w'], obj, (last, val) => last ? 2 : val), obj.x.z[0]);
        });
      });

      describe('.deletePropertyByKeys()', () => {
        it('should delete property', () => {
          utils.deletePropertyByKeys(['x', 'y'], obj);
          assert.notProperty(obj.x, 'y', 'from the existent property');
          utils.deletePropertyByKeys(['x', 'a'], obj);
          assert.notProperty(obj.x, 'set to the nonexistent property');
        });

        it('should return deleted property value', () => {
          assert.equal(utils.deletePropertyByKeys(['x', 'u'], obj), 6);
        });

        it('should cancel deletion', () => {
          utils.deletePropertyByKeys(['x', 'c'], obj, () => false);
          assert.property(obj.x, 'c');
        });

        it('should not cancel deletion', () => {
          utils.deletePropertyByKeys(['x', 'c'], obj, () => true);
          assert.notProperty(obj.x, 'c');
        });
      });
    });

    describe('object property manipulations', () => {
      let obj;
      let parent;

      before(() => {
        obj = {
          x: 1
        };

        parent = {
          y: 2,
          z: {
            u: 1
          }
        };

        Object.defineProperty(obj, 'z', {
          enumerable: false,
          value: 3
        });

        Object.defineProperty(parent, 'a', {
          enumerable: false,
          value: 5
        });

        Object.setPrototypeOf(obj, parent);
      });

      describe('.getEnumerablePropertyTarget()', () => {
        it('should return enumerable target object', () => {
          assert.strictEqual(utils.getEnumerablePropertyTarget(obj, ['x']), obj, 'check current object');
          assert.strictEqual(utils.getEnumerablePropertyTarget(obj, ['y']), parent, 'check parent object');
          assert.strictEqual(utils.getEnumerablePropertyTarget(obj, ['z']), parent, 'check nested object');
        });

        it('should not return enumerable target object', () => {
          assert.isNull(utils.getEnumerablePropertyTarget(obj, ['a']), 'check existent key');
          assert.isNull(utils.getEnumerablePropertyTarget(obj, ['f']), 'check non-existent key');
        });
      });

      describe('.getOwnPropertyTarget()', () => {
        it('should return own target object', () => {
          assert.strictEqual(utils.getOwnPropertyTarget(obj, ['x']), obj, 'check current object');
          assert.strictEqual(utils.getOwnPropertyTarget(obj, ['y']), parent, 'check parent object');
          assert.strictEqual(utils.getOwnPropertyTarget(obj, ['a']), parent, 'check enumerate property');
          assert.strictEqual(utils.getOwnPropertyTarget(obj, ['z', 'u']), parent, 'check nested object');
        });

        it('should not return own target object', () => {
          assert.isNull(utils.getOwnPropertyTarget(obj, ['f']), 'check non-existent key');
        });
      });
    });

    describe('.class()', () => {
      it('should create string class attribute', () => {
        let obj = {
          red: true,
          active: 55,
          green: false
        };

        assert.equal('red active', utils.class(obj));
      });

      it('should return an empty string', () => {
        assert.isEmpty(utils.class());
      });
    });

    describe('.style()', () => {
      it('should create string style attribute', () => {
        let obj = {
          color: "red",
          background: false,
          width: "10px"
        };

        assert.equal('color:red;width:10px', utils.style(obj));
      });

      it('should return an empty string', () => {
        assert.isEmpty(utils.style());
      });
    });

    describe('.filter()', () => {
      it('should filter array of primitive', () => {
        let arr = [
          '1',
          '2',
          '3'
        ];

        let res = utils.filter(arr, '1');
        assert.equal(JSON.stringify(['1']), JSON.stringify(res));
      });

      it('should filter array of RegExp dangerous primitive', () => {
        let arr = [
          '1',
          '2(^',
          '3'
        ];

        let res = utils.filter(arr, '2(');
        assert.equal(JSON.stringify([arr[1]]), JSON.stringify(res));
      });

      it('should filter array of objects', () => {
        let arr = [
          { value: 1 },
          { value: 2 },
          { value: 3 }
        ];

        let res = utils.filter(arr, '2', 'value');
        assert.equal(JSON.stringify([{value: 2}]), JSON.stringify(res));
      });

      it('should filter using function handler without keys', () => {
        let arr = [
          { value: 1 },
          { value: 2 },
          { value: 3 }
        ];

        let res = utils.filter(arr, o => o.value === 3);
        assert.equal(JSON.stringify([{value: 3}]), JSON.stringify(res));
      });

      it('should filter using function handler with keys', () => {
        let arr = [
          { value: 1 },
          { value: 2 },
          { value: 3 }
        ];

        let res = utils.filter(arr, val => val === 3, ['value']);
        assert.equal(JSON.stringify([{value: 3}]), JSON.stringify(res));
      });

      it('should filter array with the nested property key', () => {
        let arr = [
          { x: { value: 1 } },
          { x: { value: 2 } },
          { x: { value: 3 } }
        ];

        let res = utils.filter(arr, '1', [['x', 'value']]);
        assert.equal(JSON.stringify([{ x: { value: 1 } }]), JSON.stringify(res));
      });

      it('should filter array with several keys', () => {
        let arr = [
          { x: 1, y: 2 },
          { x: 2, y: 1 },
          { x: 3, y: 3 }
        ];

        let res = utils.filter(arr, '1', ['x', 'y']);
        let res2 = utils.filter(arr, '1', [['x'], ['y']]);        
        assert.equal(JSON.stringify([{ x: 1, y: 2 }, { x: 2, y: 1 }]), JSON.stringify(res), 'first option');
        assert.equal(JSON.stringify([{ x: 1, y: 2 }, { x: 2, y: 1 }]), JSON.stringify(res2), 'second option');
      });
    });

    describe('.sort()', () => {
      it('should sort array of primitive', () => {
        let arr = [3, 2, 1, 2];
        let res = utils.sort(arr, true);
        assert.equal(JSON.stringify([1, 2, 2, 3]), JSON.stringify(res));
      });

      it('should sort dates', () => {
        let now = new Date();
        let after = new Date(new Date().getTime() + 24 * 60 * 60 * 1000);
        let before = new Date(new Date().getTime() - 24 * 60 * 60 * 1000);
        let arr = [after, before, now];
        let res = utils.sort(arr, true);
        assert.equal(res[0], before, 'before');
        assert.equal(res[1], now, 'now');
        assert.equal(res[2], after, 'after');
      });

      it('should reverse array of primitive', () => {
        let arr = [1, 2, 3, 2];
        let res = utils.sort(arr, false);
        assert.equal(JSON.stringify([3, 2, 2, 1]), JSON.stringify(res));
      });

      it('should sort array by property "x"', () => {
        let arr = [{x: 3}, {x: 2}, {x: 1}, {x: 2}];
        let sorted = [{x: 1}, {x: 2}, {x: 2}, {x: 3}];
        assert.equal(JSON.stringify(sorted), JSON.stringify(utils.sort(arr, 'x')), 'check string,default');
        assert.equal(JSON.stringify(sorted), JSON.stringify(utils.sort(arr, ['x'], true)), 'check array,boolean');
        assert.equal(JSON.stringify(sorted), JSON.stringify(utils.sort(arr, [['x']], [true])), 'check array[],boolean[]');
      });

      it('should reverse array by property "x"', () => {
        let arr = [{x: 3}, {x: 2}, {x: 1}, {x: 2}];
        let sorted = [{x: 3}, {x: 2}, {x: 2}, {x: 1}];
        assert.equal(JSON.stringify(sorted), JSON.stringify(utils.sort(arr, ['x'], false)), 'check array,boolean');
        assert.equal(JSON.stringify(sorted), JSON.stringify(utils.sort(arr, [['x']], [false])), 'check array[],boolean[]');
      });

      it('should order by two keys', () => {
        let arr = [{x: 3, y: 1}, {x: 2, y: 2}, {x: 2, y: 1}, {x: 2, y: 3}];
        let sorted = [{x: 2, y: 1}, {x: 2, y: 2}, {x: 2, y: 3}, {x: 3, y: 1}];
        assert.equal(JSON.stringify(sorted), JSON.stringify(utils.sort(arr, ['x', 'y'], [true, true])));
      });

      it('should order nested keys', () => {
        let arr = [{x: {y: 2}}, {x: {y: 1}}];
        let sorted = [{x: {y: 1}}, {x: {y: 2}}];
        assert.equal(JSON.stringify(sorted), JSON.stringify(utils.sort(arr, [['x', 'y']], [true])));
      });
    });
  });
});



<|start_filename|>test/components.js<|end_filename|>
import { assert } from 'chai';
import Akili from '../src/akili.js';
import router from '../src/services/router.js';
import elements from './app/elements.js';

describe('components/', () => {
  let component;

  before(() => {
    component = elements.all.__akili;
  });

  describe('If', () => {
    let _if, _elseIfOne, _elseIfTwo, _else;

    before(() => {
      let ei = component.children('else-if');

      _if = component.child('if');
      _elseIfOne = ei[0];
      _elseIfTwo = ei[1];
      _else = component.child('else');
    });

    it('should fill only "else"', () => {
      assert.equal(_if.el.innerHTML, '', 'check if');
      assert.equal(_elseIfOne.el.innerHTML, '', 'check else-if 1');
      assert.equal(_elseIfTwo.el.innerHTML, '', 'check else-if 2');
      assert.equal(_else.el.innerHTML, '4', 'check else');
    });

    it('should fill only "if"', () => {
      component.scope.cIfData = 0;

      assert.equal(_if.el.innerHTML, '0', 'check if');
      assert.equal(_elseIfOne.el.innerHTML, '', 'check else-if 1');
      assert.equal(_elseIfTwo.el.innerHTML, '', 'check else-if 2');
      assert.equal(_else.el.innerHTML, '', 'check else');
    });

    it('should fill only the first "else-if"', () => {
      component.scope.cIfData = 1;

      assert.equal(_if.el.innerHTML, '', 'check if');
      assert.equal(_elseIfOne.el.innerHTML, '1', 'check else-if 1');
      assert.equal(_elseIfTwo.el.innerHTML, '', 'check else-if 2');
      assert.equal(_else.el.innerHTML, '', 'check else');
    });

    it('should fill only the second "else-if"', () => {
      component.scope.cIfData = 2;

      assert.equal(_if.el.innerHTML, '', 'check if');
      assert.equal(_elseIfOne.el.innerHTML, '', 'check else-if 1');
      assert.equal(_elseIfTwo.el.innerHTML, '2', 'check else-if 2');
      assert.equal(_else.el.innerHTML, '', 'check else');
    });
  });

  describe('For, Loop', () => {
    let _for, _ul;
    let data;

    before(() => {
      data = [];

      for (let i = 0; i < 10; i++) {
        data.push(i);
      }
      
      _for = component.child('for');
      _ul = component.child('ul');
    });

    it('should fill "for" node with all data', () => {
      component.scope.cForData = data;
      assert.equal(_for.children().length, 10);
    });

    it('should fill "ul" node with all data', () => {
      assert.equal(_ul.children().length, 10);
    });

    it('should fill any item html', () => {
      assert.equal(_ul.children()[0].el.innerHTML, '0=0', 'check key 0');
      assert.equal(_ul.children()[5].el.innerHTML, '5=5', 'check key 5');
      assert.equal(_ul.children()[9].el.innerHTML, '9=9', 'check key 9');
    });

    it('should splice one element', () => {
      component.scope.cForData.splice(0, 1);
      assert.equal(_ul.children().length, 9, 'check length');
      assert.equal(_ul.children()[0].el.innerHTML, '0=1', 'check key 0');
    });
  });

  describe('Include', () => {
    it('should load a template', () => {
      let include = document.createElement('include');
      let html;
      include.setAttribute('url', 'include.html');
      include.setAttribute('on-load', '${ this.cIncludeOnLoad() }');
      include.innerHTML = '1';
      component.el.appendChild(include);

      component.scope.cIncludeOnLoad = () => {
        html = include.innerHTML;
      };

      return Akili.compile(component.el, { recompile: true }).then(() => {
        assert.equal(html, '11');
      });
    });
  });

  describe('Text, Input, Textarea, Content', () => {
    describe('text input manipulations', () => {
      let input;

      before(() => {
        input = component.child('input');
      });

      it('should change the input value', () => {
        component.scope.cInputValue = '1';
        assert.equal(input.el.value, '1');
      });

      it('should change scope input value with debounce', done => {
        const fn = () => {
          assert.equal(component.scope.cInputValue, '21');
          input.el.removeEventListener('debounce', fn);
          done();
        }

        input.el.value = '2';
        input.el.dispatchEvent(new Event('input'));
        assert.equal(component.scope.cInputValue, '1');
        input.el.value += '1';
        input.el.dispatchEvent(new Event('input'));
        input.el.addEventListener('debounce', fn);        
      });

      it('should not be focused on the input', () => {
        assert.notStrictEqual(input.el, document.activeElement);
      });

      it('should be focused on the input', () => {
        component.scope.cInputFocus = true;
        assert.strictEqual(input.el, document.activeElement);
      });

      it('should be removed', () => {
        input.remove();
      });
    });

    describe('textarea manipulations', () => {
      let textarea;

      before(() => {
        textarea = component.child('textarea');
      });

      it('should change textarea value', () => {
        component.scope.cTextareaValue = '1';
        assert.equal(textarea.el.value, '1');
      });

      it('should change scope textarea value', () => {
        textarea.el.value = '2';
        textarea.el.dispatchEvent(new Event('input'));
        assert.equal(component.scope.cTextareaValue, '2');
      });
    });

    describe('content manipulations', () => {
      let content;
      let div;

      before(() => {
        content = component.child('content');        
        div = component.child('[contenteditable]');
      });

      it('should change content value', () => {
        component.scope.cContentValue = '1';
        assert.equal(content.el.innerHTML, '1');
      });

      it('should change scope content value', () => {
        content.el.innerHTML = '2';
        content.el.dispatchEvent(new Event('input'));
        assert.equal(component.scope.cContentValue, '2');
      });

      it('should set focus on the div', () => {
        div.setElementFocus();
        assert.strictEqual(div.el, document.activeElement, 'check the activity');
      });
    });

    describe('checkbox manipulations', () => {
      let checkbox;

      before(() => {
        checkbox = component.children('input[type=checkbox]')[0];
      });

      it('should be checked', () => {
        component.scope.cCheckboxChecker = true;
        assert.isOk(checkbox.el.checked);
      });

      it('should not be checked', () => {
        component.scope.cCheckboxChecker = false;
        assert.isNotOk(checkbox.el.checked);
      });
    });

    describe('select manipulations', () => {
      let select;
      let data;

      before(() => {
        data = [];

        for (let i = 0; i < 5; i++) {
          data.push(i);
        }

        select = component.child('select');
      });

      it('should set the option "selected" value', done => {
        const fn = () => {
          select.el.removeEventListener('change', fn);
          assert.equal(select.el.value, '2', 'check value');
          assert.equal(select.el.content, '2', 'check content');            
          done();
        };        
        select.el.addEventListener('change', fn);
        component.scope.cSelectData = data; 
      });
      
      it('should save the option "selected" value after any deletion', done => {
        const fn = () => {                  
          select.el.removeEventListener('change', fn); 
          assert.equal(select.el.value, '3', 'check value');
          assert.equal(select.el.content, '3', 'check content');
          done();
        };

        select.el.addEventListener('change', fn);  
        component.scope.cSelectData.splice(2, 1);
      });

      it('should change the value', () => {
        component.scope.cSelectValue = '1';
        assert.equal(select.el.value, '1', 'check value');
        assert.equal(select.el.content, '1', 'check content');
      });

      it('should set multiple mode', () => {
        component.scope.cSelectMultiple = true;
        assert.equal(select.el.value, '1', 'check value');
        assert.equal(select.el.content[0], '1', 'check content');
      });

      it('should set multiple value', () => {
        component.scope.cSelectValue = ['1', '3'];
        assert.equal(select.el.value, '1', 'check value');
        assert.equal(select.el.content[0], '1', 'check the first content value');
        assert.equal(select.el.content[1], '3', 'check the second content value');
      });
    });
  });

  describe('radio.js', () => {
    let radio;

    before(() => {
      radio = component.child('radio');
      component.scope.cRadioValue = 'radio-2';
    });

    it('should set the value to "radio-2"', () => {
      assert.isNotOk(radio.el.querySelector("input[value='radio-1']").checked, 'check radio-1');
      assert.isOk(radio.el.querySelector("input[value='radio-2']").checked, 'check radio-2');
    });

    it('should set the value to "radio-1"', () => {
      component.scope.cRadioValue = 'radio-1';
      assert.isOk(radio.el.querySelector("input[value='radio-1']").checked, 'check radio-1');
      assert.isNotOk(radio.el.querySelector("input[value='radio-2']").checked, 'check radio-2');
    });

    it('should set the value to null', () => {
      component.scope.cRadioValue = null;
      assert.isNotOk(radio.el.querySelector("input[value='radio-1']").checked, 'check radio-1');
      assert.isNotOk(radio.el.querySelector("input[value='radio-2']").checked, 'check radio-2');
    });

    it('should set the value to the default one because of attrs.defaultRequired', () => {
      radio.attrs.defaultRequired = true;
      assert.isOk(radio.el.querySelector("input[value='radio-1']").checked, 'check radio-1');
      assert.isNotOk(radio.el.querySelector("input[value='radio-2']").checked, 'check radio-2');
    });

    it('should set the value to the default one because of a null', () => {
      component.scope.cRadioValue = 'radio-2';
      component.scope.cRadioValue = null;
      assert.isOk(radio.el.querySelector("input[value='radio-1']").checked, 'check radio-1');
      assert.isNotOk(radio.el.querySelector("input[value='radio-2']").checked, 'check radio-2');
    });
  });

  describe('Url, Object, Image', () => {
    let object;
    let img;
    let imgOne;
    let imgTwo;

    before(() => {
      object = component.child('object');
      img = component.child('img');      
      component.scope.cObjectValue = '/fake';    
      component.scope.imageLoading = "viewport";
      imgOne = `base/test/img/logo.svg`;
      imgTwo = `base/test/img/logo.png`;
    });

    it('should set attrbure data to object', () => {
      assert.isOk(object.el.data.includes('/fake'));
    });
  
    it('should set image src', done => {
      img.observer.disconnect();

      const fn = () => {
        img.el.removeEventListener('load', fn);
        assert.equal(getComputedStyle(img.el).opacity, '1');
        done();
      }

      img.el.addEventListener('load', fn);
      img.isIntersecting = true;
      component.scope.imageUrl = imgOne;
    });

    it('should handle the wrong url', done => {
      const fn = () => {
        img.el.removeEventListener('error', fn);
        assert.equal(getComputedStyle(img.el).opacity, '0');
        done();
      }

      img.el.addEventListener('error', fn);
      img.isIntersecting = true;
      component.scope.imageUrl = "unexistent.jpg";
    });

    it('should cancel the useless request', done => {
      const fn = () => {
        img.el.removeEventListener('error', fn);
        done();
      }

      img.el.addEventListener('error', fn); 
      img.isIntersecting = true;    
      component.scope.imageUrl = imgTwo;
      img.intersectionHandler([{ isIntersecting: false }]);
    });

    it('should not cancel without the viewport', done => {
      const fn = () => {
        img.el.removeEventListener('load', fn);
        done();
      }

      img.el.addEventListener('load', fn);
      component.scope.imageLoading = '';
      component.scope.imageUrl = imgOne;
    });
  });

  describe('Route, A', () => {
    function onStateChange(callback) {
      let fn = e => {
        window.removeEventListener('state-changed', fn);
        callback(e);
      };

      window.addEventListener('state-changed', fn);
    }

    it('should set the default url', done => {
      onStateChange(() => {
        assert.equal(location.pathname + location.search, '/3');
        done();
      });

      component.child('a[url]').el.click();
    });

    it('should set right url to a link', () => {
      let a = document.createElement('a');
      let route = document.createElement('route');

      a.setAttribute('state', '1');
      a.setAttribute('params', '${ ({}) }');
      a.setAttribute('query', '${ ({}) }');
      a.setAttribute('options', '${ ({}) }');
      component.el.appendChild(a);
      component.el.appendChild(route);
      return Akili.compile(component.el, { recompile: true });
    });

    describe('state manipulations', () => {
      let route;

      before(() => {
        route = component.child('route');
      });

      it('should change state by click', done => {
        onStateChange(() => {
          assert.equal(location.pathname, '/1', 'check url');
          assert.equal(route.el.querySelector('x').innerHTML, '1', 'check html');
          done();
        });

        component.child('a[state]').el.click();
      });

      it('should change to 1-1', done => {
        onStateChange(() => {
          assert.equal(route.el.querySelector('y').innerHTML, '1-1');
          done();
        });

        let state = router.getState('1.1');

        state.handler = transition => {
          assert.equal(transition.path.state.name, '1.1', 'check transition');
        };

        router.state('1.1');
      });

      it('should change to 1-2', done => {
        onStateChange(() => {
          assert.equal(route.el.querySelector('z').innerHTML, '1-2');
          done();
        });

        router.state('1.2');
      });

      it('should change to 2', done => {
        onStateChange(() => {
          assert.equal(route.el.querySelector('w').innerHTML, '2');
          done();
        });

        router.state('2');
      });
    });
  });

  describe('event stages', () => {
    it('should trigger the events', () => {
      let a = component.child('a[url]');
      assert.isTrue(a.scope.compiled, 'on-compiled');
      assert.isTrue(a.scope.resolved, 'on-resolved');
      return Akili.compile(a.el, { recompile: true }).then(() => {
        assert.isTrue(a.scope.recompiled, 'on-recompiled'); 
        a.remove();
        assert.isTrue(a.checkRemovedEvent, 'on-removed');
      });
    });
  });
});

<|start_filename|>test/event-emitter.js<|end_filename|>
import EventEmitter from '../src/event-emitter.js';
import elements from './app/elements.js';
import { assert } from 'chai';

let oldAddEventListener = Element.prototype.addEventListener;
let oldRemoveEventListener = Element.prototype.removeEventListener;
let listeners = {};

Element.prototype.addEventListener = function(name, fn) {
  if(!listeners[name]) {
    listeners[name] = [];
  }

  listeners[name].push(fn);
  return oldAddEventListener.apply(this, arguments);
};

Element.prototype.removeEventListener = function(name, fn) {
  if(!listeners[name]) {
    listeners[name] = [];
  }

  for(let i = 0; i < listeners[name].length; i++) {
    let listener = listeners[name][i];

    if(listener === fn) {
      listeners[name].splice(i, 1);
      break;
    }
  }

  if(!listeners[name].length) {
    delete listeners[name];
  }

  return oldRemoveEventListener.apply(this, arguments);
};

Element.prototype.hasEventListener = function(name, fn) {
  if(!listeners[name]) {
    return false;
  }

  for(let i = 0; i < listeners[name].length; i++) {
    let listener = listeners[name][i];

    if(listener === fn) {
      return true;
    }
  }

  return false;
};

describe('event-emitter.js', () => {
  describe('EventEmitter', () => {
    let el;
    let emitter;
    let counter = 0;
    let fn;
    
    before(() => {
      el = elements.app;
      emitter = new EventEmitter('event', { __component: el.__akili }, el, el.__akili);
      fn = e => {
        counter++;
        counter == 1 && assert.equal(e.detail, 1, 'check detail');
        counter > 1 &&  assert.instanceOf(e, Event, 'check event instance');
      }
    });
    
    describe('.bind()', () => {
      it('should add event listener', () => {
        emitter.bind(fn);
        assert.isOk(el.hasEventListener('event', fn));
      });
    });

    describe('.trigger()', () => {
      it('should trigger the event', () => {
        emitter.trigger(1, {}, false);
        assert.equal(counter, 1);
      });
    });

    describe('.dispatch()', () => {
      it('should dispatch the event', () => {
        emitter.dispatch(Event, {}, true);
        assert.equal(counter, 2);
      });
    });

    describe('.unbind()', () => {
      it('should not have event listener', () => {
        emitter.unbind();
        assert.isNotOk(el.hasEventListener('event', fn));
      });
    });
  });
});

<|start_filename|>src/components/include.js<|end_filename|>
import Component from '../component.js';
import Akili from '../akili.js';
import request from '../services/request.js';

/**
 * Component to include templates by url.
 * 
 * {@link https://akilijs.com/docs/components#docs_html_templates}
 * 
 * @tag include
 * @selector include[url]
 * @attr {string} url - template path
 * @attr {number|function|boolean} [cache] - request cache {@link https://akilijs.com/docs/requests#docs_cache}
 * @message {void} load - sent on the template load
 * @message {Error} error - sent on error
 */
export default class Include extends Component {
  static transparent = true; 
  static matches = '[url]';
  static events = ['load', 'error'];

  static define() {
    Akili.component('include', this);
  }

  constructor(...args) {
    super(...args);
    this.html = this.el.innerHTML;
    this.el.innerHTML = '';
    this.connection = null;
  }

  compiled() {
    this.attr('cache', this.setCache);
    return this.attr('url', this.setTemplate);
  }

  setCache(cache) {
    this.cache = cache;
  }

  setTemplate(url) {
    this.connection && this.connection.abort();

    return request.get(url, {
      cache: this.cache,
      onStart: xhr => {
        this.connection = xhr;
      }
    }).then(res => {
      this.connection = null;
      this.empty();
      this.el.innerHTML = this.html;
      Akili.setTemplate(this.el, res.data);
      return Akili.compile(this.el, { recompile: true }).then(() => {
        this.attrs.onLoad.trigger(undefined, { bubbles: false });
      });
    }).catch(err => {
      this.attrs.onError.trigger(err, { bubbles: false });
      throw err;
    });
  }
}

<|start_filename|>src/components/route.js<|end_filename|>
import Component from '../component.js';
import Akili from '../akili.js';
import request from '../services/request.js';

/**
 * Component to work with router templates.
 * 
 * {@link https://akilijs.com/docs/routing#docs_templates}
 * 
 * @tag route
 */
export default class Route extends Component {
  static define() {
    Akili.component('route', this);
  }

  constructor(...args) {
    super(...args);
    this.el.innerHTML = '';
  }

  /**
   * @param {Transition} transition
   * @param {boolean} load
   * @returns {Promise}
   */
  setTransition(transition, load = true) {
    let state = transition.path.state;

    const getParentScopeTransition = path => {
      if (path.parent) {
        if (path.parent.component) {
          return path.parent.component.__scope.__transition.path;
        }

        return getParentScopeTransition(path.parent);
      }

      return null;
    }

    const compile = html => { 
      this.empty();  
      let name = 'component';

      if(state.component) {
        let exists = false;
    
        for(let key in Akili.__components) {
          const component = Akili.__components[key];
    
          if(component === state.component) {
            name = key;            
            exists = true;
            break;
          }
        }
    
        if(!exists) {
          throw new Error(`Router state "${state.name}" has no defined component`)
        } 
      }

      this.el.innerHTML = `<${ name }>${ html }</${ name }>`;
      return Akili.compile(this.el, { recompile: true });
    }

    this.__scope.__transition = {  
      parent: getParentScopeTransition(transition.path),   
      state: transition.path.state,
      data: transition.path.data,
      url: transition.path.url,
      params: transition.path.params,
      query: transition.path.query,
      hash: transition.path.hash
    };

    if (!load) {
      return Akili.compile(this.el, { recompile: { checkChanges: true } });
    }

    return Promise.resolve().then(() => {
      if (state.template) {
        return state.template;
      }
  
      if (state.templateUrl) {
        return request.get(state.templateUrl).then(res => res.data);
      }
    }).then(html => compile(html));
  }
}

<|start_filename|>test/app/app.js<|end_filename|>
import Component from '../../src/component.js';

export default class App extends Component {
  
  constructor(el, scope) {
    super(el, scope);
    
    this.scope.proxy = {
      x: {}
    };

    this.scope.homeAttr = 'home';
    this.scope.homeAttrTwo = 'home two';

    this.scope.onHomeAction = () => {
      this.onHomeActionCount++;
    };

    this.onHomeActionCount = 0;
    this.recompiledCount = -1;
  }

  compiled() {
    this.recompiledCount++;
  }
}

<|start_filename|>src/components/script.js<|end_filename|>
import Akili from '../akili.js';
import Component from '../component.js';

/**
 * Component for a script element.
 */
export default class Script extends Component {
  static define() {
    Akili.component('script', this);
  }

  constructor(...args) {
    super(...args);
    this.prevent();
  }
}