1

AngNoob here. I have some global navigation that uses the routeProvider to swap out external html pages inside the view. Within the view i set up a list type sub navigation (created with ng-repeat) that switches out divs in the external html file. I can get it to load up the page if I set it manually in the appCtrl:

//Here I set the initial value
    $scope.page = 'Comfort Homes of Athens';

But when I click on the span that has the ng-click. I get nothing. I started to think it was a scope issue but when i put just an ng-click='alert()' it does nothing either.

I have read around other posts but most seem to be putting a ng-click inside of an ng-switch rather than the reverse. and aren't using routing in their examples either. Still new to angular so maybe its something I haven't come across yet.

App HTML:

<body ng-app="app">
<header ng-include="header.url" ng-controller="nav"></header>
<article ng-view></article>
<footer ng-include="footer.url" ng-controller="nav"></footer>
    <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.16/angular.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.16/angular-route.js"></script>
    <script type="text/javascript" src="js/data.js"></script>
    <script type="text/javascript" src="js/model.js"></script>
</body>

External HTML File:

<div id="web" class="wrapper">
    <aside class="boxModel">
        <div id="controller" class="container">
            <div class="topBox bluebg subNavBar"><h1 class="white">Projects</h1></div>
            <div ng-controller="nav" id="controls" class="botBox whitebg">
                <span ng-repeat='item in webProjects' ng-click="page='{{item.name}}'">{{item.name}}</span>
            </div>
        </div>
    </aside><section ng-switch on="page" class="boxModel">

        <div ng-switch-when="Comfort Homes of Athens" id="sandbox" class="container round box whitebg">
           <h1>Here is link 1</h1>
        </div>

        <div ng-switch-when="Sealpak Incorporated" id="sandbox" class="container round box whitebg">
            <h1>here is Link 2</h1>
        </div>
    </section>
</div>

JS:

var app = angular.module("app", ["ngRoute"]);

function nav($scope) {
    $scope.templates = templates;
    $scope.header = $scope.templates[0];
    $scope.footer = $scope.templates[1];
    $scope.mainNav = mainNav;
    $scope.footNav = footNav;
}
app.config(function($routeProvider) {
    $routeProvider.when('/',{
        templateUrl: "templates/home.html",
        controller: "AppCtrl"
    }).when('/templates/web.html',{
        templateUrl: "templates/web.html",
        controller: "AppCtrl"
    }).when('/templates/seo.html',{
        templateUrl: "templates/seo.html",
        controller: "AppCtrl"
    }).otherwise({
        template: "This doesn't exist!"
    });
});

app.controller("AppCtrl", function($scope) {
    $scope.webProjects = webProjects;
    $scope.seoProjects = seoProjects;
//Here I set the initial value
    $scope.page = 'Comfort Homes of Athens';
});
Erik Grosskurth
  • 3,762
  • 5
  • 29
  • 44

3 Answers3

4

Unfortunately for you, ng-repeat creates child scopes which are siblings with each other and children of your parent controller (ng-controller="nav") while your <section> where ng-switch is on is not child scope of your ng-controller="nav", but AppCtrl.

You could try ng-click="$parent.$parent.page=item.name" just to understand scopes in angular.

<div id="web" class="wrapper">
<aside class="boxModel">
    <div id="controller" class="container">
        <div class="topBox bluebg subNavBar"><h1 class="white">Projects</h1></div>
        <div ng-controller="nav" id="controls" class="botBox whitebg">
            <span ng-repeat='item in webProjects' ng-click="$parent.$parent.page=item.name">{{item.name}}</span>
        </div>
    </div>
</aside><section ng-switch on="page" class="boxModel">

    <div ng-switch-when="Comfort Homes of Athens" id="sandbox" class="container round box whitebg">
       <h1>Here is link 1</h1>
    </div>

    <div ng-switch-when="Sealpak Incorporated" id="sandbox" class="container round box whitebg">
        <h1>here is Link 2</h1>
    </div>
</section>

I don't recommend using this solution as it's quite ugly. The solution of @link64 is better, but I think the inheritance of model is so implicit and creates a tightly-coupled code. Here I propose another solution which I hope is better by emitting an event:

<span ng-repeat='item in webProjects' ng-click="$emit('pageChange',item.name)">{{item.name}}</span>

