Backbone Form’da İlk Geçersiz Elemana Gitmek

Önyüzünde Backbone kullandığımız projeye eklenmesi gereken basit bir özellik var.

– Formda 12 tane alan vardır bunların *,*,* alanları zorunludur.
– Kullanıcı formu doldurur.
– Form geçerli ise listeleme sayfasına yönlendirilir.
– Geçersiz ise ilk hatalı input viewport’a gelir (scroll yapılır).
– Hem desktop hem mobilde aynı akış sağlanmalıdır.

Ne mobilde ne de masaüstü sürümde (responsive, esnek tasarım) form elemanının yerini tespit edip scroll yapamayacağımıza göre en güzel çözüm o elemanı aktif hale getirmek, yani “focus” event’i tetiklemek. Backbone size form yapısı sağlamıyor, fakat @powmedia’nın formu hemen hemen bütün ihtiyaçları karşılıyor.

Sistemde kullandığımız hiç bir kütüphaneyi abstract bir katman eklemeden kullanmıyoruz. Dolayısı ile kendi oluşturduğumuz Form, Backbone.Form u extend eden abstract katman. Yapımız AMD (Asynchronous Module Definition) ve dependency manager olarak RequireJS kullanıyoruz. Bunlarla birlikte Backbone bağımlılıkları olan jQuery, underscore gibi kütüphaneleri de sayfaya yüklememiz gerekiyor.

Çalıştığımız dizine RequireJS, Backbone ve BackboneForms u koyduktan sonra BackboneForms’dan kalıtım alan modulümüzü şöyle yazıyoruz:

Not: Ön bilgi vermekte fayda var, sayfamızda Backbone.Form github’daki powmedia’nın geliştirdiği formdur. Radiobox, selectbox gibi form elementleri editors namespace’i altında toplanmış. Siz de kendi form element türlerinizi geliştirebilirsiniz. Mesela yukarıdan gelen bir başka istekde bir sistemdeki tüm fiyat belirten inputların başına para cinsi, sonuna da “.00” şeklinde küsüratı sabitlememiz istenmişti, biz bunu Backbone.Form.editors.PriceField şeklinde tanımladık, formların şema tanımlamalarında da “type: ‘Text'” yerine “type: ‘PriceField'” yazarak basitçe çözdük.

Tüm form elementleri Backbone.Form.Field‘i extend ediyor. Backbone.Form.editors namespace’i altında da üst paragrafda bahsettiğim form eleman türleri var. Yani checkbox’ı override etmek için Backbone.Form.editors.Checkboxes, veya hidden input’u yeniden tanımlamak için Backbone.Form.editors.Hidden methodunu ezmeniz gerekiyor.

Bu blog yazısında bahsettiğimiz yer Backbone.Form.Field methodunu yeniden tanımlamak. Fakat ileriye dönük farklı ihtiyaçları da karşılayabilmek için biz burada Backbone.Forms’u da boş şekilde extend ettik.

/**
 * Author: Irfan Durmus 
 * Date:   Thu Apr 3 12:03:32 2014 +0400
 *
 * Customized Backbone Form Asynchronous Module in order 
 * to provide atuo-focus to first error field in Backbone Forms.
 */
define([
    'backbone',
    'backbone-forms'
], function(
    Backbone,
    BackboneForms
){
    // we'll need this variable to count how many element we have in our form.
    var counter,

    // extend Backbone.Form element to implement new features in the future
    Form = Backbone.Form.extend({});
    
    Form.Field = Backbone.Form.Field.extend({
        /**
         * Overwrite the setError method of Backbone.Form.Field
         * to focus first element when error occur
         */
        setError: function(msg) {
            Backbone.Form.Field.prototype.setError.call(this, msg);
            this.hasError = true;
            counter++;
            this.findError();
        },
         
        /**
         * Overwrite the clearError method of Backbone.Form.Field
         */
        clearError: function() {
            Backbone.Form.Field.prototype.clearError.call(this);
            this.hasError = false;
            counter++;
            this.findError();
        },
        
        findError: function() {
            var totalSize = _.size(this.form.schema),
                focusField = null;

            if (counter == totalSize) {
                // set counter 0 to able to manage next form action;
                counter = 0;
                _.each(this.form.fields, function(field, key, fields) {
                    if (!focusField && field.hasError) {
                        focusField = field;
                    }
                });
                if (focusField) {
                    focusField.focus();
                }
            }
        }

    });
    
    // return and use this extended form class instead of Backbone.Forms;
    return Form;
});

Form’u post ettiğimizde, form her bir elemanı için, clearError veya setError methodlarını çağırıyor. Biz yine formun sağladığı methodları static olarak Backbone.Form.Field.prototype.clearError.call ile çağırarak normal davranışını bozmadık. Sonra hata olup olmadığını anlamak için hasError’u set ettik (sanırım formda zaten var bu, satırı silip denemedim). Her bir form elemanı için her iki method’dan birisi çağrıldığına göre counter++ ile iki methodda da artırım yapmamızın bir saknıcası yok. Son olarak da kendi tanımladığımız findError methodunu çağırıyoruz.

Not: Diğerleri nasıl çözüm üretmiş diye araştırdığımda ingilizce kaynaklarda bile object orient bir çözüm olmaması çok garip gelmişti.

Frontend’e “pure” yaklaşım ve mimari üzerine

