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?
Kamil Bieganowsky
bo było by więcej pracy
Tomasz Kuc
mógłbyś rozwinąć?