Thursday, February 4, 2016

Javascript ile İstemci Tarafta MVC Uygulaması Geliştirmek


Uzun süredir Web uygulaması türünde uygulamalar geliştiriyoruz. İstemci sunucu mimarisindeki bu uygulamalar, dağıtık bir mimariye sahipler. Sunucu tarafta çok katmanlı bir mimariyi tercih ediyoruz. Sunucu en azından sunum, iş mantığı ve tümleştirme katmanlarını içeriyor. Sorumlulukları bu şekilde daha iyi yönetebildiğimizi düşünüyoruz. İstemci tarafta ise ağırlıklı olarak Web tarayıcıları yer alıyor. Son dönemde istemciler ise çeşitlendi: masaüstü bilgisayarlar, dizüstü bilgisayarlar, tablet bilgisayarlar, akıllı telefonlar, akıllı saatler, akıllı televizyonlar. Görünen o ki her cihaz akıllanıyor! Ya bu cihazları kullananlar? Artık kullanıcılar uygulamalara her zaman ve her yerden erişebiliyorlar. Kullanıcıların uygulamalardan beklentileri de artmışa benziyor. Kullanıcılar bu web tabanlı uygulamaların daha etkileşimli olmasını, daha akıllı davranmasını, daha çevik kararlar vermesini, daha zengin içerik sunmalarını, kısacası daha yüksek bir kullanıcı arayüzü deneyimi yaşatmasını bekliyorlar. Üstelik bu deneyimi hareket halindeyken de durağan iken de, bağlantı varken de yok iken de ya da bağlantının bant genişliği dalgalanırken de yaşamak istiyorlar. Bu nedenle artık sunum mantığını sunucu tarafta değil de istemci tarafta oluşturmayı tercih ediyoruz. Üstelik HTML 5 ile birlikte istemci tarafta uygulama geliştirmek için çok sayıda nedenimiz, yani API'miz (Application Programming Interface) bulunuyor: Canvas, WebGL, GeoLocation, MultiMedia, WebStorage, Drag-and-Drop ve diğeleri. Tüm bu API'lere Javascript ile erişiyoruz. Javascript istemci tarafta uygulama geliştirmek için artık standart bir dil. Ancak bu dil bu tür uygulamalar geliştirmek için pek de hazır olduğu söylenemez. ES6 (EcmaScript) ve ES7 ile gelen yenilikler dildeki sorunları gidermeyi amaçlıyor. Javascript (JS)'i sunum yapmak için kullanıyoruz. Uygulamanın kullanıcıya dönük yüzünde JS oturacak. Sunumda iyi bildiğimiz bir kalıp var: MVC. Bu yazıda JS kullanarak istemci tarafta MVC yapmaya çalışacağız.  
Bunun için sayı tahmini oyununu kodlayacağız: Bilgisayar [1-100] arasında bir sayı tutar ve oyuncu ise bu sayıyı en fazla ya da en kötü 7 adımda bulmaya çalışır. Bilgisayar girilen tahmine göre kullanıcının bir sonraki tahmini daha iyi yapabilmesini sağlamak amacıyla tahmini değerlendirir: "daha büyük bir sayı seç!" ya da "daha küçük bir sayı seç!" Aslında her zaman kazandıracak bir strateji var: 50 ile başla ve bilgisayarın değerlendirmesine göre 25 ya da 75 tahmini ile devam et. Sürekli olarak tahmin aralığını yarıya düşürerek en kötü 7 tahminde sayıyı bulmak mümkün. Peki ya bulmaya çalıştığımız sayı [1-1000] aralığında olsaydı? Şimdi bu strateji, en kötü kaç adımda sonuca ulaşmamızı sağlayacaktı? 10 adım (ipin ucu: 2^10=1024)! 

Salt JS Kullanılarak MVC Uygulamak