Web uygulamalarında frontend, uygulamanızın en kolay “çığrından çıkacağı” kısım. Uygulamanın veritabanı ve sunucu tarafını klasik yazılım geliştirme yöntemleri ile tamamlayabiliriz. Markup kalitesini yüksek tutmak, taşınabilirliği ve performansı yüksek uygulamalar yazmak farklı çözümler gerektiriyor.

Yakın zamanda tamamladığımız bir projenin önyüzü Symfony 1.4 üzerinde templateler ile oluşturulmuştu. Fakat geliştirmeler o kadar javascript bağımlı ki tarayıcı kısa sürede tıkanıyor, bir hatayı gidermek bazen işkenceye dönüşüyor, çoğunda da uzun zaman alıyordu. İş geliştirme’den gelen isteklerden biri “Facebook Wall”‘a bir diğeri de “Linkedin Profil” sayfasına benzer istekler. Tasarımlar ve işlevler farklı aslında fakat daha populer sitelerden daha benzer örnekler bulamadım. Özetle proje yüksek oranda javascript bağımlı sayfalardan oluşuyor.

Projenin sonuna yakın dahil oldum, ne kadar “yanlış yapılmış, düzeltmeliyiz” desem de anlatamadım derdimi, uzun bir süre “jquery eklentileri” ile çabaladıktan sonra “tamam, frontend mimarisini değiştirecez” kararını aldırdım.

İki parça “demo” yaptıktan sonra ekip üyelerinin ortak fikirleri: “Backend’deki bu böyle olmaz dediğimiz herşeyi sildik, frontend yazmıyoruz sanki backend’in kötü kısmını siliyoruz”.

Yanlış açıkca ortadaydı, “projenin JavaScript bağımlılık oranı çok yüksek” markup’ı javascript ile çizersek hem backend’i temizlemiş, hem de taşınabilir bir uygulama elde etmiş olacaz. Frontend’de oluşacak memory leakleri de büyük oranda engellemiş olacağız. Arama motoru derdimiz yok, içerik sadece üyelere açık olacak.

Yol Haritası ve Karar : Object Orient JavaScript (Pure)

Projeye başlamadan önce disiplini sağlayabilmemiz ve geliştiriciler arasında kod standartını sağlayabilmek için bazı kurallar koyduk. Projeye başladığımızda izleyeceğimiz yol:

– Kayıt, giriş gibi statik sayfalar geleneksel yolla oluşturulacak, markup, data ile birlikte sunucudan gelecek.
– DOM ile ilişiği olan her bir “obje”nin “.element”i olacak
– DOM elementleri (mümkün ise) tekrar kullanılabilir olacak.
– Her class/modul ayrı bir dosyada tutulacak.
– ‘new’ ile çağırılacak her bir class ‘B’üyük harf ile başlar
– Private methodlar ve değişkenler ‘_’ ile başlar
– Bize bir dependency manager lazım! (app.provide ve app.depends methodları)
– Basit bir araç kutusu lazım. DOM, ajax, extend, interface gibi.
– Uygulamanın her bir parçası ayrı derlenecek (Uygulamanın boyutu 5MB’ye ulaşırsa?).
– Her bir sayfaya tekil bir kimlik verilecek (userRegister, userWall, userProfile, userInbox gibi).
– Javascript kısımını 3 ayrı katmanda çözeceğiz. (Yazıda bu katmanları detaylandırmayacağım Core (çekirdek), Application (uygulama), Implementation (entegrasyon)).
– Tarayıcıya sunucudan HTML olarak sadece temel layout gelecek.
– Tüm sayfaların entegrasyon kodu sayfaya özel oluşturduğumuz isim ile aynı isimde olan ayrı bir dosyada olacak.
– Uygulamanın parçaları manalı namespace’ler altında toplanacak.
– Uygulamanın hiç bir parçası global scope’a çıkmayacak.

Şu an hatırladıklarım bunlar, bir kaç madde daha vardı sanırım. Yazıyı mümkün olduğunca kısa tutabilmek için oluşturduğum 2 demo’dan sadece birinden bahsedeceğim.

Diyagramlar (Wireframe)

İlk yaptığım demo aşağıdaki gibi bir menü.

Request, Notification ve bir tane daha menümüz var (üçüncüyü çizime gerek duymadım). İleride dördüncü/beşinciyi eklemeyeceğimizin, veya olanlardan birini çıkartmayacağımızın garantisi yok.

Menü’nün temel özellikleri:
– Kullanıcı menüye tıkladığında kırmızı daireler rakamlarıyla birlikte kaybolsun, yeni bir uyarı/istek var ise tekrar görünsün.
– “Notification” menüsünde uyarı yeni ise farklı arkaplan rengi ile belirtilsin
– Kullanıcı “action’lardan” birine tıkladığında farklı mesaj gösterebilelim.
– Zaman objeleri kendini güncelleyebilsin. (Bu yazıda bu modulden bahsetmeyeceğim)

UML’e geçmeden önce menüyü mantıksal parçalara ayırıp namespace’ler ile birlikte isimlendirelim.
Mavi renk app.menu namespace‘ini gösterir.
Sarı renk app.navigation namespace‘ini gösterir.

Gördüğünüz gibi tasarımımız iki farklı namespace altında. Bunun amacı, ileride “notifiable” fakat farklı görünen (facebook’da sol tarafda bulunan, groups, favorites, apps gibi bir menü düşünün) bir menü istediğimizde aynı yapıyı kullanabilmek istiyoruz. JavaScript “class less” olduğu için bundan sonra “class” yerine “constructor” olarak isimlendireceğim.

Constructor Diyagramı

Evet, henüz kod yazmıyoruz, biraz daha sabır. Şimdi constructor’ları UML diyagramı ile belirleyelim. Tabii ki abstract class veya parent class görevini üstlenen constructor’larımız da olacak. Interface implementasyonlarını, testleri burada pas geçiyorum. Yazının sonunda neleri atladığımızı bulabilirsiniz. Önce diyagramı görelim, sonra hangi pattern’leri nerelerde kullandığımızı açıklayalım.

notifiable.Container constructor’ın tam path’i app.menu.notifiable.Container, notifiable.Item da aynı şekilde app.menu.notifiable.Item olarak okuyun. Sadeleştirmek adına tam yol’u yazmadım. Diğer tüm constructor’lar app.navigation.* namespace’i altında bulunuyor. (UML’i hatırladığım kadarıyla çiziyorum, hata varsa bildirebilirsiniz)

 

 

Kullandığımız Tasarım Desenleri (Design Patterns)

Notifiable Menu

Composite ve Virtual Proxy

notifiable.Container ve alt objeleri olan notifiable.Item composite pattern ile oluşturduk. notifiable.Item yapılandırılırken ‘this.menuEL’e gerekli ‘click’ event’ini bağlıyor. Oluşturduğumuz objeyi Container’a argüment olarak geçiyoruz. Burada aynı zamanda virtual proxy kullandık. Kullanıcı tıklayıncaya kadar richMenu.Container çalıştırılmıyor. Aynı zamanda ‘this.notify’ (şu okunmamış mesaj numarası)’i güncellemek için sunucuyu dinliyoruz. Bu dinlemeyi de notifiable.Container’da yapıyoruz, her bir element için ayrı ayrı dinleme yapmıyoruz.

Rich Menu

Genel yapı için Composite ve Bridge, butonlar için Command

richMenu.Container diğer constructor’lar gibi sadece html elementden oluşmuyor, aslında manager bir constructor. Menü daha açık mı kapalı mı, daha önceden çalıştırılmış mı yoksa ilk defa mı çalıştırılıyor bu objede tutuyoruz. Bu class’ın open/close/update/destroy gibi method’ları da var. app.menu.richMenu.types.Notification menümüz bir “tür”, aynı constructor’dan kalıtım alan bir diğer constructor’da aynı namespace altındaki Request constructor’ı. İki menünün ortak özellikleri de var, farklı özellikleri de. Her ikisinin de “leaf” (yaprak, composite patterndeki en küçük birim)larını tutan “children” değerleri var. Bunun yanında RequestHeader class’ı sadece “Request” menüde çalıştırılıyor.

Command pattern’i butonlarda uyguladık. Her bir butonun “gotAction”  method’u var. Karmaşık kısmı burada yapıp, click event’e bu methodu bağlıyoruz. Bu sayede yeni bir tür “action” oluşturduğumuzda “gotAction” yok ise Interface (diyagramda yok) aracılığıyla hata fırlatıp developer’i “gotAction” methodu oluşturması için uyarabiliyoruz.

Bridge pattern’i neredeyse bu tasarımın heryerinde kullandık. “this.init” methodları ile constructor’dan sonra oluşturulması gereken objelerin constructor’larını burada çağırdık.

Constructor’lar

Ana konumuz mimari olduğu için anlaşılabilir olması açısından detaylara girmeden sadece UML diyagramında göründüğü kadar kod yazacağız.

app.navigation.notifiable.Container

app.provide('app.navigation.notifiable.Container');
app.navigation.notifiable.Container = function(menus, parent) {
  this.element = document.createElement('ul');
  this.menus = menus;
  this.parent = parent;
  // burada daha fazla yapılandırıcı kod var 
};
app.navigation.notifiable.Container.prototype = { 
  updateNotify: function(num){
    // "notify" rakamı güncelliyoruz
    // composite patten'e örnek görebilmeniz için sadece bu methodu implement ettim.
    var i;
    for (i in this.menus) {
      if (this.menus[i].hasOwnProperty(i)) {
        this.menus[i].updateNotify(num);
      }
    }
  },
  clearNotify: function(){ 
    // "notify" deki rakamı kaldırıyoruz 
  },
  setNotify: function(){
    // notify alan ekliyoruz 
  }
  // burada daha fazla prototype methodu var. 
};

app.navigation.notifiable.Item

app.provide('app.navigation.notifiable.Item');
app.navigation.notifiable.Item = function(classNames) {
  this.element = document.createElement('li');
  this.element.className = classNames;
  this.notifyNum = 0;
  // burada daha fazla yapılandırıcı kod var 
};
app.navigation.notifiable.Item.prototype = { 
  updateNotify: function(num){ 
    this.notifyNum = num;
    this.notify.innerHTML = num;
  },
  clearNotify: function(){ 
    this.notify.style.display = 'none';
    this.notifyNum = 0;
  },
  setNotify: function(){
    this.notify = document.createElement('span');
    this.element.appendChild(this.notify);
  }
  // burada daha fazla prototype methodu var. 
};

