Skip to content

Instantly share code, notes, and snippets.

@ryanflorence
Last active October 30, 2019 01:42
Show Gist options
  • Star 22 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ryanflorence/6833014 to your computer and use it in GitHub Desktop.
Save ryanflorence/6833014 to your computer and use it in GitHub Desktop.
Comparing an avatar implementation with angular and ember. http://www.angularails.com/articles/creating_simple_directive_in_angular
// Because people can't seem to find the gist description, here is the source
// of this code, a post found in last weeks JS Weekly, it is not my code
// http://www.angularails.com/articles/creating_simple_directive_in_angular
angular.module('ui-multi-gravatar', [])
.directive('multiAvatar', ['md5', function (md5) {
return {
restrict: 'E',
link:function(scope, element, attrs) {
var facebookId = attrs.facebookId;
var githubUsername = attrs.githubUsername;
var email = attrs.email;
var tag = '';
if ((facebookId !== null) && (facebookId !== undefined) && (facebookId !== '')) {
tag = '<img src="http://graph.facebook.com/' + facebookId + '/picture?width=200&height=200" class="img-responsive"/>'
} else if ((githubUsername !== null) && (githubUsername !== undefined) && (githubUsername !== '')){
tag = '<img src="https://identicons.github.com/' + githubUsername + '.png" style="width:200px; height:200px" class="img-responsive"/>';
} else {
var hash = ""
if ((email !== null) && (email !== undefined) && (email !== '')){
var hash = md5.createHash(email.toLowerCase());
}
var src = 'https://secure.gravatar.com/avatar/' + hash + '?s=200&d=mm'
tag = '<img src=' + src + ' class="img-responsive"/>'
}
element.append(tag);
}
};
}]);
App.XAvatarComponent = Ember.Component.extend({
tagName: 'img',
classNames: 'img-responsive',
width: 200,
height: 200,
service: 'gravatar',
src: function() {
return this[this.get('service')+'Url']();
}.property('service', 'user'),
facebookUrl: function() {
return 'http://graph.facebook.com/' + this.get('user') + '/picture?type=large';
},
githubUrl: function() {
return 'https://identicons.github.com/' + this.get('user') + '.png'
},
gravatarUrl: function() {
var hash = md5.createHash(this.get('user').toLowerCase());
return 'https://secure.gravatar.com/avatar/' + hash + '?s=200&d=mm';
}
});
Angular
<multi-avatar data-facebook-id="717976707"/>
<multi-avatar data-github-username="jwo"/>
<multi-avatar data-email="jesse@rubyoffrails.com"/>
Ember
{{x-avatar service="github" user="rpflorence" width=50 height=50}}
{{x-avatar service="facebook" user="717976707"}}
{{x-avatar user="ryanflorence@example.com"}}
@ryanflorence
Copy link
Author

Consider the author of this library is MIA and github has changed their avatar url. Also, consider you want to add more services to this component. In Ember you would:

App.XAvatarComponent.reopen({
  githubUrl: function() {
    return 'new github url implementation';
  },

  googleUrl: function() {
    return 'google url';
  }
});

I'm not sure how that would work in angular, I have asked and been told "angular uses object composition, not inheritance". I don't know how to translate that into this use case.

Also note the use of width and height. You can pass those attributes in to override the defaults.

@RyanHirsch
Copy link

Preview - http://jsbin.com/ExamAxI/5

var app = angular.module("AngularApp", ['ui-multi-gravatar']);

angular.module('ui-multi-gravatar', [])
  .directive('multiAvatar', ['imgService', function (services) {
    return {
      restrict: 'E',
      replace: true,
      template: "<img class='img-responsive' />",
      link: function(scope, element, attrs) {
        var serviceName = attrs.service || "gravatar";
        var user = attrs.user;

        var imgSrc = services[serviceName + 'Url'](user);
        var height = attrs.height || 200;
        var width = attrs.width || 200;

        element.attr({
          src: imgSrc,
          height: height,
          width: width
        });
      }
    };
  }])
