Z tym artykułem stworzysz prostą aplikację webową czasu rzeczywistego. Zobaczysz w jaki sposób dodawać/pobierać dane i wyświetlać je w widoku na bieżąco. Czym jest Firebase i jak z niego korzystać pisałem już wcześniej. Jako kontynuację tego tematu, specjalnie dla czytelników Devcorner przygotowałem poradnik jak stworzyć prosty czat w AngularJS i Firebase.

Zobacz demo

Przygotowanie środowiska

Konfigurację rozpoczynamy od utworzenia nowego projektu w Firebase Console. W nowym projekcie przechodzimy do zakładki „Database” i wybieramy „Realtime Database” klikając w przycisk „Rozpocznij”. To w tym miejscu będzie wyświetlało się pełne drzewko danych. Na tym etapie zawiera jedynie główny węzeł o ID, które jednoznacznie identyfikuje nasz projekt.

W tym miejscu należy wspomnieć, że każda struktura danych w Firebase, posiada zestaw reguł. Reguły definiują, kto i w jaki sposób będzie miał dostęp do poszczególnych węzłów w drzewie. Aplikacja, którą stworzymy na potrzeby demonstracyjne, nie będzie w żaden sposób uwierzytelniała użytkowników bo jest to temat na odrębny artykuł. Skupiamy się tutaj jedynie na tym w jaki sposób synchronizować aplikację AngularJS z bazą danych. W związku z tym, musimy umożliwić nieautoryzowanym użytkownikom odczytywać i zapisywać dane w drzewie (pamiętaj, że w praktyce takie rozwiązanie jest niedopuszczalne ze względów bezpieczeństwa). W edytorze zmieniamy domyślne wartości .read i .write na true i potwierdzamy przyciskiem Opublikuj.

Kolejnym krokiem jest stworzenie struktury plików dla naszej aplikacji. Niezależnie od tego z jakich narzędzi korzystasz, struktura niczym nie różni się od zwykłej strony internetowej. Moje drzewo plików wygląda w sposób następujący:

  • /index.html
  • /scripts/app.js
  • /styles/main.css

W pliku index.html tworzymy szkielet strony i załączamy framework AngularJS, Firebase SDK a także bibliotekę angularFire, która ułatwi nam synchronizację kontrolerów z bazą danych. Oprócz tego ładujemy moduł ngCookies. Ciasteczka wykorzystamy do podtrzymania sesji użytkownika.

Szkielet HTML strony

<!doctype html>
<html>
  <head>
    <meta http-equiv="X-UA-Compatible" content="IE=Edge"/>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>Czat Angular + Firebase</title>
    <link rel="stylesheet" href="styles/main.css">
  </head>
  <body>
    <section ng-app="chat" class="chat">

    </section>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular.min.js"></script>
    <script src="https://www.gstatic.com/firebasejs/3.6.6/firebase.js"></script>
    <script src="https://cdn.firebase.com/libs/angularfire/2.3.0/angularfire.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.1/angular-cookies.min.js"></script>    
    <script src="scripts/app.js"></script>
  </body>
</html>

Połączenie z Firebase

W pierwszej kolejności będziemy musieli skonfigurować połączenie z Firebase. Całość sprowadza się do skopiowania gotowego kodu z panelu zarządzania.

Przechodzimy do Overview > Ustawienia projektu. W sekcji „Twoje aplikacje” wybieramy „Dodaj Firebase do swojej aplikacji internetowej” i wklejamy kod do pliku app.js.

Połączenie z Firebase

(function () {
  'use strict';

  // Inicjacja Firebase
  var config = {
    apiKey: "AIzaSyCn-1QIqWrrejw0ajXz8Zk7KngqYadaUNQ",
    authDomain: "test-1433d.firebaseapp.com",
    databaseURL: "https://test-1433d.firebaseio.com",
    projectId: "test-1433d",
    storageBucket: "test-1433d.appspot.com",
    messagingSenderId: "655366613901"
  };
  firebase.initializeApp(config);
  
  // Nowa aplikacja Angular
  var app = angular.module('chat', ['firebase', 'ngCookies']);
  // Odwołanie do #chat-messages
  var chat_container = document.getElementById('chat-messages');

})()

Ostatnią rzeczą jaką potrzebujemy by dobrać się do danych, to utworzenie referencji. W AngularJS warto odchudzać kontrolery (jak niektórzy piszą: „odseparować osobne byty”) stosując tzw. „services”. W tym przypadku wykorzystamy najprostszy ich typ czyli „factory”. Nasze factory będzie składało się z dwóch metod:

  • getUsers,
  • oraz getMessages.

Obie funkcje zwracają obiekt typu $firebaseArray, o którym dowiesz się więcej z dokumentacji angularFire.

Factory dla danych z Firebase

(function () {
  'use strict';
  // Inicjacja Firebase
  var config = {
    apiKey: "AIzaSyCn-1QIqWrrejw0ajXz8Zk7KngqYadaUNQ",
    authDomain: "test-1433d.firebaseapp.com",
    databaseURL: "https://test-1433d.firebaseio.com",
    projectId: "test-1433d",
    storageBucket: "test-1433d.appspot.com",
    messagingSenderId: "655366613901"
  };
  firebase.initializeApp(config);

  var app = angular.module('chat', ['firebase', 'ngCookies']);
  var chat_container = document.getElementById('chat-messages');

  // Dostęp do bazy danych
  app.factory('firebaseSrv', ['$firebaseArray', function($firebaseArray) {
    return {
      // Lista użytkowników
      getUsers: function(){
        var ref = firebase.database().ref().child("users");
        return $firebaseArray(ref);
      },
      // Lista wiadomości
      getMessages: function(){
        var ref = firebase.database().ref().child("messages");
        return $firebaseArray(ref);
      }
    } 
  }]);

})()

