1

Below is some code to clear angular model values when the corresponding input to the model is hidden via ng-show, using classnames and jquery, but it has a bad smell bc it manipulates DOM in controller (Edit- it doesn't manipulate the DOM it changes the scope model values, but i am not crazy about using jquery) . Is there an "angular way" to do this?

I should add that the code below is just for a proof of concept to show that a solution is possible. The actual project has very complicated business rules to show sections, sub-section and sub-sections etc that have many logical branches... so it would be difficult to code that logic in the watch as @New Dev suggests... in addition, I would not want to have the logic in two places: both in all the divs that have show and hide AND in a function ...

    <!doctype html>
<html  xmlns:ng="http://angularjs.org" ng-app="app">
<head>
    <meta http-equiv="X-UA-Compatible" content="IE=Edge">  


</head>

<body ng-controller="MainCtrl">

    <div style="padding:20px; background-color:silver;color:blue">{{person | json }}</div>  

    Name: <input ng-model="person.name" name="name" >

    <div ng-show="person.name.length">

        Age: <input ng-model="person.age" name="age" class="hide-clear">

        <div ng-show="person.age.toString().length">
            Hobby: <input ng-model="person.hobby" name="hobby" class="hide-clear">
        </div>

    </div>

    <Script>

        angular.module('app', [])

        .controller('MainCtrl', function($scope,$log,$timeout){             

            $scope.person = {
                name: 'mr smith',
                age: 51,
                hobby: 'coding'                 
            }   

            $scope.$watchCollection(
                //return the value to be watched
                function($scope){ 
                    return $scope.person
                },
                //function to be called when changed
                function(newValue,oldValue){
                    $timeout( function() {  
                        $(".hide-clear").each(function(){
                            var t = $(this);                            
                            if( !  t.is(":visible") ) {
                                $scope.person[t.attr('name')] = '';
                            }
                        })

                    })
                }               
            )           
        })

    </Script>
</body>
</html>
gabriel
  • 326
  • 1
  • 2
  • 12

2 Answers2

4

I'm glad that you recognized the approach above as a poor design (or "bad smell", as you put it). Indeed, an Angular way (or more generally, an MVVM way) would be to only manipulate the View Model, and let the View Model drive the View.

For example, you are attempting to set $scope.person.age = "" and $scope.person.hobby = "" when their parent container is hidden with ng-show="person.name.length" (i.e. when $scope.person.name is empty). Instead of using the resulting invisibility of the container as an indicator, use the original data that caused the container to be invisible in the first place.

$scope.$watch("person.name", function(val){
  if (val === "") { // or, if (!val.length), to make it completely equivalent
    $scope.person.age = "";
    $scope.person.hobby = "";
  }
});

The code above watches for $scope.person.name to be empty (and/or undefined, whatever your definition is) to set the other properties. It doesn't matter at all to the controller how the View reacted to empty person.name at all - it could have done some animation or other UI tricks. The logic only deals with the View Model state.

The code above could be further improved to avoid a $watch and instead react to the event that caused $scope.person.name to become empty. From your example it appears to be only caused by the user deleting the name from the textbox.

<input ng-model="person.name" ng-change="onPersonChanged()">
$scope.onPersonChanged = function(){
   if (!$scope.person.name) {
      $scope.person.age = "";
      $scope.person.hobby = "";
   }
};

This is preferable to $watch since $watch fires on every digest cycle, whereas ng-change fires only when there is a change to the input field.

New Dev
  • 48,427
  • 12
  • 87
  • 129
0

Below is my best attempt. I still use jquery to detect if an element is visible, and the directive does not use an isolated scope but at least all the logic is contained in two directives which enables re-use in other projects:

The directive code (clearmModelWhenHidden.js)

angular.module('clearModelWhenHidden', [])

.directive('clearModelWhenHiddenContainer', function() {

    return {
      scope: false,
      controller: function($scope, $parse, $timeout) {

        $scope.registeredElements = [];

        //since we dont' have an isolate scope, namespace our public API to avoid collision
        this.clearModelWhenHidden = {};

        //to share a method with child directives use the "this" scope and have children require the parent controller... 
        this.clearModelWhenHidden.register = function(e) {
          $scope.registeredElements.push(e);

        }

        $scope.$watchCollection(

          function() {
            //convert the registered elements ng-model attribute from a string to an angular
            //object that can be watched for changes
            var parsedArray = [];
            angular.forEach($scope.registeredElements, function(item, i) {
              parsedArray.push($parse(item.attributes.ngModel)($scope))
            });
            return parsedArray;
          },
          function(newvalue) {

            $timeout(function() {
              angular.forEach($scope.registeredElements, function(item, i) {

                var isVisible = $(item.element).is(':visible');

                if (!isVisible) {

                  var value = $parse(item.attributes.ngModel)($scope);

                  //create a string that sets the ng-model of each element to an empty string,
                  //for example, person.name=''
                  var stringToEval = item.attributes.ngModel + '=""  ';

                  console.log(stringToEval)

                  $parse(stringToEval)($scope);
                }
              })
            });
          }
        );
      }
    }
  })
  .directive('clearModelWhenHidden', function() {
    var link = function(scope, element, attributes, parentController) {
      //pass in the element itself so we can used jquery to detect visibility and the attributes so the container can create a watch on the models
      parentController.clearModelWhenHidden.register({
        'element': element[0],
        'attributes': attributes
      });
    }
    return {
      link: link,
      require: '^clearModelWhenHiddenContainer'
    }
  })

and a demo page

<!doctype html>
<html xmlns:ng="http://angularjs.org" ng-app="app">

<head>
  <meta http-equiv="X-UA-Compatible" content="IE=Edge">

  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0-alpha1/jquery.min.js" type="text/javascript"></script>
  <script language="javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.3/angular.js"></script>
  <script language="javascript" src="clearModelWhenHidden.js"></script>
</head>

<body ng-controller="MainCtrl as MainCtrl">

  <div style="padding:20px; background-color:silver;color:blue">{{MainCtrl.person | json }}</div>

  <div clear-model-when-hidden-container>

    <section>

      Name:
      <input ng-model="MainCtrl.person.name" clear-model-when-hidden>

      <div ng-show="MainCtrl.person.name.length">
        <label>Age</label>:
        <input ng-model="MainCtrl.person.age" clear-model-when-hidden>

        <section ng-if="MainCtrl.person.age.toString().length">
          <label>Hobby</label>:
          <input ng-model="MainCtrl.person.hobby" clear-model-when-hidden>
        </section>
      </div>
    </section>
  </div>

</body>

</html>
gabriel
  • 326
  • 1
  • 2
  • 12