Classes and modules

Introduction

To date, there are a large number of JS frameworks, libraries, and so on. It would seem that you choose a large and reliable framework, and write your own interface. But, firstly, different frameworks have a different approach to writing code. Each one offers its own syntax and features for solving various particular problems, such as creating elements according to a template, introducing hooks, links, data. Secondly, everyone has their own dependencies. And the size of all dependencies sometimes reaches up to gigabytes. As a result, there is a desire to write your own, anotherthe best framework.

This article lists the initial steps, and the problems that I encountered when I decided to write for myself another, universal tool for creating a web application interface.

Disclaimer

The article does not use modern solutions and standards. Although all the code was written for the latest version of the Chrome browser, the author focuses on the API standardized by the ECMAScript 5 specification. In addition, the code in this part of the article uses an API that is not compatible with the IE (Internet Explorer) browser.

Where do we start.

First, to begin with, we will decide what the framework will consist of. You can stupidly write everything in one file. As a result, we can end up with a file consisting of 100 thousand lines of code. We can separate everything into separate files, represent each file as a separate module, and define one main file that will pull everything together. Okay, let’s do this. Let’s write index.html with our main script: “widgets-all.js“. in the tag <body> ask <div> with id root. The <div> will be the application container. And through the tag <style> let’s set this element to absolute positioning, and the coordinates of the upper, left corner.

<!DOCTYPE html>
<html>
  <head>
    <title>Main GUI</title>
    <meta charset="UTF-8">
    <style>
      #root {
        position: "absolute";
        left: "0px";
        top: "0px";
      }
    </style>
    <script type="text/javascript" src="https://habr.com/ru/post/690852/./widgets-all.js">
    </script>
  </head>
  <body>
    <div id="root">
    </div>
  </body>
</html>

Now let’s make our “widgets-all.js” load modules sequentially and synchronously. And here comes the FIRST PROBLEM. If we dynamically create tags <script> and embed them in the document, some browsers load them asynchronously even though we explicitly set the “async” into meaning false. And this means that the script may load later than desired. As a result, due to dependencies, we will have the classic “varialbeName is not a function” error. The easiest way to dynamically load scripts synchronously and sequentially is to use the document method – document.write(htmlStr).

Let’s write this code

//Файл widgets-all.js

//Заставляем HTMLParser прерваться и синхронно записать содержимое
//Данные элементы будут правыми братьями текущего тэга.
//Ссылку на сам тэг можно получить либо заранее по id (если его задать самому)
//либо через свойство document.currentScript.

document.write('<script type="text/javascript" src="https://habr.com/ru/post/690852/./Module1.js"></script>');
document.write('<script type="text/javascript" src="https://habr.com/ru/post/690852/./Widgets.js"></script>');

Excellent. Now two tags will be added after our “widgets-all.js” <script>. If there were other tags after “widgets-all.js” itself <script>, they won’t clog because the document hasn’t been loaded yet. When the document is loaded, then call document.write it is forbidden, otherwise you can erase the entire content of the page. It should be noted that this solution is a hack, because according to the specification, some browsers will ignore the insertion and code execution of passed

But if you want to put your script in a tag <head> before <body>then for it to work correctly with the document, it must be launched in the "interactive" or "complete".

Below is the code how to do it.

//Файл Widgets.js
//Этот модуль будет содержать в себе все зависимости.
//В нём аналогично определяется resolveConflict 
//(которая вызывает одноимённую функцию у каждого модуля)