Şimdi herhangi bir üçüncü parti JS kütüphanesi kullanmadan, sadece JS kullanarak MVC yapmaya çalışalım. Önce arayüzün görünümüne bakalım:
index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Game</title>
<style type="text/css">
@import url('css/bootstrap.css');

@import url('css/bootstrap-theme.css');
</style>
<script type="text/javascript" src="js/lib/jquery-1.11.3.js"></script>
<script type="text/javascript" src="js/lib/bootstrap.js"></script>
<script type="text/javascript" src="game.js"></script>
<script type="text/javascript" src="controller.js"></script>
</head>
<body>
    <p />
    <div class="container" role="main">
       <div class="panel panel-primary">
           <div class="panel-heading">
             <h3 class="panel-title">Number Guessing Game</h3>
           </div>
           <div class="panel-body">
              <div class="form-group">
                 <label for="tries">Tries</label> 
                 <span id="tries" class="badge"></span>
                 <div class="progress">
                    <div class="progress-bar progress-bar-striped" id="pbTries" role="progressbar" 
                      aria-valuenow="0" aria-valuemin="0" aria-valuemax="7" style="width: 0%"></div>
                 </div>
              </div>
              <div class="form-group">
                 <label class="active">Counter</label> 
                 <span id="counter" class="badge"></span>
                 <div class="progress">
                     <div class="progress-bar progress-bar-danger" id="pbCounter" role="progressbar" 
                       aria-valuenow="0" aria-valuemin="0" aria-valuemax="25" style="width: 0%"></div>
                 </div>
              </div>
              <div class="form-group">
                 <label for="guess">Guess:</label> 
                 <input type="text" id="guess"class="form-control" />
                 <button id="playButton" class="btn btn-success">Play</button>
              </div>    
              <div id="validationMessageContainer" class="alert alert-danger" role="alert">
                   <strong id="validationMessage"></strong>
              </div>    
          </div>
      </div>
      <div class="panel panel-info">
          <div class="panel-heading">
               <h3 class="panel-title">History</h3>
          </div>
          <div class="panel-body">
               <table id="history" class="table table-stripe">
                     <thead>
                         <tr>
                            <th>Guess</th>
                            <th>Message</th>
                          </tr>
                     </thead>
                     <tbody id="moves">
                     </tbody>
               </table>
          </div>
      </div>
      <div class="panel panel-info">
           <div class="panel-heading">
               <h3 class="panel-title">Game Statistics</h3>
           </div>
           <div class="panel-body">
               <div class="form-group">
                 <label class="active">Wins</label> 
                 <span id="wins" class="badge"></span>
               </div>
               <div class="form-group">
                 <label class="active">Average Win Time</label> 
                 <span id="avgWinTime" class="badge" data-bind="text: "></span>
                 <div class="progress">
                    <div class="progress-bar progress-bar-danger" id="pbAvgWinTime" role="progressbar"
                     aria-valuenow="0" aria-valuemin="0" aria-valuemax="25" style="width: 0%"></div>
</div>
           </div>  
      </div>
   </div>  
 </div>
</body>
</html>
controller.js:
var emptyElement = function(element) {
  var node = element;
  while (element.hasChildNodes()) {
    if (node.hasChildNodes()) {
      node = node.lastChild;
    } else {
      node = node.parentNode;
      node.removeChild(node.lastChild);
    }
  }
};


var View = function(document){
 var self= this;
 
 self.document= document;
 
 self.guess= self.document.getElementById("guess"); 
 self.tries= self.document.getElementById("tries");
 self.pbTries= self.document.getElementById("pbTries"); 
 self.counter= self.document.getElementById("counter");
 self.pbCounter= self.document.getElementById("pbCounter");
 self.wins= self.document.getElementById("wins");
 self.avgWinTime= self.document.getElementById("avgWinTime");
 self.pbAvgWinTime= self.document.getElementById("pbAvgWinTime");
 self.validationMessageContainer= self.document.getElementById("validationMessageContainer");
 self.validationMessage= self.document.getElementById("validationMessage");
 self.playButton= self.document.getElementById("playButton");
 self.history= self.document.getElementById('history').getElementsByTagName('tbody')[0];
};

