Постове 6 - 10 от 11 с таг Control Depo

Apr 18

Наскоро (преди около месец де) излезе Prototype 1.6.1 RC1, основната цел на тази версия е съвместимост с ново излезлият IE8, и Element.Store ( за който бях писал преди време ).

Toчно за  Element.Store исках да драсна някои ред, защото изненадващо за този тип storage няма много материали в нет-а.

Основната идея на Element.Store е да може да се добавя информация (като обекти) към даден  елемент, без да се страхуваме от memory leaks. Това в Prototype е реализирано със следните методи (под $(element) имам предвид референция към prototype extend element):

  • $(element).getStorage() – директен достъп до Storage-а на даденият елемент,  той просто представлява един Hash в който се запазват всички обекти, които искаме да прикачим към елемента. Дефакто store/retrive се явяват shortcut-и на get/set методите на този Hash.
  • $(element).store(key, value) – записва информация в даденият елемент, ключът трябва да е string (или обект с метод toString), после когато искаме да си вземем информацията отново използваме ключа. Възможно е наведнъж да се запишат повече от една двойка ключ – стойност: $(element).store({ key1: ‘value1′, key2: ‘value2′ /* и така на татък */});
  • $(element).retrieve(key, defaultValue) – извлича информация от елемента по даден ключ, като ако не съществува такъв ключ се връща  defaultValue.
  • вероятно $(element).unsetStorage() (ticket | gist) – този метод все още го няма, но е много вероятно да се появи във финалната 1.6.1 версия, той трябва да премахва цялата информация за дадения обект от Storage-a му. Ако искате в момента да ползвате unsetStorage може да вземете моята примерна версия от тук.

Как обаче това би се ползвало в реалния свят ? Първо най-вероятно във финалната версия 1.6.1 цялата event система на Prototype ще използва Element.Store. А аз ще си позволя да дам един малък пример за това как и къде се ползва Element.Store:

Имаме примерно масив с футболисти на Арсенал, и на страницата имаме бутони, чийто id-та се образуват от ‘button_’ + номера на футболиста. Целта е при натискане на бутон за даден футболист да се показва името му с alert.

var footballers = [
	{number: 14, firstName: 'Theo',     lastName: 'Walcott'},
	{number: 12, firstName: 'Carlos',   lastName: 'Vela'},
	{number: 23, firstName: 'Andrei',   lastName: 'Arshavin'},
	{number: 4,  firstName: 'Francesc', lastName: 'Fabregas'},
	{number: 5,  firstName: 'Kolo',     lastName: 'Toure'},
	{number: 25, firstName: 'Emmanuel', lastName: 'Adebayor'}
];

Общо взето без имаме 4 възможни варианта:

// Вариант 1: използваме closure за достъп до футболиста
footballers.each(function(f){
	$('button_' + f.number).observe('click', function(){
		alert('#' + f.number + ' ' + f.firstName + ' ' . f.lastName);
	});
});

// Вариант 2: използване на скит closure
function show(f){
	alert('#' + f.number + ' ' + f.firstName + ' ' . f.lastName);
}

footballers.each(function(f){
	$('button_' + f.number).observe('click', show.curry(f));
});

// Вариант 3: използване на референция в самия обект
function show(){
	var f = this.footballer;
	alert('#' + f.number + ' ' + f.firstName + ' ' . f.lastName);
}

footballers.each(function(f){
	var button = $('button_' + f.number);
	button.footballer = f;
	button.observe('click', show);
});

// Вариант 4: използване на Element.Store
function show(f){
	var f = this.retrieve('footballer');
	alert('#' + f.number + ' ' + f.firstName + ' ' . f.lastName);
}

footballers.each(function(f){
	$('button_' + f.number).store('footballer', f).observe('click', show);
});

