17

I asked this question but the specific question I'm asking has changed dramatically.

I have a piece of code:

  <div ng-attr-controller="{{pings || 'PingsCtrl as pings' }}">
    <h1 ng-click="pings.press()">asdf</h1>
  </div>

This code is injected into two html pages. One page already calls PingsCtrl. The other doesn't. I'm really trying to keep this code DRY and I only want to have one reference of the code above.

How can I write the code above to generate ng-controller if PingsCtrl hasn't already instantiated.

Here are the two html pages.

HTML

// First page
<html ng-app="coolApp">
  <div ng-controller="PingsCtrl as pings">
    <div ng-attr-controller="{{pings || 'PingsCtrl as pings' }}">
      <h1 ng-click="pings.press()">asdf</h1>
    </div>
  </div>
</html>

// Second page
<html ng-app="coolApp">
  <div ng-attr-controller="{{pings || 'PingsCtrl as pings' }}">
    <h1 ng-click="pings.press()">asdf</h1>
  </div>
</html>

Javascript is here:

angular.module('coolApp', [])

.controller('PingsCtrl', function() {
  var vm = this;

  vm.press = function() {alert(123)};
})

What's wrong and how do I fix this?

thank_you
  • 11,001
  • 19
  • 101
  • 185
  • can you put together a plunker or jsfiddle? – SoluableNonagon Dec 21 '15 at 19:51
  • 1
    Consider putting common functions in a **factory service**. They are singletons and automatically loaded only once. See https://docs.angularjs.org/guide/services#creating-services – georgeawg Dec 21 '15 at 20:05
  • I'll try and put something together in a bit. The code I posted is all that's needed though. – thank_you Dec 21 '15 at 20:09
  • Do you actually have two pages with `ng-app="coolApp"`, or are you just illustrating the generated HTML? – tasseKATT Dec 22 '15 at 07:01
  • 1
    Just one page. Understand I'm using Rails framework which injects the snippets of html. I excluded that piece of information because it has zero bearing on the issue at hand. – thank_you Dec 24 '15 at 00:29
  • Can you create two controllers and share the data between them? check this out: http://jsfiddle.net/786c1hgr/3/ – JavaSheriff Dec 28 '15 at 21:04
  • 1
    @user648026 I thought of that too. The more I think about it, that is the best and only way. Was just hoping to see if there was an alternate means via conditionally applying `ng-controller`. – thank_you Dec 28 '15 at 22:22
  • can I post it as an answer? – JavaSheriff Dec 29 '15 at 14:55
  • 1
    why not just use a dummy div that will contain either the first or second page? I found a link to actually instantiate a controller based on a condition but the problem here is that the scope of your pings controller is restricted to that div and it's children so your second page will get pings as undefined. In any case here's the link http://stackoverflow.com/questions/20866954/angularjs-change-controller-by-condition – martskins Dec 29 '15 at 20:20
  • 1
    What is it that you're trying to achieve exactly ? If you go from first page to second page with a full page refresh then you loose all JS context angular or otherwise. – Arkantos Dec 31 '15 at 14:39
  • your question, coupled with the comments, doesn't make one thing particularly clear. Are you using a **rails app** that uses angular on each page, or an **angular app** with a rails api? The way your code and comments are formed, it seems like you have rails generating multiple pages with angular on it, which will cause a full page reload each time. – Claies Jan 02 '16 at 14:33
  • Hi, just wait an hour. I'll show you how to implement it using directive and service – Alexander Elgin Jan 03 '16 at 07:53
  • You can check a working example in my answer – Alexander Elgin Jan 03 '16 at 08:22

3 Answers3

7

Just use a service. It's really the intended structure for having common data and functionality between pages.

Part of the problem with what you were attempting is, whether or not you manage to preserve the controller, Angular has its own management that won't follow you with that, and will be refreshing components without you. You'll run into things like a $scope that doesn't actually match the page you're looking at, and it ends up causing more problems than it's worth.

Harris
  • 1,775
  • 16
  • 19
3

I do have a solution but I also echo other people's concerns about the approach. You may want to have a global controller that you drop on the body for things that can happen anywhere and in most of the other controllers and just call through that. Eg

<body ng-controller="GlobalCtrl as gc">
    <h1 ng-click="gc.pingPress()"></h1>
</body>

Anyway here is what I came up with.

<div ng-if="pings">
    <h1 ng-click="pings.press()">asdf</h1>
</div>
<div ng-if="!pings">
    <div ng-controller="PingsCtrl as pings">
        <h1 ng-click="pings.press()">asdf</h1>
    </div>
</div>

This will work if it is dropped inside or outside of an existing PingsCtrl.

Here is a plunker.

https://plnkr.co/edit/4x0kSazcg0g0BsqPKN9C?p=preview

matthewdaniel
  • 1,842
  • 4
  • 21
  • 35
  • I'm seeing the controller be reinstantiated in your Plunker demo. Is there a particular way to go through it that demonstrates controller reuse? Also, I'd be wary about using global controllers. IIRC, they're supposed to be deprecated and discouraged. – Harris Jan 03 '16 at 00:02
  • If you open up the console on the plunker you can see that each page is instantiating the controller only once since i console log the instantiation. Also, i am not suggesting any feature that isn't just a regular controller and just call it global controller. Call it CommonCore as cc if global implies a feature that I'm unaware of. – matthewdaniel Jan 03 '16 at 00:51
  • You might want to double-check it. It's logging "instantiated" every navigation. – Harris Jan 03 '16 at 00:55
  • It should as every navigation is a new page load. Each http request is instantiating the controller only once. It isn't using angular routing but since the OP is using rails that is probably what he is doing. – matthewdaniel Jan 03 '16 at 00:59
  • The thing he is trying to avoid is the one page doing "instantiated" and the other doing "instantiated instantiated" – matthewdaniel Jan 03 '16 at 01:00
  • Although I think the best practice is to create a service, you answered the question correctly. You resolved my curiosity. Thanks! – thank_you Jan 03 '16 at 20:01
  • 1
    no problem. Definitely good to learn new tricks even if this isn't the use case for it; may come in handy for something else – matthewdaniel Jan 04 '16 at 01:47
1

Please, check my solution to see how to share data between controllers

var app = angular.module('myApp', []);

app.controller("aCtrl", function ($scope, PingList) {
  $scope.addPing = function() {
    PingList.add('Ping A');
  };
});

app.controller("bCtrl", function ($scope, PingList) {
  $scope.addPing = function() {
    PingList.add('Ping B');
  };
});

app.factory('PingList', function () {
  var pings = ['Ping1', 'Ping2'];

  return {
    add: function(ping) {
      pings.push(ping);
    },
    get: function () {
      return pings;
    }
  };
});

app.directive('pingList', function(PingList) {
  return {
    restrict: 'EA',
    link: function($scope) {
      $scope.pings = PingList.get();
      $scope.press = function(ping) {
        alert(ping);
      }
    },
    template: '<ul><li ng-repeat="ping in pings" ng-click="press(ping)">{{ping}}</li></ul>'
  };
});
a, li {
  cursor: pointer;
}

a {
  color: blue;
  text-decoration: underline;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.min.js"></script>

<div ng-app="myApp">
  <div ng-controller="aCtrl" style="float: left">
    <a ng-click="addPing()">click to add A ping</a>
    <ping-list></ping-list>
  </div>
  <div ng-controller="bCtrl" style="float: right">
    <a ng-click="addPing()">click to add B ping</a>
    <ping-list></ping-list>
  </div>
  <div style="clear: both"></div>
</div>
Alexander Elgin
  • 6,796
  • 4
  • 40
  • 50