.service('imgService', function() {
    var facebookUrl = function(user) {
      return 'http://graph.facebook.com/' + user + '/picture?type=large';
    };

    var githubUrl = function(user) {
      return 'https://identicons.github.com/' + user + '.png';
    };

    var gravatarUrl = function(user) {
      var hash = CryptoJS.MD5(user.toLowerCase());
      return 'https://secure.gravatar.com/avatar/' + hash + '?s=200&d=mm';
    };

    var services = {
      'facebookUrl' : facebookUrl,
      'githubUrl': githubUrl,
      'gravatarUrl': gravatarUrl
    };

    return services;
});

@RyanHirsch
Copy link

Something I didn't know before working on this, custom angular directives can't be self-closing, you really need to have a otherwise it can get confused and break. (starts doing weird nesting)

@geelen
Copy link

geelen commented Oct 8, 2013

This example trolled me good, just spent the last few mins refactoring the angular for a closer comparison :)

Running here: http://jsbin.com/uhUCeLO/4/edit?html,js,output

Code:

var app = angular.module("AngularApp", ['ui-multi-gravatar']);

angular.module('ui-multi-gravatar', [])
  .directive('multiAvatar', function (UserImages) {
    return {
      restrict: 'E',
      replace: false,
      scope: {
        width: '@', 
        height: '@', 
        service: '@', 
        user: '@'
      },
      template: "<img class='img-responsive' width='{{width || 100}}' height='{{height || 100}}' ng-src='{{src}}' />",
      link: function(scope, element, attrs) {
        scope.src = UserImages[scope.service || "gravatar"](scope.user);
      }

    };
  })
.service('UserImages', function() {
    this.facebook = function(user) {
      return 'http://graph.facebook.com/' + user + '/picture?type=large';
    };

    this.github = function(user) {
      return 'https://identicons.github.com/' + user + '.png';
    };

    this.gravatar = function(user) {
      var hash = CryptoJS.MD5(user.toLowerCase());
      return 'https://secure.gravatar.com/avatar/' + hash + '?s=200&d=mm';
    };
});

@dcherman
Copy link

dcherman commented Oct 8, 2013

@rpflorence I think in the event of a MIA author, you'd want to use a decorator in order to do those same functionality changes in angular

http://docs.angularjs.org/api/AUTO.$provide#decorator

@ryanflorence
Copy link
Author

Why has nobody rewritten my ember example yet? 🧌

@chee
Copy link

chee commented Oct 8, 2013

var services = {
  facebook: 'http://graph.facebook.com/$id/picture?type=large',
  github: 'https://identicons.github.com/$id.png',
  gravatar: 'https://secure.gravatar.com/avatar/$id?s=200&d=mm'
}

function getAvatarUrl (id, service) {
  var url = services[service];
  if (url && service != 'gravatar') {
    return url.replace('$id', id);
  } else {
    url = services.gravatar;
    id = md5.createHash(id.toLowerCase());
    return url.replace('$id', id);
  }
}

function makeAvatar (id, service, size) {
  var img = document.createElement('img');
  img.width = img.height = (size || 200);
  img.src = getAvatarUrl(id, service);
  return img;
}

@ryanflorence
Copy link
Author

@chee so the user changes their name while on the page, now what? In ember/angular its updated automatically.

@ryanflorence
Copy link
Author

@dcherman yes, if you're using a service, the original example was not.

@bclinkinbeard
Copy link

@rpflorence Because you know what you're doing with Ember, but not with Angular? :)

@angus-c
Copy link

angus-c commented Oct 8, 2013

@rpflorence seems unlikely for this example, but if they did then just bind to that event and update the image source - all this smoke and mirrors seems like overkill and not at all obvious

@chee
Copy link

chee commented Oct 8, 2013

what about if the user deletes their github profile while they're on the page

@iammerrick
Copy link

Did you intentionally butcher the angular example here?

@ryanflorence
Copy link
Author

@iammerrick @bclinkinbeard Read the description of the gist?

