📜 ⬆️ ⬇️

(Archive) Matreshka.js - Inheritance

The article is outdated. The new documentation contains the most current information from this post. See Class .


Greetings to all readers and writers of Habr.
In the previous article we talked about the basics of working with the Matryoshka. In this, I want to tell how to inherit the Nested doll and how to build small applications so far on its base.

The nested doll is arranged in the form of a class constructed using the custom function Class . This is a slightly modified version of the function I wrote about on the javascript.ru forum ( link to the dock ).
')
So why classes? A class is only a word that does not contradict the prototype programming paradigm. If you look at the documentation of the same Backbone.js, then you will see that they also operate on the word “class” without any constraints. We can argue that there are no classes in Javascript, there are constructors, and I agree with you, but, in fact, does this argument make sense? If the constructor looks like a class, floats like a class, and quacks like a class, then this is probably the class?

From the lyrics to the case. So, the Nested doll is created in the form of a class:
 window.MK = window.Matreshka = Class({ ... }); 

Class argument is a prototype of a constructor that can be defined as:
 var MyClass = Class({ constructor: function() { ... } }); 
... which then returns from the Class function. If the constructor is not defined, then it will be an empty function.

One class can be inherited from another class (in this case, MyClass inherited from Nested Doll):
 var MyClass = Class({ 'extends': MK }); 

(For 'extends' quotes are needed not only to avoid syntax errors (extends is a reserved word), but also to highlight the syntax. Other properties can be without quotes.)

When inheriting Matryoshka, there is an important rule: the constructor must always be there and the .initMK method must be called .initMK , which, in this case, initializes the pseudo- __id properties: __id (instance id for internal use), the .__events object (event object), and the .__special object (storing values ​​of "special" properties, their accessories and associated elements). The same rule is true for classes, which will be explained in the following articles: MK.Array and MK.Object .

Let's write a trivial application that works with the login form.
(you can immediately look at the result: jsbin.com/qohilowu/1/edit )

 <form class="login-form"> <input type="text" class="user-name" placeholder="Username"> <input type="password" class="password" placeholder="Password"> <label> <input type="checkbox" class="show-password"> Show Password </label> <input type="submit" value="Sign In" class="submit"> <label> <input type="checkbox" placeholder="Password" class="remember-me"> Remember me </label> </form> 
(Here, I removed the extra blocks and classes that are responsible only for a nice appearance, the resulting example uses Bootstrap).

We have two text fields: login and password. There are two checkboxes: “show password” and “remember me”. There is one button: “enter”. Let's say that the form validation is completed when the login length is at least 4 characters, and the password length is at least 5 characters.

Create a class LoginForm . In my work, I stick to the rule: one class per one complex element (form, widget ...). Now LoginForm will be the only class in our tiny application.
 var LoginForm = Class( ... ); 

The first thing we need to do is declare that our class is inherited from the Nested doll:
 var LoginForm = Class({ 'extends': MK }); 

