angularjsangularjs-controlleras

controllerAs, bindToController and watches


I had this directive which has a controller. It looked like this:

.directive('pkSlider', ['$timeout', function ($timeout) {
    return {
        restrict: 'A',
        scope: {
            question: '=pkSlider',
            options: '=',
            onSelect: '&'
        },
        controller: 'PKSliderController',
        templateUrl: function (element, attrs) {
            if (attrs.type === 'image')
                return 'assets/templates/directives/pk-image.html';

            return 'assets/templates/directives/pk-slider.html';
        },
        link: function (scope, element, attrs, controller) {

            // Get our visible answers
            scope.answers = controller.getVisibleAnswers(scope.question.answers);

            // Increase the answers if they are less than the slides to show
            if (scope.answers.length <= scope.options.slidesToShow)
                scope.answers = scope.answers.concat(scope.answers);

            // Watch our answers
            scope.$watch('answers', function (answers) {

                // When we have our answers
                if (answers.length) {

                    // Extend our options
                    angular.extend(scope.options, {
                        event: {

                            // Set our first slide and sort/filter our products
                            init: function (event, slick) {

                                // Get the actual index stored in the answer
                                var index = scope.answers[slick.currentSlide].index;

                                // Set our current slide to the physical current slide, this is for the images to be shown
                                scope.currentSlide = slick.currentSlide;

                                // Invoke our methods using the answer index
                                controller.afterChange(scope.question, index);
                                scope.onSelect({ index: index, direction: slick.direction });
                            },

                            // Set our current slide and sort/filter our products
                            afterChange: function (event, slick, currentSlide) {

                                // Get the actual index stored in the answer
                                var index = scope.answers[currentSlide].index;
                                // Set our current slide to the physical current slide, this is for the images to be shown
                                scope.currentSlide = currentSlide;

                                // Invoke our methods using the answer index
                                controller.afterChange(scope.question, index);
                                scope.onSelect({ index: index, direction: slick.direction });
                            }
                        }
                    });

                    // We have loaded
                    scope.loaded = true;
                }
            });
        }
    };
}]);

and the controller looked like this:

.controller('PKSliderController', ['$timeout', '$interval', 'TrackingProvider', 'AnswerProvider', function ($timeout, $interval, tracking, provider) {
    var self = this,
        timer,
        setActiveImage = function (answers, activeIndex) {

            // If we have a current timer running, cancel it
            if (timer)
                $timeout.cancel(timer);

            // For each answer
            answers.forEach(function (answer, index) {

                // Get our images
                var images = answer.images;

                // If we have an image
                if (images) {

                    // Get our image
                    var image = images[0];

                    // If we are active
                    if (index === activeIndex) {

                        // For each text
                        image.imageText.forEach(function (text) {
                            text.active = false;

                            // Activate our text after the delay
                            $interval(function () {
                                text.active = true;
                            }, text.delay, 1);
                        });
                    }
                }
            });
        };

    // Get our answers
    self.getVisibleAnswers = provider.getVisible;

    // Updates the question with your selected answer
    self.afterChange = function (question, answerIndex) {

        // Get our answer
        var answers = question.answers,
            answer = answers[answerIndex];

        // Set our active image
        setActiveImage(answers, answerIndex);

        // This is for the last step, because some options might not actually be available
        if (answer) {

            // Set our selected answer
            question.radioChoice = answer.text;
        }
    };
}])

I have been converting my code according to the angular style guide and found a new option for directives that I didn't realise existed called bindToController. So I am trying to implement this. I have converted my directive to this:

(function () {
    'use strict';

    angular.module('widget.directives').directive('pkSlider', pkSlider);

    pkSlider.$inject = ['$timeout'];

    function pkSlider($timeout) {
        return {
            restrict: 'A',
            scope: {
                question: '=pkSlider',
                options: '=',
                onSelect: '&'
            },  
            controller: 'PKSliderController',
            controllerAs: 'controller',
            bindToController: true,
            templateUrl: getTemplate
        };

        function getTemplate(element, attrs) {
            if (attrs.type === 'image')
                return 'app/directives/pkImage.html';

            return 'app/directives/pkSlider.html';
        };
    }
})();

Which is already much cleaner. I have then tried to convert my controller:

(function () {
    'use strict';

    angular.module('widget.directives').controller('PKSliderController', PKSliderController);

    PKSliderController.$inject = ['$scope', '$timeout', '$interval', 'answerProvider'];

    function PKSliderController($scope, $timeout, $interval, answerProvider) {
        var self = this,
            timer;

        // Bindings
        self.afterChange = afterChange;
        $scope.$watch('controller.answers', watchAnswers);

        // Init
        init();

        function init() {

            // Get our answersw
            self.answers = answerProvider.getVisible(self.question.answers);

            // Increase the answers if they are less than the slides to show
            if (self.answers.length <= self.options.slidesToShow)
                self.answers = self.answers.concat(self.answers);
        };

        // Watches the answers for any changes
        function watchAnswers(answers) {

            // When we have our answers
            if (answers.length) {

                // Extend our options
                self.options = angular.merge(self.options, {
                    event: {

                        // Set our first slide and sort/filter our products
                        init: function (event, slick) {

                            // Get the actual index stored in the answer
                            var index = self.answers[slick.currentSlide].index;

                            // Set our current slide to the physical current slide, this is for the images to be shown
                            self.currentSlide = slick.currentSlide;

                            // Invoke our methods using the answer index
                            controller.afterChange(self.question, index);
                            self.onSelect({ index: index, direction: slick.direction });
                        },

                        // Set our current slide and sort/filter our products
                        afterChange: function (event, slick, currentSlide) {

                            // Get the actual index stored in the answer
                            var index = self.answers[currentSlide].index;
                            // Set our current slide to the physical current slide, this is for the images to be shown
                            self.currentSlide = currentSlide;

                            // Invoke our methods using the answer index
                            controller.afterChange(self.question, index);
                            self.onSelect({ index: index, direction: slick.direction });
                        }
                    }
                });

                // We have loaded
                self.loaded = true;
            }
        };

        // Updates the question with your selected answer
        function afterChange(question, answerIndex) {

            console.log('we are changing');

            // Get our answer
            var answers = question.answers,
                answer = answers[answerIndex];

            // Set our active image
            setActiveImage(answers, answerIndex);

            // This is for the last step, because some options might not actually be available
            if (answer) {

                // Set our selected answer
                question.radioChoice = answer.text;
            }
        };

        // Sets the active image
        function setActiveImage(answers, activeIndex) {

            // If we have a current timer running, cancel it
            if (timer)
                $timeout.cancel(timer);

            // For each answer
            answers.forEach(function (answer, index) {

                // Get our images
                var images = answer.images;

                // If we have an image
                if (images) {

                    // Get our image
                    var image = images[0];

                    // If we are active
                    if (index === activeIndex) {

                        // For each text
                        image.imageText.forEach(function (text) {
                            text.active = false;

                            // Activate our text after the delay
                            $interval(function () {
                                text.active = true;
                            }, text.delay, 1);
                        });
                    }
                }
            });
        };
    };
})();

But I am having issues with the watch. It doesn't appear to change the options. I have added the options to my view and it returns this:

{"slidesToShow":1,"centerPadding":0,"event":{}}

which is interesting, because I only ever pass this:

{ slidesToShow: 1, centerPadding: 0 }

So it is updating a bit, but not actually binding the methods to the event object. Does anyone know why or even how to get this to work?


Solution

  • It was my fault, it was creating everything correctly. I was not binding it to "controller" in the view.