22 Haziran 2020 Pazartesi

BackboneJs dedikleri nedir

Selam ,

Önceki yazıyı bitirir bitirmez yeni yazım için sabırsızlıkla başlıyorum. Backbon.js web uygulama iskeleti.

Bu yazımı daha iyi anlayabilmeniz için biraz JavaScript ve biraz da jQuery bilmek yeterli olacaktır. Bu konuda daha önce yazdığım JavaScript'e Giriş ve JavaScript Orta Seviye yazılarım yeterli olacaktır.

Backbone.js ve Vue.js benim en favori JavaScript iskeletlerim. Çünkü anlaması ve kullanması kolay. Backbone.js hakkında da bir yabancı sitede çok yararlı olduğunu düşündüğüm bir döküman buldum ve bunun üzerinden gideceğim.

Backbone.js'de en sevdiğim özellik Node gerekmeden standart bir web sayfası olarak çalıştırabilmek. Node kullanmayacak mıyız? Biraz kullanacağız, ama sadece bize bir server ile test etmek gerektiğinde. O da daha bu yazının çok ilerilerinde olacak.

Backbone.js MVC yapılardan bir tanesi (Model View Controller). Bu yapıdan kısaca bahsedersek, model verilerin saklanması ve işlenmesini sağlar, controller program lojiğini içerir ve view kullanıcıya gösterilen görselleri içerir.


Backbone'da view ve controller parçaları bir arada gibidir. Ayrıca Router denilen bir yapı da var ki bununla tek sayfalık uygulamalar yapılabilir. Bunlar tek bir web sayfasından oluşan uygulamalardır. Bu halleriyle masaüstü uygulamasına benzerler. Bir örnek GMail internet sayfasına bakabilirsiniz.

Backbone temelde şu parçalardan oluşuyor,

  • Olaylar - Nesnelere belirlenen olaylar sonucunda işlemler yapabilme kabiliyeti kazandırır.
  • Modeller - Uygulama verilerini saklar ve bağlantılı lojiği içerir.
  • Kolleksiyonlar - Modellerden oluşan bir topluluk. Model ile belirlenen verileri saklamak için kullanılır.
  • Görseller - Modelleri kullanıcıya göstermek için gereken görsel kodlarını ve kullanıcı etkileşimi için olaylara dayalı lojiği kapsar.
  • Router'lar - Tek sayfalık uygulamalar için kullanılan program yönlendiricileri. Adres olarak değişik değerler vererek aynı sayfa içinde kullanıcıya başka şeylerin gösterilmesini ve işlevlerin değişmesini sağlarlar.


Kurulum


Peki nasıl kullanmaya başlayacağız. Önce öğrenirken kullanacağımız uygulama için yeni bir klasör oluşturalım. Bu klasör içinde index.html adında boş bir metin dosyası oluşturalım. Kodumuzun düzenli olması için yardımcı dosyaları saklamak için js , css ve images adında alt klasörleri ekleyelim.

Backbone.js'yi etkili kullanmak için yanında jQuery ve Underscore JavaScript kütüphanelerini de dahil etmemiz gerekiyor. Bu kütüphane dosyaları için js klasörü altında lib adında bir klasör ekleyelim ve onun içine indirelim. Bacbone.js kütüphanesi buradan indirilebilir. Underscore.js kütüphanesi de buradan indirebilirsiniz. jQuery kütüphanesinin ben bu yazıyı yazarken en son versiyonu burada verilmişitr. Bir de uygulamamızda kendi yazacağımız JavaScript kodları koymak için js klasörü içinde main.js adında boş bir JavaScript dosyası oluşturalım. Dosya yapımız şöyle olacak.

📂 app 
  📃 index.html
  📂 js
    📃 main.js
    📂 lib
      📃 backbone-min.js
      📃 jquery-3.5.1.min.js
      📃 underscore-min.js
----
Şimdi index.html dosyamızı bu JavaScript dosyaların tümünü içerecek şekilde hazırlayalım.

app/index.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Backbone Eğitimi</title>
    </head>
    <body>
        <h1>Backbone Öğreniyorum</h1>
        
        <script src="js/lib/jquery-3.5.1.min.js"></script>
        <script src="js/lib/underscore-min.js"></script>
        <script src="js/lib/backbone-min.js"></script>
        <script src="js/main.js"></script>
    </body>
</html>

Site görseli oluştuktan sonra JavaScript dosyalarını yüklemek sağlıklı bir alışkanlıktır.

Artık hazırız. İlk başlarda tüm öğrendiklerimizi main.js dosyası içinde ve tarayıcının geliştirici araçları bölümünde kodlayacağız. Temeller bitip görsellere başlayınca tekrar bu index.html dosyamıza dönüp güzel işler yapacağız.






Modeller


Modeller uygulama verilerini saklayan kutulardır. Standart JavaScript nesnelerinden farklı olarak verilerini işlemek için yöntemleri ve veri değişimlerini takip eden olay tetiklemeleri vardır. Ayrıca verilerin bir server'daki verilerle senkron olabilmesi için AJAX çağrıları yapabilme kabiliyetleri vardır.

jQuery'de kolaylaştırılmış olan bir server'a kayıt işlemini düşünelim,

$.post( "birUrl", {
    isim: "Ümit",
    soyad: "Kayacık"
}).done( function(data){
    console.log("Başarılı");
});

Bu işlemi Backbone model kullanarak yaptığımızda,

kişi.save({
    success: function() {
        console.log("Başarılı");
    }
});



Model oluşturmak

Modeller veri ve etrafındaki lojiği barındırırlar. Şimdi böyle öğreticilerde örnek verilirken kullanılan 3 ana veri örneği otomobiller, kitaplar ve şarkılar olarak tercih edilir. Biz burada şarkılar örneği üzerinden gideceğiz. Bir şarkının ismi, söyleyeni, yılı, kaç defa indirme yapıldığı gibi özellikleri olabilir.

main.js dosyamız içinde ilk modelimizi oluşturalım.

app/js/main.js
var Şarkı = Backbone.Model.extend();

Şarkı isimli değişkene Backbone.Model nesnesinin bir kopyasını aldık. Teamül olarak sınıflara büyük harflerle başlayan isimler vermeliyiz.

initialize özelliği modeli kullanarak yeni bir nesne üretildiğinde çalışacak fonksiyonu tanımlayacağımız yerdir. Bunu test etmek için özelliğe bir fonksiyon tanımlayalım.

var Şarkı = Backbone.Model.extend({
    initialize: function(){
        console.log("Yeni bir şarkı üretildi");
    }
});

Artık Şarkı modelini kullanarak yeni bir oluşum nesne üretildiğinde konsola mesaj yazarak işlemi bildirecek. Hadi bir yeni şarkı üreterek bunu tarayıcıda test edelim.

app/js/main.js
var Şarkı = Backbone.Model.extend({
    initialize: function(){
        console.log("Yeni bir şarkı üretildi");
    }
});

var şarkı = new Şarkı();

Şarkı sınıfından yeni bir şarkı nesnesini new metodu kullanarak ürettik. Şimdi tarayıcımızda index.html dosyasını açalım ve geliştirici araçları penceresini açarak konsola gelen yazıya bakalım.


Şimdi şarkı nesnesini inceleyelim. Geliştirici araçları konsolda şunu yazalım,

şarkı
r {cid: "c1", attributes: {…}, _changing: false, _previousAttributes: {…}, changed: {…}, …}

Gelen cevabın solundaki oka tıklayarak model nesnemizin özelliklerini inceleyebiliriz.



Özelliklerle çalışmak

JavaScript nesnelerin özelliklerinin tanımlaması ile Backbone modelin özelliklerinin tanımlanması farklı olur. Örnek olarak JavaScript bir nesneye özellik eklenmesi ve özelliğin okunması şöyle olur,

var kişi = {};
kişi.isim = "Ümit";
console.log(kişi.isim);

Backbone modellerinde özellik tanımlamaları nesnenin set metodu kullanarak yapılır.

app/js/main.js
var Şarkı = Backbone.Model.extend();

var şarkı = new Şarkı();
şarkı.set("isim", "Neden saçların beyazlamış arkadaş");
şarkı.set({
    söyleyen: "Tanju Okan",
    yılı: "1970'ler"
});

set metodu iki şekilde kullanılabilir. İlkinde özellik ve değeri tırnak içinde parametreler şeklinde veriliyor, bu şekilde bir tane özelliğe değer verilebilir. İkinci şekil ise aynı anda birden fazla değer vermek için hash şeklinde özellik ve değerlerin girilmesi. Burada dikkat edersek özellikler tırnak içinde yazılmıyor ve yanında : işareti var. Her özellik - değer çiftinin arasında da virgül ile ayrılmış. Bunu tarayıcıda deneyip konsolda nesnemizi ve özelliklerini görelim



Bu ikinci yöntemi nesneyi üretirken direk kullanarak kodu daha kompact bir hale getirebiliriz.

app/js/main.js
var Şarkı = Backbone.Model.extend();

var şarkı = new Şarkı({
    isim: "Neden saçların beyazlamış arkadaş",
    söyleyen: "Tanju Okan",
    yılı: "1970'ler"
});

Bu kod yukarıdaki ile aynı sonucu verecektir. Modelimizin JSON olarak dönüştürülmüş haline ise toJSON metodu ile erişebiliriz. Konsolda şunu yazalım,

şarkı.toJSON();
{isim: "Neden saçların beyazlamış arkadaş", söyleyen: "Tanju Okan", yılı: "1970'ler"}

Konsoldan sadece isim değeri olan yeni bir Şarkı nesnesi ekleyelim,

var yeniŞarkı = new Şarkı({ isim: "Hatasız kul olmaz" });

yeniŞarkı değişkenini incelediğimizde sadece isim özelliği olan bir nesne olarak göreceğiz.


Şimdi modeller üzerindeki birkaç metodu konsolda deneyelim. get metodu ile model nesnesi özelliklerinden birine ulaşırız.

şarkı.get("isim");
"Neden saçların beyazlamış arkadaş"

unset ile model nesnesi özelliklerinden birini yok ederiz,

şarkı.unset("isim");
r {cid: "c1", attributes: {…}, _changing: false, _previousAttributes: {…}, changed: {…}, …}
  attributes:
    söyleyen: "Tanju Okan"
    yılı: "1970'ler"
    __proto__: Object

clear metodu model nesnesinin tüm özelliklerini siler,

şarkı.clear();
r {cid: "c1", attributes: {…}, _changing: false, _previousAttributes: {…}, changed: {…}, …}
  attributes:
    __proto__: Object
  changed: {söyleyen: undefined, yılı: undefined}

Tüm özellikler yok oldu. Dikkat edersek changed özelliğinde de en son yapılan değişiklik gösteriliyor.

has metodu model nesnemizde bir özelliğin olup olmadığını sorma işine yarar.

şarkı.has("isim");
false

Bazen özelliğin tanımlanmadığı halde bile o isimde bir özellik olamasını isteriz (ki ben hep isterim). Bu özellikleri sınıf tanımlanırken defaults kısmında belirtiriz,

app/js/main.js
var Şarkı = Backbone.Model.extend({
    defaults: {
        tür: "Arabesk"
    }
});

var şarkı = new Şarkı({
    isim: "Neden saçların beyazlamış arkadaş",
    söyleyen: "Tanju Okan",
    yılı: "1970'ler"
});

Bu şekilde yaptığımızda eğer nesne üretilirken tür değeri girilmemişse bile değerini "Arabesk" yaparak default olarak nesneye ekler. Bunu tarayıcıda konsolda görelim. Sayfayı yenileyelim ve konsolda şarkı değişkenini soralım,

şarkı
r {cid: "c1", attributes: {…}, _changing: false, _previousAttributes: {…}, changed: {…}, …}
attributes:
  isim: "Neden saçların beyazlamış arkadaş"
  söyleyen: "Tanju Okan"
  tür: "Arabesk"
  yılı: "1970'ler"

Al işte , türünü bildirmeden şarkıyı tanımlarsan Tanju Okan şarkısı Arabesk oluverir 😊..




Validation - Doğrulama

Diyelim bir Şarkı nesnesi üretirken isim değerinin girilmesini ve eğer girilmezse nesnenin geçersiz olarak işaretlenmesini istiyoruz. Bu amaçla sınıf tanımında validate bölümünde kontrolü yapmalıyız. main.js dosyamızın içindeki kodu silelim ve bu konuyu görmek için deneme yapalım.

app/js/main.js
var Şarkı = Backbone.Model.extend({
    validate: function(attrs){
        if (!attrs.isim)
            return "İsim girilmesi gerekir.";
    }
});

attrs değişkeni aslında özellikler ve değerlerin yer aldığı bir JavaScript nesnesi. İçinde ne olduğunu görmek için konsolda şarkı.attributes; girmeniz yeterli. İşte bu özelliklerin içinde isim özelliği verilmemişse fonksiyon geriye bir mesaj döndürerek uyarısını yapıyor. Peki bu mesaj nerede, nesnemiz geçersiz olarak nasıl işaretleniyor görmek için bir tane model nesnesi üretelim.

var şarkı = new Şarkı();

Bu model oluşumunun geçersiz bir nesne olması gerekiyor çünkü isim değeri vermedik. Bunu tarayıcıda görelim.

şarkı.isValid()
false

isValid metodu false dönerek bize nesnenin geçersiz olduğunu bildiriyor.

şarkı.validationError
"İsim girilmesi gerekir."

validationError özelliği bize doğrulama hatasını bildirmek için girdiğimiz mesajı verir.

Şimdi şarkıya isim verip geçerliliğini tekrar test edelim.

şarkı.set("isim", "Neden saçların beyazlamış arkadaş");
r {cid: "c1", attributes: {…}, _changing: false, _previousAttributes: {…}, changed: {…}, …}
şarkı.isValid()
true

Gördüğümüz gibi artık geçerli bir nesnemiz var. validationError özelliğini sorarsak da artık null değeri alırız.



Kalıtım

Bildiğimiz gibi yeni modeli oluştururken BackboneModel sınıfından extend metoduyla oluşturuyoruz. Bu şekil bir sınıftan diğerini oluşturmaya kalıtım yoluyla sınıf üretmek denir. Kalıtım yoluyla yeni sınıf oluşturduğumuzda üst sınıfın tüm özellik ve metodları alt sınıfa da geçer. Bir örnek verelim.

app/js/main.js
var Hayvan = Backbone.Model.extend({
    yürü: function(){
        console.log("Hayvan yürüyor...");
    }
});

var Köpek = Hayvan.extend();

Hayvan sınıfının yürü metodu, ondan kalıtım yoluyla üretilen Köpek sınıfında da geçerli olacaktır.
Test edelim,

var köpek = new Köpek();
köpek.yürü();

Konsolda Hayvan yürüyor... yazısını görürüz. Ancak istersek kalıtım yoluyla üretilen sınıflar içinde üst sınıfın metodunu yeni üretilen sınıf için değiştirebiliriz. Buna metodun üzerine yazmak denir.

var Köpek = Hayvan.extend({
    yürü: function(){
        console.log("Köpek yürüyor...");
    }
});

Artık Hayvan.yürü() ve Köpek.yürü() metodları ayrı çalışıyor. Tarayıcıda kontrol edip görebiliriz.

Metod üzerine yazmak gerekli olduğunda genelde tamamı değiştirilmek istenmez, üst sınıfın metodu yine çalışsın ama ilave bir şeyler de olsun istenir. Bu durumda üst sınıfın metoduna ait kodu komple kopyalayacak mıyız? Hayır, bunun da bir yolu var,

var Köpek = Hayvan.extend({
    yürü: function(){
        Hayvan.prototype.yürü.apply(this);
        console.log("Köpek yürüyor...");
    }
});

Bu şekilde üst sınıftaki kodları yazmaya gerek kalmadan aynı kodların ama bu nesneye göre çalışmasını sağlamış oluruz. Sonra da yeni eklediğimiz kodlar çalışır.



Server'a bağlanmak

Bu modellerin içindeki veriyi saklamak için bize bir Rest API JSON server lazım. burada amacımız öğrenmek. Bu yüzden lokal çalışan bir JSON server kurmamız yeterli. Bunu nasıl yapacağımızı önceki yazımızda JavaScript orta seviye'de görmüştük. Tekrar edelim.

Sisteminizde Node yüklüyse çok kolay bir yolu var. json-server paketini yükleyip kullanabilirsiniz. Konsolda ,

npm install -g json-server

ve yüklendikten sonra web sayfamızın olduğu klasörde bir konsol açıp şu komutu girelim,

json-server --watch db.json --static ./

  \{^_^}/ hi!

  Loading db.json
  Oops, db.json doesn't seem to exist
  Creating db.json with some default data

  Done

  Resources
  http://localhost:3000/posts
  http://localhost:3000/comments
  http://localhost:3000/profile

  Home
  http://localhost:3000

  Type s + enter at any time to create a snapshot of the database
  Watching...

