(Archive) Matreshka.js - Inheritance

  • Tutorial
The article is out of date. The new documentation contains the most relevant 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 Matryoshka. In this, I want to tell you how to inherit Matryoshka and how to build small applications based on it for now.

Matryoshka is arranged in the form of a class constructed using a custom function Class. This is a slightly modified version of the function that I wrote about on the javascript.ru forum ( link to the dock ).

So why classes? A class is just a word that does not contradict the paradigm of prototype programming. If you look at the documentation of the same Backbone.js, you will see that they also use the word "class" without any hesitation. 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, swims like a class, and quacks like a class, then this is probably the class?

From lyrics to action. So, Matryoshka is created as a class:
window.MK = window.Matreshka = Class({ ... });

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

One class can be inherited from another class (in this case it is MyClassinherited from Matryoshka):
var MyClass = Class({
  'extends': MK
});

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

When you inherit Matryoshka, there is an important rule: the constructor should always be called in it a method .initMKthat, in this case, initializes pseudo-private properties: __id(instance identifier for internal use), an object .__events(event object) and an object .__special(storing the values ​​of "special" properties, their accessors and associated elements). The same rule is true for classes, which will be explained in the following articles: MK.Arrayand MK.Object.

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

(Here I removed the extra blocks and classes that are responsible only for the pleasant appearance; in the resulting example, Bootstrap is used).

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 the work, I adhere to the rule: one class for one complex element (form, widget ...). Now it LoginFormwill be the only class in our tiny application.
var LoginForm = Class( ... );

The first thing we must do is declare that our class inherits from Matryoshka:
var LoginForm = Class({
  'extends': MK
});

Second - declare the constructor:
var LoginForm = Class({
	'extends': MK,
	constructor: function() {
		this.initMK(); // инициализируем объект событий и "специальных" свойств
	}
});

Next, bind the elements to the corresponding properties. Personally, I always put the bindings into a separate method that I call .bindings. This is a matter of taste and you can, without problems, bind elements in the constructor (but only after the call .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 a string mean ".bindNode( this, '.login-form' )"? If the .bindNodecurrent instance is passed in as the first argument, Matryoshka actually binds a 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 special meaning "__this__"? In order to use a method .$that allows you to set the context of the elements to be attached (sandbox). This binding is optional but desirable. With it and using the method, .$you can avoid theoretical conflicts of binding the same element in different classes.
Documentation for the method .$: http://finom.github.io/matreshka/docs/Matreshka.html#$
(2) Here we attach the element this.$( '.submit' )to the property 'isValid'(which will be described below) this way: if isValid == false, then we add a class to this element 'disabled'if no, then we remove this class.
Remark from the author
This article was written before the release of the shortcut for the binding that sets the class to an element. Starting with version 0.1, the Matryoshka contains the static method MK.binders.className Documentation
link: finom.github.io/matreshka/docs/Matreshka.binders.html#className
This code:
.bindNode( 'isValid', this.$( '.submit' ), {
	setValue: function( v ) {
		$( this ).toggleClass( 'disabled', !v );
	}
})

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


About code design
Pay attention to how the method is framed. In the process of developing applications based on Matryoshka. I am a great connoisseur of chain method calls ( Method chaining ), so if the method does not return anything specific, then we increase this. The method .bindNode, like many other methods (.set, .defineGetter ...) does just that. In addition, I made the rule of block chain blocks:
объект (открытие блока)
[табуляция].метод1()
[табуляция].метод2()
...
[табуляция].методN()
точка с запятой (закрытие блока)

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

Add .bindingsto the constructor:
	...
	constructor: function() {
		this
			.initMK()
			.bindings() // все привязки должны следовать за вызовом .initMK()
		;
	},
	...

Now we will declare the events, and we will also put it into 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 the form element (bound to thisor '__this__'), or rather its jQueryinstance, and call the method jQuery.fn.onwith the event 'submit'.
Documentation for the method .boundAll: http://finom.github.io/matreshka/docs/Matreshka.html#boundAll
(2) Bind the context of the event handler using the Function.prototype.bind method . This is necessary 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 good this is). I get the element itself using event.target, event.delegatedTargetso it's easy giving up the standard context.

About future versions (Already implemented)
In future versions of Matryoshka, most likely, an alternative way to add a DOM event to the bound element will appear. In version 0.1, this method is already 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. Note the last argument passed to the method .on(with value true). This is not the context of the handler (because the type is - boolean), this argument tells us that the handler should be started immediately, immediately after the declaration (that is, it does not need to be called .trigger).

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

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


Now we make the .login method, which, so far, does not send anything:
	...
	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();


Whole 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 four years of programming experience in Javascript, every time I run into someone else’s code and library for a certain period of time (from several seconds to several hours), I don’t understand what is going on in this code. But then, after reading more lines or studying the documentation, what seemed incomprehensible to me becomes completely transparent.

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

What's next?

Surely experienced programmers noticed an absurd data collection in the method .login.
data = {
	userName: this.userName,
	password: this.password,
	rememberMe: this.rememberMe
};

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

Thank you so much for your attention. Good coding.

Also popular now: