Shouldn't there be an AngularJS ngWith directi

2019-02-16 07:44发布

问题:

Maybe I'm crazy, or too used to KnockoutJS, but I keep looking for an ngWith directive in the docs to define the scope on an element, in a controller, or for an included (ngInclude) partial.

For example:

I'd like to write a controller that augments MyItem like:

MyModule.controller('MyItemCtrl', function($scope) {
    $scope.doSomethingToItem = function() {
        $scope.name = "bar";
    };
});

Or a view/template for MyItem like:

<div ng-controller="MyItemCtrl">
    {{name}}
    <button ng-click="doSomethingWithItem()">Do Something</button>
</div>

But in both of these cases I'm imagining my $scope to be prototypically inherit from my model, MyItem.

But the scope doesn't inherit from the model!!

Which baffles me.

Instead, my model is a property on the scope.

In the case of a repeater:

<div ng-repeat="item in list">
    <div ng-controller="MyItemCtrl">
        {{item.name}}
        <button ng-click="doSomethingWithItem()">Do Something</button>
    </div>
</div>

which means everywhere I have to use item.this or item.that instead of just this and that. I have to remember which functions are native to the model, and which were applied directly to the scope by a controller.

If I want to have a partial to display names (stupid example, I know, but you get the idea):

<h3>{{name}}</h3>

I have to write it

<h3>{{item.name}}</h3>

and then ensure the model is always item. Usually by wrapping it in a directive simply to defines a scope with an item property.

What I often feel like I want to do is simply:

<div ng-include="'my/partial.html'" ng-with="item"></div>

or

<div ng-repeat="list" ng-controller="MyItemCtrl">            
    {{name}}
    <button ng-click="doSomethingWithItem()">Do Something</button>
</div>

Is there some magical directive out there that I haven't found? Or am I completely wrong and just looking for trouble?

Thanks.

EDIT:

Many thanks to Brandon Tilley for explaining the dangers of using scopes as models. But I still often find the need for some quick declarative scope manipulation and wish for an ng-with directive.

Take, for example, you have a list of items which, when clicked, shows an expanded view of the selected item. You might write it something like:

<ul>
    <li ng-repeat="item in items" ng-click="selection = item">{{item.minView}}</li>
</ul>
<div ng-controller="ItemController">
    {{selection.maxView}}
</div>

now you have to get properties of the selected item using selection.property rather than what I'd want: item.property. I'd also have to use selection in ItemController! Making it wholly coupled with this view.

I know, in this simple example I could have a wrapping controller to make it work, but it illustrates the point.

I've written a very basic with directive:

myApp.directive('with', ['$parse', '$log', function(parse, log) {

    return {
        scope: true,
        link: function(scope, el, attr) {
            var expression = attr.with;
            var parts = expression.split(' as ');

            if(parts.length != 2) {
                log.error("`with` directive expects expression in the form `String as String`");
                return;
            }

            scope.$watch(parts[0], function(value) {
                scope[parts[1]] = value;
            }, true);
        }
    }

}]);

that simply creates a new scope parsing one expression onto another value, allowing:

<ul>
    <li ng-repeat="item in items" ng-click="selection = item">{{item.minView}}</li>
</ul>
<div with="selection as item" ng-controller="ItemController">
    {{item.maxView}}
</div>

This seems infinitely useful to me.

Am I missing something here? Just making trouble for myself down the line somehow?

回答1:

This is a great question. I can see how this may be confusing coming from another front-end framework, but in Angular, the scope has a reference to the model, and the syntax you're describing is normal. I personally like to describe the scope as more like a view model.

Miško Hevery, the author of AngularJS, does a good job of explaining this concept in this video, starting at about the 30 minute mark and lasting about 3 minutes:

People oftentimes think that the scope is the model, and that's not the case. Scope has references to the model. [...] So in the view, you say model dot whatever property you want to access.

While it may be possible to write an ngWith directive that does kind-of what you're looking for, since Angular uses prototypal inheritance for scopes, you will likely run into the same issues that Miško describes in the aforementioned video at 31:10 (where you think you're updating a value on the parent scope but you're actually not). For more details on prototypal inheritance in AngularJS, check out the excellent article The Nuances of Scope Prototypal Inheritance on the AngularJS wiki.



回答2:

I've found I can just put an array around the source of ng-repeat and it becomes functionally an ng-with. :)

<div ng-repeat="name in [vm.dto.Person.Name]" >
  <input type="text" ng-model="name.First" />
  <input type="text" ng-model="name.Last" />
</div> 

Seems like some purests may not like this... Also doesn't seem like there is a good alternative.



回答3:

Another approach would be to set a new variable via ng-init:

<div ng-init="name = vm.dto.Person.Name" >
  <input type="text" ng-model="name.First" />
  <input type="text" ng-model="name.Last" />
</div>


回答4:

I think one side effect of this when coming from knockout is that is encourages you to create more components with less responsibility, and of course within a component the 'this' is the viewmodel for that component. If you get to a point where it is really annoying you then it might be a sign you need a new component.

And it sure is nice being able to just refer to something in the component view model without thinking about the scope you're in. And you can't tell me you miss $parents[2] and $root ;-)

PS. Yes I know you can use let in knockout, which means fewer $ stuff.

I definitely miss knockout when doing Angular stuff (I still use both) but it sure is nice to do certain things things like title="Customer #{{ row.customerId }} - {{ row.fullName }}" and not have everything under data-bind.