var Widgets = (function(){

  //Стандартная проверка имён.
  //Восстанавливается по resolveConflict()
  if(typeof Widgets != "undefined")
    var _____Widgets  = Widgets;
  
  var m = {
    utils: {
      module1: module1 //global name defined previously at Module1.js script.
    }
  };

  var isBoundReady = false; //Зарегистрирован ли обработчик
  var isReady = false; //Загружен ли DOM.
  var readyList = []; //Список функций f, которые должны быть вызваны, когда DOM будет загружен.

  var userAgent = navigator.userAgent.toLowerCase(); //name of current browser

  //browser names
  var browser = {
		version: (userAgent.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/) || [])[1],
		safari: /webkit/.test(userAgent),
		chrome: /chrome/.test(userAgent),
		opera: /opera/.test(userAgent),
		msie: /msie/.test(userAgent) && !/opera/.test(userAgent),
		mozilla: /mozilla/.test(userAgent) && !/(compatible|webkit)/.test(userAgent)
  };
  
  //private handler
  function ready(){
    if(isReady) //DOM loaded only once.
      return;
    
      isReady = true; //DOM is loaded.

      //exec each f on readyList
      for(var i = 0; i < readyList.length; i++){
        readyList[i]();
      }

      readyList = null; //clear list.

      //and remove event handler.
      if(browser.mozilla || browser.opera || browser.chrome)
        document.removeEventListener("DOMContentLoaded", ready, false);

      //for IE browser
      var s = document.getElementById('__ie_init');
      if(s)
        s.remove(); //with onreadystatechange hndlr.
  }; //end ready()

  //registrate ready handler.
  function bindReady(){
    if(isBoundReady) //execute only once.
      return;
    
    isBoundReady = true; //flag that handler registrated.

    if (browser.mozilla || browser.opera || browser.chrome ){
      document.addEventListener("DOMContentLoaded", ready, false );
    }
	
    // If IE is used, use the excellent hack by Matthias Miller
    // http://www.outofhanwell.com/blog/index.php?title=the_window_onload_problem_revisited
    else if (browser.msie ) {
			
      // Again use document.write to synchronous add <script>
      document.write("<scr" + "ipt id=__ie_init defer=true " + 
        "src=//:><\/script>");
	
      // Use the defer script hack
      var script = document.getElementById("__ie_init");
			
      if ( script ) { 
        script.onreadystatechange = function() {
          if ( this.readyState != "complete" ) return;
          ready();
        };
      }
      
    } else if (browser.safari ){
		
        // Continually check to see if the document.readyState is valid
        timers.safariTimer = setInterval(function(){
          if ( document.readyState == "loaded" 
            || document.readyState == "complete"
            || document.readyState == "interactive") {
            clearInterval( timers.safariTimer );
            timers.safariTimer = null;
            ready();
          }
        }, 10);
    }

  }; //end bindReady()

  //public function
  //Executes f only when document has been loaded or parsed
  m.onReady = function(f){
    if(typeof f !== 'function')
      throw new TypeError('onReady(): Argument f is not a function');

    bindReady(); //registrate handler.
    if(isReady){
      f.apply(this, f.arguments); //call f with its arguments.
    }
    else {
      //append f. 
      //anonymous function is wrapper to preserve context and arguments.
      readyList.push(function(){
        return f.apply(this, f.arguments);
      });
    }
  }; //end onReady()

  m.resolveConflict = function(){
    module1.resolveConflict();//restore original value of global name module1
    window.Widgets = _____Widgets; //original value of Widgets
    return this; //new value of Widgets
  }
  
  return m;
}());

Now it is enough to call the public function of the module Widgets.onReady and pass it a function that will be called when the document is processed. Let's add a simple test to the index.html file. To do this, we define two tags <script>, one before the framework, the other after it. The first will define global names that match module names. The second will contain the application code that the framework will use.

<head>
  ...
  <!--Заранее определим старые значения для имён модулей-->
  <script type="text/javascript">
    var Widgets = 1111, module1 = 2222;
  </script>
  <script type="text/javascript" src="https://habr.com/ru/post/690852/./widgets-all.js"> <!--framework/libs-->
  </script>
  <script type="text/javascript"> <!--application-->
    Widgets.onReady(function(){
      console.log(document.readyState); // => interactive or complete.
      var lib = Widgets.resolveConflict();
      console.log(Widgets);
      console.log(module1);
      window.lib = lib;
      console.log("%o", lib);
    });
  </script>
</head>

As a result, we should get the following output:

interactve
1111
2222
{
  resolveConflict: function(){...},
  onReady: function(f){...},
  utils: { module1: {...} },
  ...other properties inherited from Object.prototype
}

Type definition.

Now that we've dealt with loading and resolving module names, we can now think about the module elements themselves. What is a component? How to create it? How to work with him? What does it contain?

Let's define the component as an instance of the class, and the class - the definition of the component, its scheme (drawing). Classes can be implemented using constructor functions, if you do not take into account modern standards (ES6). We will implement the class manually. Therefore, instances of the class will be created by calling the constructor function through the operator "new".

Okay, but what will be related to the class itself, and what to a specific instance? The simplest example, class name is a class property, not an instance property. For example, the class name Human -"Human" is common to all instances of people that can have their own name (John, Smith, Billy). And where and how to store class properties?

Class properties - object. This object must be available to retrieve class metadata. We will store it not as a separate property of the constructor function, but as a property of the module object itself. Let's define what properties this object will contain:

  • className - class name

  • callConstructor - class constructor function

  • callParent - parent class constructor function

  • beforeCreate - validator function, called before creating an instance of the class, checks the arguments passed to the constructor, reading constructorParameters.

  • noDefaultConstructor - a flag indicating whether it is forbidden to use the default constructor. Those. constructor without parameters.

  • constructorParameters - an object that describes the parameters of the constructor. Object properties - parameter names. Each parameter is represented by a separate object with the following properties:

    • required - whether the parameter is required.

    • type - parameter type as a string.

    • oridnal - position in the array of constructor function arguments.

    • getter - converter function. Converts an arbitrary type parameter to the corresponding type specified in typeif possible.

  • count is the number of instances of this class.

  • getCount - a method that returns a value count.

With the properties of the class defined, we can write a function that will create an object that represents the class, from which we can instantiate. But before that, we must decide three questions: will the same names be redefined in child classes? Should the parent class constructor be called directly, or should the framework handle this (automatically calling the parent constructor when the child class constructor is called)? If we override a method, should we include the logic of the parent's old method, or are we erasing it entirely?

Redefining properties from a parent is only allowed for primitive values, as well as functions. When redefining functions, let's save the parent's logic in such a way that when calling the overridden method of the child class, the parent's method is called first. The parent class constructor will be called automatically.

Okay, let's go implement. Let's start with class names and method overrides.

//Файл Widgets.js
//В теле функции модуля.
//1. Определяем вспомогательные функции.

var protoprops = ['toString', 'toJSON', 'valueOf', 'constructor',
                  'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable',
                  'toLocaleString'];

//Добавим метод extend, который копирует свойства из объектов.
//Defined at ECMAScript 5.
//Not worked at IE.
Object.defineProperty(Object.prototype, "extend", {
  value: function
    for(var i = 0; i < arguments.length; i++){
      var source = arguments[i];
      for(var p in source)
        if(!(p in this))
          this[p] = source[p];
      for(var j = 0; j < protoprops.length; j++){ //IE
        p = protoprops[j];
        if(source.hasOwnProperty(p)) this[p] = source[p]; //Object.prototype props at some IE treat as ownProperties.
      }
    }
    return this;
  }, writable: false, configurable: false, enumerable: false
});

//Определим функции извлечения имени типа (класса).
function classof(o){
  return Object.prototype.toString.call(o).slice(8, -1);
}