Всички варианти работят и правят едно и също (или поне така си мисля :) ), но всеки си има и своите особености. Така Вариант 1 изглежда добър вариант и също така е доста се използва именно този метод, но това което ме притеснява е че анонимната функция към бутона се пре-декларира за всеки бутон (въпреки, че това пак зависи от интерпретатора). Също така при по сложни скриптове може да се получат обърквания кое от къде идва и на къде отива, а и от самия код не е много “чист”. Във Вариант 2 се използва curry, което прави кода малко по-чист, но въпреки, че не е веднага видимо пак се използва closure ( в curry метода ) и отново има генериране на доста анонимни функции.  Вариант 3 променя още структурата кода, като се маха curry, и се добавя референция към футболиста в самия обект, така не се генерират множество функции, но вероятността да се появят memory leak-ове е много голяма ( а и самия код изглежда най-зле от всички варианти ). За мен лично вариант 4 е най-удачния (особено при по-големи проекти), сега към обекта пак се записва референция към дадения футболист, но този път се използва безопасния Element.store .

Надявам се с този малък пример да съм показал част от възможностите който предоставя новия Element.Store механизъм и защо е полезен той :)

В заключения ще дам за пример с CD3.Select класа ми, който се използва за правенето на custom html select контроли. Като просто като се напише new CD3.Select(selectElement), автоматично selectElement-а се замества с група от div/ul/li елементи и така се дава възможност на дизайнерите ни в Pixeldepo, да ги настройват както намерят за добре (всъщност в моите ControlDepo 3 Widgets имам още цял арсенал от компоненти, с подобни функции). Та преди в CD3.Select за капсулирането на връзката между options обектите и техните производни li елементи използвах closure, като след появата на Element.Store промених нещата. При което се забелязах че доста по-добре и глатко  работят самите контроли и много по-малко памет се използва :)

Element.Store използвам още за Event.deletege, който много се надявам да попадне в core-a на Prototype.js и за който в следващата седмица мисля да напиша един пост.

Dec 01

Работейки по новата ми CMS система – Control Depo 3 ми се наложи да имам малко по advanced конфигурационен файл, така че за в бъдеще да ми е по-лесно да се наместват различните части на системата. Преди ползвах стандартното за едно PHP приложение – един файл config.php:

// start config
$_CONFIG = array();

// database
$_CONFIG['db_host'] = 'localhost';
$_CONFIG['db_name'] = 'project';
$_CONFIG['db_user'] = 'root';
$_CONFIG['db_pass'] = 'password';

// languages
$_CONFIG['default_language'] = 'bg';
$_CONFIG['laguages'] = array('bg' => 1, 'en' => 2, /* ... */);

// session
$_CONFIG['session_salt'] = 'SD23aeda';
$_CONFIG['session_expire'] = 4*60*60;
// ... и така много много реда код

После тази глобална променлива( $_CONFIG) когато ми трябва се вика със global и се ползва. Обаче от една страна че не е много красиво така написано, но от друга и това с global просто прави кода малко разхвърлян(на английски имат страхотна дума за това – messy) .

Това което ми трябваше основно е може да имам няколко enviroment конфигурационни файла подобно на Rails. Другото важно нещо беше да се побира в един екран, така че да не се налага да скролирам и да мога с един поглед да виждам всичко което ми трябва. И естествено да не е много сложно и да работи достатъчно бързо.

Първо погледнах Zend_Config, защо все пак са component-based-framework и може директно да видя как работи конфигурационната им система. И общо взето това което видях не ми хареса въобще. Много ми напомня на LEGOs, Play-Doh, and Programming от Jamis Buck, за която бях писал преди време(даже има я качена на видео в confreaks заедно с цялото rubyconf 2008).
От една страна колко пъти ще искам да ползвам XML и INI за конфигурация?! От една страна php си има array(), с която идеално може да се запишат всички неща който ни трябват и да са достатъчно четими. От друга самото четене на огромен xml/ini/yml/… файл отнема време и ресурси и са доста по-бавни от прост php код.

Добре че поне тук от Zend са сложили подразбиране да се ползва само php масив. Но от тук дойде 2рото ми учудване. Това а именно самата работа на имплементация на Zend_Config. Идеята е доста проста подава се масив, който се обгръща(wrap) от Zend_Config обект и след това се работи само със Zend_Config обект. Пример:

// Create the object-oriented wrapper upon the configuration data
$config = new Zend_Config(array(
'webhost'  => 'www.example.com',
'database' => array(
'adapter'   => 'pdo_mysql',
'params'    => array(
'host'      => 'db.example.com',
'username'  => 'dbuser',
'password'  => 'secret',
'dbname'    => 'mydatabase'
)
)
));

