3

I have a simple AngularJS project where I take input data from the user and produce a chart based on that data. I'm trying to figure out how to organize the code so it conforms to the MVC design pattern. In particular, I'm stuck figuring out how to propagate a form's submit event to the parent of custom directive. I'm looking for some kind of call-back mechanism.

It seems like there are several options but I haven't gotten any of them to work yet. I've considered using custom directives, ui-Router, and services (as per AngularJS: How can I pass variables between controllers?)

So far I've been trying to make the approach of custom directives work. I have a custom directive, <input-form> that is a form which when submitted should pass its input to another custom directive, <index-chart>. I have three controllers: one for the main app, NavigationController, one for input InputController that's tied to the directive <input-form>, and one for output OutputController that's tied to the <index-chart> directive.

I think the NavigationController should know how to extract the input data from the InputController and pass it to the OutputController. And the InputController and OutputController should remain agnostic so they can be reused.

I guess I've figured out everything except the flow control. The <input-form> contains the <form ... ng-submit, so even though I want it to remain agnostic, it is responsible for triggering the action that responds to the input being submitted by the user. Yet the code for that action should be in OutputController which InputController shouldn't know about.

How can I make NavigationController respond to the submit event contained within the custom directive <input-form> whose controller is InputController? And how can the NavigationController then extract data from the instance of the InputController and invoke the code contained in OutputController that should render the chart (i.e. renderChart() in the code below)?

The code below is also on Plunker: http://plnkr.co/edit/wm4suXMcSUE6obYFk3hp?p=preview index.html

<html ng-app="a3d">
    <div ng-controller="NavigationController as navCtrl">
        <input-form ng-show="navCtrl.shouldShowInputForm()"></input-form>
        <index-chart ng-show="navCtrl.shouldShowOutputChart()"></index-chart>
    </div>
</html>

a3j.js

(function(){
    var app = angular.module('a3d', ['input-form', 'index-chart']);

    app.controller('NavigationController', function(){
        this.inputMode = true;

        this.shouldShowInputForm = function(){
            return this.inputMode;
        };

        this.shouldShowOutputChart = function(){
            return !this.inputMode;
        };

        this.flipMode = function(){
            this.inputMode = !this.inputMode;
        }
    });
})();

input-form.html

<form name="inputForm" ng-controller="InputController as inputCtrl"
                          ng-submit="inputForm.$valid && ???" novalidate>
    <textarea name="topic1Data" ng-model="inputCtrl.inputValues.topic1Data" rows="10" cols="30" required></textarea>
    <button type="submit" class="btn btn-info btn-lg" ng-disabled="!inputForm.$valid">Compare</button>
</form>

inputForm.js

(function(){
    var app = angular.module('input-form', [ ]);

    app.directive('inputForm', function(){
        return {
            restrict: 'E',
            templateUrl: 'input-form.html',

        };
    });

    app.controller('InputController', ['$window', '$log', function($window, $log, appData){
    // ...
    }]);

index-chart.html

<!-- I haven't really gotten to this part yet -->
<div id="indexchart" style="min-width: 310px; max-width: 800px; height: 900px; margin: 0 auto"></div>

indexChart.js

(function(){
    var app = angular.module('index-chart', [ ]);

    app.directive('indexChart', function(){
        return {
            restrict: 'E',
            templateUrl: 'index-chart.html'
        };
    });

    app.controller('OutputController', ['$window', '$log', function($window, $log, appData){
        this.renderChart = function(){

            // This is where the chart should get rendered

        };
    }]);    
})();
Community
  • 1
  • 1
Michael Osofsky
  • 11,429
  • 16
  • 68
  • 113
  • Sorry, that's a lot to process. :-( Any chance of a Plunker or Fiddle? – camden_kid Apr 09 '15 at 16:29
  • I'm on to a solution but that's a good suggestion, thank you @camden_kid. I don't think the code I've written is that important, it's more of a question about how does AngularJS want us to do event propagation. – Michael Osofsky Apr 09 '15 at 17:03
  • 1
    I think the best way will be to use `ng-model` on the directives, and use a watcher `$scope.$watch('inputModel',fn...` on your form controller – Matias Fernandez Martinez Apr 09 '15 at 17:19
  • @camden_kid I did go ahead and put it in Plunker: http://plnkr.co/edit/wm4suXMcSUE6obYFk3hp?p=preview – Michael Osofsky Apr 09 '15 at 19:54
  • Thanks @Matho but can ng-model be used on the directives? The solution to http://stackoverflow.com/questions/14115701/angularjs-create-a-directive-that-uses-ng-model doesn't use ng-model directly; it uses isolate scope and defines an attribute `myDirectiveVar`. Yet I can't figure out how to apply this idea to my custom directive for input (the one I called `inputForm`). The isolate scope approach makes sense for the output directive (the one I called `indexChart`). – Michael Osofsky Apr 09 '15 at 23:20

1 Answers1

1

Here is a way to do it - Plunker.

a3j.js

app.controller('NavigationController', function(){
  var navCtrl = this;
  navCtrl.data = null;
});

index.html

<div ng-controller="NavigationController as navCtrl">
    <input-form data="navCtrl.data"></input-form>
    <index-chart data="navCtrl.data"></index-chart>
</div>

inputForm.js

inputForm.directive('inputForm', function() {
    return {
      restrict: 'E',
      templateUrl: 'input-form.html',
      scope: {data: "="},
      controllerAs: 'inputCtrl',
      bindToController: true,
      controller: function() {
        var inputCtrl = this;
        inputCtrl.inputValues = {topic1Data: 123456789};

        inputCtrl.emitData = function() {
          inputCtrl.data =  inputCtrl.inputValues.topic1Data;
        };
      }
    };
});

input-form.html

<form name="inputForm" ng-submit="inputForm.$valid && inputCtrl.emitData()" novalidate>
  <textarea name="topic1Data" ng-model="inputCtrl.inputValues.topic1Data" rows="10" cols="30" required></textarea>
  <button type="submit" class="btn btn-info btn-lg" ng-disabled="!inputForm.$valid">Compare</button>
</form>

indexChart.js

  indexChart.directive('indexChart', function() {
    return {
      restrict: 'E',
      templateUrl: 'index-chart.html',
      scope: {data: "="},
      controllerAs: 'chartCtrl',
      bindToController: true,
      controller: ['$scope', function($scope) {
        var chartCtrl = this;

        $scope.$watch('chartCtrl.data', function(newValue) {
          if (angular.isDefined(newValue)) {
            console.log(newValue);
          }
        });
      }]
    };
  });

index-chart.html

{{chartCtrl.data}}

The main points to note are:

  • Each directive has an isolate scope with a data property
  • NavigationController passes the same value to these directives
  • Any change in the value can be watched and acted upon
  • Each directive is self-contained without the need for a separate controller
  • Each directive acts independently of each other
camden_kid
  • 12,591
  • 11
  • 52
  • 88
  • Thank you @camden_kid! Your solution does indeed work :-) I did post a follow-up question as a new entry on StackOverflow: http://stackoverflow.com/questions/29566078/why-does-controlleras-in-javascript-work-but-not-ng-controller-as-in-h – Michael Osofsky Apr 10 '15 at 16:14
  • @MichaelOsofsky I could have made it work with ng-controller but I preferred to show you how it is *supposed* to be done. :-) – camden_kid Apr 10 '15 at 16:22