var Controller = function(model,view){
 var self= this;

 self.view= view;
 self.model= model;

 self.updateCounterView = function(){
  self.model.counter--;
  self.view.counter.innerHTML= self.model.counter;
  if (self.model.counter<=0){
   self.model.init();
   self.model.history.push(new Move('Time is out!','Game over!'));
   self.updateView();
  }
  self.view.pbCounter.setAttribute('ariavalue',self.model.counter);
  self.view.pbCounter.setAttribute('style',"width: "+self.model.counter*4+"%");
 };
 
 self.updateStatisticsView = function(){
  self.view.wins.innerHTML= self.model.statistics.wins;
  self.view.avgWinTime.innerHTML= self.model.statistics.avgWinTime;
  self.view.pbAvgWinTime.setAttribute('ariavalue',self.model.statistics.avgWinTime);
  self.view.pbAvgWinTime.setAttribute('style',"width: "+self.model.statistics.avgWinTime*4+"%");
 };
 
 self.updateHistoryTable = function() {
  emptyElement(self.view.history);
  for (i in self.model.history) {
   newRow = self.view.history.insertRow(self.view.history.rows.length);
   cellGuess = newRow.insertCell(0);
   cellMessage = newRow.insertCell(1);
   move = self.model.history[i];
   cellGuess.appendChild(document.createTextNode(move.guess));
   cellMessage.appendChild(document.createTextNode(move.message));
  }
 };

 self.updateTries = function(){
  if (self.model.tries>0){
   self.view.tries.innerHTML= self.model.tries;
  }
  else{
   self.view.tries.innerHTML= 'New game'; 
  }
  self.view.pbTries.setAttribute('ariavalue',self.model.tries)
  self.view.pbTries.setAttribute('style',"width: "+((self.model.tries*100)/7)+"%")
 };

 self.updateValidationView = function(){
  self.view.validationMessage.innerHTML= self.model.validationMessage;
  if (self.model.validationMessage.length>0){
   self.view.validationMessageContainer.style.visibility = "visible";  
  } else {
   self.view.validationMessageContainer.style.visibility = "hidden";  
  }
 };

 self.updateView = function(){
  self.updateHistoryTable();
  self.updateTries();
  self.updateValidationView();
  self.updateStatisticsView();
 }; 
 
 self.clickPlay = function(){
  self.model.play(self.view.guess.value);
  self.updateView();
 }
 
 self.init = function() {
  self.model.init();
  self.updateView();
  setInterval(self.updateCounterView, 1000);
  self.view.playButton.onclick = self.clickPlay;
 };
};

var Application = function() {
 var self= this;

 self.run = function(){
  self.view= new View(document);
  self.model= new GameViewModel();
  self.controller= new Controller(self.model,self.view);
  self.controller.init();
 };
};

var app= new Application();

window.onload= app.run;
game.js:
'use strict';

class Move {
   constructor (guess,message){
      this.guess= guess;
      this.message= message;
   }
};

class GameStatistics {
   constructor (){
      this.wins= 0;
      this.totalWinTime= 0;
      this.avgWinTime= 0;
   }
};

class GameViewModel {
   constructor() {
      this.tries = 0;
      this.secret= 0;
      this.history = [];
      this.counter=25;
      this.validationMessage= "";
      this.statistics= new GameStatistics();
   }

   init (){
      this.history= [];
      this.tries= 0;
      this.secret= Math.floor(Math.random()*100)+1;
      this.counter=25;
      this.validationMessage= "";
   }

