How do I access the ngStyle key and values in decorator?
Asked Answered
D

2

6

I have a list of colour names in my application.

let colours = {
  mango: '#e59c09',
  midnight: '#1476a0'
};

I want to extend the ngStyle directive to be able to understand my custom colour names. I'm doing this by decorating the ngStyle directive. However, I've hit an uphill battle on the decorator's compile function. I can access the elements' ngStyle attribute, but it comes up as a string (understandably). JSON.parse() doesn't work on it as it isn't always a valid JSON string due to bind once etc...

I simply want to step-in, iterate over all style keys, and if it contains color, I want to check for the value - and replace with hex if it is one of the above custom colour.

I can't seem to be able to access any ngStyle internal functions, and the source code is confusing and short; it seems to just set element CSS - where does the $parse do its job? for example, when ng-style="{color: ctrl.textColor}" - there is nothing in ngStyle source code that is pulling the value of ctrl.textColour. Am I looking at the wrong place?

Anyway, how do I access ng-style key values so that I can change custom colours to its hex codes please?

This is what I've got so far in my decorator:

$provide.decorator('ngStyleDirective', function($delegate) {

    let directive = $delegate[0];
    let link = directive.link;

    directive.compile = function(element, attrs) {

        // Expression here is a string property
        let expression = attrs.ngStyle;

        return function(scope, elem, attr) {

          // How do I iterate over and update style values here?

          // Run original function
          link.apply(this, arguments);

        }

      }

      return $delegate;

});

I tried using regex to pull out patterns etc. and inspect elements, but, it seems like the wrong way to approach the problem to me as I then have to manually update string and pass it on to base link function.

Here's a plnkr example.

IF there is a better way to do what I'm trying to do, please let me know.

Dalliance answered 27/6, 2017 at 15:30 Comment(1)
Note: This question has been answered, I wanted to award a bounty for the below answerer for an really well written and understandable answer.Dalliance
C
4

Anyway, how do I access ng-style key values so that I can change custom colours to its hex codes please?

The ngStyle property can be re-written within the compile function:

directive.compile = function(element, attrs) {
  let expression = getExpressions(attrs.ngStyle);
  attrs.ngStyle = expression;

  return function(scope, elem, attr) {
    // Run original function
    link.apply(this, arguments);  
  }
}

JSON.parse()

JSON.parse() can be used if the HTML is updated so that the keys are surrounded by double quotes, which means the ng-style attribute needs to be delimited with single-quotes (though if one really wanted, one could try to escape the double quotes...)

<p ng-style='{ "color": "#e59c09" }'>Hello {{name}}!</p>
<p ng-style='{ "padding": "20px 10px", "background-color": "#1476a0", "color": "#ddd" }'>It is dark here</p>

Then parsing that string should yield a valid object, and Object.keys() can be used to iterate over the keys, checking for the word color. If the key contains color, Array.indexOf can be used to check if the value exists in the colors array. If it does exist in the array, then String.replace() can be used to substitute the value for the variable (i.e. the key in colours).

function getExpressions(str) {
    var parsed = JSON.parse(str);
    Object.keys(parsed).forEach(function(key) {
        if (key.indexOf('color') > -1) {
            if (Object.keys(colours).indexOf(parsed[key]) > -1) {
                str = str.replace(parsed[key], colours[parsed[key]])
            }
         }
    });
    return str;
}

See it demonstrated in the example below. By the way, I had to remove the unused variable colours declared within the scope of the function getExpressions(), as it was hiding access to the variable defined above on line 3. Here is an updated plunker.

let app = angular.module('plunker', []);
let colours = {
  mango: '#e59c09',
  midnight: '#1476a0'
};
app.controller('MainCtrl', function($scope) {
  $scope.name = 'World';
});
app.config(function($provide) {
  // Extract colour values from the string
  function getExpressions(str) {
    var parsed = JSON.parse(str);
    Object.keys(parsed).forEach(function(key) {
      if (key.indexOf('color') > -1) {
        if (Object.keys(colours).indexOf(parsed[key]) > -1) {
          str = str.replace(parsed[key], colours[parsed[key]])
        }
      }
    });
    return str;
  }

  $provide.decorator('ngStyleDirective', function($delegate) {
    let directive = $delegate[0];
    let link = directive.link;

    directive.compile = function(element, attrs) {
      let expression = getExpressions(attrs.ngStyle);
      attrs.ngStyle = expression;
      return function(scope, elem, attr) {
        // Run original function
        link.apply(this, arguments);
      }
    }
    return $delegate;
  });
});
div + div {
  margin-top: 60px;
}