Bu server komutu ile bulunulan klasördeki db.json adındaki dosya veri dosyası olarak sunucuya çıkıyor. Bizim klasörümüzde bu isimde bir json veri dosyası olmadığı için örnek veriler ile otomatik olarak bu dosya üretiliyor. Bu dosyayı editörde açıp benzer şekilde kendi verilerimizi ekleyip testlerimizde kullanacağız.

Server çalıştıktan sonra artık sayfamıza http://localhost:3000 adresini tarayıcıya yazarak ulaşabiliriz.

Backbone modellerinin server ile etkileşimde kullandığı metodlar şunlar,

  • fetch() - GET metoduyla AJAX çağrısı yaparak veriyi server'dan okur.
  • save() - Eğer yeni kayıt ekleniyorsa POST metodu veya mevcut kayıt düzenlenip kaydediliyorsa PUT metodu kullanan bir AJAX çağrısı ile veriyi server'a kaydeder.
  • destroy() - DELETE metoduyla AJAX çağrısı yaparak kaydı server'dan siler.
Şimdi biz şarkılara takıntılı olduğumuza göre db.json dosyamızın içine örnek veri ekleyelim.

app/db.json
{
  "songs": [
    {
      "id": 1,
      "isim": "Neden saçların beyazlamış arkadaş",
      "söyleyen": "Tanju Okan"
    },
    {
      "id": 2,
      "isim": "Hatasız kul olmaz",
      "söyleyen": "Orhan Gencebay"
    }
  ],
  "posts": [
...

Dosyayı kaydettiğimizde Resources bölümüne ilave olarak http://localhost:3000/songs adresi şarkılara ait veri tablosuna ulaşmamızın yolu olarak bize gösteriliyor. Tarayıcıda bu adresi girersek bize tüm şarkı verilerini gösterecektir. Ama bize bu lazım değil modelimiz nasıl bu veriyi kullanacak? Önce modeli tanımlarken verileri alacağımız adresi bildirmemiz gerekiyor.

app/js/main.js
var Şarkı = Backbone.Model.extend({
    urlRoot: "/songs"
});

urlRoot özelliği ile modelin server bağlantı adresini bildiriyoruz. Bu adres internet üzerinde herhangi bir server'dan yayın yapan bir Rest API JSON server adresi de olabilir. Ancak biz test amaçlı lokal server adresimizi veriyoruz. Zaten internet üzerindeki server'lara ulaşmak için ilave güvenlik tedbirleri gerekiyor ama bu bizim konumuz değil. Şimdi id değeri 1 olan şarkıyı server'dan okuyalım,

var şarkı = new Şarkı({ id: 1 });
şarkı.fetch();

Bu http://localhost:3000/songs/1 adresinde sunulan şarkı verilerini alıp bizim şarkı nesnemize yükleyecektir. Tarayıcımızda bakalım.


Şimdi veride değişiklik yapıp kaydedelim,

şarkı.set("isim", "Yeni isim");
şarkı.save();

Tarayıcıda sayfayı yenilediğimizde artık ilk şarkının ismi değişecektir. db.json dosyasını açtığımızda bunu görürüz.

...
  "songs": [
    {
      "id": 1,
      "isim": "Yeni isim"
    },
...

Gitti güzelim kayıt, işin yoksa yine yaz.. save metodu bu örnekte PUT metoduyla AJAX çağrısı yaptı. Çünkü daha önce fetch ile server'dan okuduğumuz veriyi değiştirdik ve kaydettik.

POST metodu kullanan bir örnek için bunları silip yeni bir şarkı ve özelliklerini tanımlayıp save metoduyla kaydedelim.

app/js/main.js
var Şarkı = Backbone.Model.extend({
    urlRoot: "/songs"
});

var şarkı = new Şarkı({ 
    isim: "Bayıra karşı yatır beni",
    söyleyen: "Grup Vitamin" 
});
şarkı.save();

Yeni şarkımız db.json dosyasına eklenecektir,

app/db.json
...
    {
      "isim": "Bayıra karşı yatır beni",
      "söyleyen": "Grup Vitamin",
      "id": 3
    }
...

Son örneğimiz de destroy metodu kullanımı için,

var şarkı = new Şarkı({ id: 1 });
şarkı.destroy();

Al işte önce zavallı şarkının adını değiştirdik şimdide sildik gitti, sanki hiç olmamış gibi..

Çok gerekli bir şey olmasa da default olan id özelliğinin adını farklı bir isim yapabiliriz,

app/js/main.js
var Şarkı = Backbone.Model.extend({
    urlRoot: "/songs",
    idAttribute: "şarkıId"
});

var şarkı = new Şarkı({ şarkıId: 1 });
şarkı.fetch();

Bu kod şarkıId özelliğine değer girilmesine rağmen db.json dosyasından id değeri 1 olan kaydı getirecektir.




Görev fonksiyonları

Modellerin server ile haberleşmesi için olan rutinlere işlem başarılı olduğunda veya hata olup veriye ulaşılamadığında da çalıştırılabilecek fonksiyonlar tanımlanabilir. bu fonksiyonlar şöyle tanımlanır,

var şarkı = new Şarkı();
şarkı.fetch({
    success: function(){ ... },
    error: function(){ ... }
});

success özelliğinde tanımlanan fonksiyon server ile iletişim sağlıklı yapıldığında çalışır. error özelliğinde verilen fonksiyon is server ile iletişim başarısız olursa çalışacaktır.

fetch ve destroy metodlarında aynı yapı geçerlidir. Yani,

şarkı.destroy({
    success: function(){ ... },
    error: function(){ ... }
});

save metodu yapısı biraz farklıdır,

şarkı.save({}, {
    success: function(){ ... },
    error: function(){ ... }
});

Burada verilen ilk parametre değiştirmek istediğimiz özelliklerin yazılı olduğu bir hash nesnesi. Ama özellikler daha önceden belli olduysa buradaki gibi boş bir hash parametre olarak girilir (bir null nesnesi de parametre olarak verilebilir).

Şimdi küçük bir test yapalım

app/js/main.js
var Şarkı = Backbone.Model.extend({
    urlRoot: "/songs2"
});

var şarkı = new Şarkı({ id: 1 });
şarkı.fetch({
    success: function(){
        console.log("Yükleme başarılı");
    },
    error: function(model, cevap){
        console.log(cevap.statusText);
    }
});

urlRoot özelliğine yanlış değer verdiğimiz için hata olacak ve konsola Not Found mesajı gelecektir. urlRoot değerini hiç vermezsek script tamamen çökecek ve urlRoot değeri girilmesi gerektiğini belirten hata verecektir. Olmayan bir id değeri ile kayıt istersek yine Not Found cevabı gelecektir. Herşey doğru ve server da cevap vermişde Yükleme başarılı mesajı gelecektir.









Kolleksiyonlar


Kolleksiyonlar modellerin içine konduğu şeylerdir. Modeli daha önce verileri ve olayları içinde saklayan bir kutu gibi örnek vermiştik. Kolleksiyonlar da bu model kutularını sakladığımız depolar gibi düşünülebilir. Web dünyasından örnek vermek gerekirse Twitter, Facebook gibi sosyal medya ortamlarında listelenmiş olan her bir gönderi modeldir, sayfada gördüğümüz tüm gönderiler ise kolleksiyondur. 

Kolleksiyonlar da server ile etkileşimde bulunabilirler. Bunları da modeller gibi adım adım inceleyelim.


Kolleksiyon Tanımlamak

Modellerin Backbone.Model sınıfından üretilmesi gibi kolleksiyonlar da Backbone.Collection sınıfından kalıtım yoluyla üretilir. Bir örnek,

app/js/main.js
var Şarkı = Backbone.Model.extend();

var Şarkılar = Backbone.Collection.extend({
    model: Şarkı
});

Kolleksiyon tanımı yaparken model özelliğinde bu kolleksiyon içinde toplanan verilerin hangi model tipinde olduğunu veriyoruz.

Kolleksiyona yeni veriler ekleme işi iki şekilde yapılır. Ya kolleksiyonun oluşum nesnesi üretilirken bir array içinde ya da oluşum nesnesi üretildikten sonra add metodu kullanarak. Örnek,

var şarkılar = new Şarkılar([
    new Şarkı({ isim: "Şarkı 1" }),
    new Şarkı({ isim: "Şarkı 2" }),
    new Şarkı({ isim: "Şarkı 3" })
]);

şarkılar.add(new Şarkı({ isim: "Şarkı 4" }));

Bunu tarayıcıda test edelim, konsolda şarkılar değişkenini sorduğumuzda,


Gördüğümüz gibi models özelliği altında 4 eleman mevcut. Bunları da inceleyerek eklediğimiz modeller olduğunu görebiliriz.

at() metodu ile kolleksiyonun istediğimiz sıradaki elemanına erişebiliriz.

şarkılar.at(0);
r {cid: "c1", attributes: {…}, _changing: false, _previousAttributes: {…}, changed: {…}, …}

0 numaralı eleman yukarıda da gördüğümüz gibi ilk eleman. Burada kayıtların yanında cid diye bir özellik daha var. Bu özellik Client Id lafından geliyor ve Backbone tarafından veriliyor. Özellikle server'da veri saklanan durumlarda (ki zaten niye yapıyoruz bu işi), bir çok kullanıcı sisteme bağlıyken client taraftaki kayıtları ayrı id ile takip etmek gerekebilir.

Aynı modeli cid değerine göre getirmek için get metodu kullanırız,

şarkılar.get("c1");
r {cid: "c1", attributes: {…}, _changing: false, _previousAttributes: {…}, changed: {…}, …}

Kolleksiyondan bir modeli silmek istediğimizde remove metodunu kullanırız. Bu metod parametre olarak bizden silmek istediğimiz model nesnesini ister. Öyle ya bilmediğin şeyi niye silesin?


Gördüğümüz gibi cid değeri c1 olan model silindi. Artık şarkılar.at(0) modelinde cid değeri c2 olan model var.

Kolleksiyonlarda kullanılan bu metodların çoğu Underscore.js'den geliyor.



Kolleksiyonlarla çalışmak

Önceki bölümde kolleksiyona add metodu kullanarak yeni model nesnesi eklemeyi görmüştük. Metoda ikinci bir parametre vererek nesnenin nereye konulacağını belirtebiliriz. 

şarkılar.add(new Şarkı({ isim: "Şarkı 5", tür: "Caz", yüklemeler: 110 }), { at: 0 });

Bu durumda yeni şarkı modeli en sona değil ilk sıraya konacak, diğerleri bir öteye kayacaktır.


Standart push metodunu da kullanarak kolleksiyonun en sonuna yeni model nesnesi ekleyebiliriz.

şarkılar.push(new Şarkı({ isim: "Şarkı 6", tür: "Caz", yüklemeler: 90 }));

Şimdi kolleksiyon içinde aramaların nasıl yapılacağına bakalım. where ve findWhere metodları ile kolleksiyon içinde arama yaparız,

var cazŞarkılar = şarkılar.where({ tür: "Caz" });
var ilkCazŞarkı = şarkılar.findWhere({ tür: "Caz" });
console.log("Caz Şarkılar", cazŞarkılar);
console.log("İlk caz şarkı", ilkCazŞarkı);

where metodu seçime uyan metod nesnelerinden oluşan bir array dönerken findWhere metodu seçime uyan ilk model nesnesini geri döner. Sonucu tarayıcıda geliştirici araçları konsolda görelim.


Fİltreleme parametresi olarak birden fazla değer de verebiliriz. Mesela Caz şarkıları içinde ismi Şarkı 6 olan şarkıyı bulmak için,

var filtrelenmişŞarkılar = şarkılar.where({ tür: "Caz", isim: "Şarkı 6" });
console.log("Filtrelenmiş şarkılar", filtrelenmişŞarkılar);

Peki yüklenme sayısı 100'den çok olan şarkıları bulmak istersek nasıl bir yol izleriz? filter metodu ile aramamıza bir fonksiyon olarak istediğimiz kriteri katabiliriz.

var çokYüklenenler = şarkılar.filter(function(şarkı){
    return şarkı.get("yüklemeler") > 100;
});
console.log("Çok yüklenen şarkılar", çokYüklenenler);

Buna yükleme değil de indirme deseydik daha doğru olurmuş ama neyse oldu bi kere. Siz siz olun değişkenlere yanlış isim vermeyin.

Bu programda verilen fonksiyon aslında true ya da false değeri dönen bir fonksiyon. filter metodu tüm model nesnelerini geziyor, fonksiyondan true değeri dönen şarkıları sonuç array'ine ekliyor. İstediğimiz kriterleri karşılaştıran bir fonksiyon yazarak her türlü filtrelemeyi bu şekilde yapabiliriz. Bu metod da Underscore.js'den gelmeymiş, ne faydalı bir eser yapmışlar.

Ya, biz filtreleme falan yapmak istemiyoruz ama biz de böyle tüm model nesneleri üzerinde bir işlemler yapalım dersek each metodunu kullanırız.

şarkılar.each(function(şarkı){
    console.log(şarkı.get("isim"));
});

Bu kod bize tüm şarkıların isim değerlerini yazacaktır. each metodu, parametresinde verilen fonksiyona parametre olarak o anda sırası gelen model nesnesini verir. Böylece her bir model nesnesi için fonksiyon tekrarlanarak tüm kolleksiyon sıradan işlenir.



Server Bağlantısı

Kolleksiyonların server'a bağlanması modeller ile oldukça benzerdir. 

app/js/main.js
var Şarkı = Backbone.Model.extend();

var Şarkılar = Backbone.Collection.extend({
    model: Şarkı,
    url: "/songs"
});

var şarkılar = new Şarkılar();
şarkılar.fetch();

Kolleksiyonlarda url özelliği ile server bağlantısı adresi veriliyor ilk fark bu. Öncekinde urlRoot özelliği vardı. İkinci fark ise kolleksiyonun fetch metodu belirtilen kısıma ait tüm verileri çekiyor. Tarayıcıda kontrol edelim.







Görseller


Yarabbi şükür sayfaya gözle görülür birşeyler eklemeye sıra geldi. Görsel kodları modelleri kullanarak ve DOM elemanlarını işleyerek sayfada kullanıcıya görünenleri yayınlarlar. Sosyal medya örneğinden gidersek, gönderilerdeki yazılar, resimler videoların sayfada gösterini tıklanınca beğeni bildirilmesi , aşağıda yorum yapılacak kısım vs görsel kodunun yaptığı işler. 



Görsel tanımlamak

Backbone görsellerinin tek amacı sayfada gördüğümüzü üretmek değil, şunlar da var,
  • İçeriği göstermek - bu zaten bir görselden beklenen default iş.
  • DOM olaylarına cevap vermek - Tıklama , tutup çekme , veri girişi gibi olayları izlemek ve oluştuğunda isteneni yapmak.
  • MVC yaıdaki kontrolöre benzer - Standart MVC yapıdaki View'dan daha çok Controller'ın yaptıklarını yapar.
  • Her görsel bir DOM elemana bağlıdır - Görsel kodu göstereceği şeyleri bu eleman içinde gösterir.
Görseli tanımlamak modeller ve kolleksiyonlara benzer. Backbone.View sınıfından kalıtım yoluyla üretilir. İlk örnekle başlayalım,

app/js/main.js
var ŞarkıGörsel = Backbone.View.extend({
    render: function(){
        this.$el.html("Merhaba Backbone");
        return this;
    }
});

Bir şeyi öğrenirken ilk merhabayı bu kadar ilerledikten sonra yaptınız mı hiç? Neyse bir bakalım koda , render görselimizin yayınlama fonksiyonu. $el ise görselimizin bağlı olduğu DOM elemanı temsil eden bir jQuery nesnesi. render fonksiyonundan görsel nesnesini geri döndürmek ise bir jQuery teamülü. Böylece zincirleme şekilde metod çağırmaları yapabiliriz.

Şimdi görsel sınıfımızın bir oluşumunu üretelim,

var şarkıGörsel = new ŞarkıGörsel({ el: "#container" });
şarkıGörsel.render();

İlk önce oluşum nesnesi üretirken görsel nesnesini hangi DOM elemanına yayınlayacağımızı bildiriyoruz. Bu bir jQuery seçici, yani id değeri container olan DOM elemanı hedefimizde. Daha sonra yeni ürettiğmiz görsel nesnesinin render metodunu çağırarak gösterme işini gerçekleştiriyoruz.

Şimdi dönelim index.html dosyamıza ve oradaki başlık yazısını silip yerine id değeri container olan bir DOM elemanı yerleştirelim.

app/index.html
...
    <body>
        <div id="container"></div>
        
        <script src="js/lib/jquery-3.5.1.min.js"></script>
        <script src="js/lib/underscore-min.js"></script>
...

Sayfayı yenileyelim bakalım ne olacak?


Pek de güzel olmadı. Biraz stil katalım devam etmeden önce. css klasörü içinde styles.css adında yeni bir stil dosyası ekleyelim ve içine şunları yazalım,

app/css/styles.css
body {
 padding: 40px;
}

body, p { font-size: 24px; }

Bu dosyayı index.html dosyamızın head kısmına ekleyelim

        <title>Backbone Eğitimi</title>
        <link rel="stylesheet" type="text/css" href="css/styles.css">
    </head>

Şimdi sayfayı yenilersek,


Off , süper oldu. Konsolda görsel nesnemizi inceleyelim.

şarkıGörsel
r {cid: "view1", el: dıv#container, $el: S.fn.init(1)}
$el: S.fn.init [dıv#container]
cid: "view1"
el: dıv#container
__proto__: e.View

İlginç div olması gereken yazı bende dıv diye çıkıyor. Burada $el ve el özellikleri dikkat çekiyor. el özelliği DOM elemana referans eden bir jQuery seçicisi, $el ise bu DOM elemanı içeren bir jQuery nesne. Yani $el üzerinde jQuery fonksiyonları çalıştırılabilir.Yani

şarkıGörsel.$el.css("color", "red");

yazarsak yazı rengini kırmızıya çevirebiliriz.

Diyelim görsel nesnesini üretirken el özelliğine değer vermedik, ne olur? Deneyelim bakalım,

var şarkıGörsel = new ŞarkıGörsel();
şarkıGörsel.render();

Sayfaya bir şey çıkmaz ama konsolda bakarsak nesnemiz hala mevcut.


$el elemanı bir div elemanı olarak mevcut ama sayfaya ilave olmadı. Bir satır jQuery komutuyla gösterimi gerçekleştirebiliriz.

$("#container").html(şarkıGörsel.$el);

Elemanlara bakarsak div içinde div eklenmiş görürüz. Default olarak yayın yapılan eleman div elemanıdır. Ama istersek bu elemanı ısmarlama yapabiliriz. Örnek,

var ŞarkıGörsel = Backbone.View.extend({
    tagName: "span",
    className: "song",
    id: "1234",
    attributes: {
        "data-genre": "Caz"
    },
    render: function(){
        this.$el.html("Merhaba Backbone");
        return this;
    }
});

Şimdi elemmanı incelersek,


tagName ile elemanı div'den başka eleman yapabiliriz, className ile elemana class değeri atarız, id ile elemana id değeri veririz, attributes ile istediğimiz herhangi bir özelliği elemana verebiliriz.

$el'nin jQuery nesnesi olmasından yararlanarak render olayını kısaltabiliriz.

var şarkıGörsel = new ŞarkıGörsel();
$("#container").html(şarkıGörsel.render().$el);

Şimdiye kadar Backbone görsellerinin temellerini gördük. Ancak sadece ekrana bir mesaj yazdık. Modelden gelen bilgileri göstermeye geldi sıra.



Görsele veri aktarmak

Örnekle başlayalım,
,
app/js/main.js
var Şarkı = Backbone.Model.extend();

var ŞarkıGörsel = Backbone.View.extend({
    render: function(){
        this.$el.html(this.model.get("isim"));
        return this;
    }
});

var şarkı = new Şarkı({ isim: "Neden saçların beyazlamış arkadaş" });
var şarkıGörsel = new ŞarkıGörsel({ el: "#container", model: şarkı });
şarkıGörsel.render();

render metodu tanımlamasında modelin isim özelliğini html koda ekliyoruz. Görsel nesnesini üretirken de model özelliğine model nesnesini veriyoruz.


Görsele model gönderebileceğimiz gibi bir kolleksiyon da gönderebiliriz.

app/js/main.js
var Şarkı = Backbone.Model.extend();
var Şarkılar = Backbone.Collection.extend({
    model: Şarkı,
    url: "/songs"
});

var ŞarkıGörsel = Backbone.View.extend({
    render: function(){
        this.$el.html(this.model.get("isim"));
        return this;
    }
});

var ŞarkılarGörsel = Backbone.View.extend({
    render: function(){
        var self = this;
        this.model.each(function(şarkı){
            var şarkıGörsel = new ŞarkıGörsel({ model: şarkı });
            self.$el.append(şarkıGörsel.render().$el);
        });
    }
});

var şarkılar = new Şarkılar();
şarkılar.fetch({
    success: function(){
        var şarkılarGörsel = new ŞarkılarGörsel({ el: "#container", model: şarkılar });
        şarkılarGörsel.render();
    }
});

Bir tane şarkıyı göstermek için ŞarkıGörsel sınıfı tanımlamıştık. Bütün şarkıları göstermek için de ŞarkılarGörsel sınıfını tanımlıyoruz. Bu görsele model olarak bütün şarkıları içeren kolleksiyonu gönderiyoruz. each metodu ile her şarkıyı tek tek alırken bu sefer $el içeriğini komple değiştirmek yerine append ile ekleme yaparak öncekini silmeden yeni geleni ekliyoruz. Görselin yayınlanmasını ise server^dan şarkıları okuma işlemi gerçekleşince yapıyoruz. Böyle yapmazsak server'dan veriler gelene kadar render fonksiyonu çalışır ve henüz veri olmadığı için sayfada bir şey olmaz.

Bir tricky shot daha var, each için yazılan fonksiyon içinde ŞarkılarGörsel'in $el elemanına müdahale edebilmek için bloğa girilmeden önce self adında bir değişkene nesne kopyalanıyor. Bu self isimlendirmesi de bir tavsiye olark verilir.

Sayfayı tazeleyip bakalım,


Elemanları incelersek şunu görürüz,

<div id="container">
  <div>Neden saçların beyazlamış arkadaş</div>
  <div>Hatasız kul olmaz</div>
  <div>Bayıra karşı yatır beni</div>
</div>

Burada div elemanları yerine li elemanı içinde listemizi göstermek daha mantıklı , ne de olsa bu bir liste. Bu amaçla önce tagName özelliğine li değeri veriyoruz.

app/js/main.js
...
var ŞarkıGörsel = Backbone.View.extend({
    tagName: "li",
    render: function(){
        this.$el.html(this.model.get("isim"));
        return this;
    }
});
...

Ayrıca #container elemanını da bir ul elemanına çevirmeliyiz.

app/index.html
...
    <body>
        <ul id="container"></ul>
        
        <script src="js/lib/jquery-3.5.1.min.js"></script>
...

Şimdi sayfayı yenilediğimizde liste elemanları olarak gösterimin yapıldığını görürüz.






DOM olaylarını kullanmak

Görseller konusu başında Backbone görsellerinin değer gösterimi yanında DOM ve model olaylarını işlemekten de sorumlu olduğunu belirtmiştik. Tekrar basit bir görsele dönerek başlayalım.

app/js/main.js
var Şarkı = Backbone.Model.extend();

var ŞarkıGörsel = Backbone.View.extend({
    render: function(){
        this.$el.html(this.model.get("isim"));
        return this;
    }
});

var şarkı = new Şarkı({ isim: "Neden saçların beyazlamış arkadaş" });
var şarkıGörsel = new ŞarkıGörsel({ el: "#container", model: şarkı });
şarkıGörsel.render();

Tabi index.html dosyasında hedef elemanı ul'den tekrar div elemanına geri çevirelim. Bu kadarıyla baştaki basit hale geri döneriz.

Şimdi diyelim şarkı adının yanına bir buton koyalım ve kullanıcı butona tıklayarak şarkıyı dinleyebilsin. Görsele butonu ekleyelim.

var ŞarkıGörsel = Backbone.View.extend({
    render: function(){
        this.$el.html(this.model.get("isim") + " <button>Şarkıyı Dinle</button>");
        return this;
    }
});

Görünüme bir bakalım,


Beğenmedim butonun yazısı küçük oldu. Stil dosyamıza şunu ekleyelim,

app/css/styles.css
...
button { font-size: 20px; }

Şimdi faha orantılı. Neyse işimize dönelim. DOM olaylarını izlemek için events özelliğini kullanırız.

var ŞarkıGörsel = Backbone.View.extend({
    events: {
        "click": "onClick"
    },
    onClick: function(){
        console.log("Şarkıyı Dinle Butonu Tıklandı");
    },
    render: function(){
...

events içinde liste halinde olay ve karşılığında çalışacak fonksiyonu veririz. Ancak bu yukarıdaki kod butona tıklanınca değil görsel elemanının neresine tıklarsak çalışır. Özelleştirme için örneğimizi yeni bir butonla geliştirelim.

var ŞarkıGörsel = Backbone.View.extend({
    events: {
        "click": "onClick",
        "click .bookmark": "onClickBookmark"
    },
    onClick: function(){
        console.log("Şarkıyı Dinle Butonu Tıklandı");
    },
    onClickBookmark: function(){
        console.log("Şarkıyı İşaretle Butonu Tıklandı");
    },
    render: function(){
        this.$el.html(this.model.get("isim") + " <button>Şarkıyı Dinle</button>"
            + " <button class='bookmark'>İşaretle</button>");
        return this;
    }
});

Modele bir buton daha ekliyoruz ama bu sefer ona özel kod yazabilmek için butona bir class değeri veriyoruz. Sonra events özelliğine bir satır daha ilave ederek verilen class değerindeki elemana tıklanınca çalışacak fonksiyonu yazıyoruz. Burada standart jQuery seçici yapısı kullanılıyor. Şimdi tarayıcıda test ederek konsolda sonuca bakalım.

main.js:9    Şarkıyı Dinle Butonu Tıklandı
main.js:12  Şarkıyı İşaretle Butonu Tıklandı
main.js:9    Şarkıyı Dinle Butonu Tıklandı

İşaretle butonu tıklanınca her iki olay işleme fonksiyonu da çalışıyor. Önce class değerine özel olan fonksiyon sonra de genel tıklama fonksiyonu çalışıyor. Neden? Çünkü yeni eklenen buton da $el elemanı içinde. Bunu önlemek için ilk butona da bir class değeri verip fonksiyonları ayırabiliriz. Fakat burada öğrenmek amacıyla yapılabilecek ikinci bir yol öneriliyor.

    onClickBookmark: function(e){
        e.stopPropagation();
        console.log("Şarkıyı İşaretle Butonu Tıklandı");
    },

Olay işleme fonksiyonuna bir parametre yazarsak bu gerçekleşen olay nesnesi olacaktır. Burada e değişkeni ile gerçekleşen olayı fonksiyon içine alıyoruz. Sonra da stopPropagation metodu ile olayın bu andan itibaren aktif olmasını engelliyoruz. Olayı öldürünce artık genel olarak tıklama için yazılan kod olay kalmadığı için çalışmaz. Bunu tarayıcıda test edip sonucu görelim.

main.js:9    Şarkıyı Dinle Butonu Tıklandı
main.js:12  Şarkıyı İşaretle Butonu Tıklandı

Bu da aklımızın bir köşesinde bulunsun. İstediğimiz anda olayın yok olmasını sağlayabiliriz.



Model olaylarını kullanmak

Sosyal medya örneğini düşünelim, bir gönderiyi beğendiğinizde (anlamayanlar için "like'ladığınızda"), tüm sayfayı yenilemek gerekmeden hemen gönderi görseline beğeniniz gösterilir. Başkaları de beğenirse sayı otomatik olarak artar. Sayfayı yenilemeden nasıl oluyor da haber alınıp yeni değer yazılıyor? Bunun iki yolu var:
  • Poll - Client sürekli tarayıcıya sorar
  • Push - Server client'a mesaj yollar
Vaay, çok heyecanlı koskoca server bizim kıytırık tarayıcımıza mesaj gönderecek. 

Önce model değişimlerinin işlenmesini görelim. Silin herşeyi baştan başlıyoruz.

app/js/main.js
var Şarkı = Backbone.Model.extend({
    defaults: {
        dinleyenler: 0
    }
});

var ŞarkıGörsel = Backbone.View.extend({
    render: function(){
        this.$el.html(this.model.get("isim") + " - Dinleyenler: " 
            + this.model.get("dinleyenler"));
        return this;
    }
});

var şarkı = new Şarkı({ isim: "Neden saçların beyazlamış arkadaş" });
var şarkıGörsel = new ŞarkıGörsel({ el: "#container", model: şarkı });
şarkıGörsel.render();

Sayfayı yenilediğimizde alacağımız görüntü,


Konsolda dinleyenler sayısını değiştirelim.

şarkı.set("dinleyenler", 5);

Değer değişmiş olmasına rağmen görüntüye yansımaz. Değişebilmesi için model veri değişimi olayı işleyen bir kod yazmalıyız.

var ŞarkıGörsel = Backbone.View.extend({
    initialize: function(){
        this.model.on("change", this.render, this);
    },
    render: function(){
        this.$el.html(this.model.get("isim") + " - Dinleyenler: " 
            + this.model.get("dinleyenler"));
        return this;
    }
});

initialize bölümüne modelin change olayını izleyen bir kod yazdık. Bu olay model değerlerinden biri değişince tetiklenir. Standart jQuery on metodu yapısı dışına çıkılarak verilen 3. parametredeki this değeri this.render fonksiyonu çağırıldığı içindir yoksa normal bir fonksiyon kodunu oraya yazdığımızda buna gerek yok. Büyük ihtimalle görsel nesnesi içinden metodlar çağıracağımıza göre bu çoğunlukla kullanılacak, unutmayalım.

Şimdi sayfayı yenileyip test ettiğimizde değişimlerin anında görsele yansıdığını görürüz.

Sadece render fonksiyonu çağırarak görseldeki veriyi yenilemek istemeyebiliriz. Mesela yeni etkileşim olduğunda görsel arkaplan rengini değiştirerek dikkati çekmek isteyebiliriz. İstediğimiz esneklikte çalışabilmek için yeni bir metodu ekleyerek , eklediğimiz metod içinde istediğimizi yapabiliriz.

var ŞarkıGörsel = Backbone.View.extend({
    initialize: function(){
        this.model.on("change", this.onModelChange, this);
    },
    onModelChange: function(){
        this.render();
        this.$el.addClass("class-ext");
    },
...

Saire vesaire.



Kolleksiyon olaylarını kullanmak

Kolleksiyona bir örnek verirsek sosyal medyadaki mesajlaşmaları gösterebiliriz. İki kişinin yaptığı görüşmeyi kolleksiyon olarak, gönderilen her mesaj da bir model olarak düşünülebilir. kolleksiyona yeni mesaj eklendiğinde otomatik olarak mesajlaşma ekranı yeni gelen mesajı yayınlayarak sayfayı yukarı kaydırır. Bunu mesela yapmak için kolleksiyon olaylarını izlemek ve yeni kayıt eklendiğinde işlemi yapmak gerekir. 

Hatırlayalım kolleksiyon görselleri incelerken bir kod yazmış ve li elemanları içinde şarkı isimlerini listelemiştik. Oraya geri dönelim.

app/js/main.js
var Şarkı = Backbone.Model.extend();
var Şarkılar = Backbone.Collection.extend({
    model: Şarkı,
    url: "/songs"
});

var ŞarkıGörsel = Backbone.View.extend({
    tagName: "li",
    render: function(){
        this.$el.html(this.model.get("isim"));
        return this;
    }
});

var ŞarkılarGörsel = Backbone.View.extend({
    render: function(){
        var self = this;
        this.model.each(function(şarkı){
            var şarkıGörsel = new ŞarkıGörsel({ model: şarkı });
            self.$el.append(şarkıGörsel.render().$el);
        });
    }
});

var şarkılar = new Şarkılar();
şarkılar.fetch({
    success: function(){
        var şarkılarGörsel = new ŞarkılarGörsel({ el: "#container", model: şarkılar });
        şarkılarGörsel.render();
    }
});

index.html içindeki div bölümü de ul haline getirmiştik. Bunu da tarayıcımızda denerken konsoldan yeni bir şarkı eklersek

şarkılar.add(new Şarkı({ isim: "Şarkı 4" }));

doğal olarak bunun da listeye eklenmediğini görürüz. Şimdi buna da kolleksiyonun add olayını izleyen bir kod yazalım.

var ŞarkılarGörsel = Backbone.View.extend({
    initialize: function(){
        this.model.on("add", this.şarkıEklendi, this);
    },
    şarkıEklendi: function(){
        console.log("Yeni şarkı eklendi");
    },
    render: function(){

Kafa karıştırmadan gitmek için önce konsolda mesaj çıkartarak olay izlemenin çalışmasını gördük. Adım adım gitmek bazen daha hızlı ilerlememizi sağlar.

şarkılar.add(new Şarkı({ isim: "Şarkı 4" }));
main.js:20 - Yeni şarkı eklendi

Bu geliştirici araçları konsolu da iyi ki icat etmişler. Daha başka kullanım örneklerini burada görebilirsiniz.

Şimdi yeni eklenen şarkıyı listeye ekleyecek şekilde fonksiyonu düzenleyelim.

    şarkıEklendi: function(şarkı){
        var şarkıGörsel = new ŞarkıGörsel({ model: şarkı });
        this.$el.append(şarkıGörsel.render().$el);
    },

Şarkılardan birinin silinmesini izlemek için de bir kod yazalım. remove olayı kolleksiyondan bir kayıt silindiğinde oluşur.

var ŞarkılarGörsel = Backbone.View.extend({
    initialize: function(){
        this.model.on("add", this.şarkıEklendi, this);
        this.model.on("remove", this.şarkıSilindi, this);
    },
    şarkıSilindi: function(){
        console.log("Bir şarkı silindi");
    },

Şarkı silince mesaj çıkacaktır.

şarkılar.remove(şarkılar.at(0));
main.js:21 Bir şarkı silindi

Ama bu sefer bir sıkıntı var hangisini sileceğiz, nereden bileceğiz? Eklerken bir li elemanı daha en sona ekliyorduk, ama şimdi hangi li elemanı hangi şarkıya ait belli değil. Bunu çözmek için jQuery kullanacağız. İlk önce model görselini üretirken li elemanına id değeri olarak modelin id değerini yazalım.

var ŞarkıGörsel = Backbone.View.extend({
    tagName: "li",
    render: function(){
        this.$el.html(this.model.get("isim"));
        this.$el.attr("id", this.model.id);
        return this;
    }
});

Yeni şarkı ekleme testi yaparken artık bir id değeri de vererek denemeliyiz. Şimdi görselden silme kodunu yazalım.

    şarkıSilindi: function(şarkı){
        this.$el.find("li#" + şarkı.id).remove();
    },

Konsolda

 şarkılar.remove(şarkılar.at(0));

dediğimizde ilk şarkı görselden silinecektir.




Kalıplar - Templates

Buraya kadar görseller biraz kumda oynamak gibiydi desem kızar mısınız? Çünkü bu şekilde ancak basit görsel tasarımları yapılabiliyor. Oysa ki webde gördüğümüz görseller çok daha ayrıntılı. Bunları bir string içinde yazabiliriz tabi ama hakim olması çok zor olur. Twitter'da bir tek gönderinin geliştirici araçlarında kaç parçadan oluştuğunu incelersek bu karmaşıklığı gayet net görürüz. Bu kadar çok elemandan oluşan bir görseli tek bir string içinde ifade etmek ızdırabın ta kendisi olur. 

Bu görselleri kalıp şeklinde hazırlamak için template kullanılır. Underscore.js , Mustache.js ve HandleBars.js gibi template altyapısı sunan kütüphaneler var. Bilin bakalım biz hangisini kullanacağız. Tabii ki underscore.js, şu anda zaten projemizde var.

Basit model ve görseli ile başlayalım.

app/js/main.js
var Şarkı = Backbone.Model.extend();

var ŞarkıGörsel = Backbone.View.extend({
    render: function(){
        this.$el.html(this.model.get("isim") + " <button>Şarkıyı Dinle</button>");
        return this;
    }
});

var şarkı = new Şarkı({ isim: "Neden saçların beyazlamış arkadaş" });

var şarkıGörsel = new ŞarkıGörsel({ el: "#container", model: şarkı });
şarkıGörsel.render();

Bunu kalıp kullanarak nasıl yaparız bakalım.İlk önce index.html dosyamıza gidip bir kalıp elemanı ekleyeceğiz.

app/index.html
        <script src="js/lib/backbone-min.js"></script>

        <script id="şarkıTemplate" type="text/html">
            <%= isim %>
            <button>Şarkıyı Dinle</button>
        </script>

        <script src="js/main.js"></script>

main.js dosyamızı eklemeden hemen öncesine tanımlıyoruz. Şimdi render metodumuzu bu kalıbı içerecek şekilde düzenleyelim.

var ŞarkıGörsel = Backbone.View.extend({
    render: function(){
        var template = _.template($("#şarkıTemplate").html());
        var html = template(this.model.toJSON());
        this.$el.html(html);
        return this;
    }
});

Önce id değeri şarkıTemplate olan elemanın içini template olarak tayin ediyoruz. Sonra template içine modelimizi JSON olarak uyguluyoruz. Çünkü underscore.js Backbone model nesnesini tanımaz standart JSON nesne olarak veriyi vermeliyiz.  Burada <%= isim %> elemanı yerine bizim modeldeki isim özelliğimizin değeri yazılır. HTML kod içinde bir değişkeni yayınlamak için bu yapı kullanılıyor. En son elde ettiğimiz html kodu görsele enjekte ediyoruz.

Artık görsel render koduyla oynamadan istediğimiz kadar karmaşık kalıplar yapabiliriz. Bu aynı zamanda lojik ve görselin oluşturulmasını ayırmak için çok faydalıdır.

Diyelim şarkı tanımımız şöyle olsun

var şarkı = new Şarkı({ 
    isim: "Neden saçların beyazlamış arkadaş",
    söyleyen: "Tanju Okan",
    yılı: "1970 ler" 
});

Template de şöyle olsun

        <script id="şarkıTemplate" type="text/html">
            <b><%= isim %></b><br>
            <i><%= söyleyen %></i><br>
            <sup><%= yılı %><br></sup>
            <button>Şarkıyı Dinle</button>
        </script>

Elde ettiğimiz sonuç


İşte böyle böyle gelişecek. Template içinde kod çalıştırma örneği olarak bir şey yapalım. Diyelim dinlemeler özelliğinde şarkının kaç kere dinlenmiş olduğunu saklıyoruz. 1000 kereden fazla dinlenen şarkının yanında popüler olduğunu belirten yazı çıkaracağız. Önce şarkı tanımımıza bu özelliği ekleyelim.

var şarkı = new Şarkı({ 
    isim: "Neden saçların beyazlamış arkadaş",
    söyleyen: "Tanju Okan",
    yılı: "1970 ler", 
    dinlemeler: 110
});

Kalıp elemanımıza istediğimiz lojiği ekleyelim

        <script id="şarkıTemplate" type="text/html">
            <b><%= isim %></b>
            <% if (dinlemeler > 1000) { %>
                <span style="color: green"> *** Popüler *** </span>
            <% } %>
            <br>
            <i><%= söyleyen %></i><br>
            <sup><%= yılı %><br></sup>
            <button>Şarkıyı Dinle</button>
        </script>

Burada JavaScript kodu html template içinde çalıştırıyoruz. Zaten daha önceki <%= isim %> kodu da bir JavaScript koduydu. isim kod olarak çağrılınca isim değeri geri dönüyor ve dönen bu değer <%= ... %> yapısı içinde olduğundan bulunulan noktaya yazılıyor. Burada is bir fark var <% ... %> yapısı içindeki kod çalışır ama çıktı olarak bir şey yazılmaz. Yukarıyı incelersek, bir if bloğu içine html birşeyler yazılmış. Bloğun içindekiler koşul geçersizse html'e eklenmez.

İşte böyle JavaScript içinde yazı ifade ederken tırnak içine alırken , yazının içine JavaScript koymak için baya anırmak gerekiyor. Ama bu yapıyı kullandıkça , ne kadar doğru bir şey olmuş diyeceksiniz.

Dönelim işimize , girdiğimiz kayıt dinlemeler değeri 110 olduğu için ekrana bir şey çıkmayacak. Geliştirici araçları içinde elemanlarda arasak da bulamayız. Ama dinlemeler değerini 1100 yapıp sayfayı tazelersek.












Olaylar , olaylar ...


Şu ana kadar Backbone özelinde olay işlemesini inceledik. Biraz da diğer olayları inceleyelim. 


Kullanıcı tanımlı olayları tetiklemek ve bağlanmak

Herhangi bir nesneye isim vererek bir olay tanımlayabiliriz. Ortalığı temizleyelim ve basit bir JavaScript nesnesi tanımlayarak başlayalım. 

app/js/main.js
var kişi = {
    isim: "Ümit"
};

Kestirme yoldan bu nesneye Backbone olay yapısını ekleyelim.

var kişi = {
    isim: "Ümit"
};
_.extend(kişi, Backbone.Events);

Şimdi tarayıcımızda nesneyi inceleyelim.


Biz sadece isim özelliğini tanımlamıştık. Geri kalan özellikler ve metodlar Bacbone.Events alt yapısından buraya Underscore.js yardımıyla aldıklarımız. Bir kaç örnek, trigger metodu ile bir olayı ateşleyebiliriz (yani programın o olay olmuş gibi davranmasını sağlarız. on ve off metodları nesneye bir olay izlemesi eklemek ya da olan bir izlemeyi iptal etmek için.

Şimdi bu nesneden nasıl bir olay oluşturacağımızı görelim. Bu nesne kişi olduğuna göre buna yürü adında bir metod ekleyelim ki yürüsün. Yürümeye başlayınca da bunu yürüyor adında bir olay olarak aleme bildirsin.



app/js/main.js
var kişi = {
    isim: "Ümit",
    yürü: function(){
        this.trigger("yürüyor");
    }
};
_.extend(kişi, Backbone.Events);

yürüyor olayı Backbone olaylarından birinin adı olmadığına göre bu bir kullanıcı tanımlı olay. Şimdi de olay için bir görev fonksiyonu yazalım.

kişi.on("yürüyor", function(){
    console.log("Kişi yürüyor");
});

kişi.yürü();

Tarayıcıda baktığımızda mesajı göreceğiz. click olayına görev fonksiyonu yazmaktan farkı yok. Eğer olayı ateşlerken ilave veri eklemek gerekirse olayı ateşleyen komuta ilave parametre olarak bu verileri JavaScript nesnesi olarak verileri yazabiliriz.

var kişi = {
    isim: "Ümit",
    yürü: function(){
        this.trigger("yürüyor", {
            hız: 1,
            zaman: "08:00"
        });
    }
};

Bu değerleri görev fonksiyonu içinde görmek için fonksiyonu parametre ile çağırırız.

kişi.on("yürüyor", function(e){
    console.log("Kişi yürüyor");
    console.log("Veriler :", e);
});

Bu parametre nasıl görünüyor bakalım,


Standart JavaScript nesnesi. Bunu fonksiyon içinde istediğimiz gibi kullanabiliriz.

yürüyor olayını yok etmek istersek off metodunu kullanırız.

kişi.off("yürüyor");
kişi.yürü();

off metodu işlendikten sonra artık yürüyor olayı görev fonksiyonu tanımı iptal olmuştur. Geliştirici araçları konsolda kişi nesnesine baktığımızda _events özelliğinde görev fonksiyonu tanımlanan olayları görürüz. off metodu kullandıktan sonra yürüyor olayının yok olduğunu görürüz.

off metoduna parametre vermeden çağırırsak nesneye ait tüm olaylar iptal olur.

on metodundan başka once metoduyla da olay için görev fonksiyonu tanımlayabiliriz. Aynı şekilde yapılır. Ama bu sefer fonksiyon her olay ateşlendiğinde değil sadece ilk ateşlendiğinde çalışır.




Olay toplayıcı - Bir çok görseli koordine etmek

Bu bölümde olay toplayıcı ya da diğer adıyla event bus yapısını inceleyeceğiz. Foursquare diye bir sosyal medya uygulaması var bunu örnek verelim. Backbone ile yapılsa bölümlerin yapılandırılması nasıl olurdu? Tarayıcımızda açtığımızda sol tarafta paylaşılan konumların bir listesi var. Bu listeyi model ve kolleksiyonlar ile nasıl yapacağımızı bu kadar mükemmel olmasa da tahmin edebiliriz. Sağ taraftaki bölümde ise harita üzerinde listedeki konumlar gösteriliyor. Bu da ayrı bir görsel. Soldaki listeden birinin üstüne geldiğimizde sağda ona karşı gelen konum haritada gösterilir. Bu sağdaki ve soldaki görseller birbiri ile etkileşimde bulunuyor. Ana fikir olarak bu bölümde bunun nasıl yapılacağına örnek vereceğiz.

Event bus yapısı şekil ile şöyle gösterebiliriz.

Görsel 1
Görsel 2
yayınla
izle
yayınla
izle
Event Bus

Bu arada yukarıdaki şekili de inat ettim div'ler ve CSS ile çizdim. Olay toplayıcıyı olayların yayınlandığı veya izlendiği ortak bir alan olarak düşünebiliriz. Bu sayede görseller birbiriyle direk temasa geçmeden diğerlerine olay bildiriminde bulunabilir ya da diğerlerindeki olayları takip edebilir.

Şimdi Foursquare örneğindekine benzer bir haberleşmenin nasıl yapılacağını inceleyeceğiz. Diyelim sol tarafta mekanların listesi ve sağ tarafta harita ver. İki görsel yapıp nasıl haberleştiklerini açıklamaya çalışalım.

Önce index.html içinde görseller için div yerleşkeleri tanımlayalım.

app/index.html
        <div id="container">
            <div id="mekanlar-container"></div>
            <div id="harita-container">
                <span id="mekan-adı"></span>
            </div>
        </div>

CSS çalışmasını yeri geldiğinde yapacağız. main.js dosyamızda da temel ihtiyaçlar ve örnek veri içeren bir başlangıç yapalım.

app/js/main.js
var Mekan = Backbone.Model.extend();

var Mekanlar = Backbone.Collection.extend({
    model: Mekan
});

var MekanGörsel = Backbone.View.extend({
    tagName: "li",
    events: {
        "click": "onClick"
    },
    onClick: function(){

    },
    render: function(){
        this.$el.html(this.model.get("isim"));
        return this;
    }
});

var MekanlarGörsel = Backbone.View.extend({
    tagName: "ul",
    id: "mekanlar",
    render: function(){
        var self = this;
        this.model.each(function(mekan){
            var görsel = new MekanGörsel({ model: mekan });
            self.$el.append(görsel.render().$el);
        });
        return this;
    }
});

var HaritaGörsel = Backbone.View.extend({
    el: "#harita-container",
    render: function(){
        if (this.model)
            this.$("#mekan-adı").html(this.model.get("isim"));
        return this;
    }
});

var mekanlar = new Mekanlar([
    new Mekan({ isim: "Hayal Kahvesi" }),
    new Mekan({ isim: "Tosca" }),
    new Mekan({ isim: "Cemil" })
]);

var mekanlarGörsel = new MekanlarGörsel({ model: mekanlar });
$("#mekanlar-container").html(mekanlarGörsel.render().$el);

var haritaGörsel = new HaritaGörsel();
haritaGörsel.render();

Lütfen kodu ve görsellerin geleceği HTML noktaları satır satır inceleyin. Şimdi sayfamızı tarayıcıda açalım bir görelim neye benzemiş.


Şimdi biraz CSS ile şekili benzetmeye çalışalım. styles.css dosyamıza şu stilleri ekleyelim,

app/css/styles.css
#mekanlar-container {
 float: left;
 width: 500px;
}

#mekanlar {
 list-style: none;
 border-top: 1px solid #ccc;
 border-left: 1px solid #ccc;
 padding-left: 0;
 margin-top: 0;
}

#mekanlar li {
 padding: 30px;
 border-bottom: 1px solid #ccc;
 border-right: 1px solid #ccc;
 cursor: pointer;
}

#mekanlar li:hover {
 background: #F7F5F5;
}

#harita-container {
 float: left;
 width: 800px;
 height: 800px;
 background: #EDE8E8;
}

Of , acayip benzedi.. Harita görseli şu anda modeli olamadan boş olarak başladı. Şimdi mekan listesinden birine tıklanınca harita görselinin bundan haberi olması ve içeriğini ona göre değiştirmesini sağlayalım. İlk önce event bus olarak boş bir events nesnesi üretiriz.

app/js/main.js
...
});

var bus = _.extend({}, Backbone.Events);

var mekanlar = new Mekanlar([
    new Mekan({ isim: "Hayal Kahvesi" }),
...

Bu yeni olay toplayıcı nesneyi her iki görselimizi üretirken parametre olarak girelim. Şu iki satırı bulup şöyle değiştirelim,

...
var mekanlarGörsel = new MekanlarGörsel({ model: mekanlar, bus: bus });
...
var haritaGörsel = new HaritaGörsel({ bus: bus });
...

MekanlarGörsel bu olay toplayıcıyı görebiliyor olsa da bunu aşağıya MekanGörsel'e de iletmemiz gerekiyor. MekanlarGörsel sınıf tanımını şöyle değiştirelim,

var MekanlarGörsel = Backbone.View.extend({
    tagName: "ul",
    id: "mekanlar",
    initialize: function(opsiyonlar){
        this.bus = opsiyonlar.bus;
    },
    render: function(){
        var self = this;
        this.model.each(function(mekan){
            var görsel = new MekanGörsel({ model: mekan, bus: self.bus });
            self.$el.append(görsel.render().$el);
        });
        return this;
    }
});

Önce sınıfın initialize fonksiyonu içinde verilen bus nesnesini sınıf değişkenine kopyalıyoruz, sonra MekanGörsel oluşumu üretilirken bu nesneyi aşağıya gönderiyoruz.

Aynı initialize yapısını MekanGörsel ve HaritaGörsel tanımlarına da ekleyelim.

var MekanGörsel = Backbone.View.extend({
    initialize: function(opsiyonlar){
        this.bus = opsiyonlar.bus;
    },
...
var HaritaGörsel = Backbone.View.extend({
    initialize: function(opsiyonlar){
        this.bus = opsiyonlar.bus;
    },
...

Artık her görsel bus nesnemize erişebilir. Şimdi MekanGörsel oluşumlarından birine tıklanınca bus nesnesine bir olay yayınlayalım. MekanGörsel sınıf tanımında daha önce içini boş bıraktığımız onClick fonksiyonunu şöyle düzenleyelim.

    onClick: function(){
        this.bus.trigger("mekanSeçildi", this.model);
    },

MekanGörsel harita marita tanımaz o kendisine tıklandığını ve hangi modele tıklandığını bus olay toplayıcı nesneye bildiriyor. İsteyen ordan alıp kullanabilir.

HaritaGörsel bu olayla ilgileniyor, ne yaparız? Olay izleme fonksiyonu ekleriz.

var HaritaGörsel = Backbone.View.extend({
    initialize: function(opsiyonlar){
        this.bus = opsiyonlar.bus;
        this.bus.on("mekanSeçildi", this.mekanSeçildiFn, this);
    },
    mekanSeçildiFn: function(mekan){
        this.model = mekan;
        this.render();
    },
...

İşte bu kadar. Aslında burada gördüğümüz Backbone.js'ye özel bir şey değil. Bu bir programlama tekniği ve oldukça düzenli iş yapmayı ve dağılmadan takip edebilmeyi mümkün kılıyor.

Şimdi tarayıcımızı açıp sonucu görelim.


Mantık bu. Artık haritayı falan kendiniz çizersiniz. Alat alta sevmedim orjinali gibi görseller biri solda biri sağda olsun derseniz CSS dosyasında şu seçicileri değiştirin,

#mekanlar-container {
 float: left;
 width: 30%;
}
...
#harita-container {
 float: right;
 width: 60%;
 height: 800px;
 background: #EDE8E8;
}

Eee, başka ne vardı?








Router'lar


En başta demiştik ki router diye bir şey var , onunla tek sayfa web uygulamaları yapıyoruz. Sayfayı değiştirmeden görüneni değiştiririz falan. 

Yapalım bir örnek tek sayfa uygulama da görelim router nasıl oluyor.



Router tanımlamak

Bir kere Backbone router denen şey client tarafta olur. Kullanıcı bir linke tıklar, başka bir görsel yayınlanır. Ama bunu yaparken sayfa yenilenmez ya da değişmez. Ayrıca hemen hemen veriler hariç tüm herşey sayfayla beraber yüklenir. Bazen veriler de yüklenir. 

Client tarayıcıdaki adresi değiştirir, sayfa değiştiriyormuş gibi bir simülasyon olur ve yeni içerik yayınlanır. 

Bu amaçla kullanmak için Backbone'da router nesneleri kullanılır. Bunlar adres bardaki bilgiyi takip eder ve buna göre aksiyonlar yaparlar. 

Senaryoyu şarkılar türküler üzerinden çizelim. Diyelim tek sayfa uygulamamızda yukarda bir navigasyon linkleri bölümümüz olsun. Bu linklerden Sanatçılar, Albümler ve Türler olarak 3 değişik görsele geçişler yapmayı planlayalım. Önce basit görseller tanımlayarak başlayalım,

app/js/main.js
var SanatçılarGörsel = Backbone.View.extend({
    render: function(){
        this.$el.html("SANATÇILAR GÖRSELİ");
        return this;
    }
});

var AlbümlerGörsel = Backbone.View.extend({
    render: function(){
        this.$el.html("ALBÜMLER GÖRSELİ");
        return this;
    }
});

var TürlerGörsel = Backbone.View.extend({
    render: function(){
        this.$el.html("TÜRLER GÖRSELİ");
        return this;
    }
});

Bir tane görsele yönlenen router tanımlaması ile başlayalım. main.js dosyamıza şunları ekleyelim,

var AppRouter = Backbone.Router.extend({
    routes: {
        "albumler": "albümleriGöster"
    },
    albümleriGöster: function(){
        var görsel = new AlbümlerGörsel({ el: "#container" });
        görsel.render();
    }
});

2 temeli var AppRouter sınıfının, önce routes özelliğinde adresten ookunan değer ve karşılığında işlenecek fonksiyon adını veriyoruz. Ardından fonksiyon tanımıyla o adreste yayınlanacak görseli yerine koyuyoruz.

Şu anda uygulamamızı lokalde http://localhost:3000 adresinde yayınladığımızı düşünürsek http://localhost:3000#albumler adresi girilirse yapılacak aksiyonu tarif ediyoruz.

Kısa bir bilgi daha verelim. Örneğin tek bir albümü görmek için albümün id değerine göre parametrik bir route belirlemek istersek şöyle yaparız,

var AppRouter = Backbone.Router.extend({
    routes: {
        "albumler": "albümleriGöster",
        "albumler/:albumId": "albumGöster"
    },
    albümGöster: function(albumId){

    },
    albümleriGöster: function(){
        var görsel = new AlbümlerGörsel({ el: "#container" });
        görsel.render();
    }
});

Burada albumId'nin parametre olduğunu başına ":" işareti koyarak belirtiyoruz. Örnek adres olarak http://localhost:3000#albumler/1 adresi girdiğimizde id değeri 1 olan albümü göstermesini sağlayabiliriz.

Dönelim diğer 2 route'u da ekleyelim.

    routes: {
        "albumler": "albümleriGöster",
        "turler": "türleriGöster",
        "sanatcilar": "sanatçılarıGöster",
        "*other": "defaultRoute"
    },

en alttaki *other yönlendirmesi listede olmayan bir adres gelince gidilecek fonksiyonu belirliyor. Fonksiyonları tanımlamayı da bitirelim.

var AppRouter = Backbone.Router.extend({
    routes: {
        "albumler": "albümleriGöster",
        "turler": "türleriGöster",
        "sanatcilar": "sanatçılarıGöster",
        "*other": "defaultRoute"
    },
    albümleriGöster: function(){
        var görsel = new AlbümlerGörsel({ el: "#container" });
        görsel.render();
    },
    türleriGöster: function(){
        var görsel = new TürlerGörsel({ el: "#container" });
        görsel.render();
    },
    sanatçılarıGöster: function(){
        var görsel = new SanatçılarGörsel({ el: "#container" });
        görsel.render();
    },
    defaultRoute: function(){
        
    }
});

Sırada router oluşum nesnesini üretmek ve Backbone'un adres barı takip etmesini sağlamak var.

var router = new AppRouter();
Backbone.history.start();

Geriye birtek şey kaldı, index.html sayfamıza linkleri eklemek,

    <body>
        <nav>
            <ul>
                <li><a href="#albumler">Albümler</a></li>
                <li><a href="#turler">Türler</a></li>
                <li><a href="#sanatcilar">Sanatçılar</a></li>
            </ul>
        </nav>

        <div id="container">
        </div>
...

İşte bu kadar. Her yiğidin bir yoğurt yiyişi var, bunları öğrendiğim yerde bu kısım biraz farklı anlatılıyordu ama bu şekil benim kolayıma geldi Merak edenler internette örneklere bakabilir.


Merak ettim gmail.com'a baktım, onda da adresler # ile başlayan kelimelerle sonlanıyor. Demekki client tararfta yönlendirme yapan siteler server'a gitmeden görsel değiştirmek için buna benzer şekilde adres ayrımları yapıyor.

Tek sayfa uygulamalar çok hızlı olmaları ile avantajlılar. Ancak karmaşıklığına hakim olmak biraz zor. Ayrıca arama motorlarınızın sayfanızı listeye alması için kullanılan SEO teknikleri var ya tek sayfa uygulamalarda onları uygulamak için çok değişik işler yapmak gerekir. Bu yüzden web siteleri tasarlanırken bazı kısımları tercih edilirken hız için tek sayfa geri kalanı normal sayfalar şeklinde tasarlanır.

En başta anlattığımız maddelerin tümünü gördüğümüze göre artık süper düper uygulamamızı yazmaya hazırız demektir.










Sıfırdan bir uygulama


Örnek uygulama olarak eğitim verenler ya blog ya yapılacaklar listesi uygulamasını tercih eder. Bugüne kadar böyle giriş seviye uygulama için sosyal medya uygulaması gösterene denk gelmedim. Bizim örtmen ne yapmış? Yapılacaklar listesi (ya da ToDo Application). Görelim bakalım nasıl yapmış.

Şimdiye kadar gördüklerimizi bir arada kullanacağız, onları bir arada kullanacağız, programlama üzerinde düzen ve yapılanmalar göreceğiz, Uygulamayı döne döne yapacağız. Her dönüşümüzde üzerine birşeyler daha katacağız ve sonunda tam olarak bitmiş çalışan ve iş gören bir uygulamamız olacak inşallah. 

İsterseniz önce daha önce kullandığımız klasörü todo adında bir klasöre kopyalayarak başlayalım. index.html dosyamızın içini boşaltarak başlayalım.

todo/index.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Yapılacak İşler</title>
        <link rel="stylesheet" type="text/css" href="css/styles.css">
    </head>
    <body>
        <div id="container">
        </div>
        
        <script src="js/lib/jquery-3.5.1.min.js"></script>
        <script src="js/lib/underscore-min.js"></script>
        <script src="js/lib/backbone-min.js"></script>

        <script src="js/main.js"></script>
    </body>

</html>

Yapılacak bir tek işi temsilen TodoItem adında bir model oluşturacağız ve tüm yapılacak işleri temsilen TodoItems adında bir kolleksiyon oluşturacağız. Bunları bu sefer daha yapıyı düzenli tutmak için başka dosyalarda oluşturacağız. Uygulamamızın js klasörü içinde TodoItem.js ve TodoItems.js adında iki yeni dosya ekleyelim ve içlerinde model ve kolleksiyonun basit tanımlarını yazalım.

todo/js/TodoItem.js
var TodoItem = Backbone.Model.extend();

todo/js/TodoItems.js
var TodoItems = Backbone.Collection.extend({
 model: TodoItem
});

Şimdi bu ikisini index.html dosyamızda ekleyelim.

todo/index.html
...
        <script src="js/TodoItem.js"></script>
        <script src="js/TodoItems.js"></script>

        <script src="js/main.js"></script>
    </body>

</html>

Adım adım gidelim. Tarayıcıda konsolu açıp bir test edelim buraya kadar hata var mı?


Süper! Backbone yüklenmiş, modelimiz ve kolleksiyonumuz çalışıyor. Bebek adımlarıyla ilerlemeye devam. Görsel için js klasörü içine TodoItemView.js adında bir dosya ekleyelim ve bunu da index.html dosyamızdan çağıralım.

todo/index.htnl
...
        <script src="js/TodoItem.js"></script>
        <script src="js/TodoItems.js"></script>
        <script src="js/TodoItemView.js"></script>
...

Şimdi yapılacak tek bir işi göstermek için bir görsel tasarlayalım.

todo/js/TodoItemView.js
var TodoItemView = Backbone.View.extend({
 render: function(){
  this.$el.html(this.model.get("açıklama"));
  return this;
 }
});

Mnimum olarak bize en azından render metodu lazım. Bu metod şu anda modelin açıklama özelliğinin değerini gösterecek. Bu görsel şu anda bir modele ve onun bir özelliğine bağlı. Önceden ideali gördük ama burada biraz daha ciddi davranmak gerekiyor. Eğer bu görsel bir model vermeden çağırılırsa JavaScript kodumuz hata versin istiyoruz. initialize bölümüne bir kod parçası ekleyerek ileride yazacağımız kodlardan biri model belirtmeden bu görselin oluşumunu üretmeye kalkarsa açıklayıcı bir hata mesajı verelim.

todo/js/TodoItemView.js
var TodoItemView = Backbone.View.extend({
 initialize: function(opsiyonlar){
  if (!(opsiyonlar && opsiyonlar.model))
   throw new Error("TodoItemView görseli model olmadan üretildi");
 },
 render: function(){
  this.$el.html(this.model.get("açıklama"));
  return this;
 }
});

Karşılaştırma bloğu içindeki koşul önce opsiyonlar olmasını test ediyor , sonra bu opsiyonlar içinde model var mı test ediyor. Eğer sadece opsiyonlar.model yazmış olsak ve opsiyonlar olmasa bir undefined hatası ile karşılaşırız. Bir şeyi yaparken kendi kendimize hata üretmeyelim. Şimdi tarayıcıda konsolda

new TodoItemView();

Satırını girdiğimizde belirtmiş olduğumuz hata mesajı gelecektir.

Sıra geldi oluşumları üretmeye bu amaçla main.js dosyasını kullanacağız. Kodumuzun çalışmaya başlama zamanı olarak jQuery ready fonksiyonunu kullanarak dökümanın yüklenmesi bittikten sonra çalışmasını sağlayacağız.

todo/js/main.js
$(document).ready(function(){
 var todoItem = new TodoItem({ açıklama: "Yapılacak 1 "});

 var todoItemView = new TodoItemView({ model: todoItem });
 $("body").append(todoItemView.render().$el);
});

Bu sefer container div içini kullanmadan direk body içine görseli ekledik. Test edelim bakalım görsel çalışıyor mu?


Mükemmel. Adım adım kontrol edersek bir hata ile karşılaşınca en son ne yapmıştık sorusunun cevabı büyük ihtimal hataya götürecektir bizi. En son çok fazla şey yaptıysak çok daha geniş bir kodu taramak zorunda kalırız.

Görsel bizim verdiğimiz modelin açıklama özelliği değerini gösteriyor. Model tanımına gidip doğrulama kodu ekleyelim.

todo/js/TodoItem.js
var TodoItem = Backbone.Model.extend({
 validate: function(özellikler){
  if (!özellikler.açıklama)
   return "Açıklama değeri girlmelidir.";
 }
});

Görselleri tamamlamak için bir de tüm yapılacakların kolleksiyonunu gösteren görsel lazım. TodoItemsView.js adında yeni bir dosya ekleyelim,

todo/js/TodoItemsView.js
var TodoItemsView = Backbone.View.extend({
 render: function(){
  var self = this;
  this.model.each(function(todo){
   var view = new TodoItemView({ model: todo });
   self.$el.append(view.render().$el);
  });
  return this;
 }
});

Render metodunda kolleksiyonda verilen her model nesnesi üzerinden iterasyon yapıp, tek tek görsellerini ekliyoruz. Bu kolleksiyon görselini index.html dosyamıza eklemeyi unutmayalım.

todo/index.html
...
        <script src="js/TodoItem.js"></script>
        <script src="js/TodoItems.js"></script>
        <script src="js/TodoItemView.js"></script>
        <script src="js/TodoItemsView.js"></script>
...

Şimdi main.js dosyamıza gidip yapılacaklar listesinin görselini ekleyelim.

todo/js/main.js
$(document).ready(function(){
 var todoItems = new TodoItems([
  new TodoItem({ açıklama: "Yapılacak 1"}),
  new TodoItem({ açıklama: "Yapılacak 2"})
 ]);

 var todoItemsView = new TodoItemsView({ model: todoItems });
 $("body").append(todoItemsView.render().$el);
});

Önce örnek bir yapılacaklar kolleksiyonu oluşturuyoruz, sonra kolleksiyon görselini oluşturup render edip sayfaya ekliyoruz. Sayfaya şöyle çıkacaktır.


Süper! Elemanları incelediğimizde body bölümü en sonuna div içinde div'ler şeklinde default eleman ile değerlerin eklendiğini görürüz. Bunları lliste elemanları olarak yayınlamak için bildiğimiz gibi görsellerinde kolleksiyona ul modele li olarak tagName eklememiz gerekir.

todo/js/TodoItemView.js
var TodoItemView = Backbone.View.extend({
 tagName: "li",
 initialize: function(opsiyonlar){
...

todo/js/TodoItemsView.js
var TodoItemsView = Backbone.View.extend({
 tagName: "ul",
 render: function(){
...


İleride bu böyle saf bir liste elemanı gibi görünmesin diye bazı CSS eklemeleri yapacağımız kesin. O zaman bu ul elemanına bir id değeri verelim de hazır olsun.

todo/js/TodoItemsView.js
var TodoItemsView = Backbone.View.extend({
 tagName: "ul",
 id: "todoItems",
 render: function(){
...

Yapılacaklar listesi görseli de eğer kendisine model olarak bir kolleksiyon verilmediyse bize hata mesajı versin. initialize metodu içinde bunu tanımlayalım.

todo/js/TodoItemsView.js
var TodoItemsView = Backbone.View.extend({
 tagName: "ul",
 id: "todoItems",
 initialize: function(opsiyonlar){
  if (!(opsiyonlar && opsiyonlar.model))
   throw new Error("TodoItemsView kolleksiyon verilmedi.");
 },
...

Şimdi main.js dosyasında görseli oluştururken model değeri girmeden yapalım ve sonucu test edelim.

todo/js/main.js
$(document).ready(function(){
 var todoItems = new TodoItems([
  new TodoItem({ açıklama: "Yapılacak 1"}),
  new TodoItem({ açıklama: "Yapılacak 2"})
 ]);

 var todoItemsView = new TodoItemsView();
 $("body").append(todoItemsView.render().$el);
});


OK, çalışıyor. main.js dosyasını eski haline getirelim.



Yeni bir yapılacak iş eklemek

Listenin en üstüne bir tane textbox koyalım ve oraya yeni bir iş girildiğinde listeye ekleyelim. Tasarım hakkında kısaca bir düşünürsek. Ya yeni bir görsel nesnesi tanımlayıp yeni kayıt alma ve kaydetme işini ona yükleyeceğiz ya da mevcut kolleksiyon görseline ilave yapacağız. Bunun tam doğru bir cevabı yok. Ama tavsiyesi var, programı oluşturan parçalar mümkün oldukça birbirine yakın büyüklüklerde ve hakim olabilmenin kolay olduğu büyüklüklerde olmalı.

Doğru Yazılım
Yanlış Yazılım
Daha da Yanlış Yazılım

En alttaki bizim Ruby camiasına özgü davranışı gösteriyor. Kodu milyon küçük parçaya ayırırlar, bir bakışta tüm dosyayı görürsün ama kim neredeydi bir sürü dosya aran dur.

Bunlara baktık , karar verdik yeni yapılacak iş girmek için 2 satırlık koda ayrı dosya açmayacağız. Kolleksiyon görseline ilave yapacağız.

Önce bir buton ekleyip onun tıklama olayını tanımlayarak başlayalım.

todo/js/TodoItemsView.js
...
 events: {
  "click #ekle": "ekleTıklandı"
 },
 ekleTıklandı: function(){
  console.log("Tıklandı");
 },
 render: function(){
  var self = this;

  this.$el.append("<button id='ekle'>Ekle</button>");

  this.model.each(function(todo){
...

Hemen sayfayı tazeleyip bakalım. Buton tıklanınca konsola Tıklandı mesajı gelmesi gerekir. Bebek adımlarıyla devam. Konsola mesaj yazmak yerine kolleksiyona bir örnek yapılacak iş kaydı ekleyelim.

 ekleTıklandı: function(){
  var todoItem = new TodoItem({ açıklama: "Yeni yapılacak iş" });
  this.model.add(todoItem);
 },

Bunu denemeye kalkarsak görüntüde bir şey değişmeyecek. Eklenip eklenmediğini nasıl bileceğiz? main.js dosyasındaki kodun tamamını $(document).ready fonksiyonu içine alınca değişkenleri de konsoldan göremez olduk. Bu konuyu internette aradım. Kimi demiş değişkenleri dışarda tanımla içerde kullan, kimi demiş değişkenleri expose et, kimi demiş dışarıda x adında boş bir obje ekle içerideki değişkenleri de bu objenin elemanları olarak ekle. Sonuncu kulağıma güzel geldi ve main.js dosyasını şöyle değiştirdim.

todo/js/main.js
var x = {};
$(document).ready(function(){
 x.todoItems = new TodoItems([
  new TodoItem({ açıklama: "Yapılacak 1"}),
  new TodoItem({ açıklama: "Yapılacak 2"})
 ]);

 x.todoItemsView = new TodoItemsView({ model: x.todoItems });
 $("body").append(x.todoItemsView.render().$el);
});

Konsolla arama kimse giremez. Ama isterseniz şöyle de yapabilirsiniz,

todo/js/main.js
var todoItems, todoItemsView;
$(document).ready(function(){
 todoItems = new TodoItems([
  new TodoItem({ açıklama: "Yapılacak 1"}),
  new TodoItem({ açıklama: "Yapılacak 2"})
 ]);

 todoItemsView = new TodoItemsView({ model: todoItems });
 $("body").append(todoItemsView.render().$el);
});

Vallahi benim aklıma gelmişti.

Bu ikinci daha güzel o kalsın. Şimdi test ettiğimde butona her tıkladığımda todoItems nesnesine yeni bir model eklendiğini konsolda görüyorum.

Şimdi kolleksiyona ilave olduğunu izleyen bir olay görev fonksiyonu yazarak görsele eklememizi yapalım.

todo/js/TodoItemsView.js
var TodoItemsView = Backbone.View.extend({
 tagName: "ul",
 id: "todoItems",
 initialize: function(opsiyonlar){
  if (!(opsiyonlar && opsiyonlar.model))
   throw new Error("TodoItemsView kolleksiyon verilmedi.");
  this.model.on("add", this.ekleTıklandıFn, this);
 },
 ekleTıklandıFn: function(yapılacak){
  console.log("İş eklendi");
 },
...

Fonksiyon adı yanlış oldu. Butona tıklamadan başka bir yerden de ekleme yapılabilir. Değiştirelim.

todo/js/TodoItemsView.js
var TodoItemsView = Backbone.View.extend({
 tagName: "ul",
 id: "todoItems",
 initialize: function(opsiyonlar){
  if (!(opsiyonlar && opsiyonlar.model))
   throw new Error("TodoItemsView kolleksiyon verilmedi.");
  this.model.on("add", this.işEklendi, this);
 },
 işEklendi: function(yapılacak){
  console.log("İş eklendi");
 },
...

Sayfayı yenileyip Ekle butonuna tıklayalım ve mesajın geldiğini görelim. Şimdi mesaj yerine yeni eklenen işi görsele ekleyen kodu yazalım.

 işEklendi: function(yapılacak){
  var görsel = new TodoItemView({ model: yapılacak });
  this.$el.append(görsel.render().$el);
 },

Şimdi butona tıkladığımızda yeni iş listeye eklenecektir.


Sonraki adım bir textbox ile yeni işin açıklama değerini okumak. Önce render fonksiyonuna textbox ilavemizi yapalım.

todo/js/TodoItemsView.js
...
 render: function(){
  var self = this;
  this.$el.append("<input type='text' id='yeniİş'></input>");
  this.$el.append("<button id='ekle'>Ekle</button>");
  this.model.each(function(todo){
...

Tıklama fonksiyonunda örnek değer yerine textbox değerini ekleyelim.

...
 ekleTıklandı: function(){
  var todoItem = new TodoItem({ açıklama: this.$("#yeniİş").val() });
  this.model.add(todoItem);
 },
...

Bir bakalım nasıl olmuş?


O input elemanının şeklini beğenmedim. CSS dosyamıza şunu ekleyelim.

todo/css/styles.css
...
#yeniİş {
 font-size: 20px;
 margin-right: 5px;
}

Şimdi daha iyi,


Textbox'ın giriş yapıldıktan sonra içinin boşaltılmasını sağlayalım.

todo/js/TodoItemsView.js
 ekleTıklandı: function(){
  var todoItem = new TodoItem({ açıklama: this.$("#yeniİş").val() });
  this.model.add(todoItem);
  this.$("#yeniİş").val("");
 },

Bu kodda dikkat çeken birşey aynı elemanı jQuery'ye iki defa aratıyoruz. Hem performans hem de temiz kod sebebiyle bu elemanı bir jQuery nesnesine aktarıp öyle üzerinde işlem yapalım.

 ekleTıklandı: function(){
  var $yeniİş = this.$("#yeniİş");
  var todoItem = new TodoItem({ açıklama: $yeniİş.val() });
  this.model.add(todoItem);
  $yeniİş.val("");
 },

Bu da tamam.

Kendi internet kullanımımızdan da biliriz. Oradaki butona basmak yerine nasıl olsa girilecek bir tek veri var, input elemanında enter basınca yeni iş eklemesi yapılsa daha iyi olur.

todo/js/TodoItemsView.js
...
 events: {
  "click #ekle": "ekleTıklandı",
  "keypress #yeniİş": "tuşBasıldı"
 },
 tuşBasıldı: function(e){
  if (e.keyCode == 13)
   console.log("Enter basıldı");
 },
...

Denediğimizde kutu içinde enter bastığımızda mesaj gelecektir. Diğer tuşlara basınca birşey olmaz. e değişkeni olay nesnesini içerir. Olay tuş basılması olduğu için basılan tuşun bir key kodu var. Kod 13'e eşitse enter tuşu basılmış demektir.

Şimdi yapacağımız şey konsola mesaj yazmak yerine butona tıklanınca ne oluyorsa onu yapmak. Butona tıklanınca ekleTıklandı metodu çağrılıyor. Onu çağıralım.

 tuşBasıldı: function(e){
  if (e.keyCode == 13)
   this.ekleTıklandı();
 },

Şu anda uygulamamızda bir böcük oluştu. Bir şey yazmadan enter basarsak listeye boş bir kayıt ekleniyor. Bunu düzeltmek için kayıt eklemeden önce değer var mı bakalım. ekleTıklandı metodu içinde kayıt ekleyen kısmı bir if bloğu içine alalım.

 ekleTıklandı: function(){
  var $yeniİş = this.$("#yeniİş");
  if ($yeniİş.val()){  
   var todoItem = new TodoItem({ açıklama: $yeniİş.val() });
   this.model.add(todoItem);
   $yeniİş.val("");
  }
 },

Artık boş kutuya enter basmak hiç bir şey yapmıyor. Sayfa yüklendiğinde otomatikman textbox fokuslansın istersek sayfaya eklerken autofocus özelliği veririz.

        this.$el.append("<input type='text' id='yeniİş' autofocus></input>");

Şimdi size bir tricky shot. Yapılacak iş olarak textbox içine <script>alert("Seni avladım");</script> yazıp enter basın bakalım ne olacak?


İşte size sitede güvenlik açığı denen şeylere bir örnek. Gerçi burada uygulama client tarafta koşarken pek sıkıntı olmaz ama server tarafta script koşan uygulamalarda formlardan girilen verileri içeride kullanmadan evvel iyice yıkayıp paklamamız gerekir. Bunu nasıl yapacağımızı da anlatalım.

Bu açık input box'ta veri girlince değilde onu sayfaya eklemeye kalkınca çıkıyor. Orada yeni iş eklerken <h1>büyük iş</h1> yazsak o da sayfaya konarken html kod olarak eklendiği için kocaman çıkardı. Açığı kapatmanın yolu görselde sayfaya eklemeden bilgiyi analiz etmek. açıklama özelliğini yayınlarken get metodu yerine escape metodu kullanırsak html kodları düz yazıya dönüştürülür.

todo/js/TodoItemView.js
...
    render: function(){
        this.$el.html(this.model.escape("açıklama"));
        return this;
    }
...

Açık kapanmıştır.






Bir işi işaretlemek

İşlerin her birinin başına bir checkbox koyalım ve buna tıkladığımızda o iş bitmiş olarak işaretlensin. Önce görsele checkbox ilavesi yaparak başlayalım.

todo/js/TodoItemView.js
...
    render: function(){
        this.$el.html("<input id='işaretle' type='checkbox'></input>" 
         + this.model.escape("açıklama"));
        return this;
    }
...

Sayfayı yenileyince her iş görseli başına bir checkbox eklenmiş olduğunu görürüz. Şimdi bu checkbox'a tıklama olayı için görev fonksiyonu yazmalıyız. Artık öğrendik, görsele bir olay ve görev fonksiyonu tanımı ekleyeceğiz.

todo/js/TodoItemView.js
...
    events: {
     "click #işaretle": "işaretlendi"
    },
    işaretlendi: function(){
     console.log("İşaretleme yapıldı");
    },
    render: function(){
        this.$el.html("<input id='işaretle' type='checkbox'></input>" 
...

Hemen sayfayı yenileyip konsola mesajın geldiğini görelim. Küçük küçük adımlar, baby steps. Peki görev fonksiyonumuz ne yapmalı?

Model verileri üzerinde kayıtlardan gittiğimize göre işin bitmiş olmasını da kayıtlarda saklamalıyız. Diyelim modelde açıklama dışında bir de tamamlandı özelliğimiz olsun ve biz buraya true ya da false şeklinde bool değer koyarak işin tamamlanmasını kayıt edelim. Checkbox tıklandığında değer true ise false yapalım tersi ise true yapalım.

...
    işaretlendi: function(){
     this.model.set("tamamlandı", !this.model.get("tamamlandı"));
     console.log(this.model.toJSON());
    },
...

Sayfayı yenileyip test edersek,


Aslında veri değişimine yol açan bu tip kodları model içinde yapmak çok daha doğru olacaktır. Bu amaçla modelimizin tanımna bir metod ekleyelim.

todo/js/TodoItem.js
var TodoItem = Backbone.Model.extend({
    validate: function(özellikler){
        if (!özellikler.açıklama)
            return "Açıklama değeri girlmelidir.";
    },
    işaretle: function(){
     this.set("tamamlandı", !this.get("tamamlandı"));
    }
});

ve görselimizde modele ait metodu çağıralım,

todo/js/TodoItemView.js
...
    işaretlendi: function(){
     this.model.işaretle();
     console.log(this.model.toJSON());
    },
    render: function(){
        this.$el.html("<input id='işaretle' type='checkbox'></input>" 
...

Test edelim ve çalıştığını görelim. Kodumuz artık çok object oriented..

Şimdi bir hayal edelim model server'dan alınabilir ya da kodumuzda başka bir yerde model değişebilir. Ayrıca işaretlenen işin görselinde başka değişiklikler de yapmak isteyebiliriz. Bu amaçla alt yapıyı kuralım. Görselimiz modelde değişiklik olayını takip etsin ve görseli yenilesin. Görselin initialize metoduna model veri değişikliğinde görseli yenileyen bir olay izleme kodu yazalım.

todo/js/TodoItemView.js
var TodoItemView = Backbone.View.extend({
    tagName: "li",
    initialize: function(opsiyonlar){
        if (!(opsiyonlar && opsiyonlar.model))
            throw new Error("TodoItemView görseli model olmadan üretildi");
        this.model.on("change", this.render, this);
    },
...

Modelde değişiklik yapınca görsel yenilenecek. Şu anda yaptığımız işi düşünürsek zaten biz checkbox'ı tıkladığımızda görseli değiştiriyoruz. Ama mesela şunu yapmak istiyoruz, biten işin üstü çizili yazılsın. Bir konu da yukarıda bahsettiğim eğer verileri bir server'dan okursak ve orada tamamlandı değeri true olarak gelirse ne olacak? Bizim görselin kodu checkbox'ı default seçilmemiş olarak yayınlıyor ama bu durumda seçilmiş olarak yayınlaması gerekir.

İlkinden başlayalım. Yazıyı üstü çizili yapmak için o işe ait li görseline bir class ekleyelim ve CSS içinde bu sınıfa üstü çizili yazı stili tanımlayalım. görselin render metoduna class ilavesi yaparak başlayalım.

todo/js/TodoItemView.js
...
    render: function(){
     this.$el.toggleClass("tamamlandı", this.model.get("tamamlandı"));
        this.$el.html("<input id='işaretle' type='checkbox'></input>" 
         + this.model.escape("açıklama"));
        return this;
    }
});

jQuery toggleClass fonksiyonu elemana bir class değerini ekler ya da siler. Ne yapacağını ikinci parametre belirler. Bunu test etmek için sayfayı yenilediğimizde geliştirici araçlarında her iki elemanın da class değerlerini tamamlandı olarak geldiğini görürüz. Checkbox'ları tıklayıp bıraktıkça normale dönecektir. Başta modelin tamamlandı özelliği tanımlanmamış olduğu için undefined olacaktır. Ve undefined değeri JavaScript true olarak algılar. Bunu çözmenin yolu modele tamamlandı özelliğini defaults kısmında false olarak tanımlamak.

todo/js/TodoItem.js
var TodoItem = Backbone.Model.extend({
 defaults: {
  tamamlandı: false
 },
    validate: function(özellikler){
...

Süper!. Artık sayfa yenilenince yanlış sınıf yüklemesi gelmiyor. CSS dosyamıza ilave yaparak biten işin üstünün çizili olmasını sağlayalım.

todo/css.styles.css

body {
    padding: 40px;
}

body, p { font-size: 24px; }

button { font-size: 20px; }

#yeniİş {
    font-size: 20px;
    margin-right: 5px;
}

#todoItems .tamamlandı {
    text-decoration: line-through;
}

Sıra geldi checbox'a. Tıkladığımızda görsel tekrar çizilidiği için checkbox tıklanmamış olarak geliyor. Çünkü görselimizde default olarak boş bir checkbox koyuyoruz. Biten işler için checkbox işaretlenmiş olarak gelmeli. Bunu sağlamak için checkbox'ı ekrana koyan koda checked özelliği eklemeliyiz. Görsel render metodumuzu şöyle düzenleyelim.

todo/js/TodoItemView.js
...
    render: function(){
     this.$el.toggleClass("tamamlandı", this.model.get("tamamlandı"));
     var checked = this.model.get("tamamlandı") ? "checked" : "";
        this.$el.html("<input id='işaretle' type='checkbox' " + checked + "></input>" 
         + this.model.escape("açıklama"));
        return this;
    }
...

OK! istediğimiz oldu


Baya bi yol aldık di mi?




Listeden bir işi silmek

Listeden bir elemanı silmek için bir kod yazacağız. Ama öncesinde görselini düşünelim. Listedeki işin üzerine gelindiğinde yanında bir "X" işareti çıkabilir mesela. Önce TodoItemView görselinde iş açıklamasından sonra bir silme butonu ekleyerek başlayalım. 

todo/js/TodoItemView.js
...
    render: function(){
     this.$el.toggleClass("tamamlandı", this.model.get("tamamlandı"));
     var checked = this.model.get("tamamlandı") ? "checked" : "";
        this.$el.html("<input id='işaretle' type='checkbox' " + checked + "></input>" 
         + this.model.escape("açıklama")
         + "<button id='sil'>Sil</button>");
        return this;
    }
});

Kontrol edelim,


Güzel. Şimdi tıklanma için gereken olay ilavesi ve görev fonksiyonu ilavesini yapalım.

todo/js/TodoItemView.js
...
    events: {
     "click #işaretle": "işaretlendi",
     "click #sil": "silTalebi"
    },
    silTalebi: function(){
     console.log("Sil butonu basıldı");
    },
...

Butona tıklanınca konsolda mesajı görelim ve bu adımdan emin olalım. Daha önce checkbox olayında yaptığımız gibi işi görselde yapmayıp model metodu ile yapalım. Modelimizde destroy metodu silme işini yapan metoddur ve default tanımıdır. Önce konsol mesajı yerine model metodu çağıralım.

    silTalebi: function(){
     this.model.destroy();
    },

Neden ayırıyoruz biraz daha açıklık getirelim. Server varken silme butonu tıklandığında server'a bir http DELETE isteği gönderilecek sonuçları değerlendirilecek vs. Tüm bu kodları da görsel içine karıştırmayalım diye modelde olması daha mantıklı. Sonuçta görselde amacımız zaten zor olan bir şeye, yani kullanıcı arabirime hükmedebilmek.

Modelin destroy metodu kaydı silecek. Bu durumda kolleksiyonda bir remove olayı oluşur. Kolleksiyon görselimiz remove olayı gerçekleştiğinde tekrar render ederek görseli yenilemesi lazım. Olay izleme ve görev fonksiyonu tanımlarını görsele ekleyelim.

todo/js/TodoItemsView.js
...
    initialize: function(opsiyonlar){
        if (!(opsiyonlar && opsiyonlar.model))
            throw new Error("TodoItemsView kolleksiyon verilmedi.");
        this.model.on("add", this.işEklendi, this);
        this.model.on("remove", this.işSilindi, this);
    },
    işSilindi: function(iş){
        console.log(iş, "silindi");
    },
...

Test edelim ve konsola mesajın geldiğini görelim.

Uygulama gerçekte kullanılırken verileri bir server'dan alacak. Bu durumda silinen elemanı görselden silmek yetmeyecek server'a da bu silinme bildirilecek. Hangi kaydın silineceği bildirilirken modelimizin id özelliği kullanılacak. Ancak şu anda bizim modellerde id özelliği yok. Önce örnek modelleri oluştururken id özelliği vererek başlayalım.

todo/js/main.js
var todoItems, todoItemsView;
$(document).ready(function(){
    todoItems = new TodoItems([
        new TodoItem({ id: 1, açıklama: "Yapılacak 1"}),
        new TodoItem({ id: 2, açıklama: "Yapılacak 2"})
    ]);

    todoItemsView = new TodoItemsView({ model: todoItems });
    $("body").append(todoItemsView.render().$el);
});

Modeli gösterirken ilgili modeli bulabilmek adına görsellerine id değerini ekleyelim.

todo/js/TodoItemView.js
...
    },
    render: function(){
     this.$el.attr("id", this.model.id);
     this.$el.toggleClass("tamamlandı", this.model.get("tamamlandı"));
...

Kontrol ettiğimizde html li elemanlara id değeri olarak modelin id değerinin yazıldığını görürüz.


Modellere id değeri verince Backbone server ile de iletişim kurmaya kalkıyor. Biz de modele url özelliği tanımlamadığımız için silme butonuna tıkladığımızda hata oluşuyor. Bunu görmek için geliştirici araçları konsol açıkken silme butonuna basmanız yeterli.


Server'a bağlantıyı gelecek bölümde yapacakmışız. Bu yüzden şimdilik fake bir url değeri vereceğiz.

todo/js/TodoItem.js
var TodoItem = Backbone.Model.extend({
 defaults: {
  tamamlandı: false
 },
 url: "fakeURL",
    validate: function(özellikler){
...

Bu seferde server'ın bulunamadığını bildirir ama en azından artık kod çalışıyor. Nereden anlıyoruz. Silinen modelin bilgisi artık konsola geliyor. Şimdi sırada silinen modele ait id değerini taşıyan li elemanını listeden yok etmek var. Konsola silinen modeli yazan kod yerine elemanı DOM'dan çıkaracak kodu yazalım.

todo/js/TodoItemsView.js
...
    işSilindi: function(iş){
        this.$("li#" + iş.id).remove();
    },
...

Tek satır jQuery kodu ile önce tipi li ve id değeri silinen modelin id'si olan elemanı seçiyoruz, sonra remove metodu ile DOM'dan siliyoruz. Şimdi test ettiğimizde sil butonu basınca o satır küt diye silinecektir.

Her bir adım gidişimizde yeni bir sıkıntı ortaya çıkıyor. Yeni kayıt eklerken kayda bir id değeri verilmiyor. Çünkü gerçek uygulamada id değeri server tarafından verilir. Şu anda yeni bir iş eklersek görüntüden silmemiz mümkün olmayacaktır. Neyse önemi yok. Sırada server bağlantısı var.




Server bağlantısı

Ben daga önce size lokal olarak bir server nasıl kurulur anlatmıştım. Burada ise öğreticilerin eğitimlerde kullandığı bir site kullanımını göreceğiz. https://jsonplaceholder.typicode.com sitesi bazı örnek verilerle hazırlanmış test amaçlı bir Rest API server. Verileri saklamıyor ama her hareketinize destek veriyor. Bana kalsa burayı sadece örnek veri bakmak için kullanırım ama hocaya sadık kalalım bakalım. Bunu da bilin ama yapmayın..

Sayfada aşağıda resources kısmında fake server hizmeti alabileceğimiz repo isimleri var. Bunlardan biride todos.


Linke tıkladığımızda örnek veriler gösterilecektir. Bizimkinden farklı. açıklama yerine title ve tamamlandı yerine completed özellikleri kullanılmış. Bir de bizim hiç kullanmadığımız userId özelliği var. Olsun demiş hoca değiştiririz programımızı bu verilere göre.

Daha önce hata vermesin diye bir fakeURL değeri vermiştik modele. Şimdi test server url'ini verelim.

todo/js/TodoItem.js
var TodoItem = Backbone.Model.extend({
 defaults: {
  tamamlandı: false
 },
 urlRoot: "https://jsonplaceholder.typicode.com/todos",
    validate: function(özellikler){
        if (!özellikler.açıklama)
            return "Açıklama değeri girlmelidir.";
    },
    işaretle: function(){
     this.set("tamamlandı", !this.get("tamamlandı"));
    }
});

Adres todos linkinin adresi. Bir de daha önce url yazmışız urlRoot olacaktı. Kolleksiyonlarda url oluyor. Çünkü orada bir sürü model var, yani bir kolleksiyon var. Kolleksiyona da url değeri verelim.

todo/js/TodoItems.js
var TodoItems = Backbone.Collection.extend({
    model: TodoItem,
 url: "https://jsonplaceholder.typicode.com/todos",
});

Şimdi main.js dosyamıza gidip, örnek olarak girdiğimiz değerleri silelim

todo/js/main.js
var todoItems, todoItemsView;
$(document).ready(function(){
    todoItems = new TodoItems();
    todoItems.fetch();

    todoItemsView = new TodoItemsView({ model: todoItems });
    $("body").append(todoItemsView.render().$el);
});

Boş bir kolleksiyon oluşturup, verileri server'dan çekmek için fetch metodunu çalıştırıyoruz. Burada çalıştırınca farkedemeyeceğimiz bir nokta var ama bizim program öyle denk gelmiş. fetch metodu asenkron çalışan bir metod. Normalde yukarıdaki kod server'dan isteğini yapacak ve durmadan devam edecek. Hemen arkasından görsel oluşturulup render çağrılıyor. Yani daha server cevap verip kolleksiyonun değerleri doldurulmadan boş bir sayfa olarak görsel yayınlanacaktı.

Ancak bizi yazdığımız bir olay izleyici kurtardı Kolleksiyon görselinde hatırlarsak kolleksiyona yeni veri eklenince render tekrar çağrılıyordu.

    initialize: function(opsiyonlar){
        if (!(opsiyonlar && opsiyonlar.model))
            throw new Error("TodoItemsView kolleksiyon verilmedi.");
        this.model.on("add", this.işEklendi, this);
        this.model.on("remove", this.işSilindi, this);
    },

Server'dan cevap gelip kolleksiyona her yeni veri eklendiğinde otomatikman add olayı ateşlenecek. Bizim yazdığımız kod da işEklendi metodunda  tekrar tekrar render edecek.

Ama eğer bu olay bizim kodumuzda rastgele olmasaydı, ne yapacaktık? Görselin yayınlanmasını server'dan cevap geldikten sonra yapacaktık. Bu amaçla fetch metodunda success özelliği kullanılır. fetch metodu başarılı bir şekilde server'dan verileri okumayı bitirdiğinde success özelliğinde tanımı yapılan fonksiyon çalışır. Örnek için veriyorum. Bizim kodumuzda bunu yapmamıza gerek yok. Bu durumda main.js içinde fetch işlemi şöyle yapmak gerekecekti.

todo/js/main.js
var todoItems, todoItemsView;
$(document).ready(function(){
    todoItems = new TodoItems();
    todoItems.fetch({
        success: function(){
            todoItemsView = new TodoItemsView({ model: todoItems });
            $("body").append(todoItemsView.render().$el);
     }
    });
});

Tekrar söyleyim bunu biz yapmıyoruz ama bizdeki gibi olay işleme fonksiyonu denk gelmez de görsel erken olarak boş gelirse şaşırmayın böyle yapılması gerekiyor.

Şimdi nereye geldik kontrol edelim.


Neden gelmediği belli. Başvurduğumuz veri tabanında açıklama ve tamamlandı alanları yok. Modelimizdeki bu alan isimlerini title ve completed olarak değiştirmeliyiz.

Ama önce benzer bir sorunla ileride karşılaştığımızda çözümü nasıl avlayacağız bakalım. Örnek veriler ile uygulsmsmız güzel çalışıyordu. Server bağlantısı sonrası sıkıntı gördük. İlk bakacağımız şey konsolda bir hata bildirilmiş mi? Yok. O zaman server iletişimine bir bakalım.

Geliştirici araçları penceresinde konsol gibi , elemanlar gibi bir de network sekmesi var. Network sekmesi açıkken sayfayı yenilediğimizde haberleşme bilgisi gelecektir. todos adında bir iletişim göreceğiz, tıkladığımızda ayrıntısı gelecektir. Orada sorulan sorunun ve server'dan gelen cevabın ayrıntısı görülebilir.


Burada gelen cevapta farklı olan model özellikleri görülüyor. Açıklamalar gelmiyor çünkü açıklama yerine title yazmalıyız. Buradan başlayalım.

todo/js/TodoItemView.js
...
    render: function(){
     this.$el.attr("id", this.model.id);
     this.$el.toggleClass("tamamlandı", this.model.get("completed"));
     var checked = this.model.get("completed") ? "checked" : "";
        this.$el.html("<input id='işaretle' type='checkbox' " + checked + "></input>" 
         + this.model.escape("title")
         + "<button id='sil'>Sil</button>");
        return this;
    }
});

Yeri gelmişken tamamlandı isimlerini de completed olarak değiştirdik. Model tanımı içinde de vardı bu isimler tabiki.

todo/js/TodoItem.js
var TodoItem = Backbone.Model.extend({
 defaults: {
  completed: false
 },
 urlRoot: "https://jsonplaceholder.typicode.com/todos",
    validate: function(özellikler){
        if (!özellikler.title)
            return "Açıklama değeri girlmelidir.";
    },
    işaretle: function(){
     this.set("completed", !this.get("completed"));
    }
});

Test edelim bakalım


Hey yaşasın, ne olduğunu anlamadığımız işlerin bazıları tamamlanmış bile!. Bunlardan birini sildiğimizde network'de DELETE işlemi yapıldığını görebiliriz. Ancak birini tamamlandı diye işaretlesek görselde işaretlendiğini görürüz ama network'de bir hareket görünmez. Görselde tıklanınca ne yapmıştık bakalım.

todo/js/TodoItemView.js
...
    işaretlendi: function(){
     this.model.işaretle();
    },
...

Burada modeli işaretliyoruz ama sadece hafızada oluyor. save metodu kullanarak veriyi server'a kaydetmeliyiz.

todo/js/TodoItemView.js
...
    işaretlendi: function(){
     this.model.işaretle();
     this.model.save();
    },
...

Test ettiğimizde artık network görünümünde bir PUT isteği görünecektir.

Yeni bir iş eklediğimizde ne oluyor? Hiç bir şey. Sadece listeye boş bir satır ekleniyor. açıklama kalan bir yer var kesin. Bir de network hareketinde POST işlemi görmeliydik. Bunun sebebini biliyoruz save çağırılmamış.

Kolleksiyon görselinde ekleTıklandı adında bir görev fonksiyonu tanımlamıştık ona bakalım. İşte eksik kalan açıklama alanı burada! Hemen onu değiştirelim ve yeni eklenen işi save ile server'a gönderelim.

todo/js/TodoItemsView.js
...
    ekleTıklandı: function(){
        var $yeniİş = this.$("#yeniİş");
        if ($yeniİş.val()){     
            var todoItem = new TodoItem({ title: $yeniİş.val() });
            todoItem.save();
            this.model.add(todoItem);
            $yeniİş.val("");
        }
    },
...

Test edip görelim. Burada yeni kayıt ekledik server'a kaydettik. Ama bu bir sahte veritabanı. Sanki varmış gibi davranıyor. İşlemlerimize cevap veriyor ama sayfa yenilenince veri yine ilk halindeki gibi gelir. Amaç server ile uygulama test etmek.

En son yaptığımız kod değişikliğinde biraz kısaltma yapabiliriz. Backbone kolleksiyonlarda create metodu ile hem modellere yeni kayıt eklenir hem de save işlemi yapılır.

    ekleTıklandı: function(){
        var $yeniİş = this.$("#yeniİş");
        if ($yeniİş.val()){     
            var todoItem = new TodoItem({ title: $yeniİş.val() });
            this.model.create(todoItem);
            $yeniİş.val("");
        }
    },

Server bağlantı işi de tamam. Stil pek güzel değil ama merak etmeyin ona da sıra gelecek.



Template kullanalım

Kodumuzu düzenli hale getirmek için template kullanalım. TodoItemView görselinin render fonksiyonuna bakarsak pek okunabilir değil. Ki bu basit bir görsel, ileride daha gelişmiş görseller karşımıza çıkacaktır. 

Başlayalım. index.html dosyamıza bir template tanımlayarak ilk adımı atalım. Daha önce template anlatırken underscore.js kalıpları kullanmıştık. Bu sefer mustache.js kullanalım. Bu linke sağ tıklayıp farklı kaydet deyin ve todo/js/lib klasörüne mustache.min.js dosyasını kaydedin. Sonra index.html dosyamıza bu JavaScript kütüphanesini de ekleyelim.

todo/index.html
...
        <script src="js/lib/jquery-3.5.1.min.js"></script>
        <script src="js/lib/underscore-min.js"></script>
        <script src="js/lib/backbone-min.js"></script>
        <script src="js/lib/mustache.min.js"></script>
...

TodoItemView görselimize bakarsak bize id değeri işaretle olan bir checkbox lazım, checked özelliği model verisine göre değişecek. Sonra içeriğe modelin title değerini ekleyeceğiz. Son olarak da id değeri sil olan bir buton var. Template şöyle olacak.

todo/index.html
...
        <script src="js/main.js"></script>

        <script type="text/html" id="todoItemTemplate">
            <input type="checkbox"  {{#completed}}checked{{/completed}}
                id="işaretle"></input>
            {{ title }}
            <button id="sil">Sil</button>
        </script>
    </body>
</html>

{{#completed}} ve {{/completed}} arasında kalan kısım bir if bloğu gibi değerlendirilir. Yani completed değeri true ise arada kalan checked yazısı html koda eklenecek. {{ title }} kodu ise title değişkeni değerini html koda ekler.

Şimdi görsel kodunda render metodunu bu kalıbı kullanacak şekilde ayarlayalım.

todo/js/TodoItenView.js
...
    render: function(){
     this.$el.attr("id", this.model.id);
     this.$el.toggleClass("tamamlandı", this.model.get("completed"));
     
        var template = $("#todoItemTemplate").html();
        var html = Mustache.render(template, this.model.toJSON());
        this.$el.html(html);
        
        return this;
    }
});

Buradaki kod ilavesi tüm mustache.js teplate kullanan görsellerde hemen hemen aynıdır. Böylece burada sadece standart bir iş yapıyoruz. Görsele ise kalıp dosyasında odaklanıyoruz. Modelimize yeni özellikler eklendiğinde ya da görünümü değiştirmek istediğimizde büyük ihtimalle burada hiç bir değişiklik yapmayacağız.

Bu kod ne yapıyor? Önce template değişkenine görsel kalıbımızı html kod olarak alıyoruz. Sonra bunun içindeki mustache işlemlerini gerçekleştirmek için Mustache.render metodunu kullanıyoruz. Bu metod geriye kalıp işlenmiş olarak html kodu döner. Son olarak da elde ettiğimiz veriler işlenmiş html'i görsel olarak yayınlıyoruz.

Daha iyi anlamak için değişkenlerin içinde neler olduğuna bakabiliriz.

        var template = $("#todoItemTemplate").html();
        console.log(template);
        var html = Mustache.render(template, this.model.toJSON());
        console.log(html);
        this.$el.html(html);


Gördüğümüz gibi template değişkeninde aynen bizim yazdığımız şekliyle kalıp kodumuz var. html değişkeninde ise Mustache kodu ile işlenmiş ve bitmiş hale getirilmiş html kodu var.

Bir konuya daha dikkatinizi çekeyim. Daha önce render metodu içinde script girilemesin diye html hazırlarken escape ederek kullanmıştık. Şimdi öyle bir şey yapmadık ama Mustache otomatik olarak içeriği escape ederek yayınlar. O yüzden sıkıntı olmaz.



Görselin stilini düzenlemek

Sıra geldi görüntüye şekil şemal vermeye. Bu bölümün içinde Backbone geliştiriciliğinden çok genel olarak web geliştiriciliği üzerinde anlatacağız. 

Yapılacak işlerin başında bir checkbox koyduk ve ona tıklanınca işi bitirilmiş olarak işaretliyoruz. Bu checkbox yerine yazının üzerine tıklayarak bu işi yapmak kullanıcı açısından daha kolay olacaktır. Bunu checkbox ve title elemanlarını template içinde bir label içine alarak kolayca yapabiliriz. Template kodumuzu şöyle düzenleyelim. 

...
        <script type="text/html" id="todoItemTemplate">
            <label>
                <input type="checkbox" {{#completed}}checked{{/completed}} 
                    id="işaretle"></input>
                {{ title }}
            </label>
            <button id="sil">Sil</button>
        </script>
...

Test edersek artık yazının üzerine tıkladığımızda da checbox'a tıklamak ile aynı işi yaptığını göreceğiz.

Görselin stili ile oynamaya her işin başında gördüğümüz liste elemanı noktalarını (bullet denir) yok ederek devam edelim. CSS dosyamızı açalım.

todo/css/styles.css
...
#todoItems {
 list-style: none;
}

#todoItems .tamamlandı {
    text-decoration: line-through;
}

Harika! list-style: none; ile liste işareti olmayacağı belirtiliyor.

İş listesindeki satırlar birbirine çok yapışık biraz padding ekleyelim ayrılsınlar.

...
#todoItems {
 list-style: none;
}

#todoItems > li {
 padding: 10px;
}
...

Listedeki satırlar etrafında sınır çizerek birbirlerinden daha iyi ayrılmasını sağlayalım.

...
#todoItems {
 list-style: none;
 border-top: 1px solid #ccc;
 border-left: 1px solid #ccc;
}

#todoItems > li {
 padding: 10px;
 border-right: 1px solid #ccc;
 border-bottom: 1px solid #ccc;
}
...

Listenin sol kenarında sınır çizgisi ile arada hoş olmayan bir boşluk kaldı. Her ul elemanı default bir padding değerine sahip. Bunun üzerinde yazarak sıfırlayalım.

#todoItems {
 list-style: none;
 border-top: 1px solid #ccc;
 border-left: 1px solid #ccc;
 padding-left: 0;
}

Şimdi daha güzel. Yeni iş ekleme kutusunun altında bir sınırlandırma çizgimiz yok. Kolleksiyon görselinde bu input elemanını ul içine eklemiştik. Ama bu bir li elemanı olmadığı için diğer liste elemanları gibi değerlendirilmedi. ul elemanını ve input elemanı ile butonu birbirinden ayırmak lazım. Çünkü input bir ul alt elemanı değil. Kolleksiyon görselinden tagName ve id özelliklerini kaldırıp, listeyi render kodu içinde ul elemanı içine yerleştirelim.

todo/js/TodoItensView.js
var TodoItemsView = Backbone.View.extend({
    initialize: function(opsiyonlar){
        if (!(opsiyonlar && opsiyonlar.model))
...
    render: function(){
        var self = this;
        this.$el.append("<input type='text' id='yeniİş' autofocus></input>");
        this.$el.append("<button id='ekle'>Ekle</button>");
        this.$el.append("<ul id='todoItems'></ul>");
        this.model.each(function(todo){
            var view = new TodoItemView({ model: todo });
            self.$el.append(view.render().$el);
        });
        return this;
    }
});

Sayfayı yenileyince görsel bozulduğunu görürüz. id değerini silince görsel nereye ekleneceğini karıştırdı. Kodu inceleyelim. render içinde daha önce örnek modellerimizi yayınlamak için eklediğimiz bir kısım var this.model.each diye başlıyor. Bu blok artık işe yaramıyor, verileri server'dan alıyoruz ve olay izleme fonksiyonumuzda zaten her eklenen modeli görsele ekleyen bir kod var. Önce render metodundan bu bloğu silelim.

    render: function(){
        var self = this;
        this.$el.append("<input type='text' id='yeniİş' autofocus></input>");
        this.$el.append("<button id='ekle'>Ekle</button>");
        this.$el.append("<ul id='todoItems'></ul>");
        return this;
    }

Sayfayı yenilersek bir şey değişmedi, çünkü başlangıçta hiç model olmadığından bu blok hiç çalışmıyor. Şimdi yeni model eklenince çalışan görev fonksiyonu olan işEklendi metodunu değiştirerek liste elemanlarının doğru yere konmasını sağlayalım.

    işEklendi: function(yapılacak){
        var görsel = new TodoItemView({ model: yapılacak });
        this.$("#todoItems").append(görsel.render().$el);
    },

İstediğimiz oldu görseli ayırdık.


Yukarı ayırdığımız input ve button elemanlarını da bir header elemanı içine alsak da koda bakılınca onların da bir grup olduğu belli olsa. Hem istersek sadece o kısma ait stil oynamaları da yapabiliriz. İşte böyle ilaveler yaptıkça template kullanımının gereği ortaya çıkıyor. Kolleksiyon görselinin render metodu içine bu ilaveleri yaptıkça kodumuz daha da karışmaya başlayacak. En iyisi önce template olarak yayınlama yapalım sonra görsel düzenlemeyi yapalım.

Önce index.html dosyamızda template tanımını yapalım,

todo/index.html
...
        <script type="text/html" id="todoItemsTemplate">
            <input type='text' id='yeniİş' autofocus></input>
            <button id='ekle'>Ekle</button>
            <ul id='todoItems'></ul>
        </script>
    </body>
</html>

Şimdi render metodunu kalıp kullanacak şekilde değiştirelim.

todo/js/TodoItensView.js
...
    render: function(){
        var template = $("#todoItemsTemplate").html();
        var html = Mustache.render(template);
        this.$el.html(html);

        return this;
    }

Burada görsel kalıbımızda veri işlemiyoruz sadece amacımız kodu temiz yapmak. Bu yüzden Mustache.render metodu tek parametre olarak kalıp html'ini vererek çağırıyoruz. Sayfayı yenilersek görüntü değişmedi ama artık template üzerinde görsel olarak daha anlaşılabilir şekilde düzenleme yapabiliriz.

Template üzerinde değişiklik yaparak yeni iş ekleme kısmını bir header elemanı içine alalım.

        <script type="text/html" id="todoItemsTemplate">
            <header id="yeniİşEkle">
                <input type='text' id='yeniİş' autofocus></input>
                <button id='ekle'>Ekle</button>
            </header>
            <ul id='todoItems'></ul>
        </script>

Sayfayı yenileyip elemanlara baktığımızda bir div altında iki grup halinde daha açıklayıcı bir yerleşim görürüz.


Devam edelim. Yeni iş ekleme kutusu ile iş listesi arasında bir boşluk kalıyor bunu yok edelim. Elemanları incelediğimizde bu boşluğun ul elemanı üzerinde default olan margin değeri olduğunu görürüz.


CSS ile bu margin değerini uçuralım.

#todoItems {
 list-style: none;
 border-top: 1px solid #ccc;
 border-left: 1px solid #ccc;
 padding-left: 0;
 margin-top: 0;
}

Şimdi yapıştılar. Neden yaptık? Öncelikle şu ekle butonundan kurtulalım gereksi. Kullanıcı bir buton göremezse enter basarak ekleme yapılabileceğini anlar. Böylece elini bir klavyeye bir mouse'a götürmeden işleri arka arkaya ekleyebilir.

Önce kalıptan butonu çıkaralım,

todo/index.html
...
        <script type="text/html" id="todoItemsTemplate">
            <header id="yeniİşEkle">
                <input type='text' id='yeniİş' autofocus></input>
            </header>
            <ul id='todoItems'></ul>
        </script>
...

Görsel kodunun içinden bu butona ait olay izlemeyi de iptal edelim.

todo/js/TodoItensView.js
...
    events: {
        "keypress #yeniİş": "tuşBasıldı"
    },
    tuşBasıldı: function(e){
        if (e.keyCode == 13)
            this.ekleTıklandı();
    },
    ekleTıklandı: function(){
        var $yeniİş = this.$("#yeniİş");
...

events içinden olay izlemeyi kaldırdık ama görev fonksiyonu ekleTıklandı aynı zamanda textbox keypress izlemesi tarafından da enter basılınca yeni iş eklemek için kullanılıyor. Onu silmeden önce içindeki kodu tuşBasıldı metoduna kopyalayalım.

...
    events: {
        "keypress #yeniİş": "tuşBasıldı"
    },
    tuşBasıldı: function(e){
        if (e.keyCode == 13){
            var $yeniİş = this.$("#yeniİş");
            if ($yeniİş.val()){     
                var todoItem = new TodoItem({ title: $yeniİş.val() });
                this.model.create(todoItem);
                $yeniİş.val("");
            }
        }
    },
    render: function(){
        var template = $("#todoItemsTemplate").html();
...

OK. Buton gitti ve geri kalan çalışıyor. Şimdi de textbnox stilini liste elemanları gibi görünecek şekilde ayarlamaya çalışalım. Geliştirici araçları elements sekmesinde textbox elemanı bulalım ve onun stililine müdahale etmek için styles sekmesinde bizim verdiğimiz stilleri bulalım ve üzerinde tıklayarak düzenleme moduna geçelim. Burada yaptığımız her değişiklik anında görüntüye etki eder.


Önce padding ekleyelim. padding: 10px; ve yükseklik ve şekil olarak benzedi. Sonra genişliği %100'e çıkaralım,  width: 100%; ve tüm genişliği kapladı.


Yalnız biraz dışarı taştı. İncelersek aslında textbox'ın içinde bulunduğu header elemanında böyle bir taşma olmadığını görürüz. Textbox header'ın dışına uzadı. Genişlik %100 yapılınca üstüne de sağdan ve soldan paddingler girince dış çerçeve %100'ü aştı. box-sizing: border-box; ekleyerek taşmayı engelleriz. Bu stil elemanın paddingler dahil olarak boyut hesaplanacağını gösterir.

Elde ettiğimiz stilleri oradan kopyalayıp css içine yapıştıralım.

todo/css/styles.css
...
#yeniİş {
    font-size: 20px;
    margin-right: 5px;
    padding: 10px;
    width: 100%;
    box-sizing: border-box;
}
...

Kullanıcı sayfayı ilk defa açtığında ne yapacağını bilsin diye textbox içine bir ipucu yazısı (placeholder derler) ekleyelim. Bu iş basit, template kodunda textbox kodunu şöyle yaparız.

                <input type='text' id='yeniİş' autofocus 
                    placeholder="Sırada ne iş var?"></input>

Çok güzel


Listenin tüm sayfa genişliğini kaplamasını istemiyoruz. Diyelim 600 piksel olsun ve sayfayı ortalasın. Önce tüm liste ve yeni iş giriş kutusunu kapsayan div elemanını geliştirici araçları penceresinde bulalım ve stilinde şunları yapalım.

    width: 600px;
    margin: 0 auto;

margin: 0 auto; satırı div elemanını yatayda sayfa ortasına getirir. Ancak buradaki div elemanın bir id ya da class değeri yok stil dosyası için önce ona bir id değeri verelim.

todo/js/TodoItemsView.js
var TodoItemsView = Backbone.View.extend({
    id: "todoItemsDiv",
    initialize: function(opsiyonlar){
...

Sonra css dosyasına ilavemizi yapalım,

todo/css/styles.css
...
#todoItemsDiv {
 width: 600px;
 margin: 0 auto;
}


Sıra geldi Sil butonlarına. Darmadağın duruyorlar. Grubu geliştirici araçlarında bulalım ve sağ kenara kaydırmaya çalışalım. Grubu seçebilmek için butonun birine sağ tıklayıp "inspect element" veya Türkçe "öğeyi denetle" seçtiğimizde bildiğimiz gibi sadece o butonu seçiyor. Ama stilleri gösteren kısmın üzerinde + şekline tıklayınca aynı id ya da class değerine sahip elemanlar için stil girebileceğimiz bir editör kısmı ekler. Burada değişikliğimizi deneyelim.

button#sil {
    float: right;
}

Bütün butonlar sağa kaydı. Bunu css dosyasında eklerken sadece görselde ilgili bölgeye gelen butonlar için yapmamız doğru olacaktır. İleride çok daha fazla görseller içeren projelerde aynı id değere sahip başka Sil butonları da olabilir. Bunun kıymetini dediğim gibi projeler büyüdükçe görürüz.

todo/css/styles.css
...
#todoItems > li {
    padding: 10px;
    border-right: 1px solid #ccc;
    border-bottom: 1px solid #ccc;
}

#todoItems > li > #sil {
    float: right;
}

#todoItems .tamamlandı {
...

#todoItems > li > #sil diye seçerek sadece bu görsel içindeki o Sil butonlarını seçiyoruz.


Sil buton takıntısına devam. Butonlar çok kaba görünüyor, daha bir yakışıklı olsa?

#todoItems > li > #sil {
    float: right;
    font-size: 14px;
    color: white;
    background: orangered;
    border: 0;
    border-radius: 9px;
    padding: 7px;
    width: 50px;
}


Of çok yakışıklı!.. Sayfa arka planı acık koyulaştırıp içinde listemizi beyaz yapalım.

body {
    padding: 40px;
    background: whitesmoke;
}
...
#todoItems {
...
    background: white;
}
...


Aşağılara doğru gittikçe bazı Sil butonlarının yerlerinde üstüste binmeler görüyoruz. Bunu önlemek için de label elemanlarına maximum bir genişlik verip yatayda geri kalan kısmı butona bırakalım.

todo/css/styles.css
...
#todoItems > li > label {
    width: 80%;
    display: inline-block;
}
...


Süper!. Ama bir sıkıntı oluştu biten işlerin üstü çizilmez oldu. Biz stilde tüm li elemanı için üstü çizili demiştik. Bu özel stil yazdığımız label için etkisini kaybetti. Bu efekti sadece label için istiyoruz, bu yüzden label olarak belirtelim. Stilde ilgili #todoItems .tamamlandı seçicisini #todoItems .tamamlandı > label yapalım.

#todoItems .tamamlandı > label {
    text-decoration: line-through;
}

Şükür geri geldi. Elimiz değmişken tamamlanan işlerin yazı stilini de eğik yapalım.

#todoItems .tamamlandı > label {
    text-decoration: line-through;
    font-style: italic;
}


Biten işler daha bir net ayrıldı. İsterseniz biten işlerin rengini daha silik yaparak veya başka renk yaparak extra bir etki de yaratabilirsiniz.

Nerden nereye geldik . CSS web tasarımında yan bir şeymiş gibi öğrenmesi eziyet gelirken, aslında müşteriye vereceğimiz görsel tasarım ile işimizin değerini arttırabiliriz.

Görseli işlemenin çeşit çeşit yolu var ama burada html kodu olarak çok anlaşılabilir kalmaya çalışmak önemli. Mesela tüm bu listeyi div elemanları ile yapabilirdik, ancak ul ve li kullanarak html kod incelendiğinde burada bir liste olduğu , header kullanarak bölgenin üst kısmında bir şey olduğu gibi çok daha anlamlı kod yazmış oluruz.

Yazıyı başından beri benimle beraber takip edip buralara kadar geldiyseniz artık Backbone.js kullanarak uygulama geliştirebilecek düzeye gelmişsiniz demektir. Tabi herşeyi akılda tutmak kolay değil o yüzden ya bu sayfayı bookmark edin ya da bilgisayarınıza download ederek ileride uygulama yaparken referans olarak kullanmaya bakın. Yapısal olarak burada verilen örnek kodlar gibi hem adım adım, hem parça parça, hem her kodu olması en doğru olan yere koymaya çalışarak devam edin. Basit ve kolay aradığınızı bulabileceğiniz kodlar yazmaya gayret edin.

Çok uzun bir yazı oldu. Umarım aradığınızı bulmuşsunuzdur, ben çok şey öğrendim. Bir dahaki yazıda görüşmek üzere kalın sağlıcakla..


















Hiç yorum yok:

Yorum Gönder