   play (guess){
      this.validationMessage= "";
   
      if (isNaN(guess)){
         this.validationMessage= "Enter a valid integer!";
         return false;
      }
      guess = Number(guess);
      if (guess<1 || guess >100){
         this.validationMessage= "Enter an integer between 1 and 100!";
         return false;     
      }
      for (let i=0;i<this.history.length;++i){
         let move= this.history[i];
         if (guess==move.guess){
            this.validationMessage= "You have already played "+guess+"!";
            return false;
         }
      }
      if (this.secret==guess){
         this.statistics.wins++;
         this.statistics.totalWinTime += 25 - this.counter;
         this.statistics.avgWinTime = this.statistics.totalWinTime / this.statistics.wins ;
         this.init();
         this.history.push(new Move(guess,"You win!"));
         return true;
      } 
      let evaluation = "Pick smaller!"; 
      if (guess < this.secret){
         evaluation = "Pick larger!";
      }
      this.history.push(new Move(guess,evaluation));    
      this.tries++;
      if (this.tries>=7){
         this.init();
         this.history.push(new Move('You have 7 tries.',"Game over!"));
      }
      return true;
   } 
}
Oyuna bu adresten erişebilirsiniz.

JS ve jQuery Kullanılarak MVC Uygulamak

DOM API'yi kullanarak dokümanı değiştirmek epey vaktimizi aldı. Şimdi bu işi jQuery kütüphanesine yıkalım. game.js içinde modelimiz yer alıyor. Bir önceki çözümde kullandığımız model sınıflarında bir değişiklik yok, aynısını kullanacağız. Tüm değişiklik denetçinin yer aldığı controller.js dosyasında yapacağız. Daha önce salt DOM API'yi kullanarak görünümde yaptığımız değişiklikleri şimdi jQuery kütüphanesi kullanarak gerçekleştiriyoruz:

var View = function(document){
   var self= this;

   self.document= document;
 
   self.guess= $("#guess"); 
   self.tries= $("#tries");
   self.pbTries= $("#pbTries"); 
   self.counter= $("#counter");
   self.pbCounter= $("#pbCounter");
   self.wins= $("#wins");
   self.avgWinTime= $("#avgWinTime");
   self.pbAvgWinTime= $("#pbAvgWinTime");
   self.validationMessageContainer= $("#validationMessageContainer");
   self.validationMessage= $("#validationMessage");
   self.playButton= $("#playButton");
   self.history= $('#history');
};

var Controller = function(model,view){
   var self= this;

   self.view= view;
   self.model= model;

   self.updateCounterView = function(){
      self.model.counter--;
      self.view.counter.text(self.model.counter);
      if (self.model.counter<=0){
         self.model.init();
         self.model.history.push(new Move('Time is out!','Game over!'));
         self.updateView();
      }
      self.view.pbCounter.attr('ariavalue',self.model.counter);
      self.view.pbCounter.attr('style',"width: "+self.model.counter*4+"%");
   };
 
   self.updateStatisticsView = function(){
      self.view.wins.text(self.model.statistics.wins);
      self.view.avgWinTime.text(self.model.statistics.avgWinTime);
      self.view.pbAvgWinTime.attr('ariavalue',self.model.statistics.avgWinTime);
      self.view.pbAvgWinTime.attr('style',"width: "+self.model.statistics.avgWinTime*4+"%");
   };
 
   self.updateHistoryTable = function() {
      self.view.history.empty();
      for (i in model.history){
         move= model.history[i];
         self.view.history.append("<tr><td>"+move.guess+"</td><td>"+move.message+"</td></tr>");
      }
   };

   self.updateTries = function(){
      if (self.model.tries>0){
         self.view.tries.text(self.model.tries);
      }
      else{
         self.view.tries.text('New game'); 
      }
      self.view.pbTries.attr('ariavalue',self.model.tries)
      self.view.pbTries.attr('style',"width: "+((self.model.tries*100)/7)+"%")
   };

   self.updateValidationView = function(){
      self.view.validationMessage.text(self.model.validationMessage);
      if (self.model.validationMessage.length>0){
         self.view.validationMessageContainer.show();  
      } else {
         self.view.validationMessageContainer.hide();  
      }
   };

   self.updateView = function(){
      self.updateHistoryTable();
      self.updateTries();
      self.updateValidationView();
      self.updateStatisticsView();
   }; 
 
   self.clickPlay = function(){
      self.model.play(self.view.guess.val());
      self.updateView();
   }

   self.init = function() {
      self.model.init();
      self.updateView();
      setInterval(self.updateCounterView, 1000);
      self.view.playButton.click(self.clickPlay);
   };
};