Przygotujmy teraz kontroler i sprawdźmy czy mamy dostęp do Firebase. Głównym kontrolerem naszej aplikacji będzie „chatCtrl”, w nim utworzymy tablice i obiekty, które będą widoczne dla kontrolerów niższego poziomu.

By mieć pewność, że dane zostały w pełni pobrane wykorzystuje się tzw. „promise” – w skrócie funkcję, która wywoła się w momencie otrzymania zwrotnych informacji. W angularFire taką „obietnicę” realizuje funkcja $loaded().

Pobranie użytkowników i wiadomości

(function () {
  'use strict';

  // Inicjacja Firebase
  var config = {
    apiKey: "AIzaSyCn-1QIqWrrejw0ajXz8Zk7KngqYadaUNQ",
    authDomain: "test-1433d.firebaseapp.com",
    databaseURL: "https://test-1433d.firebaseio.com",
    projectId: "test-1433d",
    storageBucket: "test-1433d.appspot.com",
    messagingSenderId: "655366613901"
  };
  firebase.initializeApp(config);

  var app = angular.module('chat', ['firebase', 'ngCookies']);
  var chat_container = document.getElementById('chat-messages');

  // Dostęp do bazy danych
  app.factory('firebaseSrv', ['$firebaseArray', function($firebaseArray) {
    return {
      // Lista użytkowników
      getUsers: function(){
        var ref = firebase.database().ref().child("users");
        return $firebaseArray(ref);
      },
      // Lista wiadomości
      getMessages: function(){
        var ref = firebase.database().ref().child("messages");
        return $firebaseArray(ref);
      }
    } 
  }]);

app.controller('chatCtrl', ['$scope', 'firebaseSrv', '$firebaseArray', '$timeout', function($scope, firebaseSrv, $firebaseArray, $timeout){
   $scope.users = [];
   $scope.messages = [];
   $scope.current_user = {
     online: false,
     nickname: null,
     uid: null
   };
   // Pobieranie listy użytkowników
   firebaseSrv.getUsers().$loaded().then(function(data){
     $scope.users = data;
     // Pobieranie wiadomości
     firebaseSrv.getMessages().$loaded().then(function(data){
       $scope.messages = data;
     });
   });
}]);

})()

Na tym etapie warto wstawić testowe dane przez panel i sprawdzić czy aplikacja ma do nich dostęp:

  • console.log($scope.messages)
  • console.log($scope.users)

Jeżeli tablice zawierają testowe rekordy możesz wziąć łyk kawki, rozciągnąć się i przygotować na dalsze etapy pracy.

Przygotowanie widoku

Zanim przejdziemy dalej, warto popracować nad plikiem HTML, który będzie wyświetlał poszczególne widoki. Cały ekran podzielimy na 3 części (lista wiadomości, formularz do wpisywania wiadomości oraz listę użytkowników). Dodatkowo wyświetlimy formularz logowania.

Szkielet HTML elementu z czatem

<!doctype html>
<html>
  <head>
    <meta http-equiv="X-UA-Compatible" content="IE=Edge"/>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>Czat Angular + Firebase</title>
    <link rel="stylesheet" href="styles/main.css">
  </head>
  <body>
<section ng-app="chat" class="chat">
   <div ng-controller="chatCtrl">
      <div class="chat-view" ng-show="current_user.online"> 
         <div class="chat-messages" id="chat-messages">
         <!-- Lista wiadomości --> 
         </div>
         <div ng-controller="inputCtrl" class="chat-input">
            <form><!-- Pisanie nowej wiadomości --></form>
         </div>
         <div class="chat-users">
            <div class="chat-users__current">
            <!-- Aktywny użytkownik -->
            </div>
            <div class="chat-users__list">
            <!-- Lista użytkowników -->
            </div>
         </div>
      </div>
      <div ng-controller="loginCtrl" ng-show="!current_user.online" class="chat-view login-form">  
         <h1>Czat Demo</h1>
         <h2>Angular + Firebase</h2>
         <form><!-- Formularz logowania --></form>
      </div>
   </div>
</section>

    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular.min.js"></script>
    <script src="https://www.gstatic.com/firebasejs/3.6.6/firebase.js"></script>
    <script src="https://cdn.firebase.com/libs/angularfire/2.3.0/angularfire.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.1/angular-cookies.min.js"></script>    
    <script src="scripts/app.js"></script>
  </body>
</html>

Logowanie i lista użytkowników

Żeby nie komplikować tego poradnika a skupić się na pokazaniu jak działa czat w AngularJS i Firebase, pominiemy typowy model autoryzacji użytkowników. Musimy jednak w jakiś sposób identyfikować osoby piszące wiadomości. Dlatego też, zrobimy prosty trik: wykorzystamy pliki cookies.

Widok formularza logowania

<!doctype html>
<html>
  <head>
    <meta http-equiv="X-UA-Compatible" content="IE=Edge"/>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>Czat Angular + Firebase</title>
    <link rel="stylesheet" href="styles/main.css">
  </head>
  <body>