I'm not sure if angular is able to resolve $emit('pageChange',item.name) expression in the template. If you run into any problems, you could write inside your controller:

<span ng-repeat='item in webProjects' ng-click="setPageChange(item.name)">{{item.name}}</span>

In your nav controller:

$scope.setPageChange = function (pageName) {
         $scope.$emit("pageChange",pageName);
   }

In your AppCtrl, listen to the event and update the page.

app.controller("AppCtrl", function($scope) {
    $scope.webProjects = webProjects;
    $scope.seoProjects = seoProjects;
//Here I set the initial value
    $scope.page = 'Comfort Homes of Athens';

    $scope.$on("pageChange", function (event, newPage){
         $scope.page = newPage;
    }
});
Khanh TO
  • 48,509
  • 13
  • 99
  • 115
  • Still grasping the scope structure of Angular... I agree that using $parent.$parent is not the right way. How do you suggest building the scope to fit the multiple layers of navigation? – Erik Grosskurth Sep 20 '14 at 00:38
  • @DJERock: In this case, it could be hard to setup the scope structure to make it **just** work. Just keep the current scope structure and emit event. – Khanh TO Sep 20 '14 at 00:45
  • I have tried your pageChange few different ways but the ng-click still doesn't switch the inner page. The main Navigation works but the inner pages don't switch. – Erik Grosskurth Sep 20 '14 at 01:30
  • Its triggering the functions correctly(alerts fire in both pageChange and setPageChange functions) but not changing the pages... – Erik Grosskurth Sep 20 '14 at 01:46
  • BAHHH STupid me... I forgot i used @link64's $scope.model.page and didn't add it to my pageChange function – Erik Grosskurth Sep 20 '14 at 01:49
  • you are the jam Do you mind cleaning up the answer/code a little and I will mark it correct and you get the bounty – Erik Grosskurth Sep 20 '14 at 01:50
  • app.controller("AppCtrl", function($scope) { $scope.webProjects = webProjects; $scope.seoProjects = seoProjects; //Here I set the initial value $scope.model = {}; $scope.model.page = 'Comfort Homes of Athens'; $scope.$on("pageChange", function (event, newPage){ $scope.model.page = newPage; }); }); – Erik Grosskurth Sep 20 '14 at 01:50
  • $scope.setPageChange = function (pageName) { $scope.$emit("pageChange",pageName); } – Erik Grosskurth Sep 20 '14 at 01:51
  • @DJERock: What do you want me cleanup? Both `$scope.model.page` and `$scope.page` should work as long as we update the corresponding property in the event. – Khanh TO Sep 20 '14 at 02:41
  • just trying to make it easier for noobs to understand how to navigate the angular rabbit hole that is $scope... to be honest I didn't know anything about $emit... You still get the bounty either way just more upticks for you down the line – Erik Grosskurth Sep 20 '14 at 03:21
  • 1
    @DJERock: $emit is a way to dispatch events in angularJS. In other frameworks, you could find another method. This is event-driven programming used in this case to decouple code. – Khanh TO Sep 20 '14 at 03:46
  • BTW, $emit can be called from the view. – Joel Jeske Sep 21 '14 at 01:25
1

In addition to @KhanhTo's answer, I wanted to point you toward another tool to use instead of ngRoute; UI-Router. This is not the answer to your original question, but it is a better solution that avoids your issue entirely.

UI-Router enhances the page routing of ngRoute and is more centered around states. You transition to states that have templates and optional controllers. It emits its own events such as $stateChangeStart or $stateChangeSuccess. You can invoke these state transitions with the function command $state.go(stateName) or by a directive ui-sref="my.state({name: item.name})

UI-Router is a very powerful tool and I cannot go into all the details here but the documentation and community is great.

A simple rewrite of your code could look like the following.

Template for web.html

<div class="wrapper">
    <aside class="boxModel">
        <div id="controller" class="container">
            <div class="topBox bluebg subNavBar"><h1 class="white">Projects</h1></div>
            <div ng-controller="nav" id="controls" class="botBox whitebg">
                <span ng-repeat='item in webProjects' ui-sref="app.web.page({name: {{item.name}})">
                    {{item.name}}
                 </span>
            </div>
        </div>
    </aside> 
    <section class="boxModel">

        <div ui-view class="container round box whitebg">
           <!-- Page content will go here -->
        </div>
    </section>
</div>

JavaScript

app.config(function($stateProvider) {
     $stateProvider
          .state('app', {
            abstract: true,
            template: '<div ui-view></div>', //Basic template 
            controller: "AppCtrl",
        }).state('app.home', {
            templateUrl: "templates/home.html",
            url: '/home'
        }).state('app.web',{
            templateUrl: "templates/web.html",
            url: '/web'
        }).state('app.web.page',{
            templateUrl: "templates/page.web.html",
            url: '/web/page/:name' //Note here the ':' means name will be a parameter in the url
        }).state('app.seo',{
            templateUrl: "templates/seo.html",
            url: '/seo'
        });
    });

app.controller('AppCtrl', function($scope){
    $scope.webProjects = webProjects;
    $scope.seoProjects = seoProjects;

    $scope.$on("$stateChangeStart", function (event, toState, toParams, fromState, fromParams){
        if(newState.name == 'app.web.page'){
            var pageName = newStateParams.name; //Variable name matches 
            $scope.linkText = fetchPageContent(pageName);
        }
    });
});

Template for page.web.html

<h1>{{linkText}}</h1>

With these changes you will be able to reuse the same instance of your controller. In addition to allowing your paging content to be more scalable.

Notes on $scopes

Every $scope has a parent except for the $rootScope. When you ask for an object in the view, it will look at its $scope to find the reference. If it does not have the reference, it will traverse up to its parent scope and look again. This occurs until you get to the $rootScope.

If you assign something to the $scope in the view, it will assign it to the current $scope as opposed to searching up the $scope chain for an existing property. That is why ng-click="model.page = ..." works; it looks up the $scope chaing for model and then assigns to the page property whereas ng-click="page = ..." assigns directly to the current $scope.

Notes on Controller re-use

To my knowledge, ngRoute does not support nested views. When you go to a new route, it will destroy the current view and controller as specified in the $routeProvider and then instantiate a new controller for the new view. UI-Router supports nested states (i.e. child states with child $scopes). This allows us to create a parent controller that can be re-used amongst all the child states.

Joel Jeske
  • 1,618
  • 14
  • 17
  • Since I want to avoid writing in any static data and pull the data from external sources (i.e. DB or XML) with jquery/ajax/rest, it seems I would acheive this with much less code. The idea that I have to write redundant code for each asynchronously loaded page seems too much. chat? – Erik Grosskurth Sep 21 '14 at 04:15
  • having a way to roll through each .state{()} or .when() with something like .each() I feel is the way but I'm not sure if it exists – Erik Grosskurth Sep 21 '14 at 04:17
  • With my proposal, you would not have any redundant code. The fetchPageContent() would ask a server for the data with that given page name. It would be scalable and the right way to do it. You only need one state for the page content and the actual content would be completely dynamic. – Joel Jeske Sep 21 '14 at 06:47
  • With your question and your currently chosen answer, the content must be statically embedded within the template. That is bad practice and not scalable. – Joel Jeske Sep 21 '14 at 06:48
  • How is that? All of my content is loaded from the DB now... Its the repeated routing elements that is taking up so many redundant lines of code – Erik Grosskurth Sep 23 '14 at 23:58
  • This is an example of your $stateChangeStart code. It fetchs the same route based on the last part of the path being your page name variable. Note: Replace /db/webpage/:pagenameVar with your path to your web page route (e.g. /db/webpage/Comfort%20Homes%20of%20Athen) `var resourceFetcher = $resource('/db/webpage/:pagenameVar');` `var pageName = newStateParams.name;` `$scope.linkText = resourceFetcher.get({pagenameVar: pageName});` – Joel Jeske Sep 24 '14 at 15:00
-1

I think this may be related to some misunderstanding of how scope works.

ng-repeat creates its own scope. When attempting to set page, angular creates it on the scope of the ng-repeat.

In your AppCtrl, create an object on the scope as follows:

$scope.model = {};
$scope.model.page = 'Comfort Homes of Athens';//Default value

On your ng-click, refer to model.page instead of just page. Angular will then traverse up the scope to find model.page instead of just create a property on the local scope of the ng-repeat.

<span ng-repeat='item in webProjects' ng-click="model.page='{{item.name}}'">{{item.name}}</span>

Also, your AppCtrl is going to be recreated every time you change pages. You should probably use a service to persist the state between page changes

link64
  • 1,944
  • 1
  • 26
  • 41