var Application = function() {
   var self= this;

   self.run = function(){
      self.view= new View(document);
      self.model= new GameViewModel();
      self.controller= new Controller(self.model,self.view);
      self.controller.init();
   };
};

var app= new Application();

$(document).ready(app.run);
controller.js içindeki satır sayısı şimdi biraz azaldı, daha okunur ve bakımı kolay yapılabilir oldu. Ama hala nispeten basit bir arayüz için bile karmaşık bir denetçi yazmak zorunda kaldık. Sayfa daha da karmaşık hale geldiğinde değişimi yönetemeyeceğimiz kesin. Oyuna bu adresten erişebilirsiniz.





Knockout.js ile MVVM Uygulamak

Şimdi denetçiyi ortadan kaldıracak bir çözüm inceleyelim. Amacımız View değiştiğinde otomatik olarak ViewModel'i değiştirecek, ViewModel değiştiğinde ise View'i otomatik olarak değiştirecek bir çözüm oluşturmaktır. Knockout.js bunu, ViewModel ile View arasında çift yönlü bağlantı tanımlayarak gerçekleştirmemize yardımcı oluyor. Knockout.js'de View ile ViewModel arasında üç tür bağlantı kurulabilir:
1. ko.observable()
Eğer bağlanacak değişken skaler bir değişken ise bu değişken ko.observable() çağrısı ile yaratılır.
2. ko.observableArray()
Eğer bağlanacak değişken bir dizi ise bu değişken ko.observableArray() çağrısı ile yaratılır.
3. ko.computed()
Eğer bağlanacak değer, diğer "observable" değişkenlerden hesaplanarak oluşturulacak ise bu durumda ko.computed() çağrısı ile bir fonksiyon olarak tanımlanır.
Yukarıdaki örnek uygulamadaki ViewModel'i şimdi bu "observable" yapıları kullanarak yeniden tanımlayalım:

var Profile = {
   TIME_LIMIT: 60,
   INIT_GUESS: 50,
   MAX_TRIES: 7
};

var Move = function(guess,message) {
   var self= this;

   self.guess= guess;
   self.message= message;
};

var GameViewModel = function() {
   var self= this;
 
   self.tries = ko.observable(0);
   self.secret= ko.observable(0);
   self.guess= ko.observable(Profile.INIT_GUESS);
   self.history = ko.observableArray([]);
   self.counter= ko.observable(Profile.TIME_LIMIT);
   self.wins= ko.observable(0);
   self.totalWinTime= ko.observable(0);
 
   self.avgWinTime= ko.computed(function(){
      var ratio= self.totalWinTime()/self.wins();
      if (isNaN(ratio)) return 0;
      return ratio;
   });
 
   self.pbTries = ko.computed(function(){
      return 100*self.tries()/Profile.MAX_TRIES+'%';
   });
 
   self.pbCounter = ko.computed(function(){
      return (100*self.counter())/Profile.TIME_LIMIT + '%';
   });
 
   self.pbAvgWinTime = ko.computed(function(){
      return (100*self.avgWinTime())/Profile.TIME_LIMIT + '%';
   });
 
   setInterval(function(){
      self.counter(self.counter()-1);
   }, 1000);
 
   self.init = function(){
      self.history([]);
      self.tries(0);
      self.secret(Math.floor(Math.random()*100)+1);
      self.counter(Profile.TIME_LIMIT);
   }
    
   self.timeOut= ko.computed(function(){
      if (self.counter()<=0){
         self.init();
         self.history.push(new Move("Time is out!","Game is over!"));    
      } 
   }) ;
   
   self.play = function(){
      if (self.secret()==self.guess()){
         self.wins(self.wins()+1);
         self.totalWinTime(Profile.TIME_LIMIT+self.totalWinTime()-self.counter());
         self.init();
         self.history.push(new Move(self.guess(),"You win!"));
  return;
      } else {
 if (self.guess() < self.secret()){
    move = new Move(self.guess(),"Pick larger!");
 } else {
   move = new Move(self.guess(),"Pick smaller!");
 }
 self.history.push(move);    
     }
     self.tries(self.tries()+1);
     if (self.tries()>=7){
        self.init();
        self.history.push(new Move('7 tries',"Game over!"));
     }
   } 
}
Diğer bir değişiklik ise View Model'i View'ı bağladığımız index.html'de gerçekleştireceğiz. Yapacağımız eklemeler data-bind özniteliğini kullanarak modeli ön yüze bildirimsel bir şekilde bağlamak olacak:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Game</title>
<style type="text/css">
@import url('css/bootstrap.css');