<section ng-app="chat" class="chat">
   <div ng-controller="chatCtrl">
      <div class="chat-view" ng-show="current_user.online"> 
         <div class="chat-messages" id="chat-messages">
         <!-- Lista wiadomości --> 
         </div>
         <div ng-controller="inputCtrl" class="chat-input">
            <form><!-- Pisanie nowej wiadomości --></form>
         </div>
         <div class="chat-users">
            <div class="chat-users__current">
            <!-- Aktywny użytkownik -->
            </div>
            <div class="chat-users__list">
            <!-- Lista użytkowników -->
            </div>
         </div>
      </div>
<div ng-controller="loginCtrl" ng-show="!current_user.online" class="chat-view login-form">  
   <h1>Czat Demo</h1>
   <h2>Angular + Firebase</h2>
   <form name="loginForm" ng-submit="loginUser()" novalidate>
      <div class="login-form__frame">
         <h3 ng-show="!loading">Zaloguj się</h3>
         <h3 ng-show="loading">Czekaj...</h3>
         <div class="row" ng-class="{'loading':loading}">
            <input type="text" ng-model="loginForm.nickname" placeholder="Twój nick ..." required>
            <button type="submit" class="btn btn-green">Wejdź</button>
         </div>
      </div>
      <p>Ten czat wykorzystuje pliki cookies.<br>Wchodząc dalej, wyrażasz na to zgodę.</p>
   </form>
</div>

   </div>
</section>

    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular.min.js"></script>
    <script src="https://www.gstatic.com/firebasejs/3.6.6/firebase.js"></script>
    <script src="https://cdn.firebase.com/libs/angularfire/2.3.0/angularfire.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.1/angular-cookies.min.js"></script>    
    <script src="scripts/app.js"></script>
  </body>
</html>

Zwróć uwagę na atrybut ng-show=”!current_user.online”. Oznacza to to, że formularz logowania powinien być widoczny tylko wtedy gdy użytkownik jest niezalogowany (w przeglądarce nie ma jego cookie).

A jak to działa od środka? Wracamy do pliku app.js i tworzymy nowy kontroler. Wewnątrz definiujemy funkcję loginUser, która wywołamy w „on submit”. Funkcja sprawdza czy wprowadzony wymagany login i dodaje rekord w drzewie Firebase.

Metoda $add() służy właśnie do tworzenia nowych rekordów. Wywołujemy ja na zbindowanej wcześniej tablicy ($scope.users). W bazie danych zapiszemy:

  • nickname użytkownika,
  • datę dołączenia
  • aktualny status (online/offline).

Tworzenie i logowanie użytkownika

(function () {
  'use strict';

  // Inicjacja Firebase
  var config = {
    apiKey: "AIzaSyCn-1QIqWrrejw0ajXz8Zk7KngqYadaUNQ",
    authDomain: "test-1433d.firebaseapp.com",
    databaseURL: "https://test-1433d.firebaseio.com",
    projectId: "test-1433d",
    storageBucket: "test-1433d.appspot.com",
    messagingSenderId: "655366613901"
  };
  firebase.initializeApp(config);

  var app = angular.module('chat', ['firebase', 'ngCookies']);
  var chat_container = document.getElementById('chat-messages');

  // Dostęp do bazy danych
  app.factory('firebaseSrv', ['$firebaseArray', function($firebaseArray) {
    return {
      // Lista użytkowników
      getUsers: function(){
        var ref = firebase.database().ref().child("users");
        return $firebaseArray(ref);
      },
      // Lista wiadomości
      getMessages: function(){
        var ref = firebase.database().ref().child("messages");
        return $firebaseArray(ref);
      }
    } 
  }]);

app.controller('chatCtrl', ['$scope', 'firebaseSrv', '$firebaseArray', '$timeout', function($scope, firebaseSrv, $firebaseArray, $timeout){
   $scope.users = [];
   $scope.messages = [];
   $scope.current_user = {
     online: false,
     nickname: null,
     uid: null
   };
   // Pobieranie listy użytkowników
   firebaseSrv.getUsers().$loaded().then(function(data){
     $scope.users = data;
     // Pobieranie wiadomości
     firebaseSrv.getMessages().$loaded().then(function(data){
       $scope.messages = data;
     });
   });
}]);

app.controller('loginCtrl', ['$scope', '$cookieStore', '$timeout', function($scope, $cookieStore, $timeout){
  $scope.loading = false;
  // Tworzenie użytkownika
  $scope.loginUser = function(){
    if( $scope.loginForm.$valid ){
      $scope.loading = true;
      // Dodaj rekord do węzła users
      $scope.users.$add({ 
        nickname: $scope.loginForm.nickname,
        created_at: Math.floor(Date.now()/1000),
        online: true,
      }).then(function(ref){
        $timeout(function(){
          // Użytkownik dodany - zapisz dane w scope i w cookies
          $scope.current_user.online = true;
          $scope.current_user.nickname = $scope.loginForm.nickname;
          $scope.current_user.uid = ref.key;
          var now = new Date();
          var exp = new Date(now.getFullYear()+1, now.getMonth(), now.getDate());
          $cookieStore.put('chat_nickname', $scope.loginForm.nickname, { expires: exp });
          $cookieStore.put('chat_uid', ref.key, { expires: exp });
          $scope.loading = false;
          document.getElementById("chat-input").focus();
        },1000);
      });
    }
  }
}]);

})()

Jak widać, po wykonaniu funkcji $add, otrzymane wartości zapisywane są w scope oraz plikach cookies. Na koniec ustawiamy focus na polu wpisywania wiadomości.

No dobrze… użytkownik został zarejestrowany i jest online. Ale co w przypadku gdy zamknie stronę i wejdzie na nią ponownie? W kontrolerze dodajemy warunek sprawdzający istnienie ciasteczka. Jeżeli takowe istnieje, ustawiamy status użytkownika na „true” i ukrywamy panel logowania.

