One way to bind to object in angular

I would like to have a one-way (not one) binding between an attribute in a directive, but I am struggling with how to express this without attrs.$observe

. The best I can think of at the moment is to bind with &attr

and call the variables that I bind in my template, for example.{{attr()}}

app.controller('MainCtrl', function($scope) {
  $scope.names = ['Original'];
  setTimeout(function () {
    $scope.names.push('Asynchronously updated name');
    $scope.$apply();
  }, 1000);
});

app.directive('helloComponent', function () {
  return {
    scope: {
      'names': '&names'
    },
    template: '<li ng-repeat="name in names()">Hello {{name}}</li>'
  }
});

      

 <body ng-controller="MainCtrl">
    <ul>
      <hello-component names="names"/>
    </ul>
  </body>

      

Plunker

Is there a better way to do this that preserves one-way binding without having to refer to bound properties?

Edit

I have updated the example code to clarify that I want to bind to an object, not just a string. Therefore @attr

(which works with a string attribute) is not a solution.

+3


source to share


4 answers


"&"

actually the right thing. I oppose this approach (with @JoeEnzminger, here and here ) on the grounds that it is semantically questionable. But overall, Joe was right - this is a way to create a one-way binding to the actual vs. "@"

that binds to the string.

If you don't need a marquee, you can get the same effect using $parse

:

var parsedName = $parse(attrs.name);
$scope.nameFn = function(){
  return parsedName($scope);
}

      



and use it in your template like:

"<p>Hello {{nameFn()}}</p>"

      

+5


source


I haven't seen any mention of it in other answers, but as of Angular 1.5, one-way bindings for objects are supported ( see the section scope

in the $ compile docs for Angular 1.5.9
):

<

or <attr

- establish a one-way (one-way) binding between the local scope property and the expression passed through the attribute attr

. The expression is evaluated in the context of the parent scope. If no name is specified attr

, the attribute name is considered to be the same as the local name. You can also make the binding optional by adding ?

: <?

or <?attr

.

For example, with <my-component my-attr="parentModel">

and a definition directive scope: { localModel:'<myAttr' }

, the isolated real estate sphere localModel

will reflect the value parentModel

on the parent area. Any changes in parentModel

will be reflected in localModel

, but changes in localModel

will not be reflected in parentModel

. There are, however, two caveats:

  • one-way binding does not copy the value from the parent to the isolation area, it just sets the same value. This means that if your bound value is an object, changes to its properties in the isolated scope will be reflected in the parent scope (since both refer to the same object).
  • one way bindings look at the id of the parent value. This means that $watch

    the parent value is triggered only when the reference to the value changes. In most cases this should not be a concern, but it can be important to know if you are anchored to a one-way binding to an object, and then replace that object in the isolated area. If you now change a property on an object in your parent scope, the change will not propagate to the isolated scope because the object ID in the parent scope has not changed. Instead, you must assign a new object.

One-way binding is useful if you don't plan to propagate changes to your isolated scope bindings back to the parent. However, this does not make it completely impossible.

The example below uses one-way binding to propagate changes to an object within the scope of a controller in a directive.

Update



As @Suamere pointed out, you can indeed change the properties of a one way bound object; however, if the entire object is changed from the local model, then the link to the parent model will be broken, since the parent and local scope will refer to different objects. This is for two-way binding. The code snippet has been updated to highlight the differences.

angular.module('App', [])

.directive('counter', function() {
  return {
    templateUrl: 'counter.html',
    restrict: 'E',
    scope: {
      obj1: '<objOneWayBinding',
      obj2: '=objTwoWayBinding'
    },
    link: function(scope) {
      scope.increment1 = function() {
        scope.obj1.counter++;
      };
      scope.increment2 = function() {
        scope.obj2.counter++;
      };

      scope.reset1 = function() {
        scope.obj1 = {
          counter: 0,
          id: Math.floor(Math.random()*10000),
          descr: "One-way binding",
          creator: "Directive"
        };
      };

      scope.reset2 = function() {
        scope.obj2 = {
          counter: 0,
          id: Math.floor(Math.random()*10000),
          descr: "Two-way binding",
          creator: "Directive"
        };
      };
    }
  };
})

.controller('MyCtrl', ['$scope', function($scope) {
  $scope.increment = function() {
    $scope.obj1FromController.counter++;
    $scope.obj2FromController.counter++;
  };

  $scope.reset = function() {
    $scope.obj1FromController = {
      counter: 0,
      id: Math.floor(Math.random()*10000),
      descr: "One-way binding",
      creator: "Parent"
    };

    $scope.obj2FromController = {
      counter: 0,
      id: Math.floor(Math.random()*10000),
      descr: "Two-way binding",
      creator: "Parent"
    };
  };

  $scope.reset();
}])

;
      

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.js"></script>

<div ng-app="App">

  <script type="text/ng-template" id="counter.html">
    <h3>In Directive</h3>
    <pre>{{obj1 | json:0}}</pre>

    <button ng-click="increment1()">
      Increment obj1 from directive
    </button>

    <button ng-click="reset1()">
      Replace obj1 from directive (breaks binding)
    </button>

    <pre>{{obj2 | json:0}}</pre>

    <button ng-click="increment2()">
      Increment obj2 from directive
    </button>

    <button ng-click="reset2()">
      Replace obj2 from directive (maintains binding)
    </button>

  </script>

  <div ng-controller="MyCtrl">
    
    <counter obj-one-way-binding="obj1FromController"
             obj-two-way-binding="obj2FromController">
    </counter>
    
    <h3>In Parent</h3>

    <pre>{{obj1FromController | json:0}}</pre>
    <pre>{{obj2FromController | json:0}}</pre>

    <button ng-click="increment()">
      Increment from parent
    </button>
    
    <button ng-click="reset()">
      Replace from parent (maintains binding)
    </button>

  </div>
</div>
      

Run codeHide result


+1


source


Attribute execution literally passes a string. So instead of this:

<hello-component name="name"/>

      

You can do it:

<hello-component name="{{name}}"/>

      

0


source


This could be essentially the same approach suggested by New Dev, but I solved a similar problem for myself by removing the object from my selection area and creating a getter function for it called scope.$parent.$eval(attrs.myObj)

.

In a simplified version that looks more like yours, I changed:

app.directive('myDir', [function() {
    return {
        scope : {
            id : '@',
            otherScopeVar : '=',
            names : '='
        },
        template : '<li ng-repeat="name in names">{{name}}</li>'
    }
}]);

      

to

app.directive('myDir', [function() {
    return {
        scope : {
            id : '@',
            otherScopeVar : '='
        },
        template : '<li ng-repeat="name in getNames()">{{name}}</li>',
        link : function(scope, elem, attrs) {
            scope.getNames() {
                return scope.$parent.$eval(attrs.myList);
            };
        }
    }
}]);

      

This way, whenever a digest is done, your object is pulled, just like from the parent scope. For me, the advantage of this was that I was able to change the directive from two-way binding to one side (which made my performance unusable) without changing the views that used that directive.

EDIT Secondly, I'm not sure if this is exactly one-way binding, because updating the variable and running the digest will always use the updated object, there is no built-in way to trigger other logic when it changes, as there would be with $watch

.

-2


source







All Articles