@import url('css/bootstrap-theme.css');
</style>
<script type="text/javascript" src="js/lib/jquery-1.11.3.js"></script>
<script type="text/javascript" src="js/lib/bootstrap.js"></script>
<script type="text/javascript" src="js/lib/knockout-3.4.0.js"></script>
<script type="text/javascript" src="game.js"></script>
<script type="text/javascript">
 var vm = new GameViewModel();
 $(document).ready(function() {
  vm.init();

  ko.applyBindings(vm);
 });
</script>
</head>
<body>
 <p />
 <div class="container" role="main">
  <div class="panel panel-primary">
   <div class="panel-heading">
    <h3 class="panel-title">Number Guessing Game</h3>
   </div>
   <div class="panel-body">
    <div class="form-group">
     <label for="tries">Tries</label> 
     <span id="tries" class="badge"
        data-bind="text: tries"></span>
     <div class="progress">
      <div class="progress-bar progress-bar-striped" 
           id="pbTries"
        role="progressbar" 
        aria-valuenow="0" 
        aria-valuemin="0"
        data-bind="attr: { 'aria-valuemin': tries }, style: { width: pbTries}"
        aria-valuemax="7" style="width: 0%"></div>
     </div>
    </div>
    <div class="form-group">
     <label class="active">Counter</label> 
     <span id="counter"
        class="badge" 
        data-bind="text: counter"></span>
     <div class="progress">
      <div class="progress-bar progress-bar-danger" id="pbCounter"
       role="progressbar"
       data-bind="attr: { 'aria-valuemin': counter }, style: { width: pbCounter}"
       aria-valuenow="0" 
       aria-valuemin="0" 
       aria-valuemax="25"
       style="width: 0%"></div>
     </div>
    </div>
    <div class="form-group">
     <label for="guess">Guess:</label> 
     <input type="text" id="guess"
         data-bind="value: guess" class="form-control" />
     <button id="playButton" 
             data-bind="click: play"
          class="btn btn-success">Play</button>
    </div>
   </div>
  </div>
  <div class="panel panel-info">
   <div class="panel-heading">
    <h3 class="panel-title">History</h3>
   </div>
   <div class="panel-body">
    <table id="history" class="table table-stripe">
     <thead>
      <tr>
       <th>Guess</th>
       <th>Message</th>
      </tr>
     </thead>
     <tbody id="moves" data-bind="foreach: history">
      <tr>
       <td data-bind="text: guess"></td>
       <td data-bind="text: message"></td>
      </tr>
     </tbody>
    </table>
   </div>
  </div>
  <div class="panel panel-info">
   <div class="panel-heading">
    <h3 class="panel-title">Game Statistics</h3>
   </div>
   <div class="panel-body">
    <div class="form-group">
     <label class="active">Wins</label> 
     <span id="wins" 
           class="badge"
        data-bind="text: wins"></span>
    </div>
    <div class="form-group">
     <label class="active">Average Win Time</label> 
     <span id="avgWinTime" class="badge" 
           data-bind="text: "></span>
     <div class="progress">
      <div class="progress-bar progress-bar-danger" 
           id="pbAvgWinTime"
       role="progressbar"
       data-bind="attr: { 'aria-valuemin': avgWinTime }, style: { width: pbAvgWinTime}"
       aria-valuenow="0" 
       aria-valuemin="0" 
       aria-valuemax="25"
       style="width: 0%"></div>
     </div>
    </div>
   </div>
  </div>
 </div>