Autologowanie

(function () {
  'use strict';

  // Inicjacja Firebase
  var config = {
    apiKey: "AIzaSyCn-1QIqWrrejw0ajXz8Zk7KngqYadaUNQ",
    authDomain: "test-1433d.firebaseapp.com",
    databaseURL: "https://test-1433d.firebaseio.com",
    projectId: "test-1433d",
    storageBucket: "test-1433d.appspot.com",
    messagingSenderId: "655366613901"
  };
  firebase.initializeApp(config);

  var app = angular.module('chat', ['firebase', 'ngCookies']);
  var chat_container = document.getElementById('chat-messages');

  // Dostęp do bazy danych
  app.factory('firebaseSrv', ['$firebaseArray', function($firebaseArray) {
    return {
      // Lista użytkowników
      getUsers: function(){
        var ref = firebase.database().ref().child("users");
        return $firebaseArray(ref);
      },
      // Lista wiadomości
      getMessages: function(){
        var ref = firebase.database().ref().child("messages");
        return $firebaseArray(ref);
      }
    } 
  }]);

app.controller('chatCtrl', ['$scope', 'firebaseSrv', '$firebaseArray', '$timeout', function($scope, firebaseSrv, $firebaseArray, $timeout){
   $scope.users = [];
   $scope.messages = [];
   $scope.current_user = {
     online: false,
     nickname: null,
     uid: null
   };
   // Pobieranie listy użytkowników
   firebaseSrv.getUsers().$loaded().then(function(data){
     $scope.users = data;
     // Pobieranie wiadomości
     firebaseSrv.getMessages().$loaded().then(function(data){
       $scope.messages = data;
     });
   });
}]);

app.controller('loginCtrl', ['$scope', '$cookieStore', '$timeout', function($scope, $cookieStore, $timeout){
  $scope.loading = false;
  // Tworzenie użytkownika
  $scope.loginUser = function(){
    if( $scope.loginForm.$valid ){
      $scope.loading = true;
      // Dodaj rekord do węzła users
      $scope.users.$add({ 
        nickname: $scope.loginForm.nickname,
        created_at: Math.floor(Date.now()/1000),
        online: true,
      }).then(function(ref){
        $timeout(function(){
          // Użytkownik dodany - zapisz dane w scope i w cookies
          $scope.current_user.online = true;
          $scope.current_user.nickname = $scope.loginForm.nickname;
          $scope.current_user.uid = ref.key;
          var now = new Date();
          var exp = new Date(now.getFullYear()+1, now.getMonth(), now.getDate());
          $cookieStore.put('chat_nickname', $scope.loginForm.nickname, { expires: exp });
          $cookieStore.put('chat_uid', ref.key, { expires: exp });
          $scope.loading = false;
          document.getElementById("chat-input").focus();
        },1000);
      });
    }
  }

// Jeżeli cookie istnieje
if( $cookieStore.get('chat_nickname') && $cookieStore.get('chat_uid') ){
  $scope.loading = true;
  // Logowanie automatyczne - ustaw status na true
  firebase.database().ref().child('users').child($cookieStore.get('chat_uid')).child('online').set(true).then(function(){
    $scope.current_user.online = true;
    $scope.current_user.nickname = $cookieStore.get('chat_nickname');
    $scope.current_user.uid = $cookieStore.get('chat_uid');
    $scope.loading = false;
    document.getElementById("chat-input").focus();
  });
}

}]);

})()

Powinniśmy obsłużyć jeszcze zmianę statusu na offline. Z pomocą przyjdzie nam zdarzenie „onBeforeUnload”.

Zmiana statusu na offline

(function () {
  'use strict';

  // Inicjacja Firebase
  var config = {
    apiKey: "AIzaSyCn-1QIqWrrejw0ajXz8Zk7KngqYadaUNQ",
    authDomain: "test-1433d.firebaseapp.com",
    databaseURL: "https://test-1433d.firebaseio.com",
    projectId: "test-1433d",
    storageBucket: "test-1433d.appspot.com",
    messagingSenderId: "655366613901"
  };
  firebase.initializeApp(config);

  var app = angular.module('chat', ['firebase', 'ngCookies']);
  var chat_container = document.getElementById('chat-messages');

  // Dostęp do bazy danych
  app.factory('firebaseSrv', ['$firebaseArray', function($firebaseArray) {
    return {
      // Lista użytkowników
      getUsers: function(){
        var ref = firebase.database().ref().child("users");
        return $firebaseArray(ref);
      },
      // Lista wiadomości
      getMessages: function(){
        var ref = firebase.database().ref().child("messages");
        return $firebaseArray(ref);
      }
    } 
  }]);

app.controller('chatCtrl', ['$scope', 'firebaseSrv', '$firebaseArray', '$timeout', function($scope, firebaseSrv, $firebaseArray, $timeout){
   $scope.users = [];
   $scope.messages = [];
   $scope.current_user = {
     online: false,
     nickname: null,
     uid: null
   };
   // Pobieranie listy użytkowników
   firebaseSrv.getUsers().$loaded().then(function(data){
     $scope.users = data;
     // Pobieranie wiadomości
     firebaseSrv.getMessages().$loaded().then(function(data){
       $scope.messages = data;
     });
   });
}]);

app.controller('loginCtrl', ['$scope', '$cookieStore', '$timeout', function($scope, $cookieStore, $timeout){
  $scope.loading = false;
  // Tworzenie użytkownika
  $scope.loginUser = function(){
    if( $scope.loginForm.$valid ){
      $scope.loading = true;
      // Dodaj rekord do węzła users
      $scope.users.$add({ 
        nickname: $scope.loginForm.nickname,
        created_at: Math.floor(Date.now()/1000),
        online: true,
      }).then(function(ref){
        $timeout(function(){
          // Użytkownik dodany - zapisz dane w scope i w cookies
          $scope.current_user.online = true;
          $scope.current_user.nickname = $scope.loginForm.nickname;
          $scope.current_user.uid = ref.key;
          var now = new Date();
          var exp = new Date(now.getFullYear()+1, now.getMonth(), now.getDate());
          $cookieStore.put('chat_nickname', $scope.loginForm.nickname, { expires: exp });
          $cookieStore.put('chat_uid', ref.key, { expires: exp });
          $scope.loading = false;
          document.getElementById("chat-input").focus();
        },1000);
      });
    }
  }

// Jeżeli cookie istnieje
if( $cookieStore.get('chat_nickname') && $cookieStore.get('chat_uid') ){
  $scope.loading = true;
  // Logowanie automatyczne - ustaw status na true
  firebase.database().ref().child('users').child($cookieStore.get('chat_uid')).child('online').set(true).then(function(){
    $scope.current_user.online = true;
    $scope.current_user.nickname = $cookieStore.get('chat_nickname');
    $scope.current_user.uid = $cookieStore.get('chat_uid');
    $scope.loading = false;
    document.getElementById("chat-input").focus();
  });
}

// Wyloguj użytkownika gdy wychodzi ze strony
var exitEvent = window.attachEvent || window.addEventListener;
var onExitEvent = window.attachEvent ? 'onbeforeunload' : 'beforeunload'; 
exitEvent(onExitEvent, function(e) {  
  // Zmień status na false
  firebase.database().ref().child('users').child($scope.current_user.uid).child('online').set(false);
});


}]);

})()

