trevorc
trevorc

Reputation: 3031

In AngularJs, how do I identify a specific scope within a controller that is used multiple times on a page?

I have an application that uses multiple tabs with a grid in each tab. I have multiple pages using this same configuration.

I have a single controller that works for all grids. I would like to reuse this controller throughtout my application.

My problem is that I am trying to lazy load the data for each tab until the tab is clicked. What I can't figure out is how to tell the controller which scope belongs to which tab and ONLY get the data for that grid. I know internally AngularJs does this because if I just load all the data for each tab at once I can click on my paginator, search, etc. for each tab and only that scope is updated.

I have ng-click setup for each tab and I can get my controller to fire when a tab is clicked. However, the controller calls all instantiated scopes to load data for their respective grids.

My approach may not be the best but it seems silly to create seperate controllers that do exactly the same thing.

Note: Angular UI tabs with bootstrap is not an option.

My view

<div ng-app="TabsApp">
    <div tabs>
        <div class="tabs">
            <a ng-click="clickTab(0)" ng-class="{selected: selectedTab==0}">Localized Text From Server</a>
            <a ng-click="clickTab(1)" ng-class="{selected: selectedTab==1}">Localized Text From Server</a>
            <a ng-click="clickTab(2)" ng-class="{selected: selectedTab==2}">Localized Text From Server</a>
        </div>
        <div class="tab-content">
            <div ng-show="selectedTab==0" ng-init="init(@Model.UserId, 'useraddresses')" ng-controller="ListCtrl">@Html.Partial("_Grid0")</div>
            <div ng-show="selectedTab==1" ng-init="init(@Model.UserId, 'userphones')" ng-controller="ListCtrl">@Html.Partial("_Grid1")</div>
            <div ng-show="selectedTab==2" ng-init="init(@Model.UserId, 'usernotes')" ng-controller="ListCtrl">@Html.Partial("_Grid2")</div>
        </div>
    </div>
</div>

My app and factory url

var TabsApp = angular.module("TabsApp", ['ngResource', 'ngRoute']);

TabsApp.factory('Api', function($resource){
    return $resource('/api/user/:userId/:ctrl', { userId: '@userId', ctrl: '@ctrl'});
});

My controller - child scope(s)

var ListCtrl = function ($scope, $location, Api){
    $scope.search = function () {
        Api.get({
            userId: $scope.userId,
            ctrl: $scope.ctrl,
            q: $scope.query
            //etc.
            },
                function(data){
                    $scope.items = data.items;
                    //other mapping
                });
    };

    $scope.init = function(userId, ctrl){
        $scope.userId = userId;
        $scope.ctrl = ctrl;
    };
    $scope.reset = function(){
        $scope.items = [];
        $scope.search();
    };

    //kind of a hack but broadcasting was the only way I could figure out to listen for tab changes
    $scope.tabModelChange = { 'isChanged': false };
    $scope.$on('tabModelChange', function(event, args) {
        $scope.tabModelChange.isChanged = true;
        var activeTab = args.val[$scope.selectedTab];
        if (!activeTab.isLoaded) {
            $scope.reset();
        }
    }); 

    //filtering, sorting, pagination, etc.
};

My directive: Parent scope

TabsApp.directive('tabs', function () {
    return {
        controller: function ($scope, $element) {
            $scope.selectedTab = 0;
            $scope.tabModel = [];

            //I use localized text and any number of tabs on my views from the server so the client wont know how many tabs each view will have
            var tabs = angular.element($element.children()[1]).children();
            angular.forEach(tabs, function (value, key) {
                $scope.tabModel.push({'isLoaded' : false, 'isActive': false});
            });
            $scope.clickTab = function(tab) {
                $scope.selectedTab = tab;
                $scope.tabModel[$scope.selectedTab].isActive = true;
            };
            $scope.$watch('tabModel', function(newVal, oldVal) {
                if (newVal !== oldVal) {
                    $scope.$broadcast('tabModelChange', { 'val': newVal });
                }
            }, true);
        }
    };
});

I suspect that when my controller receives a broadcast that the tab model has changed it calls $scope.reset with $scope being a parent scope which then traverses the child scopes looking for 'reset'. Because there are 3 instances of ListCtrl, parent scope finds 3 child scopes with 'reset'. Hence, all the data gets loaded at once.

How can I get $scope.reset to match the tab that was clicked? Thanks.

Upvotes: 1

Views: 2433

Answers (2)

Jesus is Lord
Jesus is Lord

Reputation: 15399

With minimal change to your existing code, here's a plunker that does what you want, I think.

http://plnkr.co/edit/TVwWQehgWJay7ngA8g6B?p=preview

Basically I made a second directive called tab that accepts an argument which is a function to evaluate when that tab is switched to.

TabsApp.directive('tab', function () {
    return {
        require: '^tabs',
        link: function (scope, element, attrs, tabsCtrl) {
            tabsCtrl.add({
                'isLoaded' : false, 
                'isActive': false, 
                'changed': function () { scope.$eval(attrs.tab); }
            });
        }
    };
});

Upvotes: 2

Dan Doyon
Dan Doyon

Reputation: 6720

Here's what we do (i'm a bit lazy and going to pull use our code), We load when tab is clicked.

Here's the tabs

    <ul class='nav nav-pills'>        
        <li ng-class="{ active : mode == 'incomplete'}"><a tabindex="-1" href="#/incomplete" ng-click='mode="incomplete"'>Incomplete orders</span></a></li>        
        <li ng-class="{ active : mode == 'completed'}"><a tabindex="-1" href="#/completed" ng-click='mode="completed"'>Completed orders</a></li>
    </ul>

We setup routes

.config(['$routeProvider', '$env', function ($routeProvider, $env) {
     $routeProvider.when("/completed", {
        templateUrl: $env.urlRoot + "content/partials/orders/completed.htm",            controller: 'CompletedOrdersCtrl'
     });
    $routeProvider.when("/incomplete", {
        templateUrl: $env.urlRoot + "content/partials/orders/incomplete.htm",  controller: 'IncompleteOrdersCtrl'
     });
     $routeProvider.otherwise({
        templateUrl: $env.urlRoot + "content/partials/orders/incomplete.htm", controller: 'IncompleteOrdersCtrl'
     });
} ]);

We then have the two controllers IncompleteOrdersCtrl and CompleteOrdersCtrl that call the service to get the correct data. You could possibly consolidate into one controller with a parameter passed in the route config.

Hopefully this works for you

--dan

Upvotes: 1

Related Questions