3

I would like to trigger an angular animation from a controller method.

I have come up with something that I am not satisfied with (see code below).

The issue is that for my animation to work, I need to track the state of a $scope variable i.e. $scope.shake:

$scope.signin = function (formCtrl) {
    $scope.shake = false;
    if ($scope.credentials) {
        signinService.signin($scope.credentials, function (status, memberRole) {
                $scope.shake = false;
                //TODO: necessary to check status?
                if (status === 200) {
                    var memberType;
                    if (memberRole === 'ROLE_BASIC_PARENTS') {
                        memberType = 'parents';
                    }
                    if (memberRole === 'ROLE_BASIC_CHILDCARE_WORKER') {
                        memberType = 'childcare-worker';
                    }
                    $rootScope.globals = {
                        memberType: memberType,
                        authenticated: 'OK'
                    }

                    $cookies.globalsMemberType = $rootScope.globals.memberType;
                    $cookies.globalsAuthenticated = $rootScope.globals.authenticated;

                    $state.go('dashboard', {memberType: memberType});
                }
            },
            function () {
                $scope.shake = true;
            });
    }
    else {
        $scope.shake = true;
    }
};

<form ng-class="{shake: shake}" name="formCtrl" ng-submit="signin(formCtrl)" novalidate>

Can someone please advise a cleaner solution?

edit 1:

Here is the css code as requested:

@-webkit-keyframes shake {
    0% {
        -webkit-transform: translateX(0);
        transform: translateX(0);
    }

    12.5% {
        -webkit-transform: translateX(-6px) rotateY(-5deg);
        transform: translateX(-6px) rotateY(-5deg);
    }

    37.5% {
        -webkit-transform: translateX(5px) rotateY(4deg);
        transform: translateX(5px) rotateY(4deg);
    }

    62.5% {
        -webkit-transform: translateX(-3px) rotateY(-2deg);
        transform: translateX(-3px) rotateY(-2deg);
    }

    87.5% {
        -webkit-transform: translateX(2px) rotateY(1deg);
        transform: translateX(2px) rotateY(1deg);
    }

    100% {
        -webkit-transform: translateX(0);
        transform: translateX(0);
    }
}

@keyframes shake {
    0% {
        -webkit-transform: translateX(0);
        transform: translateX(0);
    }

    12.5% {
        -webkit-transform: translateX(-6px) rotateY(-5deg);
        transform: translateX(-6px) rotateY(-5deg);
    }

    37.5% {
        -webkit-transform: translateX(5px) rotateY(4deg);
        transform: translateX(5px) rotateY(4deg);
    }

    62.5% {
        -webkit-transform: translateX(-3px) rotateY(-2deg);
        transform: translateX(-3px) rotateY(-2deg);
    }

    87.5% {
        -webkit-transform: translateX(2px) rotateY(1deg);
        transform: translateX(2px) rotateY(1deg);
    }

    100% {
        -webkit-transform: translateX(0);
        transform: translateX(0);
    }
}

.shake {
    -webkit-animation: shake 400ms ease-in-out;
    animation: shake 400ms ease-in-out;
}
balteo
  • 23,602
  • 63
  • 219
  • 412

3 Answers3

4

I'd suggest you wrap the form in a directive, and trigger the animation via an event. e.g. in the controller you would do:

$scope.$broadcast('FORM_ERROR');

and in the directive, do something more like