Aby zwieńczyć dzieło, wyświetlimy listę wszystkich użytkowników wraz z ich statusami. By tego dokonać musimy jedynie przygotować widok HTML. Wszystkie potrzebne dane są już w kontrolerze – wystarczy je tylko wyświetlić.

Widok HTML listy wiadomości

<!doctype html>
<html>
  <head>
    <meta http-equiv="X-UA-Compatible" content="IE=Edge"/>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>Czat Angular + Firebase</title>
    <link rel="stylesheet" href="styles/main.css">
  </head>
  <body>
<section ng-app="chat" class="chat">
   <div ng-controller="chatCtrl">
      <div class="chat-view" ng-show="current_user.online"> 
         <div class="chat-messages" id="chat-messages">
         <!-- Lista wiadomości --> 
         </div>
         <div ng-controller="inputCtrl" class="chat-input">
            <form><!-- Pisanie nowej wiadomości --></form>
         </div>
<div class="chat-users">
  <div class="chat-users__current">
    <!-- Aktywny użytkownik -->
    <h2>{{ current_user.nickname }}</h2>
    <p class="status status-on">Online</p>
  </div>
  <div class="chat-users__list">
    <!-- Lista użytkowników -->
    <div ng-repeat="user in users | orderBy: ['-online','-created_at']" ng-show="user.$id!=current_user.uid" class="item-user">
      <p>{{ user.nickname }}</p>
      <p ng-show="user.online" class="status status-on">Online</p>
      <p ng-show="!user.online" class="status">Offline</p>
    </div>
  </div>
</div>

      </div>
<div ng-controller="loginCtrl" ng-show="!current_user.online" class="chat-view login-form">  
   <h1>Czat Demo</h1>
   <h2>Angular + Firebase</h2>
   <form name="loginForm" ng-submit="loginUser()" novalidate>
      <div class="login-form__frame">
         <h3 ng-show="!loading">Zaloguj się</h3>
         <h3 ng-show="loading">Czekaj...</h3>
         <div class="row" ng-class="{'loading':loading}">
            <input type="text" ng-model="loginForm.nickname" placeholder="Twój nick ..." required>
            <button type="submit" class="btn btn-green">Wejdź</button>
         </div>
      </div>
      <p>Ten czat wykorzystuje pliki cookies.<br>Wchodząc dalej, wyrażasz na to zgodę.</p>
   </form>
</div>

   </div>
</section>

    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular.min.js"></script>
    <script src="https://www.gstatic.com/firebasejs/3.6.6/firebase.js"></script>
    <script src="https://cdn.firebase.com/libs/angularfire/2.3.0/angularfire.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.1/angular-cookies.min.js"></script>    
    <script src="scripts/app.js"></script>
  </body>
</html>

Pisanie wiadomości

Ufff… tyle tekstu a czat w AngularJS nie pozwala jeszcze nic pisać. Stwórzmy zatem prosty formularz składający się z pola tekstowego i przycisku.

Formularz pisania wiadomości

<!doctype html>
<html>
  <head>
    <meta http-equiv="X-UA-Compatible" content="IE=Edge"/>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>Czat Angular + Firebase</title>
    <link rel="stylesheet" href="styles/main.css">
  </head>
  <body>
<section ng-app="chat" class="chat">
   <div ng-controller="chatCtrl">
      <div class="chat-view" ng-show="current_user.online"> 
         <div class="chat-messages" id="chat-messages">
         <!-- Lista wiadomości --> 
         </div>
<!-- Pisanie nowej wiadomości -->
<div ng-controller="inputCtrl" class="chat-input">
  <form name="messageForm" ng-submit="sendMessage()" autocomplete="off" novalidate>
    <input type="text" ng-model="messageForm.text" id="chat-input" placeholder="Co Ci chodzi po głowie?" required>
    <button type="submit" class="btn btn-green">Wyślij</button>
  </form>