// Print a configuration datum (results in 'www.example.com')
echo $config->webhost;

// Use the configuration data to connect to the database
$db = Zend_Db::factory($config->database->adapter,
$config->database->params->toArray());

// Alternative usage: simply pass the Zend_Config object.
// The Zend_Db factory knows how to interpret it.
$db = Zend_Db::factory($config->database);

Идеята като цяло е много хубава, и при добро желание може човек да си направи основната част класовете му приемат Zend_Config обекти. Но има едно НО и то доста голямо. Защо ние е това? Единствения плюс който се сетих е че може да се направи immutable config обект. Но за сметка на това ще има доста излишен код и много памет за съхраниението на Zend_Config обекти и техните атрибути. А и колкото и да обичам __set/__get магиите тук ми се виждат напълно излишни защото както в примера:

$config->database->params->toArray()
// Това би представлявало нещо такова като backtrace
$config->__get('database')   // магически се търси атрибута 'database'
$config->_data['database']   // това което се връща тук пак е Zend_Config обект, който ще го нарека $object
$object->__get('params')     // пак същото за
$object->_data['params']     // друг $object
$object->toArray();          // автоматично всеки Zend_Config обект, който се стрещне му се вика Zend_Config::toArray()
$object->_data               // това за което ни трябва

Zend_Config си има и своите плюсове(immutable, секции, default стойности) и може за някое наистина “enterprise” приложение с много разчленена конфигурация (и много стабилен и мощен сървър :) ) да върши работа. Но не е за мен и аз си предпочитам “голите” array(). Все пак

No code is faster than no code

- някъде го чух това.

Така че какво реших ? Ами в предишния ми пост – PHP tips – include и return, описах метода за връщане на стойности от php файл и реших него да ползвам плюс малко правила и подрежанки. Сега имам следните файлове:

  • /config/config.php – тук ще е основната конфигурация
  • /config/enviroment/ – в тази папка ще се съдържат конфигурационните файлове за различните среди за работа, като стандартно имам 3 вида среди
  • /config/enviroment/development.php
  • /config/enviroment/test.php
  • /config/enviroment/production.php
  • /config/mailer.php – тук ще има конфигурация за mail системата, тя е изнесена в отделен файл, защото тук няма само да се връщат config данни, а и ще се вързва към smtp сървър (ако трябва), ще се настройват достъпи и други подобни неща. За разлика от другите конфигурационни файлове mailer.php се вика само когато се налага да се изпращат писма не по-рано.
  • /config/routes.php – това са настройките ми за различните пътища, подобно на Rails (пак)

Това са различните конфигурационни файлове (поне за сега).

return array(
'database'  => array(
'engine'    => 'mysql',
'host'      => 'localhost',
'name'      => 'project',
'user'      => 'root',
'pass'      => 'password'
),
'i18n'      => array(
'default'   => 'bg',
'languages' => array('bg' => 1, 'en' => 2, /* ... */)
),
'session'   => array(
'engine'    => 'cookie',
'salt'      => 'vW34Aaasa',
'expire'    => 4*60*60
), // на последния ред имам , защото при добавяне на нов ред да не ми се налага да я слагам
// а и SVN/Git ще го сметне като изтрит ред и после добавен ред
// ... още конфигурация ...
);
// /config/enviroment/development.php - примерно
return array(
'database'  => array(
'engine'    => 'mysql',
'host'      => 'localhost',
'user'      => 'root',
'pass'      => '',
'name'      => 'project_dev'
),
'smarty'    => array(
'compile_check'     => true,
'force_compile'     => false,
'debugging'         => false,
'caching'           => false,
'cache_lifetime'    => 0
),
'logging'           => true,
'display_errors'    => 1,
);

А в bootstrap-а имам просто това


// ENVIROMENT е просто константа в която казва в кой режим на работа е приложението
$_CONFIG = array_merge_recursive(include($_CFGDIR . '/config.php'), include($_CFGDIR . '/enviroments/' . ENVIROMENT . '.php'));