app.menu.richMenu.* namespace’i altındaki yapılandırıcılara (class veya constructor) geçmeden önce yukarıdaki kodun entegrasyon kodunu verelim. Bu constructor entegrasyon katmanında olduğu için UML diyagramına koymadım.

Örneğin sayfamız kullanıcı profil sayfası olsun, sayfamızdaki body elementinin id’si “userProfile” olarak ele alalım. Bu durumda implementasyon kodumuz şu şekilde olacaktır:
app.pages.userProfile

app.provide('app.pages.userProfile');
app.depends('app.navigation.notifiable.Item', 'app.navigation.notifiable.Container');
app.pages.userProfile = function() {
  var parent = $('header')[0];
  new app.navigation.notifiable.Container([
    new app.navigation.notifiable.Item('Request'),
    new app.navigation.notifiable.Item('Notification')
  ], parent);
  // burada daha fazla entegrasyon kodu var
}

Yukarıdaki gibi sayfaya özel kodları app.pages namespace’i altında tutuyoruz. Sayfa yüklendiğinde bu namespace ile body id’yi map eden bir objemiz var. O da bu yazıda yer almıyor maalesef. Ayrıca yukarıdaki örnek kodlarda gördüğünüz app.depends ve app.provide gibi oluşturduğumuz methodları da bu yazıda vermiyorum. app.provide verilen parametredeki namespace’i oluşturuyor. app.depends de verilen parametrelerdeki constructor’ları aynı namespace yapısı altındaki dosyaları sunucudan çekmekle yükümlü. Dosya organizasyonumuz da aslında bu namespace’ler ile aynı dizin yapısı içerisinde tutuluyor. Detaylara takılmadan “mimariye” odaklanmaya çalışın.

app.menu.richMenu.* namespace’i altında yazacağımız constructor’larımız da şu şekilde:

app.menu.richMenu.Container

app.provide('app.menu.richMenu.Container');
app.menu.richMenu.Container = function(parent) {
  this.element = document.createElement('div');
  this.parent = parent;
  this.initialized = {};  // oluşturulmuş menüleri burada tutuyoruz
  this.status = null;  // burada da açık menü'nün tekil kimlik ID'sini tutuyoruz.
  // burada çok daha fazla constructor kod var 
};
app.menu.richMenu.Container.prototype = {
  init: function(){
    // oluşturulacak menü ye switch/case ile burada karar veriyoruz
  },
  open: function(){
    // "rich menü" yü açacak kodlar burada
  },
  close: function(){
    // daha iyi anlaşılabilmesi için bu methodu implement ettim
    // "rich menü" yü kapatacak kodlar şöyle:
    this.status = 'closed';
    this.element.style.display = 'none';
    return this;
  },
  destroy: function(){
    // oluşmuş bir menü'nün tüm event'lerini ve elementlerini kaldır
  }
};

app.menu.richMenu.types.Abstract
Bu constructor’ımız “parent” olduğu için this.element tanımlamıyoruz. Bu constructor’ı extend eden “child” de this.element tanımlayacağız. Burada parent olarak UL tanımladık. Bu constructor zaten küçük, tamamını veriyorum:

app.provide('app.menu.richMenu.types.Abstract');
app.menu.richMenu.types.Abstract = function(container) {
  this.parent = document.createElement('ul');
  this.parent.className = 'menuContainer';
  this.menuItems = [];
  container.appendChild(this.parent);
};
app.menu.richMenu.types.Abstract.prototype = { 
  init: function(){
    // bu methodu her bir alt constructor'da tekrar tanımlamak gerekli,
    // developer'i bir hata fırlatarak uyarıyoruz:
    app.error('Parent class does not support this operation: Called: ',
               arguments.callee, ' Caller: ',  this.constructor);
  }
};

Sıra geldi menü türlerimizi tanımlamaya. Şimdilik iki tür menümüz var, biri Request diğeri Notification.
Daha sonra RequestHeader yapılandırıcısını görüp menü elementlerini oluşturacak constructor’lara geçeceğiz.
app.menu.richMenu.types.Request

//namespace'i olutşturuyoruz
app.provide('app.menu.richMenu.types.Request');
// bağımlılıkları belirliyoruz
app.depends(
  'app.menu.richMenu.items.Request', 
  'app.menu.richMenu.types.Abstract', 
  'app.menu.richMenu.types.RequestHeader'
);
app.menu.richMenu.types.Request = function(){
  // RequestHeader'i henüz oluşturmadık Notification'dan sonra oluşturacağız.
  this.header = new app.menu.richMenu.types.RequestHeader(this.element);
}
// extend methodu da kendi yazdığımız bir method, bu yazıya dahil değil.
app.extend('app.menu.richMenu.types.Request', 'app.menu.richMenu.types.Abstract'); 
app.menu.richMenu.types.Request.prototype.init = function(){
  //ajax request yap ve dönen data için app.menu.richMenu.items.Request'i oluştur.
  // bu method'u developer unutursa "parent"den hata mesajı gösteriyoruz.
};

app.menu.richMenu.types.Notification
Bu constructor’ımız da neredeyse Request ile aynı, şöyle ki:

app.provide('app.menu.richMenu.types.Notification');
// bağımlılıkları belirliyoruz
app.depends(
  'app.menu.richMenu.items.Notification', 
  'app.menu.richMenu.types.Abstract'
);
app.menu.richMenu.types.Notification = function(){};
app.extend(
  'app.menu.richMenu.types.Notification',
  'app.menu.richMenu.types.Abstract'
);
app.menu.richMenu.types.Notification.prototype.init = function(){
  //ajax request ve diğer işlemler burada
};

Notification menümüzde başlık yoktu fakat Request menümüzde bir başlık var. Şimdi bu constructor’ı yazalım:
app.menu.richMenu.types.RequestHeader

app.provide('app.menu.richMenu.types.RequestHeader');
app.menu.richMenu.types.RequestHeader = function(){
  this.element = document.createElement('div');
  this.element.className = 'requestMenuHeader';
  // burada daha başka constructor işlemleri yapılıyor
};
app.menu.richMenu.types.Notification.prototype = {
  // burada bazı prototype methodları var
};

Evet, iki farklı menümüzün farklı yapıları olduğu için farklı dosyalar da farklı constructor ile menümüzü neredeyse tamamladık. Sonraki adım olarak *.items.Abstract, *.items.Request, *.items.Notification constructor’larını oluşturuyoruz:

app.menu.richMenu.items.Abstract

app.provide('app.menu.richMenu.items.Abstract');
app.menu.richMenu.items.Abstract = function(){
  // daha fazla detay kod ile kafa karışıklığına sebep olmayalım.
  // buralar hep constructor kod
};
app.menu.richMenu.items.Abstract.prototype = {
  // burada bazı prototype methodları var, setTime, setUserName, setActions gibi.
  // bir tane örnek yazalım;
  setAvatar: function(){
    this.avatar = new Image();
    this.avatar.src = this.data.user.avatar;
    this.avatar.className = 'richUserMenuAvatar';
    this.element.appendChild(this.avatar);
    return this;
  }
};

app.menu.richMenu.items.Request

app.provide('app.menu.richMenu.items.Request');
app.menu.richMenu.items.Request = function(){
  this.setAvatar().setTime().setUsername();
  // gibi parent constructor'dan gerekli methodları çağırıyoruz
  // diğer constructor methodlarımız da burada.
};
app.extend(
  'app.menu.richMenu.items.Request',
  'app.menu.richMenu.items.Abstract'
);
// burada prototype'a ihtiyacımız yok.

app.menu.richMenu.items.Notification

app.provide('app.menu.richMenu.items.Notification');
app.menu.richMenu.items.Notification = function(){
  // Bu menümüzde sadece tarih ve text objemiz var. 
  // Request constructor'da olduğu gibi burada sadece onları çağırıyoruz.
};
app.extend(
  'app.menu.richMenu.items.Notification',
  'app.menu.richMenu.items.Abstract'
);
// burada da ekstra prototype methoduna ihtiyacımız yok

Son kısıma geldik, action’lar. Hani şu “connection” isteği geldiği zaman kabul veya red ile cevap verebileceğiniz butonlar. Parent constructor’ımız şöyle:
app.menu.richMenu.actions.Abstract

app.provide('app.menu.richMenu.actions.Abstract');
app.menu.richMenu.actions.Abstract = function(){
  this.element = document.createElement('a');
  // burada event binding işlemlerini felan yapıyoruz ki, 
  // developer yeni buton oluşturduğunda onClick 
  // methodunu eklemesi gerektiğini söyleyelim.
};
app.menu.richMenu.actions.Abstract.prototype = {
  onClick: function(){
    app.error('Parent class does not support this operation: Called: ',
               arguments.callee, ' Caller: ',  this.constructor);
  },
  onSuccess: function(){
    // göstermek istediğimiz ön tanımlı uyarı mesajını burada implement ediyoruz.
  }
}

app.menu.richMenu.actions.Accept

app.provide('app.menu.richMenu.actions.Accept');
app.menu.richMenu.actions.Accept = function(){
  this.element = document.createElement('a');
  // burada event binding işlemlerini felan yapıyoruz ki, 
  // developer yeni buton oluşturduğunda onClick 
  // methodunu eklemesi gerektiğini söyleyelim.
}
app.extend(
  'app.menu.richMenu.actions.Accept',
  'app.menu.richMenu.actions.Abstract'
);
app.menu.richMenu.actions.Accept.prototype.onClick = function(){
  // burada methodun implementasyonu var
}
app.menu.richMenu.actions.Accept.prototype.onSuccess = function(){
  // Eğer bağlantı isteğini kabul etmişseniz, objemizin görünüşü, 
  // state'i değişiyordu. 'X artık arkadaşınız' gibi bir mesaj 
  // gösteriyoruz. Bu entegrasyonu burada yapıyoruz.
}

app.menu.richMenu.actions.Ignore

app.provide('app.menu.richMenu.actions.Ignore');
app.menu.richMenu.actions.Ignore = function(){
  this.element = document.createElement('a');
  // app.menu.richMenu.actions.Accept'deki aynı işlemleri Ignore için yapıyoruz
};
// Parent constructor'dan kalıtım alalım
app.extend(
  'app.menu.richMenu.actions.Ignore',
  'app.menu.richMenu.actions.Abstract'
);
app.menu.richMenu.actions.Ignore.prototype.onClick = function(){
  // onClick de yapılacak işlemler
}
app.menu.richMenu.actions.Ignore.prototype.onSuccess = function(){
  // ajax request sonrası neler yapacağız?
}

 