</div>
<div class="chat-users">
  <div class="chat-users__current">
    <!-- Aktywny użytkownik -->
    <h2>{{ current_user.nickname }}</h2>
    <p class="status status-on">Online</p>
  </div>
  <div class="chat-users__list">
    <!-- Lista użytkowników -->
    <div ng-repeat="user in users | orderBy: ['-online','-created_at']" ng-show="user.$id!=current_user.uid" class="item-user">
      <p>{{ user.nickname }}</p>
      <p ng-show="user.online" class="status status-on">Online</p>
      <p ng-show="!user.online" class="status">Offline</p>
    </div>
  </div>
</div>

      </div>
<div ng-controller="loginCtrl" ng-show="!current_user.online" class="chat-view login-form">  
   <h1>Czat Demo</h1>
   <h2>Angular + Firebase</h2>
   <form name="loginForm" ng-submit="loginUser()" novalidate>
      <div class="login-form__frame">
         <h3 ng-show="!loading">Zaloguj się</h3>
         <h3 ng-show="loading">Czekaj...</h3>
         <div class="row" ng-class="{'loading':loading}">
            <input type="text" ng-model="loginForm.nickname" placeholder="Twój nick ..." required>
            <button type="submit" class="btn btn-green">Wejdź</button>
         </div>
      </div>
      <p>Ten czat wykorzystuje pliki cookies.<br>Wchodząc dalej, wyrażasz na to zgodę.</p>
   </form>
</div>

   </div>
</section>

    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular.min.js"></script>
    <script src="https://www.gstatic.com/firebasejs/3.6.6/firebase.js"></script>
    <script src="https://cdn.firebase.com/libs/angularfire/2.3.0/angularfire.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.1/angular-cookies.min.js"></script>    
    <script src="scripts/app.js"></script>
  </body>
</html>

W kontrolerze tworzymy funkcję sendMessage odpowiedzialną za dodanie wiadomości w bazie danych. Każdy rekord będzie składał się z:

  • treści,
  • ID użytkownika,
  • loginu użytkownika,
  • oraz daty wysłania.

Zapisanie nowej wiadomości

(function () {
  'use strict';

  // Inicjacja Firebase
  var config = {
    apiKey: "AIzaSyCn-1QIqWrrejw0ajXz8Zk7KngqYadaUNQ",
    authDomain: "test-1433d.firebaseapp.com",
    databaseURL: "https://test-1433d.firebaseio.com",
    projectId: "test-1433d",
    storageBucket: "test-1433d.appspot.com",
    messagingSenderId: "655366613901"
  };
  firebase.initializeApp(config);

  var app = angular.module('chat', ['firebase', 'ngCookies']);
  var chat_container = document.getElementById('chat-messages');

  // Dostęp do bazy danych
  app.factory('firebaseSrv', ['$firebaseArray', function($firebaseArray) {
    return {
      // Lista użytkowników
      getUsers: function(){
        var ref = firebase.database().ref().child("users");
        return $firebaseArray(ref);
      },
      // Lista wiadomości
      getMessages: function(){
        var ref = firebase.database().ref().child("messages");
        return $firebaseArray(ref);
      }
    } 
  }]);

app.controller('chatCtrl', ['$scope', 'firebaseSrv', '$firebaseArray', '$timeout', function($scope, firebaseSrv, $firebaseArray, $timeout){
   $scope.users = [];
   $scope.messages = [];
   $scope.current_user = {
     online: false,
     nickname: null,
     uid: null
   };
   // Pobieranie listy użytkowników
   firebaseSrv.getUsers().$loaded().then(function(data){
     $scope.users = data;
     // Pobieranie wiadomości
     firebaseSrv.getMessages().$loaded().then(function(data){
       $scope.messages = data;
     });
   });
}]);

app.controller('loginCtrl', ['$scope', '$cookieStore', '$timeout', function($scope, $cookieStore, $timeout){
  $scope.loading = false;
  // Tworzenie użytkownika
  $scope.loginUser = function(){
    if( $scope.loginForm.$valid ){
      $scope.loading = true;
      // Dodaj rekord do węzła users
      $scope.users.$add({ 
        nickname: $scope.loginForm.nickname,
        created_at: Math.floor(Date.now()/1000),
        online: true,
      }).then(function(ref){
        $timeout(function(){
          // Użytkownik dodany - zapisz dane w scope i w cookies
          $scope.current_user.online = true;
          $scope.current_user.nickname = $scope.loginForm.nickname;
          $scope.current_user.uid = ref.key;
          var now = new Date();
          var exp = new Date(now.getFullYear()+1, now.getMonth(), now.getDate());
          $cookieStore.put('chat_nickname', $scope.loginForm.nickname, { expires: exp });
          $cookieStore.put('chat_uid', ref.key, { expires: exp });
          $scope.loading = false;
          document.getElementById("chat-input").focus();
        },1000);
      });
    }
  }

// Jeżeli cookie istnieje
if( $cookieStore.get('chat_nickname') && $cookieStore.get('chat_uid') ){
  $scope.loading = true;
  // Logowanie automatyczne - ustaw status na true
  firebase.database().ref().child('users').child($cookieStore.get('chat_uid')).child('online').set(true).then(function(){
    $scope.current_user.online = true;
    $scope.current_user.nickname = $cookieStore.get('chat_nickname');
    $scope.current_user.uid = $cookieStore.get('chat_uid');
    $scope.loading = false;
    document.getElementById("chat-input").focus();
  });
}

// Wyloguj użytkownika gdy wychodzi ze strony
var exitEvent = window.attachEvent || window.addEventListener;
var onExitEvent = window.attachEvent ? 'onbeforeunload' : 'beforeunload'; 
exitEvent(onExitEvent, function(e) {  
  // Zmień status na false
  firebase.database().ref().child('users').child($scope.current_user.uid).child('online').set(false);
});

}]);

app.controller('inputCtrl', ['$scope','$timeout', function($scope, $timeout){
  // Wysyłanie wiadomości
  $scope.sendMessage = function(){
    if( $scope.messageForm.$valid ){
      // Dodaj rekord do węzła messages
      $scope.messages.$add({ 
        text: $scope.messageForm.text,
        user_uid: $scope.current_user.uid,
        user_nickname: $scope.current_user.nickname,
        created_at: Math.floor(Date.now()/1000)
      });
      // Wyczyść input
      $scope.messageForm.text = '';
    }
  }
}]);

})()

