Angular.js: Can't update DOM-node attr while using {{}} expression

Created on 10 Sep 2015  Â·  9Comments  Â·  Source: angular/angular.js

There is a kind of race condition between dom-node attr internal recalculation when we are using {{}}-expression and dom-node attr external modifying. This topic is based on https://github.com/angular/angular.js/issues/12763 issue and also related to https://github.com/angular/angular.js/issues/9109 issue. Also I'd like to thank @gkalpak for he clarified a problem.


Here is a little repro on this issue: http://jsfiddle.net/dhilt/8tceyw2w/9/

I have an element with style-attr contains {{}}-expression:

<div id="myElement" style="padding-left: 15px; color:{{toggle ? 'red' : 'black' }}">

and I want to play with some style property which isn't presented in the markup:

document.getElementById('myElement').style.paddingTop = '20px'; 

Let's run the demo!

1. Initial state. Before 1st digest ends.

Run the demo. Get the alert before out from $watch.
We see that both of padding-left and padding-top properties are set to 15 and 20 px.
And all of them are applied properly; we can see it despite the UI data is still unbound cause of alert-interruption.

2. Initial state. The end of initial digest.

Close the alert. Let the 1st digest cycle end.
See that padding-top is no longer applied to the element.
Something is broken and initial template's value of style-attr has a destructive priority over further updates on it.

3. Recalculate style expression via $scope.toggle changing.

Click on checkbox, start the new digest cycle.
We see that there is no problem with inline padding-left property.
But padding-top property is completely removed from element's style. Just an empty string.
And myElementStyle.paddingTop = '20px' within $watch doesn't work.


.
Thanks and good luck)

$compile low confusing

Most helpful comment

I ran into this in Angular v1.5.5 as well. I just fixed it like this...

<div class="static-class" ng-class="{ dynamic-class1: conditional1, dynamic-class2: conditional2}">

to

<div ng-class="{ static-class: true, dynamic-class1: conditional1, dynamic-class2: conditional2}">

or if your class name contains a variable

<div ng-class="{ static-class-{{variable}}: true, dynamic-class1: conditional1, dynamic-class2: conditional2}">

All 9 comments

You shouldn't be doing any DOM manipulation in a controller only time a DOM access/mutation should occur is in a directive. If you use the ng-style directive you can avoid the destructive behavior even leaving the DOM manipulation in the controller but I would strong strongly advise against it http://jsfiddle.net/8tceyw2w/10/

@shusain, the point is that trying to externally change an attribute's interpolated value is broken. (Where that happens from (e.g. directive, controller etc) is not really important here.)

So, what you say is true in general, but this is just a POC of an actual bug.

I see what you're saying but following certain rules even when just reproducing an issue seems reasonable to do anyhow. (seems you both know what you're talking about but less knowledgeable devs may see this and think it's the right way to do things)

I can see how this would happen since when the interpolation happens to process the style attribute it's not aware of the styles set on the element directly and so it ignores those but the padding-left is still part of the attribute value that it'll apply after parsing/processing the interpolated value. I suppose the behavior you're expecting is that the style interpolation doesn't remove any existing styles that the interpolated value itself doesn't replace but this seems like it will be a confusing solution itself.

I'd also say that this can be expected, given how angular works.
Question is, where can we document it so that people stumble upon an explanation?
Otherwise, I think we need a PR that implements some sort of logic that special cases existing attributes to see if it works or if it's worth it.

(Moved this comment from the closed #9109 issue).

This issue is not fixed untill latest 1.4.7 version.

I'm using Angular 1.4.7 (also tested in 1.4.5 and 1.4.6) — I have the same issues as described in this topic — if I have 2 or more {{...}} in class attribute then ng-class and ng-show|hide directives stop working.

(In fact, they are not working only in first digest cycle — after the scope is updated second time (after some action which updates the scope, for example), everything works as expected :)

So, here ng-class does not work:
<div class="button button_{{name}} button_{{name}}_{{action}}" ng-class="{'processable': isAllowed(action), 'active': madeAction(action), 'foo': 1, 'bar': true}" ng-click="runButton(action)">

All functions return true, also foo and bar are true... but ng-class just not running and I do not see any processable|active|foo|bar classes in my class attribute.

ng-class starts working in two ways:

1) If I just remove some {{...}} from the class attribute:
<div class="button button_{{name}}" ng-class="{'processable': isAllowed(action), 'active': madeAction(action), 'foo': 1, 'bar': true}" ng-click="runButton(action)">
everything magically starts working.

2) Or, if I just update the scope (ng-click updates the scope) — both ng-class and ng-show|hide directives start workng as expected.

@artuska Yours is actually a slightly different problem (possibly caused by the same underlying issue). Question: why do you want to use interpolation and ngClass at the same time?

@Narretz because of code style:

<div class="mix-statistics mix-statistics_{{name}}">
    <ul class="mix-statistics__list mix-statistics__list_{{name}}">
        <li class="mix-statistics__item mix-statistics__item_{{name}} mix-statistics__item_{{name}}_{{action}}" ng-switch="action" ng-mouseenter="openBlock(action)" ng-mouseleave="closeBlock(action)" ng-repeat="action in actions">
            <div class="mix-statistics__button mix-statistics__button_{{name}} mix-statistics__button_{{name}}_{{action}}" ng-class="{'processable': isAllowed(action), 'active': isActive(action)}" ng-click="runAction(action)">
                <div class="mix-statistics__icon mix-statistics__icon_{{name}} mix-statistics__icon_{{name}}_{{action}}">
                    <div class="mix-statistics__symbol mix-statistics__symbol_{{name}} mix-statistics__symbol_{{name}}_{{action}}">
                        <svg class="icon icon_centered">
                            <use xlink:href="" ng-attr-xlink:href="{{'#icon-' + action}}"></use>
                        </svg>
                    </div>
                </div>

                <div class="mix-statistics__counter mix-statistics__counter_{{name}} mix-statistics__counter_{{name}}_{{action}}" ng-show="(mix.statistics[action])" ng-bind="mix.statistics[action]"></div>
            </div>
        </li>
    </ul>
</div>

Also, because I cannot set all those {{name}}_{{action}} with ng-class — well, I can, but I don't want to — it will look extremely ugly.

I ran into this in Angular v1.5.5 as well. I just fixed it like this...

<div class="static-class" ng-class="{ dynamic-class1: conditional1, dynamic-class2: conditional2}">

to

<div ng-class="{ static-class: true, dynamic-class1: conditional1, dynamic-class2: conditional2}">

or if your class name contains a variable

<div ng-class="{ static-class-{{variable}}: true, dynamic-class1: conditional1, dynamic-class2: conditional2}">

@zweissman thanks, that helped. In my case I had to wrap the interpolated classes in single quotes

<div ng-class="{ 'static-class-{{variable}}': true, 'dynamic-class1': conditional1, 'dynamic-class2': conditional2}">

Was this page helpful?
0 / 5 - 0 ratings