11

Please let me know if you need more information or want me to clarify anything. I have tried a lot of different things to figure this out but haven't found a solution.

I'm relatively new to angularJS and I am trying to build an app with several layers of data. I have some basic user information stored in the scope of the body on controller PageController. I then have a settings form that loads in using $routeParams (with controller SettingsController) that includes a couple of custom directives for templating purposes. Since the directives are nested, I am using transclusion to load the second one inside of the first. This all seems to be working alright.

My problem is that I am trying to reference the field user.firstname from within the innermost directive and want to use two-way databinding to allow changes made to the textbox to cause the value at the PageController scope to change as well. I know that a lot of these kinds of problems are caused by using primitives in ng-model, but I have tried putting everything within an extra object so that I trigger prototypal inheritance to no avail. What am I doing wrong here?

Here's a JSFiddle of my code, stripped down as much as possible to isolate the problem. In this example, if I type in the outside textbox, which is directly on the PageController scope, it will modify the inner textbox until that textbox is modified, upon which the connection is broken. This seems just like the problem of using primitives as described in other questions, but I can't figure out where the issue is here.

HTML:

<body class="event-listing" ng-app="app" ng-controller="PageController">
    <div class="listing-event-wrap">
        <input type="text" ng-model="user.firstname" />
        <div ng-controller="SettingsController">
            <section block title="{{data.updateInfo.title}}" description="{{data.updateInfo.description}}">
                <div formrow label="{{data.updateInfo.labels.firstname}}" type="textInput" value="user.firstname"></div>
            </section>
        </div>
    </div>
</body>

Angular Directives:

app.directive('formrow', function() {
return {
    scope: {
            label: "@label",
            type: "@type",
            value: "=value" 
    },
    replace: true,
    template: '<div class="form-row">' + 
            '<div class="form-label" data-ng-show="label">{{label}}</div>' + 
            '<div class="form-entry" ng-switch on="type">' + 
                '<input type="text" ng-model="value" data-ng-switch-when="textInput" />' + 
            '</div>' + 
        '</div>'
}
});
app.directive('block', function() {
return {
    scope: {
            title: "@title",
            description: "@description" 
    },
    transclude: true,
    replace: true,
    template: '<div class="page-block">' +
            '<h2 data-ng-show="title">{{title}}</h2>' + 
            '<p class="form-description" data-ng-show="description">{{description}}</p>' + 
            '<div class="block-inside" data-ng-transclude></div>' + 
            '</div>'
}
});

Angular Controllers:

app.controller("PageController", function($scope) {
    $scope.user = {
        firstname: "John"
    };
});
app.controller("SettingsController", function($scope) {
    $scope.data = {
        updateInfo: {
            title: "Update Your Information",
            description: "A description here",
            labels: {
                firstname: "First Name"
            }
        }
    }
});
Dan
  • 59,490
  • 13
  • 101
  • 110
princjef
  • 135
  • 1
  • 1
  • 9

3 Answers3

11

I'm sorry for the previous code. Try this instead: http://jsfiddle.net/CxNc2/2/

Instead of passing the actual value, I'm now passing the object + a pointer to the correct value inside. I added 'refobject' here:

<body class="event-listing" ng-app="app" ng-controller="PageController">
    <div class="listing-event-wrap">
        <input type="text" ng-model="user.firstname" />
        <div ng-controller="SettingsController">
            <section block title="{{data.updateInfo.title}}" description="{{data.updateInfo.description}}">
                <div formrow label="{{data.updateInfo.labels.firstname}}" type="textInput" refobj='user' value="firstname"></div>
            </section>
        </div>
    </div>
</body>

and I added refobj + value here:

app.directive('formrow', function() {
    return {
        scope: {
            label: "@label",
            type: "@type",
            value: "@value",
            refobj: "="
        },
        replace: true,
        template: '<div class="form-row">' + 
            '<div class="form-label" data-ng-show="label">{{label}}</div>' + 
            '<div class="form-entry" ng-switch on="type">' + 
        '<input type="text" ng-model="refobj[value]" data-ng-switch-when="textInput" />' + 
            '</div>' + 
        '</div>'
    }
Nir
  • 473
  • 1
  • 5
  • 10
  • Thanks for such a quick response! I tried out the fiddle but it seems to do the same thing as the one I posted. What is the goal of changing it to a function isolate scope? – princjef Jun 19 '13 at 02:45
  • I added refobj so you dont need to call 'firstname' within the directive as I'm sure you want this to be generic. – Nir Jun 19 '13 at 02:59
  • That's really clever! Hadn't thought about using the array notation instead of the dot. This will work for my needs. Thanks! – princjef Jun 19 '13 at 11:14
  • 1
    I would give this 10 upvotes if I could. This saved my bacon. Thank you! – JasonOffutt Dec 20 '13 at 22:21
8

Since the textbox in the directive uses a primitive instead of an object for its model (ng-model="value" rather than ng-model="someobj.somevalue"), its model is created only on the local scope and the parent does not have access to it.

The fix is to define the directive textbox model using the dot rule as an object property:

ng-model="value.firstname"

Then pass the whole user object into the directive instead of just the primitive property:

<div formrow ... value="user"></div>

Here is a demo

Community
  • 1
  • 1
Dan
  • 59,490
  • 13
  • 101
  • 110
  • Hey, I've simplified your demo just to see things clearer. It appears it works with complex objects, but not with simple properties... [Here is updated version](http://jsfiddle.net/BXRnM/4/). Can you see why? – Dmitry Efimenko Jun 13 '14 at 08:49
  • @Dmitry Your example of the simple property reintroduces OP's error. To put it briefly, Angular scope inheritance _requires objects rather than simple primitive variables_. This is a direct consequence of prototypal inheritance in JavaScript. You could have a look at [[this answer](http://stackoverflow.com/questions/16928341/update-parent-scope-variable/16929117#16929117)] I gave elsewhere to see another example or check out [[this Q&A](http://stackoverflow.com/questions/14049480/what-are-the-nuances-of-scope-prototypal-prototypical-inheritance-in-angularjs)] for a more detailed explanation. – Dan Jul 08 '14 at 07:16
  • 1
    So apparently I was too optimistic thinking strings can be bound. But you are right, in this way binding in nested directives works flawlessly. Before, I was only getting one-way (initial) binding and was going nuts about getting it to work. +10 – Micros Dec 09 '15 at 13:47
0

The problem is caused by ng-switch, From the doc Understanding scope from git.

ng-switch scope inheritance works just like ng-include. So if you need 2-way data binding to a primitive in the parent scope, use $parent, or change the model to be an object and then bind to a property of that object. This will avoid child scope hiding/shadowing of parent scope properties.

so if you type some text in the textbox. below code will be executed for the ng-switch scope.

$scope.value="the text you typed"

So it will not consult the prototype chain to search value .this will created a new property for ng-switch scope.

How to testify it ?

If you change value to $parent.value. everything will work fine. because in the ng-switch for the primitive type (angularjs would recognize the value as primitive type if there is no dot )$parent will refer to formrow directive scope.

Try to remove the ng-switch or do as the doc says. the problem will disappear.

And more important, the document recommend us always use a dot . to refer the model when apply a bi-directional binding.

If I said something wrong . Please kindly correct me and make it right .thanks.

Joe.wang
  • 11,537
  • 25
  • 103
  • 180