Wyświetlanie wiadomości

Najgorsze już za nami ale czat nadal nie wyświetla żadnych wpisów. Dodajemy kolejny widok w index.html.

HTML dla listy wiadomości

<!doctype html>
<html>
  <head>
    <meta http-equiv="X-UA-Compatible" content="IE=Edge"/>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>Czat Angular + Firebase</title>
    <link rel="stylesheet" href="styles/main.css">
  </head>
  <body>
<section ng-app="chat" class="chat">
   <div ng-controller="chatCtrl">
      <div class="chat-view" ng-show="current_user.online"> 
<div class="chat-messages" id="chat-messages">
  <!-- Lista wiadomości --> 
  <div ng-repeat="message in messages | orderBy: 'created_at'" ng-class="{'item-message-highlight':message.user_uid==current_user.uid}" class="item-message">
    <p class="item-message__user">
      <span>{{ message.user_nickname }}</span>
      <time>{{ format_date(message.created_at) }}</time>
    </p>
    <p class="item-message__text"><span>{{ message.text }}</span></p>
  </div>
</div>         

<!-- Pisanie nowej wiadomości -->
<div ng-controller="inputCtrl" class="chat-input">
  <form name="messageForm" ng-submit="sendMessage()" autocomplete="off" novalidate>
    <input type="text" ng-model="messageForm.text" id="chat-input" placeholder="Co Ci chodzi po głowie?" required>
    <button type="submit" class="btn btn-green">Wyślij</button>
  </form>
</div>
<div class="chat-users">
  <div class="chat-users__current">
    <!-- Aktywny użytkownik -->
    <h2>{{ current_user.nickname }}</h2>
    <p class="status status-on">Online</p>
  </div>
  <div class="chat-users__list">
    <!-- Lista użytkowników -->
    <div ng-repeat="user in users | orderBy: ['-online','-created_at']" ng-show="user.$id!=current_user.uid" class="item-user">
      <p>{{ user.nickname }}</p>
      <p ng-show="user.online" class="status status-on">Online</p>
      <p ng-show="!user.online" class="status">Offline</p>
    </div>
  </div>
</div>

      </div>
<div ng-controller="loginCtrl" ng-show="!current_user.online" class="chat-view login-form">  
   <h1>Czat Demo</h1>
   <h2>Angular + Firebase</h2>
   <form name="loginForm" ng-submit="loginUser()" novalidate>
      <div class="login-form__frame">
         <h3 ng-show="!loading">Zaloguj się</h3>
         <h3 ng-show="loading">Czekaj...</h3>
         <div class="row" ng-class="{'loading':loading}">
            <input type="text" ng-model="loginForm.nickname" placeholder="Twój nick ..." required>
            <button type="submit" class="btn btn-green">Wejdź</button>
         </div>
      </div>
      <p>Ten czat wykorzystuje pliki cookies.<br>Wchodząc dalej, wyrażasz na to zgodę.</p>
   </form>
</div>

   </div>
</section>

    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular.min.js"></script>
    <script src="https://www.gstatic.com/firebasejs/3.6.6/firebase.js"></script>
    <script src="https://cdn.firebase.com/libs/angularfire/2.3.0/angularfire.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.1/angular-cookies.min.js"></script>    
    <script src="scripts/app.js"></script>
  </body>
</html>

Sprawdzony model wyświetlania każdego czatu, charakteryzuje się odwróconym kierunkiem strony. Oznacza to, że najnowsza wiadomość wyświetla się na samym końcu listy – tuż nad polem pisania.

W widoku chatu, wpisy zostały już posortowane filtrem „orderBy”. Powinien jednak istnieć mechanizm, który będzie przewijał stronę do najnowszej wiadomości. Niestety nie wystarczy zrobić tego przy załadowaniu strony. Wykorzystamy tutaj $watch() by nasłuchiwać nowych wiadomości i przewijać stronę do ostatniej pozycji.

Wykrywanie nowych wiadomości