// ... код ...
// общо взето извличам всичко което ми трябва от $_CONFIG и го разпределям по обекти
// така че да не ми трябва повече $_CONFIG
// ... код ...

unset($_CONFIG /* заедно с още няколко вече не потребни ми променливи */);

Това решение ми се вижда най-елегантно, в моя случай. Може и да не може да се нарече “система” но е достатъчно надежно и ефективно да ми свърши работа. Все пак съм фен на Convention over configuration И ако ми се наложи да имам подобен на Zend_Config обект, с които да работя мисля да не променям основните неща, а просто този обект да използва ArrayAccess, но за това друг път, че сега май малко по-дълго стана от плануваното.

Надявам се някой да намери този пост полезен и ако съвети и идеи ще се радвам да ги чуя :) .

Oct 04

Днес имах малко свободно време и реших да погледна малко един проект, и по-точно javascript честа от него. И се спрях на един простичък Tab панел, нищо особено, но нещо не ми хареса как изглеждаше JavaScipt-а. HTML изглежда така:

<div class="tab-panel">
	<ul class="tab-header">
		<li class="selected">tab 1</li>
		<li>tab 2</li>
		<li>tab 3</li>
	</ul>
	<div class="tab-content">tab 1 content</div>
	<div class="tab-content" style="display: none;">tab 2 content</div>
	<div class="tab-content" style="display: none;">tab 3 content</div>
</div>

За да направя това таб панел използвах просто тази функция:

function tabulize(panel){
	// слагам div.tab-content вместо .tab-content,
	// защото когато селектора има само клас селектира всички елементи и ги проверява,
	// а ако се сложи div.tab-content се селектират се всички div-ове и те се проверяват,
	// което е по-бързо
	var elements	= panel.select('div.tab-content'),
		buttons		= panel.down('ul').select('li');

	// дефинирам тази функция тук,
	// защото искам да имам достъп до elements, buttons
	function activate(item, key){
		// тук 1во се маха клас selected от всички бутони,
		// и се скриват всичките div.tab-content
		buttons.invoke('removeClassName', 'selected');
		elements.invoke('hide');

		// после се показва избрания елемент и се слага клас selected на избрания бутон
		elements[key].show();
		item.addClassName('selected');
	}

	// на всеки бутон се добавя eventhandler за click,
	// който ще подава на activate избрания елемент и неговия номер във buttons масива
	buttons.each(function(item, key){
		item.observe('click', activate.curry(item, key));
	});
}

Това е доста простичко, но и трудно за промени като динамично добавяне на съдържание, ефекти и други. Затова реших да го направя малко по-OOП, като запазя основните идеи:

var TabPanel = Class.create({
	// това е конструктора
	initialize: function(panel){
		panel = $(panel);
		// избираме отново elements и buttons, но ги записваме като инстанс променливи
		this.elements = panel.select('div.tab-content');
		this.buttons = panel.down('ul').select('li').each(function(){
			// единствената разлика е че active не е private функция и инстанс метод
			// и за това тук ползваме bind, а не curry
			item.observe('click', this.activate.bind(this, item, key);
		}.bind(this));
	},
	// active, вече е метод и само "this." e разликата
	activate: function(item, key){
		this.buttons.invoke('removeClassName', 'selected');
		this.elements.invoke('hide');
		this.elements[key].show();
		item.addClassName('selected');
	}
});

Сега става малко по-тежко, но и доста по-extendable и податливо на бъдещи промени. Но тук видях нещо, което пак не ми се хареса е че колкото таб бутони имам толкова пъти и в двете версии викам observe и реших да видя как ще изглежда това, като добавим малко event-delegation, за което трябваха само две промени:

var TabPanel = Class.create({
	initialize: function(panel){
		panel			= $(panel);
		this.elements	= panel.select('div.tab-content');
		this.buttons	= panel.down('ul').select('li');
		// първо променяме тук
		// слагаме click eventhandler на ul (списъка с бутоните)
		// вътре проверяваме дали li елемента, който сме натиснали (ако има такъв)
		// е във масива с бутоните, ако е така активираме табулацията с този номер
		panel.down('ul').observe('click', function(e){
			var key = this.buttons.indexOf(e.findElement('li'));
			if (key != -1) this.activate(key);
		}.bind(this));
	},
	// второто нещо което променяме е тук
	// махаме item параметъра, и на го заменяме с this.buttons[key]
	activate: function(key){
		this.buttons.invoke('removeClassName', 'selected');
		this.buttons[key].addClassName('selected');
		this.elements.invoke('hide');
		this.elements[key].show();
	}
});

И поне на този етап съм доволен, при нужда мога да добавя destroy, addTab и други неща :) А как инстансирам TabPanel-а ? Ами със CD3.Behaviors:

CD3.Behaviors({
	'div.tab-panel': TabPanel
});
Oct 03

Наскоро направих, нещо което от доста време планирам – да събера публикувам част от JavaScript нещата които ползвам, като opensource. Така че ето ги ControlDepo 3 Widgets:

http://github.com/RStankov/controldepo-3-widgets/tree/master

Тук смятам да събера, основните JavaScript неща, които имам като custom form полета, ефекти, prototype допълнения и други подобни. И също така ще публикувам във блога и статии с които да описвам как се работи с дадения компонент (така най-сетне ще имам документация). И така първия и може би любимия ми :

CD3.Behaviors

CD3.Behavoirs e вдъхновен от CSS event:Selectors на Justin Palmer и LowPro на Dan Webb ( както и Behavior на  Ben Nolan, който вече го няма). Общо взето от около една и половина го ползвам и развивам и мисля че доста полезен. Работата на CD3.Behavoirs, условно се разделя на 4ри части:

нормален селектор | event-селектори | event-delegation | инстанциране на класове

Нормалния селектор просто селектира определения css селектори и вика върху всеки елемент подадената функция, като съответния елемент  се предава като първи аргумент и също така функцията се bind-ва към него т.е. като в подадената функция this е този елемент за който тя се извиква. Ето един малък пример:

CD3.Behaviors({
  '#container1': function(){
    this.insert('<strong>текст</strong>');
  },
  '#container1 a': function(a){
    a.observe('click', function(){ alert('#container1 a clicked'); })
  },
  '#container1 span': function(){
    alert('This will not excecute');
  }
});

Горния пример  има 3 селектора:

  1. #container1 – избира елемент с id = container1 и после в него добавя <strong>текст</strong> вътре в него
  2. #container1 a – избира всеки а елемент от container1 и добавя click event към него ( тук а се подава като аргумент, но може да се напише и this.observe )
  3. #container1 span – просто показва че ако няма елемент отговарящ на селектора, функцията не се вика

Event-селекторa разглежда селектора на 2 части {селектор част}:{event част}. Като намира всички елементи от дадения селектор и им добавя event listener-и (чрез Event.observe). Действията са много подобни на тези на CSS event:Selectors на Justin Palmer, да не кажа същите, с някои подобрения.

CD3.Behaviors({
  '#container2 a:click': function(){
    this.toggleClassName('clicked')
  },
  '#container2 a.mouse': {
    mouseover: function(){
      this.innerHTML = 'mouseover';
    },
    mouseout: function(){
      this.innerHTML = 'mouseout';
    }
  }
});

Какво става тук:

  1. #container2 a:click – избира всички а-та от #container2 и им добавя при click event да си сменят className -а
  2. #container2 a.mouse- е малко по-интересно. То селектира всички а-та с клас “mouse” в #container2 и започва да наблюдава (observe) две действия:
    • mouseover – просто добавя текст “mouseover” във а-тa
    • mouseout -просто добавя текст “mouseout” във а-то

Горния пример със нормален селектор би изглеждал така:

CD3.Behaviors({
  '#container2 a': function(a){
    a.observe('click', function(){
      this.toggleClassName('clicked');
    });
  },
  '#container2 a.mouse': function(a){
    a.observe('mouseover', function(){
      this.innerHTML = 'mouseover';
    });
    a.observe('mouseout', function(){
      this.innerHTML = 'mouseout';
    });
  }
});

Но просто има твърде много излишен код тук, а и в като се пише JavaScript – the size matters!