.comment { 
  font-family: courier;
  font-size: 12px;
  margin: 15px 0;
}
<script src="https://code.angularjs.org/1.4.12/angular.js"></script>
<div ng-app="plunker" ng-controller="MainCtrl">
  <div>
    <p class="comment">--- with hex --</p>
    <p ng-style='{ "color": "#e59c09" }'>Hello {{name}}!</p>
    <p ng-style='{ "padding": "20px 10px", "background-color": "#1476a0", "color": "#ddd" }'>It is dark here</p>
  </div>

  <div>
    <p class="comment">--- with custom colours --</p>
    <p ng-style='{ "color": "mango" }'>Hello {{name}}!</p>
    <p ng-style='{ "padding": "20px 10px", "background-color": "midnight", "color": "#ddd" }'>It is dark here</p>
  </div>
</div>
Corrasion answered 27/6, 2017 at 16:30 Comment(6)
Hiya, thank you for the excellent answer. I have one final question. My ng-style is used with a controller variable, like so: ng-style="{ color: ctrl.textColour }" and the textColour so in my getExpression function, the str is literally that. Is there a way I can force eval the expression, get the string mango out of it, and then apply my custom colour hex value?Dalliance
Hmm... So far I have this example - where the myColour is defined outside the controller and provider, so it can be used in both places, but I presume you would want that style updated whenever the bound controller model value changes... I am guessing a watcher would be necessary to handle that...Anguilliform
Hm, I won't always know what the variable name is going to be, it could be ctrl.item.colour or ctrl.borderColour... which is why I was wondering if I could $eval the expression and get the actual item value instead of replacing. Is that possible?Dalliance
HI, I just realised that I can call scope.$eval from within the link function and it will evaluate the expression and give me the required values instead of variables - that answers my above question. Thanks so much for the amazingly well written answer, I'll accept it soonDalliance
Noooo - I went away on a holiday and wanted to award bounty to this, but came back to see that the other answer has already been awarded automatic bounty :( How do I fix this?Dalliance
Given the bounty guidelines, I don't believe there is a way to change it, unless you request the intervention of a moderator... I appreciate your gratitude and generosity, though honestly using $parse is likely simplerAnguilliform
R
3

Actually, if you want to use parse - and you should - you can use it to parse the expression, replace attributes, and then transform the attribute back to json.

You should use $parse because if your code looks like

// in the HTML
<p ng-style="{ padding: '20px 10px', 'background-color': myController.color, color: '#ddd' }">It is dark here</p>
// in the JS
myController.color = 'midnight';

Then parsing JSON will not work. You should parse the expression using $parse and call the resulting function with your directive's scope object.

That's why your provider should look like this :

$provide.decorator('ngStyleDirective', function($delegate, $parse) {
  let directive = $delegate[0];
  let link = directive.link;

  directive.compile = function(element, attrs) {
    return function(scope, elem, attrs) {
      let ngStyleObject = $parse(attrs.ngStyle)(scope, {});

      Object.keys(ngStyleObject).forEach(function(key) {
        if (key.indexOf('color') > -1 && Object.keys(colours).indexOf(ngStyleObject[key]) > -1) {
          ngStyleObject[key] = colours[ngStyleObject[key]];
        }
      });

      attrs.ngStyle = JSON.stringify(ngStyleObject); 
      // Run original function
      link.apply(this, arguments); 
    }
  }
  return $delegate;
});

You could also copy the original ngStyle function (instead of calling its link function) as it's pretty simple to add the behavior you expected:

$provide.decorator('ngStyleDirective', function($delegate) {
  let directive = $delegate[0];

  directive.compile = function(element, attrs) {
    return function(scope, elem, attrs) {
      // here, watch will do the $parse(attrs.ngStyle)(scope) and call the callback when values change
      scope.$watch(attrs.ngStyle, function ngStyleWatchAction(newStyles, oldStyles) {
        if (oldStyles && (newStyles !== oldStyles)) {
          oldStyles.forEach(function(val, style) {  element.css(style, ''); });
        }
        if (newStyles) {
          // instead of just setting the new styles, replace colors with their values
          Object.keys(newStyles).forEach(function(key) { 
            if (key.indexOf('color') > -1 && Object.keys(colours).indexOf(newStyles[key]) > -1) {
              newStyles[key] = colours[newStyles[key]];
            }
          });
          element.css(newStyles);
        }
      }, true);

    }
  }
  return $delegate;
});

You can find the plunker (two versions) here

Rochellrochella answered 5/7, 2017 at 7:48 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.