(function () {
  'use strict';

  // Inicjacja Firebase
  var config = {
    apiKey: "AIzaSyCn-1QIqWrrejw0ajXz8Zk7KngqYadaUNQ",
    authDomain: "test-1433d.firebaseapp.com",
    databaseURL: "https://test-1433d.firebaseio.com",
    projectId: "test-1433d",
    storageBucket: "test-1433d.appspot.com",
    messagingSenderId: "655366613901"
  };
  firebase.initializeApp(config);

  var app = angular.module('chat', ['firebase', 'ngCookies']);
  var chat_container = document.getElementById('chat-messages');

  // Dostęp do bazy danych
  app.factory('firebaseSrv', ['$firebaseArray', function($firebaseArray) {
    return {
      // Lista użytkowników
      getUsers: function(){
        var ref = firebase.database().ref().child("users");
        return $firebaseArray(ref);
      },
      // Lista wiadomości
      getMessages: function(){
        var ref = firebase.database().ref().child("messages");
        return $firebaseArray(ref);
      }
    } 
  }]);

app.controller('chatCtrl', ['$scope', 'firebaseSrv', '$firebaseArray', '$timeout', function($scope, firebaseSrv, $firebaseArray, $timeout){
   $scope.users = [];
   $scope.messages = [];
   $scope.current_user = {
     online: false,
     nickname: null,
     uid: null
   };
   // Pobieranie listy użytkowników
   firebaseSrv.getUsers().$loaded().then(function(data){
     $scope.users = data;
// Pobieranie wiadomości
firebaseSrv.getMessages().$loaded().then(function(data){
   $scope.messages = data;
   $timeout(function(){
      scrollBottom();
   },50);
   //Obserwuj 'messages' i przewiń do nowej wiadomości gdy się pojawi
   $scope.messages.$watch(function(event) {
      if( event.event == 'child_added' ){
         $timeout(function(){
            scrollBottom();
         },50);
      }
   });
});
   });
}]);

app.controller('loginCtrl', ['$scope', '$cookieStore', '$timeout', function($scope, $cookieStore, $timeout){
  $scope.loading = false;
  // Tworzenie użytkownika
  $scope.loginUser = function(){
    if( $scope.loginForm.$valid ){
      $scope.loading = true;
      // Dodaj rekord do węzła users
      $scope.users.$add({ 
        nickname: $scope.loginForm.nickname,
        created_at: Math.floor(Date.now()/1000),
        online: true,
      }).then(function(ref){
        $timeout(function(){
          // Użytkownik dodany - zapisz dane w scope i w cookies
          $scope.current_user.online = true;
          $scope.current_user.nickname = $scope.loginForm.nickname;
          $scope.current_user.uid = ref.key;
          var now = new Date();
          var exp = new Date(now.getFullYear()+1, now.getMonth(), now.getDate());
          $cookieStore.put('chat_nickname', $scope.loginForm.nickname, { expires: exp });
          $cookieStore.put('chat_uid', ref.key, { expires: exp });
          $scope.loading = false;
          document.getElementById("chat-input").focus();
        },1000);
      });
    }
  }

// Jeżeli cookie istnieje
if( $cookieStore.get('chat_nickname') && $cookieStore.get('chat_uid') ){
  $scope.loading = true;
  // Logowanie automatyczne - ustaw status na true
  firebase.database().ref().child('users').child($cookieStore.get('chat_uid')).child('online').set(true).then(function(){
    $scope.current_user.online = true;
    $scope.current_user.nickname = $cookieStore.get('chat_nickname');
    $scope.current_user.uid = $cookieStore.get('chat_uid');
    $scope.loading = false;
    document.getElementById("chat-input").focus();
  });
}

// Wyloguj użytkownika gdy wychodzi ze strony
var exitEvent = window.attachEvent || window.addEventListener;
var onExitEvent = window.attachEvent ? 'onbeforeunload' : 'beforeunload'; 
exitEvent(onExitEvent, function(e) {  
  // Zmień status na false
  firebase.database().ref().child('users').child($scope.current_user.uid).child('online').set(false);
});

}]);

app.controller('inputCtrl', ['$scope','$timeout', function($scope, $timeout){
  // Wysyłanie wiadomości
  $scope.sendMessage = function(){
    if( $scope.messageForm.$valid ){
      // Dodaj rekord do węzła messages
      $scope.messages.$add({ 
        text: $scope.messageForm.text,
        user_uid: $scope.current_user.uid,
        user_nickname: $scope.current_user.nickname,
        created_at: Math.floor(Date.now()/1000)
      });
      // Wyczyść input
      $scope.messageForm.text = '';
    }
  }
}]);

})()

Na końcu pliku app.js zdefiniujmy również funkcję scrollBottom:

function scrollBottom(){
   chat_container.scrollTop = chat_container.scrollHeight;
   window.scrollTo(0,document.body.scrollHeight);
}

Od tego momentu Angular obserwuje zmiany w $scope a dokładnie w $scope.messages. Jeżeli w drzewie pojawia się nowy rekord, strona automatycznie przewija się do samego dołu.

Na koniec

Podczas przygotowywania tego wpisu gościnnego, zaskoczyła mnie ilość tekstu jaka jest potrzebna do omówienia demo. W związku z tym oszczędzimy sobie wnikliwej analizy stylów. Cały kod CSS jest dostępny w źródle strony a kto pyta – nie błądzi.

Na koniec, chciałbym was również zaprosić do dyskusji na temat tej prostej aplikacji. Zadawajcie pytania i wyrażajcie swoją opinię. Myślę, że taki kawałek kodu, zawsze przyda się w przyszłości.

 

  • veranoo

    Fajnie, ale lepiej mogłbyś użyć coś nowego, np vue. angularjs miał już swoje 5 minut.

    • Michał Wilk

      Może kolejny poradnik będzie napisany w innym frameworku. AngularJS może i miał swoje 5 minut ale to nie znaczy, że jest bezużyteczny. Ciężko jest być na bieżąco ze wszystkim bo ilość narzędzi rośnie jak grzyby po deszczu

    • Tomasz Kuc

      a dlaczego by nie na nowym angularze?