scope.$on('FORM_ERROR', function() { // code to trigger animation goes here });

Makes sense to handle this as an event for me, because then it has a strict lifecycle; event is dispatched, event gets handled. A scope variable would hang around, even though it no longer has any meaning. You might even need code to 'reset' it back to its original state so the animation can be triggered again, which adds unneeded complexity.

liamness
  • 742
  • 3
  • 5
  • but how would you turn it off? If you create a `FORM_VALID` event, what advantage does OP get over setting a scope variable? – Dave Alperovich May 10 '15 at 23:23
  • 1
    The code that triggers the animation should clean up after itself, you wouldn't need a separate event. For instance, you could create a callback for the `animationend` event and remove the class triggering the animation there. – liamness May 11 '15 at 00:39
  • But then what is the advantage of using an event. A callback would be simpler and do the trick just as well. – Dave Alperovich May 11 '15 at 15:05
  • Separation of concerns, mainly. The controller shouldn't care about the specifics of how the animation is triggered. The directive shouldn't care about why the error occurred. Keeping your DOM manipulation and business logic separate stops things getting confusing. The best way to do this in Angular is to use a directive, and the only way to communicate between a controller and a directive is an event! – liamness May 11 '15 at 15:46
  • Oh, I assumed OP was concerned with functionality. What makes you think OP is concerned with SoC? – Dave Alperovich May 11 '15 at 15:48
  • The askee came up with something that worked, but also expressed a dissatisfaction with with the solution, wanting something 'cleaner'. As such I feel a discussion of best practices is appropriate. – liamness May 11 '15 at 15:58
  • "The issue is that for my animation to work, I need to track the state of a $scope variable i.e. $scope.shake" With your solution, he would still have to keep track of state somehow to turn off the affect. You have separated the UI from Controller (which doesn't sound like good SoC, but you have not freed him from maintaining state. If not in a scope variable, then how? Unless I'm missing something, your comment section is where the real value would go. – Dave Alperovich May 11 '15 at 16:02
  • "the only way to communicate between a controller and a directive is an event" <--not entirely true! If the controller is specified as an option in the directive or if it is required, the controller gets passed as a 4th argument in the link option. – Jason Aug 29 '16 at 18:01
1

You will have another problem your animation isn't reset after the first fail, so consecutive fails won't execute the animation again. I'm afraid that there is no way to do this using pure CSS.

What you should do first is to use the validation services provided by Angular and then separate the logic of the error animation.

Here is the working solution, with the Prefix util factory working just fine.

  • First check form.$invalid inside your signin(formCtrl) function, if the form is $invalid or the response from the service returns a login error, then call a function which will handle the animation.

  $scope.signin = function(form){
    if(form.$invalid){
      $scope.animateError();
    }
  };

  • Change the CSS to only add the animation timing function and duration. So when the showAnimationError function is called it will take the form element, and add the style animationName/ webkitAnimationName with the value shake.

Also remember to add a event listener for the animation end, so you can clean this style, for consecutive calls on showAnimationError

/* css */
.shake {
    -webkit-animation-duration: 400ms;
    -webkit-animation-timing-function: ease-in-out;
    animation-duration: 400ms;
    animation-timing-function: ease-in-out;
}

$scope.animateError = function(){
  if(!resetAnimationHandler){
    addResetAnimationHandler();
  }
  myForm.style[prefixUtil.animationName] = 'shake';
};

I hope this helps you

0

Here is the solution I ended up using (see code below). It is an adaptation of Liamnes's proposed solution.

angular.module('signin')
    .controller('SigninCtrl', ['$scope', '$rootScope', '$cookies', '$state', '$animate', 'signinService', function ($scope, $rootScope, $cookies, $state, $animate, signinService) {

        var setPersonalInfo = function (param) {
            return signinService.setPersonalInfo(param.headers, $rootScope);
        };
        var goToDashboard = function (memberType) {
            $state.go('dashboard', {memberType: memberType});
        };
        var reportProblem = function () {
            $scope.formCtrl.username.$setValidity('username.wrong', false);
            $scope.formCtrl.password.$setValidity('password.wrong', false);
            $scope.$broadcast('SIGNIN_ERROR');
        };
        var resetForm = function(formCtrl){
            formCtrl.username.$setValidity('username.wrong', true);
            formCtrl.password.$setValidity('password.wrong', true);
        };

        $scope.signin = function (formCtrl) {

            resetForm(formCtrl);

            if (formCtrl.$valid) {
                signinService.signin($scope.credentials).then(setPersonalInfo).then(goToDashboard).catch(reportProblem);
            }
            else {
                $scope.$broadcast('SIGNIN_ERROR');
            }
        }
    }])
    .directive('shakeThat', ['$animate', function ($animate) {
        return {
            require: '^form',
            scope: {
                signin: '&'
            },
            link: function (scope, element, attrs, form) {
                scope.$on('SIGNIN_ERROR', function () {
                    $animate.addClass(element, 'shake').then(function () {
                        $animate.removeClass(element, 'shake');
                    });
                });
            }
        };
    }]);

HTML:

<form shake-that name="formCtrl" ng-submit="signin(formCtrl)" novalidate>
balteo
  • 23,602
  • 63
  • 219
  • 412
  • Thanks a lot to all. I like the accuracy of Matho's solution but I ended up adapting Liames 's solution. – balteo May 11 '15 at 17:27