niedziela, 21 sierpnia 2011

Własny EventListener

Kolejny raz w mojej pracy zaszła potrzeba zaimplementowania EventListener`a. Kiedy widzę, że muszę napisać drugi raz ten sam kod zawsze refaktoryzuję ten istniejący tak aby wspólną jego część przenieść do jednego niezależnego miejsca w kodzie.
Ewentualne zmiany kodu w więcej niż jednym miejscu nie wróżą niczego dobrego. Pozwólmy aby nasza dalsza praca nad kodem była miła i przyjemna. JavaScript doskonale nam w tym pomoże.

var EventListener = function(enames){
  var events = {};
  for( var i = 0; i < enames.length; ++i ){
    events[enames[i]] = [];
  }
  return {
    add: function(ename, fn){
      var that = this;
      var i = events[ename].push(fn) - 1;
      return {
        detach: function(){
          delete events[ename][i];
          return {
            attach: function(){
              return that.add(ename, fn);
            }
          };
        }
      };
    },
    remove: function(ename, fn){
      var that = this;
      delete events[ename][
        events[ename].indexOf(fn)
      ];
      return {
        attach: function(){
          return that.add(ename, fn);
        }
      };
    },
    invoke: function(ename){
      var arg = [].slice.call(arguments, 1);
      var res = [];
      for( var i in events[ename] ){
        if( events[ename].hasOwnProperty(i) ){
          res.push(events[ename][i].apply(this, arg));
        }
      }
      return res;
    },
    toString: function(){
      return '[EventListener: '+ enames.join(', ') +']';
    }
  };
};

Klasa EventListener pozwala nam na utworzenie własnego nasłuchiwacza zdarzeń. Dzięki niej w wystarczy, że w jednym miejscu aplikacji stworzymy kod odpowiedzialny za wykrycie jakiegoś zdarzenia, a w wielu innych dowolnych miejscach tylko podłączamy się do niego. Innych zalet powyższego wzorca projektowego chyba nie muszę tłumaczyć.
Obiekt zwracany przez metodę add posiada metodę detach, która pozwala odpiąć dodane zdarzenie. Ta z kolei zwraca obiekt zawietający metodę attach, która pozwala na powrotne podpięcie zdarzenia itd.
Podobnie jest z metodą remove naszej klasy. Z tą różnicą, że ta w pierwszej kolejności zwraca obiekt z metodą attach.
Metoda invoke oczywiście służy do wywołania funkcji podpiętych pod zadane zdarzenie. Pierwszym argumentem tej metody jest nazwa wywołanego zdarzenia. Kolejne argumenty są dowolne i opcjonalne. Będą one przekazane do każdej z podpiętych do zdarzenia funkcji.

czwartek, 18 sierpnia 2011

Object implicit

Object.prototype.implicit = function(obj){
  for( var i in obj ){
    if( obj.hasOwnProperty(i) && typeof this[i] === 'undefined' ){
      this[i] = obj[i];
    }
  }
  return this;
};

Całkiem przydatna funkcja umożliwiająca łączenie dwóch obiektów. Działa podobnie do już opisanej przeze mnie funkcji update. Różnica polega na tym, że metoda implicit nie nadpisuje już istniejących właściwości obiektu.

Animation, czyli jak sprawnie animować elementy strony

Poprzednio pisałem na temat animacji. Tym razem rozbudowałem tę funkcję o dodatkowe przydatne możiwości.

var Animation = function(fn, params){
  params = params || {};
  params = params.implicit({
    frames: 10,
    fn: function(){},
    t: 30,
    transition: null,
    at: 0
  });
  var frame = Math.round(params.at * params.frames);
  var progress;
  
  var run = (function(){
    return params.transition
      ? function(p){
          progress = p;
          fn.call(res, params.transition(progress), params);
        }
      : function(p){
          progress = p;
          fn.call(res, progress, params);
        };
  })();
  var iid = setInterval(function(){
    run( (++frame)/params.frames );
    if( frame >= params.frames ){
      clearInterval(iid);
      params.fn.call(res, params);
    }
  }, params.t);
  run( frame/params.frames );
  
  var res = {
    stop: function(execEndFn){
      execEndFn = execEndFn || false;
      clearInterval(iid);
      if( execEndFn ){
        params.fn.call(res, params);
      }
      return this.progress();
    },
    progress: function(){
      return progress;
    },
    toString: function(){
      return '[Animation: '+ Math.round(progress * 1e2) +'%]';
    }
  };
  return res;
};

Przede wszystkim funkcja (klasa) nie zwraca już identyfikatora zwracanego przez funkcję setInterval. Zamiast tego zwraca obiekt z dwiema (nie licząc metody toString nadpisującej tę domyślną dla obiektu) metodami.
Pierwsza metoda stop umożliwia zatrzymanie animacji. Wywołanie jej z pierwszym parametrem równym true spowoduje wywołanie również funkcji, wywoływanej w chwili normalnego zakończenia animacji. Przez normalne zakończenie rozumiemy odtworzenie animacji do końca.
Kolejna metoda zwracanego obiektu pozwala pobrać aktualny stan animacji z zakresu <0; 1>.
Dodatkowo do opisanych już wcześniej parametrów dodałem dwa następujące.
Transition - pozwala przekazać funkcję przejścia. Dzięki temu możemy w prosty sposób zrealizować np. łagodne rozpoczęcie i/lub zakończenie animacji.
Kolejny nowy parametr at jest nie mniej ciekawy. Pozwala bowiem on na rozpoczęcie animacji od zadanego etapu. Animacja z niezerową wartością dla tego parametru będzie trwała już krócej czyli nie będzie rozciągnięta na zadaną ilość klatek (frames) a zostanie ucięta, co pozwala na niesamowicie proste zaimplementowanie animacji, która może zostać w dowolnej chwili przerwana i odtworzona wstecz do punktu wyjściowego.

Na koniec dodam, że "klasa" korzysta z funkcji implicit, w celu ustawienia domyślnych parametrów wywołania funkcji.

niedziela, 14 sierpnia 2011

Object update

Object.prototype.update = function(obj){
  for( var i in obj ){
    if( obj.hasOwnProperty(i) ){
      this[i] = obj[i];
    }
  }
  return this;
};

Funkcja update pozwala w prosty sposób rozbudować lub nadpisać dowolne właściwości obiektu.
Ponieważ funkcja operuje na referencji do obiektu, oraz zwraca tę referencję możemy z niej skorzystać na dwa sposoby.

Pierwszy z nich polega na utworzeniu obiektu i nadpisaniu/rozszerzeniu jego metod/wartości.
var obj = {
  a: 1,
  b: 2
};

obj.update({
  b: 3,
  c: 4
});

console.log(obj);

Drugi sposób to wywołanie metody update na naszym obiekcie w chwili jego utworzenia i zwrócenie na jego miejsce nowego obiektu.
var obj = {
  a: 1,
  b: 2
}.update({
  b: 3,
  c: 4
});

console.log(obj);

startInterval czyli natychmiastowy setInterval

var startInterval = function(fn, delay){
  var arg = [].slice.call(arguments, 2),
      that = this;  
  var newFn = function(){
    fn.apply(that, arg);
  };
  newFn();
  return setInterval(newFn, delay);
};

Powyżej zamieszczam funkcję startInterval. Działa dokładnie tak jak setInterval (z wyjątkiem możliwości przekazania pierwszego parametru jako String) z tą różnicą, że pierwsze wywołanie funkcji następuje natychmiastowo.
Funkcja jest wygodną alternatywą dla setInterval. Dzięki niej nie trzeba w kodzie tworzyć dodatkowych nazw dla funkcji, aby wywołać ją, oraz przekazać jako parametr do setInterval.