Matreshka.js — Наследование

в 17:22, , рубрики: javascript, jquery, Matreshka, Matreshka.js, Веб-разработка, метки: ,

Приветствую всех читателей и писателей Хабра.
В предыдущей статье мы поговорили об основах работы с Матрешкой. В этой, я хочу рассказать, как наследовать Матрешку и как строить пока что небольшие приложения на её базе.

Матрешка устроенна в виде класса, сконструтированного при помощи кастомной функции Class. Это немного измененная версия функции, о которой я писал на форуме javascript.ru (ссылка на доку).

Так почему классы? Класс — это лишь слово, не противоречащее парадигме прототипного программирования. Если взглянуть на документацию того же Backbone.js, то вы увидите, что и они оперируют словом «класс» без всяких стеснений. Мы можем поспорить о том, что в Javascript нет классов, есть конструкторы, и я с вами соглашусь, но, на деле, имеет ли этот спор смысл? Если конструктор выглядит как класс, плавает как класс, и крякает как класс, то это, наверное, и есть класс?

От лирики к делу. Итак, Матрешка создана в виде класса:

window.MK = window.Matreshka = Class({ ... });

Аргумент класса — прототип конструктора, который можно определить так:

var MyClass = Class({
  constructor: function() { ... }
});

… который затем и возвращается из функции Class. Если конструктор не определен, то им станет пустая функция.

Один класс может быть унаследован от другого класса (в данном случае MyClass наследуется от Матрешки):

var MyClass = Class({
  'extends': MK
});

(Для 'extends' кавычки нужны не только для того, чтоб избежать ошибки синтаксиса (extends — зарезервированное слово), но и для подсветки синтаксиса. Остальные свойства могут быть без кавычек.)

При наследовании Матрешки есть важное правило: конструктор должен быть всегда и в нем должен вызываться метод .initMK, который, в данном случае, инициализирует псевдоприватные свойства: __id (идентификатор экземпляра для внутреннего использования), объект .__events (объект событий) и объект .__special (хранящий значения «специальных» свойств, их акцессоры и привязанные элементы). Это же правило верно и для классов, которые будут объяснены в следующих статьях: MK.Array и MK.Object.

Давайте напишем тривиальнейшее приложение, работающее с формой логина.
(можно сразу взглянуть на результат: jsbin.com/ATAPUCo/4/)

<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>

(Здесь убрал лишние блоки и классы, отвечающие лишь за приятный внешний вид, в результирующем примере используется Bootstrap).

У нас есть два текстовых поля: логин и пароль. Есть два чекбокса: «показать пароль» и «запомнить меня». Есть одна кнопка: «войти». Скажем, что валидация формы пройдена тогда, когда длина логина — не меньше 4 символов, а длина пароля — не меньше 5 символов.

Создадим класс LoginForm. В работе я придерживаюсь правила: один класс на один комплексный элемент (форма, виджет...). Сейчас LoginForm будет единственным классом в нашем крошечном приложении.

var LoginForm = Class( ... );

Первое, что мы должны сделать, — это объявить, что наш класс наследуется от Матрешки:

var LoginForm = Class({
  'extends': MK
});

Второе — объявим конструктор:

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

Дальше, привяжем элементы к соответствующим свойствам. Лично я всегда выношу привязки в отдельный метод, который называю .bindings. Это дело вкуса и вы можете, без проблем, привязывать элементы в конструкторе (но только после вызова .initMK).

	...
		bindings: function() {
		return this
			 // привязываем к нашему инстанцу форму (см. ниже (1))
			 // форма, как и многие другие элементы, имеющие .innerHTML в качестве значения, не имеет опций привязки по умолчанию
			 // это значит, что привязка не включает никакой логики по обмену значений между элементом и свойством класса
			.bindElement( this, '.login-form' )
			 // в следующем вызове не указываем опции привязки (on, getValue, setValue),
			 // так как Матрешка их найдет самостоятельно
			.bindElement({
				userName: this.$( '.user-name' ), // привязывается, как текстовое поле с событием 'keyup'
				password: this.$( '.password' ), // привязывается, как текстовое поле с событием 'keyup'
				showPassword: this.$( '.show-password' ), // привязывается, как чекбокс с событием 'click'
				rememberMe: this.$( '.remember-me' ), // привязывается, как чекбокс с событием 'click'
			})
			.bindElement( 'isValid', this.$( '.submit' ), { // кастомная привязка (см. ниже (2))
				setValue: function( v ) {
					$( this ).toggleClass( 'disabled', !v );
				}
			})
		;
	},
	...

(1) Что значит строка ".bindElement( this, '.login-form' )"? Если в .bindElement первым аргументом передается текущий инстанс, Матрешка, на самом деле, привязывает специальное свойство "__this__". Это значит, что запись

this.bindElement( this, '.login-form' );

эквивалентна этой:

this.bindElement( '__this__', '.login-form' );

Зачем нам специальное значение "__this__"? Для того, чтоб, использовать метод .$, который позволяет задавать контекст привязываемых элементов (песочницу). Эта привязка не обязательна, но желательна. С ней и использованием метода .$ можно избежать теоретических конфликтов привязки одного и того же элемента в разных классах.
Документация к методу .$: http://finom.github.io/matreshka/docs/Matreshka.html#$
(2) Здесь мы привязываем элемент this.$( '.submit' ) к свойству 'isValid' (которое будет описано ниже) таким образом: если isValid == false, то добавляем данному элементу класс 'disabled', если нет, то убираем этот класс.

Ремарка от автора

