0

I asked this question on the Programmer's stack exchange, but didn't get any replies, so I thought I'd try my luck here...

I am working on a project where I would like to encapsulate a directive library and distribute it to other developers to use. I would like to keep the changes to the model within this encapsulated code, so I don't really want the dev's changing the scope variables outside of the lib.

In my code, I have 2 different approaches to communicating with my lib from the parent controller.

The first theory is to create a lib that contains a directive and a service. The parent controller would call the service, which would handle all of the changes to the lib model and the directive would react depending on these changes.

The second theory is to put all the functions to change the model in the directive itself and call the directive scope on the parent to make the changes.

Here is a plunker that shows what I'm asking in better detail. It's a simple example, but illustrates the 2 different methods.

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

I'm leaning towards the second method as it just seems cleaner to implement from a development scenario.

Any advice from the Angular experts out there?

Plunker Html:

<body ng-app="myApp">
    This is Example 1 - Using a service to modify directive
    <div ng-controller="Example1Ctrl">
        <example1-directive ng-model='example1Model'></example1-directive>
        <br>
        <br>
        <input type="button" value="Change Example 1" ng-click='changeExample1()' />
    </div>
    <br>
    <br>
    This is Example 2 - Modifying directive in the scope of the directive
    <div ng-controller="Example2Ctrl">
        <example2-directive ng-model='example2Model'></example2-directive>
        <br>
        <br>
        <input type="button" value="Change Example 2" ng-click='changeExample2()' />
    </div>
</body>

Plunker js

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

//-------------------------------------------------- 
//-------- This is example 1
//-------------------------------------------------- 
app.controller("Example1Ctrl", function($scope, example1Svc) {
  $scope.example1Model = {
    value: "Example 1 - Original Value"
  }

  $scope.changeExample1 = function() {
    example1Svc.change($scope.example1Model, "Example 1 - Changed Value");
  }
});

/// This part would be encapsulated in a lib 
app.directive("example1Directive", function() {
  return {
    restrict: "E",
    scope: {
      model: "=ngModel"
    },
    template: "{{model.value}}"
  }
});

app.service("example1Svc", function() {
  this.change = function(example1Model, newValue) {
    example1Model.value = newValue;
  }
})
// End lib

//-------------------------------------------------- 
//-------- This is example 2
//-------------------------------------------------- 
app.controller("Example2Ctrl", function($scope, example1Svc) {
  $scope.example2Model = {
    value: "Example 2 - Original Value"
  }

  $scope.changeExample2 = function() {
    $scope.example2Model.change("Example 2 - Changed Value");
  }
});

/// This part would be encapsulated in a lib 
app.directive("example2Directive", function() {
  return {
    restrict: "E",
    scope: {
      model: "=ngModel"
    },
    template: "{{model.value}}",
    controller: function ($scope) {
      $scope.model.change = function(newValue) {
        $scope.model.value = newValue;
      }
    }
  }
});
// end lib
Scottie
  • 11,050
  • 19
  • 68
  • 109
  • I don't understand - are you trying to call a function on a directive from the controller? – New Dev Sep 24 '14 at 19:01
  • In a way. I'm trying to call a function that will modify the look of the directive. The real world directive I'm implementing is a pagable data list view and I'd like to be able to call functions like refresh(), firstPage(), lastPage(), etc. The question is, do I put those in a service and pass in the parent scope model, or do I put the functions on the parent scope model and call them without needing to pass in the model (because it's already on the model). – Scottie Sep 24 '14 at 19:37

2 Answers2

2

I'm somewhat confused by your example #1. What does exampleSvc.change do?

Example #2 definitely goes against MVVM best practice, as it couples the controller with the view. Controllers (of views) should be view-agnostic. They should only change the ViewModel to reflect the current state of the app. The View would then react (however the View chooses to) to changes in the ViewModel.

In particular, these lines "offend" the best practice in my mind:

$scope.model.change = function(newValue) {
   $scope.model.value = newValue;
}

Now your controller relies on the view to define what the function does (or whether it is defined to begin with). Also, what if another directive decides to change the .change function?

EDIT: Take a look at this SO question, and in particular an answer by Mark.

EDIT #2: There is an interesting case for when some event needs to reach whichever directives or child controllers that could be interested. Use $scope.$broadcast (in controller) and $scope.$on (in directive) to handle. Here's a plunker

Community
  • 1
  • 1
New Dev
  • 48,427
  • 12
  • 87
  • 129
  • After reading that link, it seems to be exactly what they are doing. Adding a function to the controller of the directive to handle changes to the model data of the directive's scope. I'm not entirely sure how I'm coupling the controller with the view. At some point the view has to interact with the controller, which I did in an ng-click. The controller then called the scope of the directive to modify the model data, which changed the view. I'm not sure how else to do this. I don't see a difference in doing $scope.pageNumber = 1 and $scope.setPageNumber(1). Am I missing something? – Scottie Sep 24 '14 at 21:00
  • Sorry, I meant they are adding a function to the scope in the controller of the directive. – Scottie Sep 24 '14 at 21:01
  • directives communicate with the parent scope via local scope properties bound to attributes. For functions, [&attr](https://docs.angularjs.org/api/ng/service/$compile#-scope-) is used. In your case, you are going the other way. Values in the View Model "drive" the behavior of the view (in addition to user input) – New Dev Sep 24 '14 at 21:17
  • To answer your other question, there are a few ways how your controller is coupled with the view. 1) Controller expects example1Model.change function to be defined by the View (controller is responsible for defining the ViewModel). 2) Controller expects that the change to the model value could only happen by some directives (i.e. those that know that they need to call the .change function) – New Dev Sep 24 '14 at 21:36
  • @Scottie, I added a modified version of your plunker to illustrate how a controller could cause a directive to react to some event (e.g. refresh) – New Dev Sep 24 '14 at 22:47
  • Thank you for the explanations. That makes more sense. Angular is definitely a different way to think about things. – Scottie Sep 24 '14 at 23:46
  • Sure. Did that answer your question? – New Dev Sep 25 '14 at 00:47
  • Yes, I believe it did. Thanks again. :) – Scottie Sep 25 '14 at 13:56
1

I'm going to agree with @New Dev and add a couple more thoughts. If you're building a directive library you don't want to also bundle controllers that the consumer of the library has to use. Your directives should be more or less self contained and provide enough of an api to be extensible and potentially used in other directives.

What does this mean? Your directives may want to define a controller so that they can be injected into other directives. E.g.

//your library
directiveModule.directive("example1Directive", function() {
  return {
    controller: function($scope, $element, $attrs) {
     ...
  },
    ...
  }
});

-

//application
app.directive("appDirective", function() {
  return {
    require: '?example1Directive',
    link: function(scope, element, attrs, example1Directive) {
     ...
  }
});

You may also want to specify various options that can be set on your directive with parameters e.g.

<div example1-directive="{opt1: 'val', opt2: scopeProp}"></div>

Your directive would then need to parse the the attribute and execute on the scope to generate the options. There's lots more you can do, I'd suggest taking a look at ngmodules.org and see what other people are doing.

Vadim
  • 17,897
  • 4
  • 38
  • 62