-1

Summary of the Problem: I want to dynamically apply a CSS style to several div's using Angular JS' s ng-style directive. The trouble is that the expression bound to ng-style depends on a specific scope variable that I use to define the style I want to return. But here’s the catch: this scope variable is not set initially. It is only set after a particular event handler fires, so I need to make ng-style wait for this scope variable to be set before firing.

Background: I'm using Angular JS v.1.7.8

My Code:

<!-- index.html -->
<div ng-app='myApp'>
    <div ng-controller='myCtrl as ctrl'>
        <div ng-style='clientScope.ctrl.getStyle()'></div>
        <div ng-style='clientScope.ctrl.getStyle()'></div>
        <div ng-style='clientScope.ctrl.getStyle()'></div>
        <div ng-style='clientScope.ctrl.getStyle()'></div>
    </div>
</div>
// app.js
(function(){
    'use strict';
    angular
        .module('myApp', [])
        .controller('myCtrl', function($scope) {
            var ctrl = this;

            ctrl.colorInScope = function() {
                if ($scope.colors) { return true; };
                return false;
            }

            ctrl.getStyles = function() {
                return {'color': $scope.colors['myColor'] };
            }

         /* Note I'm not actually using setTimeout() in my real
            setMyColor function, but am just using it here as an
            easy way to simulate to delayed nature of how myColor gets set. */

            function setMyColor() {
                setTimeout(function() {
                    $scope.colors = {};
                    $scope.colors['myColor'] = '#7e9cee';
                }, 3000);
            }

            setMyColor();

        });

}());

Currently, ng-style's expression is evaluated as soon as the div it is bound to loads, which happens before setMyColor is ever called. As a result, $scope.colors in ctrl.getStyles is undefined and Javascript throws this error:

TypeError: Cannot read property 'myColor' of undefined

What I've tried so far:

I see two possible approaches to fixing this:

1) Prevent ng-style's expression from being evaluted before $scope.colors["myColor"] is set.

Note: I've already looked into two Angular JS directives (ng-if and ng-show) that do something similar, but they ultimiately did not achieve what I want. Here's why:

  • ng-if="expression" removes the element it is bound to from the DOM if expression evaluates to false, thus the element is only included in the DOM if expression is true. I tried attaching ng-if="clientScope.ctrl.colorInScope()" to my div's' like so:

    <div ng-if="clientScope.ctrl.colorInScope()" ng- 
    style="clientScope.ctrl.getStyle()"></div>
    .
    .
    .
    <div ng-if="clientScope.ctrl.colorInScope()" ng- 
    style="clientScope.ctrl.getStyle()"></div>
    

    but this immediately removed the div's from the DOM upon the DOM's initial loading because clientScope.ctrl.colorInScope does not evaluate to true at that point.

  • ng-show="expression" applies the visibility: hidden style to the element it is bound to if expression evalutes to false, thus the element is only visable if expression is true. I also tried attaching ng-show="clientScope.ctrl.colorInScope()" to my div's but even though my div's visibility was initially hidden, ng-style was evaluated prematurely anyways and Javascript threw an error because $scope.colors was undefined.

2) Have some sort of method in ctrl.getStyles to pause the execution of the function until $scope.colors["myColor"] is set. Note: I do not want to use a timer in ctrl.getStyles because then the function would wait every time it is called. Basically I want the function to pause if $scope.colors["myColor"] is undefined (and wait for it's value to be set) or proceed straight away if $scope.colors["myColor"] has a value.

I've read a little bit about $scope.$watch, which is basically an Angular JS method for $scope that takes the name of a scope variable to watch and an event handler and triggers the event handler when the scope variable changes. I could potentially initialize $scope.colors to 0 at the start of my controller and then add await $scope.$watch('colors', myHandler()) to ctrl.getStyles = async () => ... and return {'color': $scope.colors["myColor"]} from myHandler but this wouldn't work because $scope.$watch would only get triggered once (after setMyColor finishes) and Javascript would eventually throw some kind of timeout error or something because it would be waiting on $scope.$watch forever (although I'm not sure exactly what my program's behavior would be in this case because I'm still learning about how async/await work).

I've also read Wait until scope variable is loaded before using it in the view in angular.js which was helpful in showing how to leverage ng-show, but it doesn't really answer my question about how to stall ng-style.

Conclusion:

Do any of you Angular JS experts out there know of a better solution for what I'm trying to achieve?

ElsaInSpirit
  • 341
  • 6
  • 16
  • `this wouldn't work because $scope.$watch would only get triggered once ` I don't think this is true. $scope.watch creates a watcher on a variable, it gets fired when it changes at every digest cycle. Why can't it be used to solve this problem? if it's updating multiple values over time, you need be polling, then pushing updates to this variable thats watched. But if you're just waiting for it to be ready (1 time event) then this should work fine. – some guy Aug 02 '19 at 19:16
  • @someguy Doesn't `$scope.$watch` only fire when `colors` changes? And in this case `colors` would change only once (when `setMyColor` terminates). – ElsaInSpirit Aug 02 '19 at 19:19
  • Yes, it will only fire when colors changes. If `colors` is expected to change more than once overtime, you need some mechanism to fetch the updated colors over time as well. Then you can simply update colors, the $scope.watch will trigger everytime, and the style will be reevaluated. – some guy Aug 02 '19 at 19:23
  • @someguy It would work for the first `div` but not the rest because `$scope.$watch` would never fire in subsequent calls to `ctrl.getStyles` (because `colors` was changed once, and never gets changed again). – ElsaInSpirit Aug 02 '19 at 19:24
  • My suggested approach is to use `color` is a model for each of the divs. Assuming the same color. Set a default color, whatever. If the event indeed only happens once, (isLoading) for example, then you can use a boolean and $scope.watch on that. in the $scope.watch, set the updated color. ng-style should reevaluate. – some guy Aug 02 '19 at 19:33
  • @someguy can you provide some example code? – ElsaInSpirit Aug 02 '19 at 19:46
  • https://stackblitz.com/edit/angularjs-kgjuda This example creates two paragraphs tied to the same model for a color. The color is updating every 3000 ms. The style gets reevaluated. If your problem isn't {how do I update the color after a specific event}, this won't help you. If it is, you can replace the $interval, with a callback after your specific event. If your event is happening over time, put it in a interval. – some guy Aug 02 '19 at 19:51

1 Answers1

1

Javascript throws this error:

TypeError: Cannot read property 'myColor' of undefined

To avoid the type error, check for the existence of the variable:

ctrl.getStyles = function() {
    if (!$scope.colors) return;
    //ELSE
    return {'color': $scope.colors['myColor'] };
}

This will avoid the type error.

Community
  • 1
  • 1
georgeawg
  • 48,608
  • 13
  • 72
  • 95