</body>
</html>
Bu çözümde artık denetçi yok. İlk iki çözümde denetçi yazmak zorundaydık. İkinci çözümde DOM'da değişikliği jQuery ile yaptığımız için kodlamak daha kolay oldu. Üçüncü çözümde bir MVVM çözümü kullandık ve böylelikle hem denetçiyi yazmadık hem de DOM güncelemelerini kodlamadık! Oyuna bu adresten erişebilirsiniz.



Angular.js ile MVC Uygulamak

İstemci tarafta MVC uygulamak ile ilgili diğer bir çatı Angular.js. Knockout.js'de olduğu gibi Angular.js'de de modeli ön yüze Angular.js'in yönergelerini (=directive) kullanarak bağlıyoruz. Önce denetçi koduna bir bakalım:
game.js:
var Profile = {
   TIME_LIMIT: 60,
   INIT_GUESS: 50,
   MAX_TRIES: 7
};

var Move = function(guess, message) {
   var self = this;
   self.guess = guess;
   self.message = message;
}

var game = angular.module("game", []).controller("gameController",
   function($scope,$interval) {
      var self = this;
      self.guess = 0;
      self.counter = Profile.TIME_LIMIT;
      self.secret = Math.floor(Math.random() * 100) + 1;
      self.history = new Array();
      self.tries = 0;
      self.avgWinTime= 0.0;
      self.totalWinTime=0.0;
      self.wins= 0;
  
      self.init = function() {
         self.tries = 0;
         self.counter = Profile.TIME_LIMIT;
         self.history = new Array();
         self.secret = Math.floor(Math.random() * 100) + 1;
         self.pbTries= {'width': '0%'};
         self.pbCounter= {'width': '100%'};
         self.pbAvgWinTime= {'width': '0%'};
      }
   
      $interval(function(){
         self.pbCounter= { 'width' : ((self.counter * 100) / Profile.TIME_LIMIT) + '%' };
         if (self.counter == 0) {
            self.init();
         } else {
            self.counter--;
         }
      }, 1000);

      self.play = function() {
         self.tries++;
         self.pbTries=  { 'width' : ((self.tries * 100) / Profile.MAX_TRIES) + '%' };
         if (self.tries > 7) {
            self.init();
            self.history.push(new Move("You have 7 moves!", "Game is over!"));     
         } else if (self.secret == self.guess) {
            self.wins++;
            self.totalWinTime = self.totalWinTime + 60 - self.counter ;
            self.avgWinTime = self.totalWinTime / self.wins;
            var moves= self.tries;
            self.init();
            self.history.push(new Move("Congratulations!", "You win in "+moves+" moves!"));
            self.pbAvgWinTime= { 'width' : ((self.avgWinTime * 100) / Profile.TIME_LIMIT) + '%' };
         } else {
            message = "Pick larger";
            if (self.guess > self.secret)
               message = "Pick smaller";
               self.history.push(new Move(self.guess, message));
         }
      }
   } );
Şimdi ise index.html dosyasına ve Angular.js yönergelerine bir bakalım:
<!DOCTYPE html>
<html ng-app="game">
<head>
<meta charset="UTF-8">
<title>Game</title>
<style type="text/css">
@import url('css/bootstrap.css');