Эта статья писалась до выхода версии, в которой появилось сокращение для привязки, устанавливающей класс элементу. Начиная с версии 0.0.2 Матрешка содержит статичный метод MK.classp
Ссылка на документацию: finom.github.io/matreshka/docs/Matreshka.html#classp
Этот код:

.bindElement( 'isValid', this.$( '.submit' ), {
	setValue: function( v ) {
		$( this ).toggleClass( 'disabled', !v );
	}
})

Можно заменить на этот:

.bindElement( 'isValid', this.$( '.submit' ), MK.classp( '!disabled' ) )

Об оформлении кода

Обратите внимание, на то, как оформлен метод. В процессе разработки приложений на базе Матрешки. Я большой ценитель цепочечного вызова методов (Method chaining), поэтому, если метод не возвращает ничего специфичного, то мы возаращаем this. Метод .bindElement, как и многие другие методы (.set, .defineGetter...) именно так и работает. Кроме этого, я взял за правило оформление блоков цепочечных вызовов:

объект (открытие блока)
[табуляция].метод1()
[табуляция].метод2()
...
[табуляция].методN()
точка с запятой (закрытие блока)

Это позволяет легко найти начало и конец цепочки.

Добавляем .bindings в конструктор:

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

Теперь объявим события, и так же вынесем в отдельный метод, который я, обычно, называю .events.

	...
	events: function() {
		this.$el().on( 'submit', function( evt ) { // привязывание обработчика к форме (см. ниже (1))
			this.login();
			evt.preventDefault();
		}.bind( this ) ); // указываем контекст вызова коллбека (см. ниже (2))
		
		return this
			// привязываем обработчик события изменения свойства "showPassword" (см. ниже (3))
			.on( 'change:showPassword', function() {
				this.el( '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) Мы получаем элемент формы (привязанный к this или '__this__'), а точнее его jQuery инстанс и вызываем метод jQuery.fn.on с событием 'submit'.
Документация методу .$el: http://finom.github.io/matreshka/docs/Matreshka.html#$el
(2) Привязываем контекст обработчика события методом Function.prototype.bind. Это нужно для того, чтоб заменить стандартный контекст выполнения обработчика событий (элемент) на наш инстанс. IE8 не поддерживает этот метод, но я настоятельно рекомендую всегда использовать библиотеку es5-shim (в следующих статьях будет видно, чем это хорошо). Сам элемент я получаю, используя event.target, event.delegatedTarget, поэтому легко отказываюсь от стандартного контекста.

О будущих версиях

В следующих версиях матрешки, скорее всего, появится альтернативный путь добавления DOM события к привязанному элементу. Синтаксис:

this.$el().on( 'submit', function() {
...
}.bind( this ) );

или

this.$el( 'key' ).on( 'click', function() {
...
}.bind( this ) );

мне хочется немного улучшить. Планируется сделать так:

this.on( 'submit::__this__', function() {
...
});
this.on( 'click::key', function() {
...
});

(3) Здесь комментарий говорит сам за себя, с одной лишь ремаркой. Обратите внимание на последний аргумент, переданный в метод .on (со значением true). Это не контекст обработчика (так как тип — boolean), этот аргумент говорит нам о том, что обработчик следует запустить немедленно, сразу после объявления (то есть не требуется вызывать .trigger).

(4) При изменении свойств "userName" или "password" мы устанавливаем свойство 'isValid' в true или false, проверяя длину логина и пароля.

Добавляем в конструктор:

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

Теперь делаем метод .login, который, пока что, ничего никуда не отправляет:

	...
	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();
Весь код

var LoginForm = Class({
	'extends': MK,
	rememberMe: true,
	constructor: function() {
		this
			.initMK()
			.bindings()
			.events()
		;
	},
	bindings: function() {
		return this
			.bindElement( this, '.login-form' )
			.bindElement({
				userName: this.$( '.user-name' ),
				password: this.$( '.password' ),
				showPassword: this.$( '.show-password' ),
				rememberMe: this.$( '.remember-me' )
			})
			.bindElement( 'isValid', this.$( '.submit' ), {
				setValue: function( v ) {
					$( this ).toggleClass( 'disabled', !v );
				}
			})
		;
	},
	events: function() {
		this.$el().on( 'submit', function( evt ) {
			this.login();
			evt.preventDefault();
		}.bind( this ) );
		
		return this
			.on( 'change:showPassword', function() {
				this.el( '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();

Результат: jsbin.com/ATAPUCo/4/

В завершение

Я заметил интересный эффект. Несмотря на четырехлетний опыт программирования на Javascript, каждый раз, сталкиваясь с чужим кодом и библиотекой, на какой-то промежуток времени (от нескольких секунд до нескольких часов), я совершенно не понимаю, что в этом коде происходит. Но затем, прочитав больше строк или изучив документацию, то, что мне казалось непонятным, становится совершенно прозрачным.

С Матрешкой мой код стал на несколько порядков стабильнее. Ошибки, допущенные по невнимательности, почти исчезли. Мой клиент рад тому, что я стал больше делать за меньшее количество времени.

Что дальше?

Наверняка программисты со стажем заметили нелепую сборку данных в методе .login.

data = {
	userName: this.userName,
	password: this.password,
	rememberMe: this.rememberMe
};

Не лучше ли было бы создать инструмент, который знал бы, где у нас состояние приложения, а где данные? Отправлять все данные, которые у нас получились в классе, не очень разумно. Базе данных совершенно не интересно, показывается ли пароль пользователю или нет. Базе данных не нужно знать, валидны ли отправленные данные, так как сервер всё равно еще раз проверит их и отправит ответ. Решение этого вопроса я опишу в следующей статье, в которой расскажу о классе MK.Object.

Огромное спасибо за внимание. Удачного кодинга.

Автор: Finom

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js