Bir gün yeni özellik gerekirse?

Yeni bir menü eklemek

Kullanıcı güvenlik ayarlarını değiştireceği yeni bir menümüz istendiğini varsayalım. Yazının başındaki şemalara tekrar göz atın, şimdi yanına bir menü daha eklemek istiyoruz. UML diyagramımızı şu şekilde değiştirdik:

Javascript UML

Kırmızı renk ile gösterilmiş constructor’ları ekledik, şimdi bu iki constructor’ı yazalım:
app.menu.richMenu.types.Settings

app.provide('app.menu.richMenu.types.Settings');
// bağımlılıkları belirliyoruz
app.depends(
  'app.menu.richMenu.items.Settings', 
  'app.menu.richMenu.types.Abstract'
);
app.menu.richMenu.types.Settings = function(){};
app.extend(
  'app.menu.richMenu.types.Settings',
  'app.menu.richMenu.types.Abstract'
);
app.menu.richMenu.types.Settings.prototype.init = function(){
  //ajax request ve diğer işlemler burada
};

app.menu.richMenu.items.Settings

app.provide('app.menu.richMenu.items.Settings');
app.menu.richMenu.items.Settings = function(){
  // constructor kodlar burada
};
app.extend(
  'app.menu.richMenu.items.Settings',
  'app.menu.richMenu.items.Abstract'
);

Bu yeni “settings” menüsünü kullanıma sunmak için implementasyon kodunda

new app.navigation.notifiable.Container([
    new app.navigation.notifiable.Item('Request'),
    new app.navigation.notifiable.Item('Notification')
  ], parent);

yerine

new app.navigation.notifiable.Container([
    new app.navigation.notifiable.Item('Request'),
    new app.navigation.notifiable.Item('Notification'),
    new app.navigation.notifiable.Item('Settings')
  ], parent);

yazıyoruz. Evet sadece bir satır.

Yeni bir action eklemek

Bir süre sonra menü ile ilgili yeni bir geliştirme isteniyor. Kullanıcı isteklerinin gönderildiği “Request” menümüzdeki Accept, Ignore butonlarının yanına ek olarak Block seçeneği eklememiz gerekiyor. UML’i aşağıdaki şekilde güncelliyoruz:

JavaScript UML

Mavi renk ile gösterilen constructor’ımızı yazmamız yeterli olacaktır.

app.menu.richMenu.actions.Block

app.provide('app.menu.richMenu.actions.Block');
app.menu.richMenu.actions.Block = function(){
  this.element = document.createElement('a');
  // burada yine event binding işlemlerini felan yapıyoruz
}
app.extend(
  'app.menu.richMenu.actions.Block',
  'app.menu.richMenu.actions.Abstract'
);
app.menu.richMenu.actions.Block.prototype.onClick = function(){
  // click methodu
}
app.menu.richMenu.actions.Block.prototype.onSuccess = function(){
  // success de ne yapıyoruz? 
}

Sonuç

Projede javascript bağımlılığı ajax/göster/gizle/slider gibi basit olsaydı bu tür bir mimariye hiç gerek kalmayacaktı. AngularJS veya EmberJS ile de bu kadar büyük bir projeyi tamamlayabilirdik. Fakat yine backend’de bir sürü markup oluşacak, ayrıca bu kadar “temiz” bir kod elde edemeyecektik. Performans ve taşınabilirlik konusunda da bu kadar yarar sağlayamayacaktık.

Ne elde ettik

Bakım: Bakımı yapılabilir, tekrar kullanılabilirliği yüksek, dökümantasyonu tam, sürdürülebilir bir ürün elde ettik. Yeni bir menü eklemek için bir constructor oluşturup, bağımlılığını singleton factory’e eklemek yeterli oldu. Yeni bir “rich menu” için ise aynı interface’leri kullanarak, diğerlerini bozma endişesi olmadan yine bir constructor ile çözebiliyoruz.

Taşınabilirlik: Bir süre sonra Symfony 1.4 den 2.0 a sadece 1 gün, evet şaka değil, sadece 1 gün de tüm frontend’i taşıdık. İleride bir gün çok bilen bir arkadaş “PHP kötü yeaaa hadi backend’i Java yapek!!1!” derse frontend de yine 1-2 günlük işimiz olacak.

Düşük library bağımlılığı: Kendi oluşturduğumuz “araç kutusu”nda bazı eksik yerleri gördük, eksikleri tamamlamak zaman alacaktı, tüm sistemi üç dört günde google closure a geçirdik. Biz sistemi oluştururken bilmiyorduk fakat closure’da bizim ile aynı yolları izliyormuş, bazı yerlerde oluşturduğumuz değişken, method isimleri, temel classların bile aynı olduğunu gördüğümüzde sadece bizim değil, google’ın da aynı metodolojiler ile çalıştığını farkettik, mutlu olduk.

Performans: Yaptığımız testlerde V8 ile (ajax request’ler hariç) hiç bir işlem 1 ms (yazıyla bir) den fazla sürmüyor. SpiderMonkey ile de 2 ms (yazıyla iki)’nin altında. (300-400 ms’e kadar kabul edelibelir olmasına rağmen).

Hata ayıklama: “Frontend çalışmıyoeaea” diyen testçimize, bir cookie set edip testleri tetiklemesini istedik. Eğer bizim maketlediğimiz json dosyaları ile tüm testlerden “pass” aıyorsak “backend’ci naber, hade iş başına” dedik. Hata giderme süreçlerimiz çok daha hızlandı.