@ryanflorence
Copy link
Author

@angus-c yeah, that's what I did with MooTools and then Backbone for several years. Bound templates + components is way better. And once you get over the declarative template hump, hopping into other's code and figuring out how it all works is ridiculously more obvious.

@heyjinkim
Copy link

EMBER FTW!

JSBin: http://jsbin.com/ember-component/2/edit

App = Ember.Application.create();

App.XAvatarComponent = Ember.Component.extend({
  tagName: 'img',
  classNames: 'img-responsive',
  attributeBindings: 'width height src'.w(),
  width: 100,
  height: 100,
  service: 'gravatar',

  src: function() {
    return this[this.get('service')](this.get('user'));
  }.property('service'),

  facebook: function(user) {
    return 'http://graph.facebook.com/' + user + '/picture?type=large';
  },

  github: function(user) {
    return 'https://identicons.github.com/' + user + '.png';
  },

  gravatar: function(user) {
    return 'https://secure.gravatar.com/avatar/' + CryptoJS.MD5(user.toLowerCase()) + '?s=200&d=mm';
  }
});

@bclinkinbeard
Copy link

I see it is not your code, and that you don't use Angular, which is why I am confused about why you'd try to provide a comparison. I can assure you if I wrote a feature in both Angular and Ember, the Ember code would be crap because I don't know/use it. Because of that, I simply wouldn't write the comparison.

@ryanflorence
Copy link
Author

@bclinkinbeard I have a pretty deep understanding of angular and have built a few (admittedly trivial) apps with it. And, again, I didn't write that angular code, it was taken wholesale from a popular blog post featured in JS Weekly.

@mgol
Copy link

mgol commented Oct 8, 2013

@RyanHirsch @rpflorence It's not just Angular, '' is interpreted as '' by HTML5 browsers; the slash is stripped. That's why the above example is incorrect.

@bclinkinbeard
Copy link

Thinking about this more (unfortunately), the problem with the original Angular code isn't really a lack of idiomatic-ness. Sure, accessing the attrs directly instead of properly defining a child scope is less than ideal, but the biggest problem is the multi level conditional spaghetti. Breaking that out into a set of clean *Url functions isn't an Angular or Ember best practice/idiom, it's just fundamental code organization.

If you took the original Ember code and removed the separate service parameter and stuffed the *Url functions into src and navigated them with nothing but conditionals it would be shitty code too. I realize the Angular code was apparently linked from a popular newsletter, but it's still fundamentally bad code IMO. Not bad Angular code, just plain old, regular bad code.

@malsup
Copy link

malsup commented Oct 9, 2013

JS Weekly. LOL.

@ryanflorence
Copy link
Author

@bclinkinbeard for background, a friend said "how would you do that in ember" so I did it, made the gist and then the fun began. Here's my original tweet: https://twitter.com/ryanflorence/status/386241210337067009 I did not intend to say this is a valid or fair comparison, I was just answering a question for a friend. People take these things way too seriously. Both frameworks are great and arrive at nearly identical usage syntax <component attr=value>, {{#component attr=value}}.

I think there is value in considering the APIs the two frameworks give you for people picking one.

Most angular APIs just give you a single function hook to fill in and possibly return from (controllers, link, module, service), so people tend to drop everything into it because angular doesn't prescribe how to structure the code inside the hook. Makes getting started super easy.

Ember requires you to extend its objects (controller, component, route), which means you provide an object literal to your new constructor definition, rather than fill in a function body and return from it, so people tend to write methods because they didn't have to decide how to structure their modules.

I won't claim one is better, I just know which one I prefer.

@coderstash
Copy link

Nice gist! I know which one I choose/use :)

@polotek
Copy link

polotek commented Oct 11, 2013

@rpflorence I'm pretty sure I'm the one that screwed you here. I tweeted this as a "comparison" of ember and angular. https://mobile.twitter.com/polotek/status/387601840516259841

I didn't read the gist description and screwed up the context for everybody. Sorry.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment