File Manager cz. III – tworzenie nowego folder

Nim zacznę opisywać dalsze kroki poszerzające funkcjonalność naszego filemanager’a, muszę najpierw się nieco cofnąć i zmienić nasz kod. Dlaczego? Już tłumaczę. W trakcie przemyśleń jak ma działać nasza aplikacja zrezygnowałem z koncepcji pojawiających się modali, które miałyby pozwalać dodawać, edytować, usuwać katalogi, czy wgrywać pliki. Chciałbym także, by można było otworzyć dowolny katalog, czy po dodaniu katalogu lub plików (które byłby w osobnych widokach) móc wrócić do widoku tego samego katalogu. W związku z tym potrzebujemy narzędzia, które umożliwi odtworzenie stanu aplikacji. W tej sytuacji z pomocą przyjdzie nam ui-router, który zastąpi ngRoute.

ui-router

W aplikacji stan opisujący otwarcie katalogu powinien reagować na identyfikator katalogu, który ma otworzyć. Zatem musi się pojawić jakiś parametr w routingu, który będzie można zinterpretować i na jego podstawie wyświetlić odpowiedni widok. Przyjmijmy zatem, że ścieżka /dir/:dirId będzie otwierała zawartość katalogu o identyfikatorze :dirId. Dodatkowo ścieżka /dir/:dirId/add powinna wyświetlić formularz umożliwiający dodanie nowego katalogu w katalogu o identyfikatorze :dirId. No i najważniejsze, jeśli ktoś wywoła pusty adres to powinien przejść do adresu /dir/0, który spowoduje wyświetlenie zawartości głównego katalogu. Całość tego założenia zrealizuje nam poniższy fragment plik app.js.

$urlRouterProvider.when('', '/dir/0');
$stateProvider
    .state('main', {
        url: '/dir/:dirId',
        templateUrl: '/dist/templates/main.html',
        controller: 'MainCtrl',
        resolve: {
            dir: ['DirStructure', '$stateParams', function(DirStructure, $stateParams){
               return DirStructure.load($stateParams.dirId)
            }]
        }
    })
    .state('main.add', {
        url: '/add',
        templateUrl: '/dist/templates/dir_add.html',
        controller: 'AddDirCtrl'
    })
;

Jak widać z powyższego kodu pojawiły się nowe obiekty w naszej aplikacji, pierwszy to DirStructure, a drugi to AddDirCtrl. Zacznijmy od tego pierwszego.

DirStructure

Jest to obiekt (service, singleton), którego zadaniem będzie wczytać zawartość katalogu na podstawie podanego parametru i współdzielić go miedzy innymi obiektami. Dodatkowo jeśli nastąpi kilkukrotne wywołanie load z tym samym parametrem, to obiekt ten zamiast pobierać dane z serwera powinien skorzystać z już posiadanych danych.

