4

I'm trying to create a directive with angularjs but I found a problem.

Here is the js code of the directive

angular.module('xxxFileUploader', [])
    .directive('xxxFileUploader', function() {
        return {
            restrict: 'E',
            templateUrl: 'fileUploader.html',
            scope: {
                onChange: '='
            },
            link: function(scope, element, attrs) {
                element.find('input[type="file"]').change(function() {
                    var input = this;
                    if (input.files && input.files[0]) {
                        scope.file = input.files[0];
                        scope.onChange(input.files[0]);
                });
            }
        };
    });

the directive template

<span style="position:relative;">
    <a class='btn btn-primary' href='javascript:;'>
        Choose File...
    </a>
    <input type="file" 
           name="{{field.name}}"
           style='position:absolute;z-index:2;top:0;left:0;filter: alpha(opacity=0);opacity:0;background-color:transparent;color:transparent;' 
           />
    &nbsp;
    <span class='label label-info'>{{file.name}}</span>
</span>

a piece of html file where I use the directive

<xxx-file-uploader on-change="loadFile(field.name)" ></xxx-file-uploader>

and the relevant piece of my controller

$scope.loadFile = function(fieldName) {
    return function(file) {
        // do things with both fieldName and file...
    };
};

This code should just customize the look of an input of type file and execute a callback when a file is uploaded.

My problem comes from the fact that at the moment I need to use a change callback that is built dynamically for every fileUploader. As you can see the callback is built with a parameter fieldName that is known at link-time and a parameter file that is known at "execution-time".

With this code I get the AngularJS “Error: 10 $digest() iterations reached. Aborting!” because (I think) the function is generated again and again and the digests don't match.

A poor-man solution would be to pass the fieldName as another scope variable. Another again would be maybe to pass the function as a string (@) and build the callback when I need it.

Any suggestion on how to make this directive work without changing its interface? I'm quite new to angularjs directives (this is my first one!) so if you see anything wrong in the code please point it out.

Thank you.

First edit:

I tried to change the scope of the directive as I've been suggested from

scope: {
    onChange: '='
},

to

scope: {
    onChange: '&'
},

and I changed the callback call from

scope.onChange(input.files[0]);

to

scope.onChange()(input.files[0]);

now it works but I'm not completely satisfied with the result.

It looks like a need to call the onChange function to get my "real" callback. Is there a way to have it executed implicitly in this case?

What I mean is that if I write

<xxx-file-uploader on-change="loadFile(field.name)" ></xxx-file-uploader>

it should understand to execute the function

if instead i write

<xxx-file-uploader on-change="doSomething" ></xxx-file-uploader>

it should recognize that doSomething is the "real" callback I want it to execute (with the file parameter).

2
  • where does field.name come from? Commented Nov 7, 2013 at 9:59
  • From my controller. This variable named field is just an object with a name {name : 'foo'}. Commented Nov 7, 2013 at 10:10

4 Answers 4

6

The way you are using onChange: '=' and the function is incorrect. The following solution DOES change the interface of the directive a bit, but I think renaming onChange to onChangeFactory reflects better on its intended usage. The name can remain onChange however if you do not mind the small inconsistency.


Pass loadFile() itself as:

<xxx-file-uploader on-change-factory="loadFile(fname)" ></xxx-file-uploader>

Modify the directive to use the function binding:

scope: {
    onChangeFactory: "&"
}

Call the factory and get the actual callback in link():

        link: function(scope, element, attrs) {
            // HOW ARE YOU GETTING FIELDNAME???
            var onChange = scope.onChangeFactory({fname: FIELDNAME});
            element.find('input[type="file"]').change(function() {
                var input = this;
                if (input.files && input.files[0]) {
                    scope.file = input.files[0];
                    onChange(input.files[0]); // <--- NOTE CHANGE HERE TOO
                }
            });
        }

Do pay attention on the way of calling onChangeFactory() from inside the directive! I guess there will be some gaps (e.g. the filename), but I believe this is a good starting point.

Sign up to request clarification or add additional context in comments.

1 Comment

The problem with this solution is indeed the fieldname inside of the directive. The directive should not know anything about the fieldname. It's something I use just in a case out of n.
3

You should use '&' in your scope param in your directive if you're passing in a reference to a function

scope: {
  onChange: '&'
},

1 Comment

I tried but the function I get is "function (a){return j(x,a)}" and not the function I'm trying to build. Do you have any idea?
0

You are almost there... the key part you're missing is $eval. Rather than calling scope.onChangeFactory({fname: FIELDNAME}); you can call: scope.$eval(scope.onChangeFactory). This means you don't need to know what parameter was passed in by the controller.

Here is some snippets of my own code that has a directive with some buttons, and I add a callback for when the buttons are clicked. Rather than using link I use controller in my directive to respond to the button clicks.

Part of the directive template with a button that can be clicked:

<button type='button' ng-click='incrementClick()'>

The directive code:

.directive('numberSpinner', function() {
  return {
    restrict: 'EA',
    scope: {
      onIncrement: '&'
    },
    controller: ['$scope', function($scope) {
      $scope.incrementClick = function() {
        // Evaluate the callback
        $scope.$eval($scope.onIncrement); 
      };
    }],
    templateUrl: 'number-spinner.tpl.html'
  };
})

The controller using the directive (see how it is passing in a model object item):

<number-spinner on-increment='myCallback(item)'></number-spinner>

And in the controller:

$scope.myCallback= function(item) {
  ...
};

Comments

0

I am still learning AngularJS myself, but this article https://stackoverflow.com/a/19890548/1959741 mentions using object map syntax. I can't find object map in the AngularJS API documentation, but by adding the {paramName: directiveVar} syntax to the callback event template, it worked for me.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.