@import url('css/bootstrap-theme.css');
</style>
<script type="text/javascript" src="js/lib/jquery-1.11.3.js"></script>
<script type="text/javascript" src="js/lib/bootstrap.js"></script>
<script type="text/javascript" src="js/lib/angular.min.js"></script>
<script type="text/javascript" src="game.js"></script>
</head>
<body ng-controller="gameController as ctrl">
<p />
<div class="container" role="main">
 <div class="panel panel-primary">
  <div class="panel-heading">
   <h3 class="panel-title">Number Guessing Game</h3>
  </div>
  <div class="panel-body">
   <div class="form-group">
    <label for="tries">Tries</label> 
    <span id="tries" class="badge">{{ctrl.tries}}</span>
    <div class="progress">
     <div class="progress-bar progress-bar-striped" 
          id="pbTries"
       role="progressbar" 
       aria-valuenow="0" 
       aria-valuemin="0"        
       aria-valuemax="7"
       ng-style="ctrl.pbTries"></div>
    </div>
   </div>
   <div class="form-group">
    <label class="active">Counter</label> 
    <span id="counter"
       class="badge">{{ctrl.counter}}</span>
    <div class="progress">
     <div class="progress-bar progress-bar-danger" id="pbCounter"
       role="progressbar"
       aria-valuenow="0" 
       aria-valuemin="0" 
       aria-valuemax="60"
       ng-style="ctrl.pbCounter"></div>
    </div>
   </div>
   <div class="form-group">
    <label for="guess">Guess:</label> 
    <input type="text" id="guess"
        ng-model="ctrl.guess" class="form-control" />
    <button id="playButton" 
            ng-click="ctrl.play()"
         class="btn btn-success">Play</button>
   </div>
  </div>
 </div>
 <div class="panel panel-info">
  <div class="panel-heading">
   <h3 class="panel-title">History</h3>
  </div>
  <div class="panel-body">
   <table id="history" class="table table-stripe">
    <thead>
     <tr>
      <th>Guess</th>
      <th>Message</th>
     </tr>
    </thead>
    <tbody id="moves" ng-repeat="move in ctrl.history">
     <tr>
      <td>{{move.guess}}</td>
      <td>{{move.message}}</td>
     </tr>
    </tbody>
   </table>
  </div>
 </div>
 <div class="panel panel-info">
  <div class="panel-heading">
   <h3 class="panel-title">Game Statistics</h3>
  </div>
  <div class="panel-body">
   <div class="form-group">
    <label class="active">Wins</label> 
    <span id="wins" 
          class="badge">{{ctrl.wins}}</span>
    </div>
   <div class="form-group">
    <label class="active">Average Win Time</label> 
    <span id="avgWinTime" class="badge">{{ctrl.avgWinTime}}</span>
    <div class="progress">
     <div class="progress-bar progress-bar-danger" 
          id="pbAvgWinTime"
       role="progressbar"
       aria-valuenow="0" 
       aria-valuemin="0" 
       aria-valuemax="25"
       ng-style="ctrl.pbAvgWinTime"></div>
    </div>
   </div>
  </div>
 </div>
</div>
</body>
</html>
Angular.js'de hepsi "ng-" öneki ile başlayan çok sayıda yönerge bulunuyor. Yukarıdaki kod parçasında bunlardan bir kaçını kullandık: ng-app, ng-controller, ng-model, ng-style, ng-clickOyuna bu adresten erişebilirsiniz.

3 comments:

  1. Emeğinize saygı duyuyorum teşekürler. Knockout yerine react veya angulardan bahsetseydiniz çok güzel olurdu.

    ReplyDelete
  2. Yazıya uygulamanın Angular.js versiyonunu ekledim. React.js burada farklı bir konuma sahip. Sunumu bileşen tabanlı bir yaklaşımla oluşturmaya çalışıyor. Knockout.js, Angular.js, Backbone.js gibi MV* çözümlerinin yerini alabilecek, onlara rakip bir yaklaşım değil. Başka bir yazıda React.js'i ele alacağım.

    ReplyDelete