Hızlı iş teslimi: “Facebook Wall”ı (sol menü ve header değil, ortadaki stream kısım) sadece 3 günde hemen hemen tüm özellikleriyle 1 developer yazdı (Aslında daha gelişmiş bir wall yazdık). Toplamda Wall’ın testi ve tamamlanması 1 developer ile sadece 5 gün sürdü. Post, comment, share, post ve comment için like buton, like edenleri göster, resim, video veya link önizleme, parent-child yorum sistemi vs. vs.

Çoklu dil desteği: Sunucudan sadece gerekli “json data objeleri” çekiyoruz, çoklu dil desteğini de frontend de tamamladık. (başka bir yazının konusu)

Nelerden bahsetmedik

Konu zaten uzun, daha da uzatmamak için hiç bahsetmediğim, eksik bıraktığım yerler var, izlediğimiz yol yazıda bahsettiğimden biraz daha detaylı;

– Buraya kadar FAZ1’di, FAZ2’den bahsetmedik.
– FAZ2’de facade pattern arkasına yerleştirdik ve addMenuItem(), bindMenuItem(‘ThirdMenu’) şeklinde menüleri oluşturduk.
– ajax, dom, extend ve interface için “light” bir library oluşturduk,
– app namespace’i altındaki “depends” methodu ile bağımlılıkları çözdük,
– Her bir constructor’un bir interface’i var aslında, interface’i implement etmiyor ise hata fırlatıyoruz.
– UML diyagramından hemen sonra testleri oluşturduk,
– Her bir ajax request için bir .json dosyası bıraktık sunucuya (mocked up!!),
– Backend’ci arkadaşlar sadece bu json’larda beklenen verileri sağladılar,
– Backend’e bıraktığımız .json dosyaları tüm “case”leri test edebilecek kadar detaylı,
– UML diyagramları çok daha detaylı, methodlar, değişkenler, bağlanacak ve dinlenecek event’leri belirledik,
– Deployment ve sürdürebilirlik için bir strateji geliştirdik,
– Dökümantasyonu JSDoc ile oluşturduk,
– Kaynak kodu web’den ulaşılamayacak bir yere koyduk, sadece derlenmiş javascript dosyalarına ulaşılabiliyor.
– Cache invalidate için sürüm numaralarını kullandık.
– JS yazarken constructor ve implementation katmanlarından değişik class’lar atadık, css hiç yazmadık.
– HTML ve JS tamamlandıktan, testlerden “pass” aldıktan sonra css uyguladık, js yazarken sitenin tasarımını görmemiştik bile.
– Sürümleme için <major>.<minor>.<bugfix> gibi bir yapı kullandık (1.0.0 gibi),
Boss‘u özelleştirdik, projeye özel scriptler ekledik, boss ile deploy ettik.

Yazımıza son verirken esenlikler (şaka şaka, trt-1 miyiz biz esenlikler felan) “happy coding” diyoruz.

Not: Yazı çok uzun olduğu için çok iyi toparlayamadım, arada bug çıkabilir, kodlarda syntax hatası olabilir. UML çizimleri sıkış tıkış olmasının sebebi yazıya tam boyut ile sığdırmaya çalıştığım için, kusura bakmayın. Diğer çizimlerin çirkin olmasının sebebi ilkokuldaki resim öğretmenimin yüzünden.

15 Dakikada Frontend Yorumu

Ne kadar iyi olsanızda iş görüşmesinde kendinizi anlatabilmeniz işi ne kadar iyi bildiğinizden daha önemli kısımdır. Yazılım sektöründeki diğer uzmanlık alanlarına göre (embeded, desktop, elektronik vs.) bizim alanımızda daha zordur. Web’in kolay ulaşılıbilirliğinden kaynaklanan “msn hekleme maceraları”ndan yetişen, cms/blog kurup kendini yazılımcı olarak gören çok insan var. Dubai’de henüz masanın karşı tarafında bulunmadım fakat bir kaç arkadaştan duyduğuma göre Türkiye’den çok farklı değilmiş.

Sizin farkınızı ortaya koymanız gerekir. “Teknik eleman işe alım süreçleri”ne hakim birisiyle iş görüşmesi yapıyorsanız işiniz daha kolay, seviyenize göre daha mantıklı sorular yöneltilecek, sizi paniğe sürüklemeyecektir. Yaptığım onlarca iş görüşmesinden Senior PHP Developer olarak başvurduğum birinde bana çift tırnak ile tek tırnak farkı sorulmuştu. Yazılımcıda lazım olan bu bilgimidir yoksa hızlı ve doğru algoritma ile kodu spagetti ye götürmeden maintainable çözüm üretmesi midir? Diğer sorularda bundan farklı değildi. Seviyenin üzerinde soru soruluyorsa “bilmiyorum” dersiniz, bu bile bir çözümdür. Fakat seviyenizin altında gelen sorular nasıl bir takım içerisinde çalışacağınız hakkında sizde kötü izlenim bırakacaktır.

