W poprzednim wpisie pokazałem w jaki sposób można korzystać z dobrodziejstw NodeJS, tworząc prostą aplikację, którą teraz jest czas dokończyć.
Na początku zmodyfikujemy nieco plik package.json.
/* package.json */ { /* pozostała część */ "scripts": { "watch": "watchify app/app.js --debug -v -o bundle/bundle.map.js", "live": "node live-reload.js" }, /* pozostała część */ }
Po dodaniu dwóch pól do obiektu scripts, możemy teraz skorzystać z nich w terminalu.
Pierwsza uruchamia lokalny serwer,
npm run live
a druga uruchamia moduł watchify
npm run watch
Na początku w pliku app.js stworzymy moduł odpowiedzialny za całą naszą aplikację.
// import jquery
var $ = require('jquery');
// import pliku events.js
var Events = require('./events');
// main app module
var app = (function(){
// hold form data
var addForm = {
form: null,
quoteText: null,
author: null
}
// get DOM elements
function cacheDOM(){
}
// bind events
function bindEvents() {
}
// initialize app
function init() {
cacheDOM();
bindEvents();
}
// start app
init();
})();
Jak widzimy powyżej do zmiennej app jest pzypisana funkcja IIFE. Jest ona wywoływana bezpośrednio po zdefiniowaniu. Dzięki temu, że funkje tworzą nowy zakres(ang. scope), zmienne zadeklarowane wewnątrz nie są dostępne za zewnątrz. Można powiedzieć, że są prywatne.
Obiekt addForm będzie zawierał elementy DOM, odnoszące się do formularza #addForm w pliku index.html tzn. sam formularz(form), pole textarea z cytatem(quoteText), pole input z danymi o autorze(author).
Funkcja cacheDOM() odpowiada za pobranie elementów DOM do zmiennych(obiekt addForm), a funkcja bindEvents() odpowiada za obsługę zdarzeń(np.: co się stanie po kliknięciu przycisku Dodaj).
Funkcja init() jest elementem startowym naszej aplikacji. To dzięki niej aplikacja wstaje do życia :).
Dalej dodajemy nową zawartość do funkcji cacheDOM()
function cacheDOM(){ addForm.form = $('#addForm'); addForm.quoteText = $(addForm.form).find('#quote'); addForm.author = $(addForm.form).find('#author'); addForm.submitBtn = $(addForm.form).find('input[type="submit"]'); }
Jak widzimy do elementu addForm.form pobieramy za pomocą funkcji jQuery obiekt formularza. Dalej do odpowiednich zmiennych pobierane są kolejne elementy DOM. Należy zwrócić uwagę na funkcję find(). Dzięki niej można szybciej dotrzeć do elementów DOM, gdyż nie przeszukuje całego drzewa DOM, lecz szuka jedynie w głąb elementu, na którym jest wywoływana( w naszym przypadku jest to formularz #addForm).
Dalej musimy obsłużyć zdarzenie dodania cytatu po kliknięciu przycisku Dodaj. Można ty wykorzystać zdarzenie submit, które posiadają formularze.
function bindEvents() { addForm.form.on('submit', Events.handleFormSubmit); }
Funkcja on(), umożliwia powiązanie zdarzenia(np.: submit, click, mousemove itd…) z odpowiednią funkcją(uchwyt, ang. handle).
Ja postanowiłem przenieść funkcje do innego pliku, gdyż umożliwi czytelność kodu(a przynajmniej staram się ją utrzymać).
Tak więc tworzymy plik events.js w folderze app
//plik app/events.js // import quotes var quotes = require('./quotes'); var Events = (function() { function handleFormSubmit(ev) { ev.preventDefault(); var data = { quote: addForm.quote.value, author: addForm.author.value } if(!data.quote || !data.author) { alert('Zapomniałeś wypełnić wszystkich pól'); } else { quotes.add(data); addForm.quote.value = null; addForm.author.value = null; } } return { handleFormSubmit: handleFormSubmit, } })(); module.exports = Events;
Plik ten zawiera moduł Events, który jest eksportowany za pomocą
module.exports = Events;
dzięki czemu możemy go użyć w pliki app.js za pomocą funkcji require().
Moduł ten zawiera funkcję handleFormSubmit(), do której jest przekazywany obiekt(ev, można go nazwać jak się chce), który zawiera dane o zdarzeniu. Na samym początku jest wywołana jest funkcja ev.preventDefault(), dzięki której standardowe zachowanie formularza(wysłanie żądania do serwera i tym samym przeładowanie strony) zostanie wstrzymane.
Obiekt data zawiera natomiast treść i autora cytatu, która są pobierane z obiektu addForm.
Dalej instrukcja warunkowa if/else sprawdza, czy użytkownik wypełnił wszystkie pola.
Jeśli nie wypełnił pojawia się standardowe okienko typu alert, które przypomina o wypełnieniu wszystkich pól.
Jeśli wszystko jest w porządku, uruchamiana jest funkcja add() modułu quotes(importowany jest na początku do zmiennej quotes).
Dalej po dodaniu cytatu, pola textarea i input są czyszczone.
Instrukcja return, zwraca funkcję handleFormSubmit(), dzięki czemu będziemy mieć do niej dostęp w pliku app.js za pomocą Events.hadleFormSubmit.
Przejdżmy teraz do pliku quotes.js w folderz app
// import jquery var $ = require('jquery'); // import handlebars, tamplating system var handlebars = require('handlebars'); // store is a wrapper for localstorage var store = require('store'); // quotes module var quotes = (function() { var quotesContainer, // compiled quoteTemplate quoteTmpl, // quotes from localStorage quotes = store.get('quotes') || [], // how many quotes quotesCount = quotes.length, noQuotesInfo; // show all quotes function showAll(){ } // adding quotes to local storage function add(data) { } // delete quote function remove(id){ } function handleDeleteQuote(ev){ } function cacheDOM() { quotesContainer = $('#quotesContainer'); quoteTmpl = handlebars.compile($('#quoteTmpl').html()); noQuotesInfo = $(quotesContainer).find('#no-quotes-info'); } function bindEvents() { // show all quotes showAll(); } function init() { cacheDOM(); bindEvents(); } init(); return { add: add, } })(); module.exports = quotes;
Na początku importowana jest biblioteka jQuery. Następnie importujemy kolejne dwa moduły:
- Handlebars – biblioteka, która umożliwia tworzenie szablonów HTML,m
- store – to moduł npm, którego zadaniem jest uproszczenie pracy localStorage HTML5
Jednak zanim to zrobimy, musimy je pobrać
npm install -S handlebars npm install -S store
Dalej deklarujemy zmienne:
- quotesContainer – będzie przechowywać element div#quotesContainer, w którym umieszczane będą cytaty
- quoteTmpl – będzie zawierać skompilowany szablon Handlebars
- quotes – to tablica, gdzie przechowywane będą dane o cytatach z localStorage
- quotesCount – będzie zawierać ilość elementów w tablicy quotes
- noQuotesInfo – będzie zawierać element div#no-quotes-info, który w zależności od zmiennej quotesCount będzie ukrywany lub pokazywany
W funkcji cacheDOM() pobieramy elementy DOM. Jednak aby móc skorzystać z funkcji handlebars.compile(), musimy w pliku index.html dodać szablon, który będzie określał sposób wyświetlania naszych cytatów.
<!-- plik index.html --> <!-- pozostała część kodu --> <script type="text/x-handlebars-template" id='quoteTmpl'> <div class="quote center"> <blockquote>{{ quote }} <br><br><small> - {{ author }}</small> <button class='deleteQuoteBtn'>Usuń</button> <div style='clear: both;'></div> </blockquote> <input type='hidden' name='id' value={{ id }} /> </div> </script> <!-- bundle js --> <script src='bundle.map.js'></script> </body> </html>
Nie ma tu nic nadzwyczajnego. Typ skryptu pokazuje, że będzie to szablon Handlebars. Dalej w znaczniku blockquote będzie wyświetlana zawartość cytatu oraz jego autor. Przycisk button.deleteQuoteBtn będzie odpowiedzialny za usuwanie cytatów. Natomiast ukryte pole input jako wartość będzie posiadać id cytatu, dzięki któremu będzie można go usunąć z localStorage.
Teraz czas na funkcję showAll(), która będzie pokazywać wszystkie nasze cytaty.
// plik quotes.js // show all quotes function showAll(){ if(quotesCount) noQuotesInfo.hide(); else return; quotes.forEach(function(quote){ var quote = quoteTmpl(quote); quotesContainer.append(quote); }); // handle delete quote $(quotesContainer).find('.deleteQuoteBtn').on('click', handleDeleteQuote); }
Na początku instrukcją if sprawdzamy czy mamy jakieś cytaty. Jeśli są, ukrywamy informacje o braku cytatów(div#no-quotes-info), a następnie wyświetlamy każdy pojedyńczo(na koniec elementu div#quotesContainer dodajemy gotowy szablon z danymi). Następnie musimy dodać zdarzenie usunięcia cytatu po kliknięciu przycisku Usuń.
Jeśli natomiast nie dodaliśmy jeszcze cytatów, przerywamy wywołanie funkcji przez instrukcję return.
Aby móc dodawać cytaty, musimy uzupełnić ciało funkcji add()
function add(data) { var quote; data.id = Date.now(); quotes.unshift(data); store.set('quotes', quotes); quote = quoteTmpl(data); quote = quotesContainer.prepend(quote); // handle delete quote $(quote).find('.deleteQuoteBtn').on('click', handleDeleteQuote); quotesCount++; if(quotesCount === 1) noQuotesInfo.hide(); }
Parametr data przekazywany do funkcji zawiera już pola author i quote, nie ma jeszcze pola id, dzięki któremu będzie można zidentyfikować dokładnie dany cytat. Jako wartości id użyję funkcji Date.now(), gdyż zwraca ona czas podany w milisekundach(czyli za każdym razem będzie inna wartość).
Następnie funkcją unshift(), dodajemy nowy cytat na początek tablicy z cytatami i aktualizujemy localStorage za pomoca store.set().
Dalej do drzewa DOM dodajemy nowy cytat, zwiększamy wartość zmiennej quotesCount i decydujemy co zrobić z divem#no-quotes-info. Jeśli zmienna quotesCount jest równa 1, wtedy go ukrywamy.
Możemy już dodawać cytaty, ale nie możemy ich jeszcze usuwać. Pozostało nam więc uzupełnienie dwóch funkcji handleDeleteQuote() i remove().
function handleDeleteQuote(ev){ // get quote to remove var quote = $(ev.target).closest('.quote'); var id = $(quote).find('input[name=id]')[0].value; quote.remove(); remove(id); }
Do zmiennej quote pobieramy element DOM odnoszący się do usuwanego cytatu. Funkcja closest() szuka od elementu ev.target(przycisk Usuń) ‚w górę’ drzewa DOM, elementu mającego klasę ‚quote’.
Następnie pobieramy id usuwanego cytatu z ukrytego pola input.
Linijka poniżej usuwa element z drzewa DOM,
quote.remove();
natomiast kolejna ma zadanie usunąć cytat z localStorage.
remove(id);
Zabierzmy się za napisanie funkcji remove(). Jako argument przyjmuje wartpość id cytatu, jednak należy zrzutować tą wartosć z typu string na int za pomocą funkcji parseInt().
Dalej trzeba usunąć cytat z tablicy quotes. Do tego celu można użyć funkcji filter(), która zostawi nam pozostałe cyataty. Po uaktualnieniu localStorage funkcją store.set(), zmniejszamy zmienną quotesCount i w razie konieczności pokazujemy informację o braku cytatów.
function remove(id){ id = parseInt(id); quotes = quotes.filter(function(quote){ return quote.id !== id ? true : false; }); store.set('quotes', quotes); quotesCount--; if(quotesCount === 0) noQuotesInfo.show(); }
Pora sprawdzić jak działa nasza aplikacja.
Świetnie, nasza aplikacja wyświetla cytaty, umożliwia ich dodawanie i usuwanie. Można pokusić się jeszcze o edytowanie cytatów, ale myślę ,że na tym poprzestanę.
Myślę, że wpis się podobał. Zachęcam do komentowania i dzielenia się wskażówkami, jak można ulepszyć kod, aplikację i sposób jej tworzenia.