// models.js
angular.module('filemanager')
    .service('DirStructure', ['$q', '$http', 'DirObj', 'FileObj', function($q, $http, DirObj, FileObj){
        /**
         * Current dir
         * @type {DirObj}
         */
        this.currentDir = false;

        this.load = function(dirId){
            var defer = $q.defer(), that = this;

            if(this.currentDir !== false && parseInt(dirId, 10) === this.currentDir.id)
            {
                return this;
            }

            $http.post('/api/directory', {dir_id: dirId})
                .success(function(data){
                    if(parseInt(dirId, 10) === 0)
                    {
                        that.currentDir = new DirObj({id: 0, name: 'Home'});
                    }

                    that.currentDir.id = dirId;
                    that.currentDir.dirs = [];
                    that.currentDir.files = [];

                    data.dirs.forEach(function(dirData){
                        that.currentDir.dirs.push(new DirObj(dirData));
                    });

                    data.files.forEach(function(fileData){
                        that.currentDir.files.push(new FileObj(fileData));
                    });

                    defer.resolve(that);
                })
            ;
            return defer.promise;
        }
    }])
    .factory('FileObj', ['FileTypes', 'FileIcons', function(FileTypes, FileIcons){
…

Może nie widać tego na pierwszy rzut oka, ale po krótkiej chwili napewno każdy zauważy, że w powyższym kodzie zmienna currentDir przechowuje następujące dane:

  • id – identyfikator bieżącego katalogu
  • files – lista plików bieżącego katalogu
  • dirs – lista podkatalogów bieżącego katalogu.

W tym miejscu należy także wspomnieć o tym, iż statyczny plik /data/directory.json został zamieniony na bardziej dynamiczną strukturę opartą o bazę mysql. Wszystko to zostało uruchomione na serwerze node, odpowiednie pliki serwera znajdziecie w katalogu server. Nie będę się tutaj rozpisywał nad tym jak zbudowany jest serwer, gdyż zakładam, że każdy podepnie sobie filemanger’a pod taki backend jaki będzie posiadał, najważniejsze jest jedynie to by zapewnić zgodność przesyłania danych.

Skoro mamy już taki singleton to należałoby go odpowiednio wykorzystać w naszym głównym kontrolerze.

MainCtrl

// MainCtrl.js
angular.module('filemanager')
    .controller('MainCtrl', ['$scope', '$state', '$http', 'DirStructure', 'DirObj', 'FileObj', 'FileTypes', function($scope, $state, $http, DirStructure, DirObj, FileObj, FileTypes){
        /**
         * List of folders in current folder
         * @type {Array}
         */
        $scope.dirs = DirStructure.currentDir.dirs;

        /**
         * List of files in current folder
         * @type {Array}
         */
        $scope.files = DirStructure.currentDir.files;
…

        $scope.showDirSection = function(){
            $state.go('main.add');
        }

MainCtrl oprócz skorzystania z naszego serwisu posiada także nową funkcję showDirSection, która pozwoli nam przejść do stanu dodawania nowego katalogu.

Dodawanie katalogu

Dodawanie katalogu jest osobnym stanem – dziedziczącym po stanie głównego widoku, dzięki czemu nie powoduje on przeładowania całości stanu aplikacji, tylko jeden mały fragment. Dodając to tego jeszcze drobną animację w CSS, otrzymujemy całkiem przyjemny dla oka efekt.
Zacznijmy jednak od szablonu:

// template/add_dir.html
<div class="panel panel-default main-panel">
<div class="panel-body">
<h2>Utwórz nowy folder</h2>
<div ng-show="folder_add.$invalid && folder_add.$dirty" class="alert alert-danger">
<p>Nazwa folderu jest wymagana i nie może być pusta</p>
</div>
<form name="folder_add" class="form form-horizontal" novalidate style="margin: 10px;">
<div class="form-group">
<input type="text" name="folder_name" ng-model="folderName" class="form-control" placeholder="Nazwa folderu" required>
</div>
</form>
<div class="btn-group pull-right">
<button ng-disabled="folder_add.$invalid" ng-click="addFolder()" class="btn btn-success"><i class="fa fa-check"></i>Utwórz</button>
<button ng-click="goBack()" class="btn btn-danger"><i class="fa fa-times"></i>Anuluj</button>
</div>
</div>
</div>

jest to prosty formularz, który zawiera prostą walidację pola folderName. Jeśli jest ona pusta to wyświetla się odpowiedni komunikat informujący o potrzebie wprowadzenia niepustej nazwy. Dodatkowo przycisk zapisu jest aktywny tylko w przypadku prawidłowo wypełnionego pola z nazwą.

Przycisk Anuluj pozwala powrócić do poprzedniego widoku, poprzez zamknięcie formularza.
Zarówno Dodanie folderu jak i Anulowanie wprowadzania zmian jest obsługiwane przez odpowiednie funkcje w kontrolerze odpowiedzialnym za tę część widoku – AddDirCtrl.

AddDirCtrl

// controllers/AddDirCtrl.js
'use strict';
angular.module('filemanager')
    .controller('AddDirCtrl', ['$scope', '$state', '$timeout', 'LastState', 'DirStructure', function($scope, $state, $timeout, lastState, DirStructure){
        $scope.folderName = '';

        $timeout(function(){
            angular.element('input[name="folder_name"]').focus();
        }, 200);


        $scope.goBack = function(){
            lastState.goBack();
        }

        $scope.addFolder = function(){
            if($scope.folderName !== '')
            {
                DirStructure.addFolder($scope.folderName, $scope.goBack);
            }
        }
    }])
;

W pierwszej kolejności tuż po załadowaniu widoku aktywowany jest $timeout, który powoduje, że focus jest ustawiany na polu input, dzięki czemu można od razu wprowadzać nazwę nowego katalogu.

Funkcja goBack wykorzystuje serwis LastState, który możemy znaleźć w pliku services.js

angular.module('filemanager')
    .service('LastState', ['$state', function($state){
        this.state = false;
        this.params = {};

        this.setLastState = function(state, params){
            this.state = state;
            this.params = params;
        }

        this.goBack = function(){
            if(this.state && this.state.name !== '')
            {
                $state.go(this.state.name, this.params);
            }
            else
            {
                $state.go('main', {dirId: 0});
            }
        }
    }])

Serwis ten przy każdej zmianie stanu aplikacji zapamiętuje poprzedni jej stan i pozwala do niego wrócić, jeśli jednak nie posiada poprzedniego stanu to powrót odbywa się do strony startowej. Operacja ustawienia poprzedniego stanu jest wykonywana z wykorzystaniem zdarzenia $stateChangeSuccess.

 
// app.js
…
fm.run(['$rootScope', 'LastState', '$state', function ($rootScope, lastState, $state) {
    $rootScope.$on('$stateChangeSuccess', function(event, to, toParams, from, fromParams) {
        lastState.setLastState(from, fromParams);
    });
}]);

Funkcja addFolder powoduje dodanie nowego folderu, wykorzystuje w tym celu funkcję addFolder z serwisu DirStructure, a jako callback na sukces wywołuje funkcję goBack.

// models.js
…
this.addFolder = function(name, callbackSuccess, callbackError){
    var that = this;
    $http.post('/api/directory/add', {dir_id: this.currentDir.id, name: name})
        .success(function(data){
            var dir = new DirObj(data);
            that.currentDir.dirs.push(dir);
            if(callbackSuccess)
            {
                callbackSuccess(new DirObj(data))
            }
        })
        .error(function(data){
            if(callbackError)
            {
                callbackError(data);
            }
        })
    ;
}
…

Wszystkie nowe funkcje zostały pokryte odpowiednimi testami jednostkowymi, które znajdziecie w katalogu tests/unit.

Dodatek

Takim małym dodatkiem, zdradzającym co będzie w następnej części, jest funkcja goToFolder w MainCtrl.js, która podpięta do zdarzenia click na ikonie katalogu pozwala nam przejść do tego katalogu. Jednak więcej szczegółów o tym jak przechodzić do katalogu i jak wracać w przyszłej części.

File Manager cz. III – tworzenie nowego folder
Przewiń na górę