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.

 

cytaty2.png

Ś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.