Mika56
Mika56

Reputation: 45

AngularJS and UI-Router: keep controller loaded

I am building a web application for our customer support. One of the needs is to be able to keep multiple tickets opened at the same time.
I was able to do the first part easily using a tabulation system and UI-Router. However, with my current implementation, each time I change active tab, the previously-current tab is unloaded, and the now-current tab is loaded (because it was unloaded with a previous tab change).

This is not at all the expected behavior. I've already spent a couple of days trying to find a way to achieve this, without any luck. The closest thing I was able to do is to use the multiple views system from UI-Router, but I need multiple instance of the same view to keep in memory (if multiple tickets are opened, they all are on the same view, with the same controller, but a different scope)

Here's my current implementation: supportApp.js: var app = angular.module("supportApp", ["ui.router", "ui.bootstrap"]);

app.config(function($stateProvider, $urlRouterProvider, $httpProvider){
    $urlRouterProvider.otherwise("/");
    $stateProvider
        .decorator('d', function(state, parent){
            state.templateUrl = generateTemplateUrl(state.self.templateUrl);
            return state;
        })
        .state("main", {
            abtract: true,
            templateUrl: "main.html",
            controller: "mainController"
        })
        .state("main.inbox", {
            url: "/",
            templateUrl: "inbox.html",
            controller: "inboxController"
        })
        .state('main.viewTicket', {
            url: '/ticket/{id:int}',
            templateUrl: "viewTicket.html",
            controller: "ticketController"
        })
    ;
});

mainController.js: (handles other stuff, minimal code here)

app.controller("mainController", function($rootScope, $http, $scope, $state, $interval){
    // Tabs system
    $scope.tabs = [
        { heading: "Tickets", route:"main.inbox", active:false, params:{} }
    ];

    var addTabDefault = {
        heading: '',
        route: null,
        active: false,
        params: null,
        closeable: false
    };

    $rootScope.addTab = function(options){
        if(!options.hasOwnProperty('route') || !options.route)
        {
            throw "Route is required";
        }

        var tabAlreadyAdded = false;
        for(var i in $scope.tabs)
        {
            var tab = $scope.tabs[i];
            if(tab.route == options.route && angular.equals(tab.params, options.params))
            {
                tabAlreadyAdded = true;
                break;
            }
        }
        if(!tabAlreadyAdded)
        {
            $scope.tabs.push($.extend({}, addTabDefault, options));
        }
        if(options.hasOwnProperty('active') && options.active === true)
        {
            $state.go(options.route, options.hasOwnProperty('params')?options.params:null);
        }
    };

    $scope.removeTab = function($event, tab){
        $event.preventDefault();
        if($scope.active(tab.route, tab.params))
        {
            $scope.go($scope.tabs[0].route, $scope.tabs[0].params);
        }
        $scope.tabs.splice($scope.tabs.indexOf(tab), 1);
    };

    $scope.go = function(route, params){
        $state.go(route, params);
    };
    $scope.active = function(route, params){
        return $state.is(route, params);
    };

    $scope.$on("$stateChangeSuccess", function() {
        $scope.tabs.forEach(function(tab) {
            tab.active = $scope.active(tab.route, tab.params);
        });
    });
});

main.html:

<div class="container-fluid" id="sav-container">
    <div class="row-fluid">
        <div class="col-lg-2">
            <form role="form" id="searchForm" action="#">
                <div class="form-group has-feedback">
                    <input class="form-control" type="search" />
                    <span class="glyphicon glyphicon-search form-control-feedback"></span>
                </div>
            </form>
        </div>
        <div class="col-lg-10" id="support_main_menu">
            <ul class="nav nav-tabs">
                <li ng-repeat="t in tabs" ng-click="go(t.route, t.params)" ng-class="{active: t.active, closeable: t.closeable}" style="max-width: calc((100% - 128px) / {{tabs.length}});">
                    <a href class="nav-tab-text">
                        <button ng-show="t.closeable" ng-click="removeTab($event, t)" class="close" type="button">&times;</button>
                        <span>{{t.heading}}</span>
                    </a>
                </li>
            </ul>
        </div>
    </div>
    <div class="row-fluid">
        <div class="tab-content" ui-view></div>
    </div>
</div>

It seems to me that what I ask is pretty standard, but I sadly couldn't find any usefull thing on the Internet

Upvotes: 0

Views: 1104

Answers (1)

Cerad
Cerad

Reputation: 48865

The basic idea is to store state (i.e. list of tickets) in a service as opposed to a controller. Services hang around for the life of the application. There are some articles on this. I'm still developing my approach but here is an example:

var RefereeRepository = function(resource)
{
  this.resource = resource; // angular-resource

  this.items = []; // cache of items i.e. tickets

  this.findAll = function(reload)
  {
    if (!reload) return this.items;

    return this.items = this.resource.findAll(); // Kicks off actual json request
  };
  this.findx = function(id) 
  { 
    return this.resource.find({ id: id }); // actual json query
  };

  this.find = function(id) // Uses local cache
  {
    var itemx = {};

    // Needs refining
    this.items.every(function(item) {
      if (item.id !== id) return true;
      itemx = item; 
      return false; 
    });
    return itemx;
  };
  this.update = function(item)
  {
    return this.resource.update(item);
  };
};

refereeComponent.factory('refereeRepository', ['$resource',
function($resource)
{
  var resource = 
  $resource('/app_dev.php/referees/:id', { id: '@id' }, {
    update: {method: 'PUT'},
    findAll:  { 
      method: 'GET' , 
      isArray:true,
      transformResponse: function(data)
      {
        var items = angular.fromJson(data);
        var referees = [];
        items.forEach(function(item) {
          var referee = new Referee(item); // Convert json to my object
          referees.push(referee);
        });
        return referees;
      }
    },
    find:  { 
      method: 'GET',
      transformResponse: function(data)
      {
        var item = angular.fromJson(data);
        return new Referee(item);
      }
    }
});
var refereeRepository = new RefereeRepository(resource);

// Load items when service is created
refereeRepository.findAll(true);

return refereeRepository;
}]);

So basically we made a refereeRepository service that queries the web server for a list of referees and then caches the result. The controller would then use the cache.

refereeComponent.controller('RefereeListController', 
  ['$scope', 'refereeRepository',
  function($scope, refereeRepository) 
  {
    $scope.referees = refereeRepository.findAll();
  }
]);

Upvotes: 1

Related Questions