An Ember JS Gauge Component

TL;DR

This is a gauge (well kind of) made with Ember Components and CSS3 transforms. The pointer angle is bound to those 2 inputs (see the jsbin just bellow) in which you can define either the pointer current value and the whole gauge maximum value.

Ember Gauge Component

The Template

The main idea of having a gauge component is that you can use and re-use it anywhere in your Ember app(s), and its main purpose is to indicate a current value relative to a maximum value. So, if it could have 2 attributes value and maxValue that would be perfect.

Well, let's just do that.

{{my-gauge value=something maxValue=something}}

And here's how it would look like under the hood.

<script type="text/x-handlebars" id="components/my-gauge">
  <div class="frame">
    <span class="pointer" {{bind-attr style="computedAngle"}}>
      <span class="pointer-cap"></span>
    </span>
    <span class="scale"></span>
  </div>
</script>

Note: for the purpose of this post the template is inside these <script type="text/x-handlebars"> tags, but you could set your own template precompiling tools quite easily (grunt-ember-templates is pretty good at this) and you would have a my-gauge.hbs file inside a components/ folder.

The most important part here is the computedAngle property which is bound to the style attribute of the pointer element. In other words, it holds the correct CSS rules to apply the right angle (CSS transform - rotate()) to the gauge pointer.

If you're curious about the {{bind-attr}} part, you should take a look at the Guides in Ember documentation, because there are many different ways of binding data to / from templates.

The MyGauge Object

The template now needs a little bit of magic logic, let's extend the Ember.Component object so we can define our gauge properties.

App.MyGaugeComponent = Em.Component.extend({ // Em = Ember
  classNames: ['gauge'],

  computedAngle: function(){
    // parseInt because we want Numbers and inputs value actually are Strings
    var value    = parseInt(this.get('value'), 10);
    var maxValue = parseInt(this.get('maxValue'), 10);

    var angle = Math.floor( 180 * value/maxValue - 90 );
    var styles = 'transform: rotate('+angle+'deg)';

    return styles;
  }.property('maxValue', 'currentValue')
});

Note: For the sake of brevity, I intentionally omitted the CSS vendor prefixes in the styles string.

The computedAngle function returns a styles which is a string containing the CSS rules that will be applied to the .pointer element.

You probably noticed the .property('') at the end of the function, this tells Ember to consider that function as a property so it can re-execute itself if the value of other properties (passed as argument) changes. In other words, if either value or maxValue changes, then computedAngle is re-executed.

Oh God! How do you rotate that stupid needle!?

CSS Fun Animated GIF

I feel a bit like this is the trickiest part of the component because actually CSS is doing like 90% of the job. I do love CSS, but sometimes it's just... omg. So, yes sorry for that 2Mb GIF I couldn't resist :)

Basically the pointer rotation is made with CSS3 Transforms, the computedAngle property updates the angle and CSS3 Transitions handle the animation when angle changes.

But the most important CSS property is the transform-origin: bottom, because it applies the rotation from the bottom of the pointer instead of from the center (the default). By the way, this is how those crazy CSS clocks are made.

Here's a little diagram, thought it might help.

ember-gauge-component_2

Ok, we get the correct angle with this formula: 180 * (value / maxValue). BUT, the pointer has to be set to 0° by default, and if you pay attention to figure n°3 in the diagram above, then you'll see that 0° is in fact -90°. So let's subtract those 90° from the formula: 180 * (value / maxValue) -90.

And now we can create a string: var styles = 'transform: rotate('+angle+'deg);', which will be injected into the pointer style attribute.

Of course, vendor prefixes will mess a little bit the final string, but it's not really a big deal.

Detect if maxValue is exceeded

Now let's see how we can detect when value exceeds maxValue, we'll simply use another computed property for that.

App.MyGaugeComponent = Em.Component.extend({
  classNames: ['gauge'],
  classNameBindings: ['isMaxValueExceeded:exceeded'],

  isMaxValueExceeded: function(){
    // parseInt because we want Numbers and inputs value actually are Strings
    var value    = parseInt(this.get('value'), 10);
    var maxValue = parseInt(this.get('maxValue'), 10);
    return (value > maxValue); // return a Boolean
  }.property('value', 'maxValue'),
  ...
  ...

It's just a test to see if value is strictly greater than maxValue, and again isMaxValueExceeded is watching for any change of value or maxValue with the property() method.

And now there's this classNameBindings property which is used to add or remove a class name on the component element if a property is either true or false. Here it adds an .exceeded class if isMaxValueExceeded is truthy. The syntax for this is a little bit like ternary operators, so a real life example could be isVisible:shown:hidden which adds a shown class if isVisible is true and a hidden class if isVisible is false.

Also, you could use isMaxValueExceeded inside the template with an {{if}} statements, so for instance we can print a warning to the user.

{{#if isMaxValueExceeded}}
  <p>OMG maxValue is exceeded!</p>
{{/if}}

Conclusion

Ember Components give an incredible amount of power to build reusable blocks that you can share across applications, and since it mimics W3C's Web Components as much as possible, it feels a little like tasting a bit of the future.

And yes of course, all of this works well for half-circle gauges. The formula to update the pointer angle, position, height or whatever, will slightly differ if you want another type of gauge, but I hope you got the main idea.

Resources

Be Sociable, Share!

    Commentaires

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>