Dubai’de bir iş görüşmesinde önce genel müdür ile görüştüm, sonra backend ile ilgili teknik bir görüşme yaptık. Arkasından benden siteyi incelememi ve frontend kısımda neler yapabileceğimizi ertesi gün göndermemi istediler. Aslında en sevdiğim yöntem bana test-case göndermeleridir. Fakat bu yöntem de fena değil. Önünüze bir ürün koyuyorlar ve “hatası ne, neleri geliştirebiliriz” diyorlar. Büyük bir sistemde backend hakkında bir şeyler söyleyebilmeniz için bir süre çalışmanız gerekir. Frontend bu kadar zor değil. Gösterilen siteye 15 dakika göz attım ve aşağıdaki maddeleri çıkarttım. Bu maddelerin bazıları Türkiye’deki bir çok büyük sitede olan eksikler. Frontend kısımda neler/nasıl geliştirilmelidir hakkında belki size bir fikir verir.

Gönderdiğim e-mailden doğrudan copy/paste, çevirmiyorum. Şimdi düşündüğümde bu liste 25 e kadar uzar. Çift yazılanlar var fakat 15 dakika inceleyip, 5 dakikada yazma şartını bozmak istemediğim için listeyi değiştirmek, gramer hatalarını düzeltmek istemedim.

1- Reduce JS / CSS file calls
2- Move all of the inline css/javascript codes to css/js files
3- Get pass all YSlow tests
4- Reduce event binding (Use delegation)
5- Reduce traffic beetween javascript and layout engine.
6- Put all the script tags before closing body tag. (Bottom of the page)
7- Use nonblocking pattern to load javascript files. (asynchronous)
8- No “error tracking code” for frontend errors? How we’ll be notice if someone has an issue in different case? We cant test 10 different OS and 100 different browser.
9- 1 year expire date is too much, it should be determined depends to deployment strategy. It can be cause some problem in some users browser.
10- Write OOP Javascript in module pattern.
11- Prepare a deployment script that it will able automaticly obfuscate and minifiy all js and css files.
12- Use jslint or jshint to improve frontend code quality.
13- Dont use any js line of codes which is invisible by javascript garbage collector. (Firefox using too much memmory because of this)
14- Before start to write js codes, we should write test codes. Use BDD (Behaviour Driven Development)
15- Use module pattern for different part of web pages.
16- jQuery is good for newbie’s, but it is not good for extreme js development. Be careful when you need jQuery!
17- Cache all jQuery selectors.
18- Apply all of the advices in the slide: http://www.slideshare.net/irfandurmus/javascript-performance-optimisation

 

Siz 15 dakikada bir site hakkında neler söyleyebilirsiniz?
Happy Coding!

Bu yolların ustasıyım firefox un hastasıyım!

Twitter’a yazdığım bi status facebook a sıçradı. Daha sonra olan oldu….

Vukuya saik olan status mesaj; (Aynı anda kaç tane dil kullanarak cümle kurdum ben bile bilmiyorum..)

İrfan : Bu yolların ustasıyım #firefox un hastasıyım!

Yuxel : kız istedim vermediler, sen opera’cısın dediler!

İrfan : Vermezler o kızı sana, #firefox hasta ona..

Yuxel : opera’ya can feda, firefox’a elveda

İrfan : Chrome bile aldı elinden şekerini, Firefox yer onun kırmızı tekerini.

(daha&helliip;)

Firefox button taginda padding ve border sorunu

Daha önce yaşadığım ve yaklaşık 30 dakikama mal olan bir problemdi bu. Aynı problemle bir başka projede tekrar karşılaştım, not almak gerekliymiş demekki -)

Button tag’ını severim kullanırım. Button tagı sayesinde;

  • input[type="submit"] gibi css atraksyonlarına gerek kalmaz.
  • button{} selector’ü input[type="submit"]{}
    selector’ünden daha hızlıdır.
  • Form’larda post/reset gibi event’lar için kullanılabilir.
  • Tag içerisinde html kullanılabilir.
  • Projenin tamamında kullanılacak buton görselleri tek etiket ile şekillendirilir.
  • Daha kolay bir regex ile tüm buton taglarını grep’leyebiliriz.
    • Button tag’ını grep’lemek için;
      $ grep -rHn '<button.*>' *
    • Input butonları grep’lemek için;
      $ grep -rHn '<input.*type=\"submit\".*' *

Firefox button tagına padding ve border -input button a da- uyguluyor. Chrome, Safari, Opera gibi tarayıcılarda bu tür bir problem ile karşılaşmadım.

Çözüm;

button::-moz-focus-inner {
border: 0;
padding: 0;
}

Şöyle ecnebice kaynaklarda var;

http://forum.userstyles.org/comments.php?DiscussionID=2773
http://www.aheadcreative.com/removing-unwanted-button-padding-in-firefox/
http://stackoverflow.com/questions/1433232/moz-focus-inner

Opera ve Firefox’da body’e background bottom çalışmıyor mu ?

Evet az önce keşfettim bende. Bunca yıldır nasılda hiç başıma gelmedi çok şaşırdım.. Firefox, Opera ve diğer bazı tarayıcılarda yaşayabileceğiniz bir problem.

html body{
	background-image: url('/img/bg.jpg');
	background-position: bottom right;
}

Şeklinde kullandığınız css çalışmıyorsa bir satır daha (background-attachment:fixed;) ekleyin, css aşağıdaki gibi olsun.

html body{
	background-image: url('/img/bg.jpg');
	background-position: bottom right;
	background-attachment:fixed;
}

Hepsi bu