AngularJS - ngClick, custom directive, and isolated scope issue

11,935

Solution 1

My suggestion is to look at what's going on with these spinners. Be a little more API focused.

Relevant part follows. We use a regular callback to indicate when we're done, so the spinner knows to reset the state of the button.

function SpinDemoCtrl($scope, $timeout, $q) {
  $scope.spinIt = false;

  $scope.longCycle = function(complete) {
    $timeout(function() {
      complete();
    }, 3000);
  };

  $scope.shortCycle = function(complete) {
    $timeout(function() {
      complete();
    }, 1000);
  };
}

app.directive('spinnerClick', function() {
  return {
    restrict: 'A',
    scope: {
      spinnerClick: "=",
    },
    link: function(scope, element, attrs) {
      var spinnerButton = angular.element("<button class='btn disabled'><i class='icon-refresh icon-spin'></i> Doing...</button>").hide();
      element.after(spinnerButton);

      element.click(function() {
        spinnerButton.show();
        element.hide();

        scope.spinnerClick(function() {
          spinnerButton.hide();
          element.show();
        });
      });
    }
  };
});

Here's one that expects use of $q. It'll work better with Angular-style asynchronous operations, and eliminates the callback functions by instead having the spinner reset on fulfilment of the promise.

Solution 2

Here is the polished version of the directive I ended up with (based on Yuki's suggestion), in case it helps someone: (CoffeeScript)

app.directive 'spinnerClick', ->
  restrict: 'A'
  link: (scope, element, attrs) ->
    originalHTML = element.html()
    spinnerHTML = "<i class='icon-refresh icon-spin'></i> #{attrs.spinnerText}"

    element.click ->
      return if element.is('.disabled')

      element.html(spinnerHTML).addClass('disabled')

      scope.$apply(attrs.spinnerClick).then ->
        element.html(originalHTML).removeClass('disabled')

Use it like so:

<button class="btn btn-primary" spinner-click="createNewTask()" 
                                spinner-text="Creating...">
  Create
</button>

Controller's code:

TasksNewCtrl = ($scope, $location, $q, Task) ->
  $scope.createNewTask = ->
    deferred = $q.defer()

    Task.save $scope.task, ->
      $location.path "/tasks"
    , (error) ->
      // Handle errors here and then:
      deferred.resolve()

    deferred.promise

Solution 3

Yes, it will call doIt in your isolated scope.

You can use $parent.doIt in that case

<button ng-click="$parent.doIt()" spinner="spinIt">Spin It</button>

Solution 4

From the AngularJS Documentation (http://docs.angularjs.org/guide/directive):

& or &attr - provides a way to execute an expression in the context of the parent scope. If no attr name is specified then the attribute name is assumed to be the same as the local name. Given and widget definition of scope: { localFn:'&myAttr' }, then isolate scope property localFn will point to a function wrapper for the count = count + value expression. Often it's desirable to pass data from the isolated scope via an expression and to the parent scope, this can be done by passing a map of local variable names and values into the expression wrapper fn. For example, if the expression is increment(amount) then we can specify the amount value by calling the localFn as localFn({amount: 22}).

so inlclude doIt: "&doIt" in your scope declaration, then you can use doIt as a function in your isolated scope.

Share:
11,935
Misha Moroshko
Author by

Misha Moroshko

I build products that make humans happier. Previously Front End engineer at Facebook. Now, reimagining live experiences at https://muso.live

Updated on June 15, 2022

Comments

  • Misha Moroshko
    Misha Moroshko about 2 years

    Consider the following directive: (Live Demo)

    app.directive('spinner', function() {
      return {
        restrict: 'A',
        scope: {
          spinner: '=',
          doIt: "&doIt"
        },
        link: function(scope, element, attrs) {
          var spinnerButton = angular.element("<button class='btn disabled'><i class='icon-refresh icon-spin'></i> Doing...</button>");
          element.after(spinnerButton);
    
          scope.$watch('spinner', function(showSpinner) {
            spinnerButton.toggle(showSpinner);
            element.toggle(!showSpinner);
          });
        }
      };
    }); 
    

    which is used like this:

    <button ng-click="doIt()" spinner="spinIt">Spin It</button>
    

    When spinner's value (i.e. the value of $scope.spinIt in this example) is true, the element should be hidden and spinnerButton should appear instead. When spinner's value is false, the element should be visible and spinnerButton should be hidden.

    The problem here is that doIt() is not in the isolated scope, thus not called on click.

    What would be the "Angular way" to implement this directive?