1

Using bootstrap with AngularJS components does not work if one needs to encapsulate inner Bootstrap elements in components, because of the extra markup of the component itself added to the DOM breaks CSS rules with direct child operator >. For example implementing a DropDown one needs to create a full component with the DropDown and should generate every dropdown option inside this full component with ng-repeat reading data from a configuration array. Something like this:

<my-bootstrap-drop-down 
    my-label="Some label" 
    my-options="[ { label: 'Option1 },  {label: 'Option2'} ]" >
</my-bootstrap-drop-down> 

From Dan Wahlin's "Creating Custom AngularJS Directives" to be able to pass a function with variable number of arguments to an AngularJS component you need a special syntax where you pass a function reference to an attribute of the element tag like this:

<my-component 
    my-action="myMethod(p1, p2)"
    my-params="{p1:1, p2:25}">
</my-componenet>

And then in the component you call the function with this code:

<a ng-click="$ctrl.myAction($ctrl.myParams)"></a>

This syntax only works right when used in element attributes mapped with the & operator as bindings of a component / directive. Even when my-action="myMethod(p1, p2) seems a function call it is in fact a passing by reference. Unfortunately if you want to use ng-repeat to generate some code inside the component like explained above, there is no way to make that syntax to work, since the myThethod(p1, p2) syntax only work in an attribute.

So how can you implement a component having an array of inner elements generated with ng-repeat and those elements having function calls with variable number of arguments, since the later syntax does not work?

<my-bootstrap-drop-down 
    my-label="Some label" 
    my-options="[ 
        { label: 'Option1', action: myMethod(p1, p2),  params: {p1:1, p2:25}},  
        ...
    ]" >
</my-bootstrap-drop-down> 

When trying to do this code, the myMethod(p1, p2) is executed when creating the component, since it is in fact a function call, not a pass by reference.

Note: In the same article referenced above it is suggested another syntax for invoking functions. The syntax asumed that the component knows how many arguments to pass, which is not the case. It could be used anyway pasing the arguments as an array and invoking the function with apply, but apply is not allowed in angular expressions.

I have added a Plunker to make it clear:

https://plnkr.co/edit/dkofEYhebp0T6lSf22RP?p=preview

1 Answer 1

1

Edit: Ok, not sure why you need this but I got it to work: https://plnkr.co/edit/uR9s5vUJxQoviTiUD2vj?p=preview

And the same but using a Directive: https://plnkr.co/edit/Onh2WonmarpUscnFFLGK?p=preview

End of Edit

You should pass a variable to "my-options" (let's call it 'dropDownOptions'):

<my-bootstrap-drop-down 
    my-label="Some label" 
    my-options="dropDownOptions" >
</my-bootstrap-drop-down> 

And the dropDownOptions array should contain the data you need in the directive, but only the data, not a function: [{"label": "Option 1", "params": {"p1": 1, "p2": 25}}, ...]

Now inside your directive, you have access to the data and can work on the action/function part. Example:

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


testApp.controller('mainCtrl', ['$scope',
    function ($scope) {

        $scope.test = "Hi";
        $scope.dropDownOptions = [{"name": "yes", "value": 2}, {"name": "no", "value": 25}];

    }]);


testApp.directive('myBootstrapDropDown', function () {
    return {
        restrict: 'E',
        scope: {
            myLabel: '@',
            myOptions: '='
        },
        controller: function ($scope) {

            $scope.myMethod = function (val) {
                alert("There was a change, new value: " + val);
            };
        },
        template: '<label>{{myLabel}}</label> <select name="myLabel" ng-model="myValue" ng-options="opt.value as opt.name for opt in myOptions" ng-change="myMethod(myValue)"><option value=""> </option></select>'
    };
});
<!DOCTYPE html>
<html lang="en" ng-app="testApp">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta charset="utf-8">
    <title>Sample</title>

    <style>
        .starter-template {
          padding: 10px 15px;
          text-align: center;
        }
        a {
            font-size: 11px;
            cursor: pointer;
        }
    </style>

</head>
<body>
    <div ng-controller="mainCtrl">

        <div class="container">

          <div class="starter-template">
            <h1>Example</h1>
            <p class="lead">{{test}}</p>
            <my-bootstrap-drop-down 
                my-label="Some label" 
                my-options="dropDownOptions" >
            </my-bootstrap-drop-down> 
          </div>

        </div>
    </div>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
    <script src="app.js"></script>
</body>
</html>
Notice that the label is bound with "@" and the array with "=".

So you don't need to bind the function with your directive, unless the directive needs to trigger something back in the controller, in which case you should put the function in a separate attribute (which, in this case, would be bound with the & as you mentioned).

For example:

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


testApp.controller('mainCtrl', ['$scope',
    function ($scope) {

        $scope.test = "Hi";
        $scope.dropDownOptions = [{"name": "yes", "value": 2}, {"name": "no", "value": 25}];
        $scope.runThis = function (val) {
            //Do Something here
            alert("There was a change, new value: " + val);
        };

    }]);


testApp.directive('myBootstrapDropDown', function () {
    return {
        restrict: 'E',
        scope: {
            myLabel: '@',
            myOptions: '=',
            myFunction: "&"
        },
        controller: function ($scope) {

            $scope.myMethod = function (val) {
                $scope.myFunction()(val);
            };
        },
        template: '<label>{{myLabel}}</label> <select name="myLabel" ng-model="myValue" ng-options="opt.value as opt.name for opt in myOptions" ng-change="myMethod(myValue)"><option value=""> </option></select>'
    };
});
    <!DOCTYPE html>
    <html lang="en" ng-app="testApp">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <meta charset="utf-8">
        <title>Sample</title>

        <style>
            .starter-template {
              padding: 10px 15px;
              text-align: center;
            }
            a {
                font-size: 11px;
                cursor: pointer;
            }
        </style>

    </head>
    <body>
        <div ng-controller="mainCtrl">

            <div class="container">

              <div class="starter-template">
                <h1>Example</h1>
                <p class="lead">{{test}}</p>
                <my-bootstrap-drop-down 
                    my-label="Some label" 
                    my-options="dropDownOptions"
                    my-function="runThis" >
                </my-bootstrap-drop-down> 
              </div>

            </div>
        </div>
        <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
        <script src="app.js"></script>
    </body>
    </html>

Notice the "()(val)" int the directive's controller. If you don't need to pass any value back to the original controller, just replace that by "()()".

I hope this helps, if you're still stuck you should share more of your code (your directive's code & html for example) so we can answer better.

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

4 Comments

I have added a plunker to the question. I hope it makes it more clear.
Edited the answer with a plunker that works. Yeah sorry I also used "underscore.js", just because I'm lazy.
Yes, you have used the apply() way. In that case it will be interesting to add to the function reference and the function params, the function context (this) as a param to the directive/controller to make sure the function is called with the right context. Also the function reference should need to be also in the my-options object, since they may not share that, but is an easy add-on to your code. I was just hoping there was a cleaner approach. As to why did I need this is just to encapsulate the markup of common controls with unknown behavior, just as in the example.
Yes. If you are going to call a function dynamically and pass a dynamic number of parameters, I'm not sure how one would do without apply(). It worked without apply() in your second button because it was explicitly pointing to action2 and had always 2 parameters...

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.