Function.prototype.getName = function(){
  if('name' in this) return this.name;
  return this.name = this.toString().match(  /function\s*([^(]*)\(/ )[1]; //pattern: function fName( where fName is first group
};

Function.prototype.setName = function(className){
  Object.defineProperty(this, "name", {value: className, configurable: false, writable: false, enumerable: false});
};

//Возвращает имя типа или класса для о.
//Для примитивных типов и функций возвращает type.
//Для объектов возвращает имя функции-конструктора.
//Для встроенных объектов возвращает значение аттрибута "class".
function type(o){
  var t, c, n;
  if(o === null) return "null";
  if(o === undefined) return "undefined";
  if(o !== o) return "NaN";

  //primitives and functions
  if((t = typeof o) !== 'object') return t;
		
  //base objects.
  if((c = classof(o)) !== 'Object') return c;
		
  if(o.constructor && typeof o.constructor === "function"
    && (n = o.constructor.getName())
  ) 
    return n;

  //extract className from classObject of instance o.
  if(o.self && o.self.className && typeof o.self.className === 'string')
    return o.self.className;
  
  return "Object"; //common type.
};

//Определить, является ли объект instance подклассом класса с именем className
//или сам instance класс с именем className.
//Если instance примитив - идёт сравнение по типу.
//Если instance потомок или объект класса className => true.
function isSubClassOf(instance, className){
  if(instance == null || instance == undefined) //null or undefined is not a class
    return false;
  if(typeof className !== 'string')
    throw new TypeError('Argument className must be String!');
  if(typeof instance !== 'object' && typeof instance === className)
    return true;
  else if(typeof instance === 'string' && className === 'cssStyle')
    return true;
  
  var cls = instance.self || instance; //if not instance => assume as classObject
  while(cls){
    if(cls.className && cls.className === className)
      return true;
    cls = cls && cls.callParent && cls.callParent.prototype && cls.callParent.prototype.self;
  }

  return false;
};

//Переопределяет методы и свойства родительского класса.
//Переопределение свойств разрешено для совместимых (дочерних) типов.
//Метод заменяется анонимной функцией, которая
//вызывает унаследованный метод родителя, а затем свой собственный.
//cls - дочерний класс.
//props - методы и свойства экземпляра данного класса (объекта-прототипа)
function overrides(cls, props){
  if(!cls || !cls.callParent || !props) //no props or superclass or noclass => nothing overrided
    return;
  var props = Object.keys(cls.callParent.prototype); //parent props

  //through enumerable and own props
  for(var i = 0; i < props.length; i++){
    var prop = props[i];
    if(prop in funs){ //defined at subclass too.
      var oldp = cls.callParent.prototype[prop]; //save old property value.
      var newp = funs[prop]; //new method/prop that must be copied.
      var t = undefined;
      if((t = typeof newp) !== typeof newp)
        continue;
      else if(t === 'function'){ //matched by type and type is fun => override.
        cls.callConstructor.prototype[prop] = function()
        {

          //calls parent then overriden version.
          //When you use recursion
          //this method also calls parent m() recursively
          oldp.apply(this, arguments);
          newp.apply(this, arguments);
        }; //end f.
      } //end else

      //check type by covariance.
      else if(isSubClassOf(newp, type(oldp))){
        cls.callConstructor.prototype[prop] = newp; //reset to new value.
      }
      //matched primitive.
      else if(t !== 'object'){
        cls.callConstructor.prototype[prop] = newp; //reset to new value.
      }
    } //endif
  }//end for
}

Using the function type we are trying to get the name of an object's constructor function, or the name of a class from a class object. Each instance of a class has a reference to an object of the class itself as a property. self. The function was also defined isSubClassOfA that determines whether the object is an instance of the class specified by name. An object is an instance of class A if it is either an instance of class A itself or an instance of one of its child classes. If it is a primitive, then the function checks if its type matches the specified type. These functions are used in the method overrideswhich overrides the methods and properties of the child class if the specified methods and properties if

  1. They are already defined in the parent class.

  2. They are of the same type and are not an object.

  3. They are an instance of the child class of the parent instance. Those. they are class compatible.

Now, you can define a class definition function - defineClass

//Файл Widgets.js
//Вспомогательные функции и проверки...

//Вспомогательная функция определения классов.
//Принимает функцию конструктор родительского класса (superCtor)
//функцию конструктор нового класса (ctor)
//функцию валидатора параметров конструктора (before)
//методы и свойства экземпляра класса (methods)
//методы и свойства самого класса (statics)
//и описания параметров конструктора класса (paramsDesc)
function defineClass(superCtor, ctor, before, methods, statics, paramsDesc){
  var c = {};
  if(superCtor && ctor){ //if no constructor => return null. 
    ctor.prototype = Object.create(superCtor.prototype); //inherit superclass prototype
    c.callParent = superCtor; //save superClass constructor function.
  }
  if(ctor){
    ctor.prototype.constructor = ctor;
    ctor.prototype.self = c;

    if(methods)
      ctor.prototype.extend(methods);
    if(statics)
      c.extend(statics);
    
    c.callConstructor = ctor;
    c.className = type(ctor.prototype);
			
    if(!before)
      before = (superCtor && superCtor.prototype && superCtor.prototype.self && superCtor.prototype.self.beforeCreate);

    //copy beforeCreate into classObject.
    if(before && typeof before === 'function')
      Object.defineProperty(c, "beforeCreate", {writable: false, enumerable: false, configurable: false,
        value: before
      });

    //inherit static properties from superclass.
    if(superCtor && superCtor.prototype && superCtor.prototype.self)
      c.extend(superCtor.prototype.self);

    //define .ctor params description.
    c.constructorParameters = null;
    c.constructorParameters = {}; //do not copy object from parent. define new with props from parent.

    //copy from superclass into ctor.constructorParameters
    if(superCtor && superCtor.prototype && superCtor.prototype.self && superCtor.prototype.self.constructorParameters)
      c.constructorParameters.extend(superCtor.prototype.self.constructorParameters);
    
    //and add own parameters.
    if(paramsDesc)
      c.constructorParameters.extend(paramsDesc); //just attach props to new object from parent and paramDesc.

    //override properties and methods of parent with owns.
    overrides(c, methods);
    return c;
  }
  else return null; //no ctor => no class object.
};

This function creates a class object that can be used to create instances of the class itself (via the callConstructor or beforeCreate property). Let's define the function of creating an instance of the class, as well as calling its parent constructor - callParent.

//Файл Widgets.js

//Публичная (открытая) функция создания экземпляра класса с именем className.
//Аргументы функции конструктора передаются в виде объекта-словаря args.
//Функция beforeCreate(), определённая ниже, парсит args
//и возвращает экземпляра класса.
Widgets.create = function(className, args){
  if(typeof className !== 'string') //name is not a string
    throw new TypeError('Expected className as String!');
  else if(!args || typeof args !== 'object') //only keyword arguments are available for object creation.
    throw new TypeError('Expected named-args (keyword arguments = kwargs) for constructor arguments!');

  var rootObj = this;
  className = className.split('.');

  for(var i = 0; i < className.length; i++)
    if(className[i] in rootObj)
      rootObj = rootObj[className[i]];
				
  var classObj = rootObj; //found ctor
  if(!classObj.beforeCreate){
    return new classObj.callConstructor(args);
  }
  else {
    return classObj.beforeCreate(args, classObj);
  }
};

//Вызывается для валидации и приведении аргументов к типам параметров конструктора.
//А также создания экземпляра класса.
function beforeCreate(){
  var kwargs = Object.keys(arguments[0]); //one single argument is object.
  var classObject = arguments[1]; //Class<T>

  //Проверяем конструктор без параметров (по умолчанию).
  if(kwargs.length === 0 && classObject.noDefaultConstructor)
    throw new TypeError('Cannot call constructor with no-args as default constructor was prohibited and now illegal!');

  else if(kwargs.length === 0) //no-args => constructor without args.
    return new classObject.callConstructor();

  //process each argument.
  var args = new Array(kwargs.length);

  //Для каждого аргумента.
  for(var i = 0; i < kwargs.length; i++){
    var paramName = kwargs[i];

    //Имя параметра не определено для конструктора.
    if(!classObject.constructorParameters || !classObject.constructorParameters[paramName])
      throw new TypeError('Property ' + paramName + ' is not defined for class ' + classObject.className + '!');

    //Дескриптор параметра и его значение.
    var paramDesc = classObject.constructorParameters[paramName];
    var paramValue = arguments[0][paramName];

    //Для обработки стилевых строк напишем пока отдельную ветку if
    //Стилевые строки будут иметь тип cssStyle
    //Данное наименование типа эквивалетно типу string.
    if(paramDesc.type === "cssStyle"){ //Стилевая строка.
      
      //Проверяем, есть ли getter для строки в cssStyleValidators
      if(typeof cssStyleValidators[paramName] !== 'function')
        throw new TypeError('Cannot find getter for cssStyle property: "' + paramName + '"!');
      
      //Если нашли getter, то приводим строку к виду "styleProperty": "styleValue";
      paramValue = "" + paramName + ":" + cssStyleValidators[paramName](paramValue);
    }

    //Проверка обязательных параметров.
    if(paramDesc.required && 
      (
        (paramValue =  (paramDesc.getter && typeof paramDesc.getter === 'function' && paramDesc.getter(paramValue)) || paramValue) !== paramValue  //NaN
					|| !isSubClassOf(paramValue, paramDesc.type) //then is null => false.
      )    
    )
      throw new TypeError('Expected required parameter ' + paramName + ' with type ' + paramDesc.type + ' but actual ' + type(paramValue));

    //Если это необязательный параметр и его значение не опущено (передано)
    //то проверяем его.
    else if(!paramDesc.required && paramValue && 
      (
					(paramValue =  (paramDesc.getter && typeof paramDesc.getter === 'function' && paramDesc.getter(paramValue)) || paramValue) !== paramValue  //NaN
					|| !isSubClassOf(paramValue, paramDesc.type) //then is null => false.
      )
    )
      throw new TypeError('Expected non-required parameter ' + paramName + ' with type ' + paramDesc.type + ' but actual ' + type(paramValue));


    //Для необязательных параметров с типом string, если они не заданы, то поставить им значение пустой строки.
    else if( (paramValue === null || paramValue === undefined) && !paramDesc.required && paramDesc.type === 'string')
      paramValue="";

    //Остальные проверки на null и undefined уже выполнены.
    //Функция isSubClassOf вернёт исключение, если первый аргумент - null.
    args[paramDesc.ordinal] = paramValue;
  }

  //check omitted required parameters. ifPresent => error.
  var missingParams = [];
  kwargs = Object.keys(classObject.constructorParameters);
  for(i = 0; i < kwargs.length; i++){
    paramDesc = classObject.constructorParameters[kwargs[i]];
    if(paramDesc.required && (paramDesc.ordinal >= args.length || args[paramDesc.ordinal] === undefined))
      missingParams.push(kwargs[i]);
  }

  if(missingParams.length > 0)
    throw new TypeError('Parameters ' + missingParams.toString() + ' are required and cannot be omitted or undefined!');

	
  var instance = Object.create(classObject.callConstructor.prototype);
  classObject.callConstructor.apply(instance, args);
  return instance;
};


//Эта функция вызывает конструктор родительского класса указанного экземпляра instance.
//Через его функцию конструктор ctor (с помощью которого instance был создан)
//получаем объект класса (classObject) 
//У объекта класса получаем и делаем косвенный вызов функции конструктора родительского класса,
//передавая ему массив аргументов args.
function callParent(instance, ctor, args){
  if(ctor.prototype.self && ctor.prototype.self.callParent && typeof ctor.prototype.self.callParent === 'function'){
    ctor.prototype.self.callParent.apply(instance, args); //call parent if defined through self property. Self is a class object (Class<T>)
  }
};

Function beforeCreatedefined above can be used as a generic class constructor function argument validator for the function defineClass. And using the function callParent, you can initiate a call to the constructor of the direct parent. Now let's define the classes and their constructor functions. Let's write two constructor functions for two classes. Define the parent class Container and its child class panel in the following way.

//Файл Widgets.js
//Функции конструкторы классов.

//Container.
function Container(width, height, background){ 
  callParent(this, Container, arguments); //check super and call super.ctor(args)

  this.self.count += 1; //count of containers (with subclasses instances)
  this.root = document.createElement('div'); //content.
  if(!background)
    background = '#CCFFFF';
  this.background = background;

  //Пока формируем строку со стилями вручную.
  var css_style_str = "";
  if(width)
    css_style_str += 'width: ' width + 'px;';
  if(height)
    css_style_str += 'height: ' + height + 'px;';
  if(css_style_str !== ""){
    this.root.style = css_style_str;
    this.root.style.setProperty('background-color', this.background);
  }
  else
    this.root.style = "width:0;height:0;";

  this.root.style.setProperty('position', 'relative');
};

//Panel
function Panel(width, height, background, title){
  callParent(this, Panel, arguments); //check super and call super.ctor(args)
  this.title = (title) ? title : '';
  
  //create Title with backgrounds
  this.titleBar = document.createElement('div');
  this.root.appendChild(this.titleBar);

  this.titleBar.style = "position:relative; width:100%; height: 15%; top: 0; left: 0;";
  this.titleBar.style.setProperty('background-color', 'black');
  this.titleBar.style.setProperty('color', 'white');
  var span = document.createElement('span');
  span.appendChild(document.createTextNode(this.title));
  this.titleBar.appendChild(span);

  var btn_close = document.createElement('button');
  btn_close.appendChild(document.createTextNode('X'));
  btn_close.type="reset";

  var c_root = this.root;
  btn_close.addEventListener('click', function(e){
    c_root.remove();
  }, false);
  this.titleBar.appendChild(btn_close);
  btn_close.style = "position: absolute; top: 0; right: 0; width: 10%; height: 100%";
};

//define classes at namespace panels.
Widgets.panels = {
  Container: defineClass(null, Container, beforeCreate, {
    getWidth: function(){return this.width;},
    getHeight: function(){return this.height;},
    setWidth: function(w){this.root.style.setProperty('width', w); this.width = w;},
    setHeight: function(h){this.root.style.setProperty('height', h); this.height = h;}
  }, {
    //properties of .self object.
    count: 0,
    getCount: function(){
      return this.count; //this is classObject. => self.getCount()
    }
  }, {
    //.ctor parameters
    width: {type: 'number', required: true, getter: Number, ordinal: 0},
    height: {type: 'number', required: true, getter: Number, ordinal: 1},
    background: {type: 'string', required: false, ordinal: 2},
  }),
  
  Panel: defineClass(Container, Panel, beforeCreate, {
    getTitle: function(){return this.title;},
    setTitle: function(txt){this.titleBar.children[0].textContent = txt; this.title = txt;}
  }, /*no own statics. All inherited from parent*/ null, {
    // .ctor parameters
    title: {type: 'string', required: false, ordinal: 3}
  })
  
};

These classes are defined in panels global module Widgets. Function create looks for classes in the Widgets object, retrieves functions from the class object beforeCreate and callConstructor. If there is beforeCreate, then it is called, otherwise the constructor (callConstructor) is called. Now let's create class instances.

<!--index.html-->
<script type="text/javascript" src="https://habr.com/ru/post/690852/./widgets-all.js"></script>
<script type="text/javascript">
  Widgets.onReady(function(){
    ...
    var r = document.getElementById('root');
    var p1 = Widgets.create('panels.Panel', {width: 200, height: 200, title: 'My Panel'});
    var p2 = Widgets.create('panels.Panel', {width: 400, height: 300});
    r.appendChild(p1.root);
    r.appendChild(p2.root);
  });
</script>

As a result, on the page in a web browser, we get the following output. (Chrome browser).

What's next?

Now it is possible to create classes. You can add a public class definition function like the one below.

//Файл Widgets.js

var aliases = {}; //Сохраним псевдонимы.

//define - открытая функция
//определяет класс с именем className и методами/свойствами экземпляров
//config, а также методами/свойствами самого класса - statics.
//config содержит три свойства, которые зарезерированы под
//настройки (указание родительского класса, конструктора, псевдонима)
Widgets.define = function(className, config, statics){
  var names = className.split('.');
  var rootObj = this;

  //traverse down through global object.
  //define nearest available namespace.
  for(var i = 0; i < names.length - 1; i++){
    if(!(names[i] in rootObj))
      rootObj[names[i]] = {};
    else if('callConstructor' in rootObj[names[i]]) //already defined
      throw new TypeError('Cannot define class with the same name "' + className + '"');
    
    rootObj = rootObj[names[i]];
  }

  if(rootObj[names[i]]) //at namespace className is defined.
    throw new TypeError('Cannot define class with the same name "' + className + '"');

  var methods = {}; //Методы и свойства экземпляров класса.
  var parentClassObj = Object; //Родительский класс.
  var parentCtr = Object; //и конструктор.
  var ctr = null; //функция-конструктор для нового класса.
  var options = Object.keys(config);
  var alias = null; //Псевдоним для имени класса.

  //for each properties at config.
  for(var j = 0; j < options.length; j++){
    var option = options[j]; //Проверяем, не является ли свойство спец. параметром.

    //Значение 'extend' => имя родительского класса.
    //Если такого класса нет (либо в объекте класса не задана функция callConstructor)
    //выбрасывается исключение.
    if(option === 'extend'){
      parentClassObj = propertyAt(this, config[option]);
      if(!parentClassObj || typeof parentClassObj !== 'object' || !parentClassObj.callConstructor || typeof parentClassObj.callConstructor !== 'function')
        throw TypeError('Cannot find constructor of parent class "' + config[option] + '"');

        parentCtr = parentClassObj.callConstructor;
    }

    //Значение свойства 'constructor' => функция конструктор для нового класса.
    else if(option === 'constructor'){
      ctr = config[option];
      if(!ctr || typeof ctr !== 'function')
        throw new TypeError('Constructor property is not a function!');
    }

    //Значение свойства 'alias' => строка, определяющая псевдоним (краткое имя для класса).
    else if(option === 'alias'){
      if(typeof config[option] !== 'string')
        throw TypeError('Alias property is not a string!');
      
      alias = config[option];
    }
    else
      methods[option] = config[option];
  }

  if(!ctr)
    var ctr2 = function(){
      callParent(this, ctr2, arguments); 
    };
  else
    var ctr2 = function(){
      callParent(this, ctr2, arguments);
      if(ctr2.c)
        ctr2.c.apply(this, arguments);

    };
				
  ctr2.setName(names[i]);

  //ctr2 - оборачивает конструктор ctr. ctr2 вызывает конструктор родителя.
  //Следовательно, вызывать конструктор родителя в своей функции не надо.
  rootObj[names[i]] = defineClass(parentCtr, ctr2, null, methods, statics);
  if(ctr)
    ctr2.c = ctr; //Функция конструктор обернута в функцию ctr2.


  
  if(alias)
    aliases[alias] = rootObj[names[i]];
}

All defined classes, or rather their objects (meta-data), are stored in the global Widgets object. Using the create/define function pairs, you can create classes from objects in Widgets, or you can define new classes in a Widgets object. Each individual class is represented by an object. And through defineClass - directly define the class, passing it the functions of the constructors of the parent and child classes, the properties of the instances and the class itself, as well as the function-validator of the constructor parameters along with their description.

All this has the following limitations:

  1. It is necessary to adhere to a clear order of parameters in the constructor function.

  2. In each subsequent child class in the hierarchy, the constructor function first lists all the parameters of the parent function, starting with the most common ancestor. For example, the following order A(p1, p2) -> B(p1, p2, p3) -> C(p1..p3, own4, own5) denotes three classes, where A is B's parent and C is B's child. B must enumerate parameters A, before declaring your own, C - the same, starting with parameters A, and ending with parameters B.

  3. A string with a style and an ordinary string are indistinguishable in type, but you can specify that the parameter is of a different type than a string. Nevertheless, it is necessary to write the processing of such strings, for the correct initialization of the attribute style.

  4. For now, you have to write getters/setters manually, since the logic for processing the value for each property may be different.

  5. No cross browser. No IE compatibility.

The next part will describe the creation of a simple widget - a panel, how to solve the problem with getters / setters, as well as managing the placement of components inside the container (panel) - layouts.

Similar Posts

Leave a Reply