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.

Sᴀᴍ Onᴇᴌᴀ
  • 8,218
  • 8
  • 36
  • 58
LocustHorde
  • 6,361
  • 16
  • 65
  • 94
  • Note: This question has been answered, I wanted to award a bounty for the below answerer for an really well written and understandable answer. – LocustHorde Jun 30 '17 at 08:30

2 Answers2

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>
Sᴀᴍ Onᴇᴌᴀ
  • 8,218
  • 8
  • 36
  • 58
  • 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? – LocustHorde Jun 28 '17 at 14:54
  • Hmm... So far I have [this example](https://plnkr.co/edit/77xhwsLHoFlKmESDSPvy?p=info) - 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... – Sᴀᴍ Onᴇᴌᴀ Jun 28 '17 at 15:28
  • 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? – LocustHorde Jun 29 '17 at 10:23
  • 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 soon – LocustHorde Jun 29 '17 at 12:41
  • 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? – LocustHorde Jul 10 '17 at 08:43
  • Given the [bounty guidelines](https://stackoverflow.com/help/bounty), 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 simpler – Sᴀᴍ Onᴇᴌᴀ Jul 10 '17 at 13:28
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

hilnius
  • 2,165
  • 2
  • 19
  • 30