Event-delegation – тук става малко по “сложно” … за обяснение.  По принцип Event-delegation-a е доста лесен, и особено при по-динамични и натоварени javascript приложения си е задължителна практика. И понеже аз доста често го използвах реших да го вкарам във CD3.Behaviors като ползвам нещо подобно на Event.delegate от LowPro. Като основната ми цел беше да го “скрия” така да не се натрапва и според мен стана доста добре:

CD3.Behaviors({
  '#container3:click': {
    'span': function(){
      alert('span was clicked, span innerHTML is "' + this.innerHTML + '"');
    },
    'a': function(){
      alert('link was clicked, span innerHTML is "' + this.innerHTML + '"');
    },
    'div': function(){
      alert('nothing was clicked');
    }
  }
});

Така, какво става тук ? Ами, със нормален event-selector избираме #container3 и му добавяме click event. Само, че когато се натисне #container3 започва да се проверяват подадените селектори – span, a, div, в случая, и когато натиснатия елемент отговаря на някоя селектор се вика съответната функция, който е била зададена към селектора ( като даже и scope-a на функцията се сетва да е съответния елемент, така че ако е натиснат ‘span’ елемент this ще е този елемент)

По начина по който съм направил event-delegation-a може да се пишат и такива неща:

CD3.Behaviors({
  '#container3': {
    mouseover: {
      'span': function(){
        this.addClassName('clicked');
       },
       'a': function(){
         this.addClassName('clicked');
       }
    },
    mouseout: {
      'span': function(){
        this.removeClassName('clicked');
      },
      'a': function(){
        this.removeClassName('clicked');
      }
    }
  }
});

Което е все едно да имаме 2 event-селектора – #container3:mouseover и #container3:mouseout, но горния код е доста по-бърз защото селектираме само ведъж #container3 :) да не говорим че е и доста по ясен.

Инстанциране на класове. Едно от най-яките неща в LowPro бяха Behavoirs класовете, обаче така и не ги използвах никъде, а и предпочитам да ползвам нормални prototype класове. Затова направих най-нормалното, което ми се виждаше, да направя всеки нормален prototype клас (без да променям нищо в prototype) да работи със CD3.Behaviors:

var TestWidget = Class.create({
  initialize: function(element, options){
    this.value = options || 0;
    element.observe('click', this.click.bind(this));
  },
  click: function(){
    alert('widget with ' + this.value + ' was clicked');
  }
});
CD3.Behaviors({
  '#container4 a.first': TestWidget,
  '#container4 a.second': [TestWidget, 5]
});

Това, може да се напише и по този начин :

CD3.Behaviors({
  '#container4 a.first': function(){
    new TestWidget(this);
  },
  '#container4 a.second': function(){
    new TestWidget(this, 5);
  }
});

Единственото, лошо тук е че [TestWidget, 5] не може да приема (за сега) повече от един параметър, който играе ролята на options. А и повечето Widget класове, които ползвам са само със element и options аргументи.

Това горе-долу са основните части от CD3.Behaviors. В идните сигурно ще напиша как работи CD3.Bahaviors.when и как аз обикновенно използвам CD3.Bahaviors.

Jul 14

Преди доста време попаднах на това:

http://www.jaisenmathai.com/blog/2008/04/17/why-everyone-should-write-a-framework-and-never-use-it/

Там става дума за това, че човек не трябва да разчита само на готово, а  трябва да от време на време да пробва да напише нещо сам, даже и никога да не ползва това което е написал. Аз лично, толкова пъти съм писал код за PHP, JavaScript, Ruby, просто така за кеф. Но някои скриптове написани по този начин са доста по-добри от тези който правя на работа притиснат от време и ограничения. Защото пишейки спокойно и за удоволствие можеш да се съсредоточиш върху решаването на един проблем и да измислиш по-нестандартно и добро решение от колкото в нормална ситуация. А и така наистина разбираш проблема и можеш да разбереш чуждите решения как са направени и защо.

От около 5-6 месеца работя над поредния Rails клонинг – Contol Depo Engine, който ще бъде използван в CMS-a Control Depo 3. Поне 4-5 пъти го пренаписвах изцяло, защото не се ползваше никъде и беше правен само за удоволствие. Но нещата който научих докато го правя ме направиха по-добър девелопър и самия framework стана доста добре.