The second is to declare the constructor:
 var LoginForm = Class({ 'extends': MK, constructor: function() { this.initMK(); //     ""  } }); 

Next, we tie the elements to the appropriate properties. Personally, I always put bindings in a separate method, which I call .bindings . This is a matter of taste and you can, without any problems, bind elements in the constructor (but only after calling .initMK ).
  ... bindings: function() { return this //      (.  (1)) // ,     ,  .innerHTML   ,       //  ,               .bindNode( this, '.login-form' ) //        (on, getValue, setValue), //       .bindNode({ userName: this.$( '.user-name' ), // ,      'keyup' password: this.$( '.password' ), // ,      'keyup' showPassword: this.$( '.show-password' ), // ,     'click' rememberMe: this.$( '.remember-me' ), // ,     'click' }) .bindNode( 'isValid', this.$( '.submit' ), { //   (.  (2)) setValue: function( v ) { $( this ).toggleClass( 'disabled', !v ); } }) ; }, ... 

(1) What does the string ".bindNode( this, '.login-form' )" mean? If the current instance is passed as the first argument in .bindNode , the Matryoshka actually binds the special property "__this__" . This means that the record
 this.bindNode( this, '.login-form' ); 

equivalent to this:
 this.bindNode( '__this__', '.login-form' ); 

Why do we need a special value "__this__" ? In order to use the method .$ , which allows you to set the context of the bound elements (sandbox). This binding is optional but desirable. With it and using the .$ method, you can avoid theoretical conflicts binding the same element in different classes.
Method documentation .$ : Http://finom.imtqy.com/matreshka/docs/Matreshka.html#$
(2) Here we bind the this.$( '.submit' ) element to the 'isValid' property (which will be described below) in the following way: if isValid == false , then add the 'disabled' class to this element, if not, remove this class.
Remark from the author
This article was written before the release of the version in which the abbreviation for the binding that sets the class to the element appeared. Starting from version 0.1 Matryoshka contains static method MK.binders.className
Reference to documentation: finom.imtqy.com/matreshka/docs/Matreshka.binders.html#className
This code is:
 .bindNode( 'isValid', this.$( '.submit' ), { setValue: function( v ) { $( this ).toggleClass( 'disabled', !v ); } }) 

You can replace this:
 .bindNode( 'isValid', this.$( '.submit' ), MK.binders.className( '!disabled' ) ) 


About code design
Pay attention to how the method is decorated. In the process of developing applications based on Matryoshka. I am a big fan of chain chaining of methods ( Method chaining ), therefore, if the method returns nothing specific, then we return this . The .bindNode method, like many other methods (.set, .defineGetter ...), does just that. In addition, I took as a rule the design of blocks of chained calls:
  ( ) [].1() [].2() ... [].N()    ( ) 

This makes it easy to find the beginning and end of the chain.

Add .bindings to the constructor:
  ... constructor: function() { this .initMK() .bindings() //       .initMK() ; }, ... 

Now we will declare events, and we will also move it to a separate method, which I usually call .events .
  ... events: function() { this.boundAll().on( 'submit', function( evt ) { //     (.  (1)) this.login(); evt.preventDefault(); }.bind( this ) ); //     (.  (2)) return this //      "showPassword" (.  (3)) .on( 'change:showPassword', function() { this.bound( 'password' ).type = this.showPassword ? 'text' : 'password'; }, true ) //      "userName"  "password" (.  (4)) .on( 'change:userName change:password', function() { this.isValid = this.userName.length >= 4 && this.password.length >= 5; }, true ) ; }, ... 

(1) We get a form element (bound to this or '__this__' ), or rather its jQuery instance, and call the jQuery.fn.on method with the 'submit' event.
Documentation for the .boundAll method: http://finom.imtqy.com/matreshka/docs/Matreshka.html#boundAll
(2) Bind the event handler context with the Function.prototype.bind method. This is needed in order to replace the standard execution context of the event handler (element) with our instance. IE8 does not support this method, but I strongly recommend that you always use the es5-shim library (in the following articles you will see how this is good). I get the element itself using event.target , event.delegatedTarget , so I easily event.delegatedTarget standard context.

About future versions (Already implemented)
In the next versions of Matryoshka, an alternative way of adding the DOM event to the bound element will most likely appear. In version 0.1, this method has already been implemented.
Syntax:
 this.boundAll().on( 'submit', function() { ... }.bind( this ) ); 

or
 this.boundAll( 'key' ).on( 'click', function() { ... }.bind( this ) ); 

I want to improve a little. It is planned to do so:
 this.on( 'submit::__this__', function() { ... }); this.on( 'click::key', function() { ... }); 


(3) Here the comment speaks for itself, with only one remark. Notice the last argument passed to the .on method (with a value of true ). This is not the context of the handler (since the type is boolean ), this argument tells us that the handler should be run immediately, immediately after the declaration (that is, you do not need to call .trigger ).

(4) When changing the "userName" or "password" properties, we set the 'isValid' property to true or false , checking the length of the login and password.

Add to the constructor:
  ... constructor: function() { this .initMK() .bindings() .events() ; }, ... 


Now we are doing the .login method, which, so far, does not send anything anywhere:
  ... login: function() { var data; if( this.isValid ) { data = { userName: this.userName, password: this.password, rememberMe: this.rememberMe }; alert( JSON.stringify( data ) ); } return this; } ... 


And create an instance of the resulting class:
 var loginForm = new LoginForm(); 


All code
 var LoginForm = Class({ 'extends': MK, rememberMe: true, constructor: function() { this .initMK() .bindings() .events() ; }, bindings: function() { return this .bindNode( this, '.login-form' ) .bindNode({ userName: this.$( '.user-name' ), password: this.$( '.password' ), showPassword: this.$( '.show-password' ), rememberMe: this.$( '.remember-me' ) }) .bindNode( 'isValid', this.$( '.submit' ), { setValue: function( v ) { $( this ).toggleClass( 'disabled', !v ); } }) ; }, events: function() { this.boundAll().on( 'submit', function( evt ) { this.login(); evt.preventDefault(); }.bind( this ) ); return this .on( 'change:showPassword', function() { this.bound( 'password' ).type = this.showPassword ? 'text' : 'password'; }, true ) .on( 'change:userName change:password', function() { this.isValid = this.userName.length >= 4 && this.password.length >= 5; }, true ) ; }, login: function() { var data; if( this.isValid ) { data = { userName: this.userName, password: this.password, rememberMe: this.rememberMe }; alert( JSON.stringify( data ) ); } return this; } }); var loginForm = new LoginForm(); 



Result: jsbin.com/qohilowu/1/edit

In conclusion


I noticed an interesting effect. Despite the four-year experience of programming in Javascript, each time I encounter someone else's code and library, for a certain period of time (from several seconds to several hours), I don’t understand at all what happens in this code. But then, having read more lines or having studied the documentation, what seemed incomprehensible to me, becomes completely transparent.

With Matryoshka, my code has become several orders of magnitude more stable. Mistakes made by inattention, almost disappeared. My client is pleased that I began to do more in less time.

What's next?

Surely programmers with experience have noticed a ridiculous data assembly in the .login method.
 data = { userName: this.userName, password: this.password, rememberMe: this.rememberMe }; 

Wouldn't it be better to create a tool that would know where the state of the application is, and where the data is? Sending all the data that we got in the class is not very reasonable. The database is not interested at all whether the password is displayed to the user or not. The database does not need to know whether the sent data is valid, since the server will still check it again and send a response. I will describe the solution of this question in the next article, in which I will talk about the class MK.Object .

Thank you so much for your attention. Successful coding.

Source: https://habr.com/ru/post/200078/


All Articles