203 Lotus blogs updated hourly. Who will post next? Home | Blogs | Search | About 
 
Latest 7 Posts
Thoughts on TypeScript
Wed, Sep 6th 2017 4
Replacing Lotus is…. complex
Fri, Sep 1st 2017 7
Web Component Thoughts….
Fri, Aug 25th 2017 3
Just…. NO,NO,NO
Fri, Aug 11th 2017 5
Goodbye Evernote
Tue, Jan 17th 2017 4
Merry Christmas!
Sun, Dec 25th 2016 4
Visual Studio Code Editor
Fri, Nov 11th 2016 4
Top 10
Work with Rich Text from DDS
Fri, Oct 21st 2016 10
Re-usability is the Goal!
Tue, Jun 14th 2016 7
Polymer app-layout Elements
Fri, Jul 1st 2016 7
Domino svg support
Fri, Aug 12th 2016 7
Replacing Lotus is…. complex
Fri, Sep 1st 2017 7
On SocialBizUg: Modern Domino: Bootstrap 3 Inheritable Layout
Fri, May 30th 2014 5
My first impressions of using Titanium Appcelerator
Sun, Dec 8th 2013 5
New Polycast is out!
Wed, Jun 15th 2016 5
I’m speaking at MWLUG 2016
Tue, Jun 28th 2016 5
Red Pill is the MWLUG Sponsor of the Week
Fri, Aug 12th 2016 5


A Polymer Avatar component
Twitter Google+ Facebook LinkedIn Addthis Email Gmail Flipboard Reddit Tumblr WhatsApp StumbleUpon Yammer Evernote Delicious
Keith Strickland    

At MWLUG a lot of people seemed surprised that I would create a component just for an avatar. My response to that is why wouldn’t you build an avatar component? So, in this post I’m going to show you how to build one for your applications.

Let’s start out with the use case. We want to show an avatar for a person in various locations throughout our application. This avatar should do a few different things:

  • Should show a picture of a person if one is attached to their person document
  • If no picture is available and the person is in the domino directory should show the first letter of their first name and have a background color that is the same throughout the system
  • If the person is not in the domino directory just show a person icon
  • Should be able to click the avatar and it should do something (i.e. navigate to a person profile or somewhere else)
  • We’ll want to hand this component a canonical name and tell it how to get to our person document
  • We don’t want to have to install anything so we should use DDS

I’ll post the code here but I’m not going to explain everything, just the interesting bits.

<link rel="import" href="../polymer/polymer.html">

<link rel="import" href="../iron-ajax/iron-ajax.html">
<link rel="import" href="../iron-flex-layout/iron-flex-layout-classes.html">
<link rel="import" href="../iron-icons/social-icons.html">
<link rel="import" href="../iron-image/iron-image.html">

<link rel="import" href="tinycolor.html">


<dom-module id="now-avatar">
  <template>
    <style is="custom-style" include="iron-flex iron-flex-alignment">
      :host {
        display: inline-block;
        position: relative;
        width: var(--now-avatar-height-width, 40px);
        height: var(--now-avatar-height-width, 40px);
        border: var(--now-avatar-border-size, 1px) solid var(--now-avatar-border-color, #afafaf);
        border-radius: 50%;
        @apply(--now-avatar);
      }
      :host:hover {
        cursor: pointer;
        @apply(--now-avatar-hover);
      }
      .avatarContainer {
        height: 100%;
        width: 100%;
      }
      .avatarImageContainer {
        height: inherit;
        width: inherit;
      }
      .avatarImage {
        border-radius: 50%;
        height: inherit;
        width: inherit;
        line-height: var(--now-avatar-height-width, 40px);
      }
      .avatarIcon {
        display: inline;
        line-height: var(--now-avatar-height-width, 40px);
      }
      .avatarLetterContainer {
        width: inherit;
        text-align: center;
      }
      .avatarLetter {
        line-height: var(--now-avatar-height-width, 40px);
      }
    </style>
    <!-- Lookup the user name in the ($Users) view of the domino directory -->
    <iron-ajax
      id="viewPeopleAjax"
      url="{{ddsViewUrl}}"
      handle-as="json"
      last-response="{{_viewPerson}}"
      last-error="{{_fetchViewPersonError}}"
      verbose>
    </iron-ajax>
    <!-- Fetch the user once they are found in the ($Users) view -->
    <iron-ajax
      id="personAjax"
      url="{{ddsPersonUrl}}"
      handle-as="json"
      on-response="_onPersonChange"
      last-error="{{_fetchPersonError}}"
      verbose>
    </iron-ajax>
    <div id="avatarContainer" class="avatarContainer layout horizontal">
      <!-- The letter -->
      <span
        class="self-center avatarLetterContainer"
        hidden$="{{_hideLetter}}">
        <span id="avatarLetter" class="avatarLetter center">{{avatarLetter}}</span>
      </span>
      <!-- The icon -->
      <span
        class="self-center avatarIconContainer"
        hidden$="{{_hideIcon}}">
        <iron-icon id="avatarIcon" class="avatarIcon" icon="social:person"></iron-icon>
      </span>
      <!-- The image -->
      <span
        class="self-center avatarImageContainer"
        hidden$="{{_hideImage}}">
        <iron-image
          sizing="cover"
          src="{{_photoUrl}}"
          hidden$="{{_hideImage}}"
          class="avatarImage fit"
          error="{{_fetchImageError}}"
          fade>
        </iron-image>
      </span>
    </div>
  </template>
  <script>
Polymer({
  is: 'now-avatar',
  properties: {
    /**
     * The person's canonical name. Not needed if you provide a photoUrl
     * @type {String}
     */
    name: {
      type: String,
      observer: '_onNameChange'
    },
    /**
     * The person document retrieved from the domino directory
     * @type {Object}
     */
    person: {
      type: Object
    },
    /**
     * Person retrieved from the view query
     * @type {Array}
     */
    _viewPerson: {
      type: Array,
      observer: '_onViewPersonChange'
    },
    /**
     * The DDS URL to the '($Users)' view in the domino directory formatted like:
     * 'http://domino.server/names.nsf/api/data/collections/unid/<ViewUNID>'
     * NOTE: With this one we need to include the protocol and host name
     * @type {String}
     */
    ddsViewUrl: {
      type: String,
      observer: '_onDdsViewUrlChange'
    },
    /**
     * The DDS Url for documents in the database. Should be formatted like:
     * '/names.nsf/api/data/documents/unid/<PersonDocUNID>'
     * NOTE: DO NOT include the protocol and host name
     * @type {String}
     */
    ddsPersonUrl: String,
    /**
     * The url to navigate to when the avatar is clicked. Will open a new tab
     * @type {String}
     */
    clickUrl: String,
    /**
     * The protocol and host name to the DDS Url formatted like:
     * 'http://domino.server.name'
     * @type {String}
     */
    ddsHostName: String,
    /**
     * The name of the field in the Domino Directory person document where user photos are uploaded
     * @type {String}
     */
    photoField: String,
    /**
     * The name of the field in the Domino Directory person document where a photo url is stored
     * @type {String}
     */
    photoUrlField: String,
    /**
     * A url where a photo can be used. If you use this, you don't need to specify a name or DDS Urls
     * @type {String}
     */
    photoUrl: {
      type: String,
      observer: '_onPhotoUrlChange'
    },
    /**
     * This is the property that iron-image will use to fetch the image
     * @type {String}
     */
    _photoUrl: String,
    /**
     * The First letter of the user's common name
     * @type {String}
     */
    avatarLetter: String,
    /**
     * True if the letter should be hidden
     * @type {Boolean}
     */
    _hideLetter: {
      type: Boolean,
      value: true
    },
    /**
     * True if the icon should be hidden
     * @type {Boolean}
     */
    _hideIcon: {
      type: Boolean,
      value: false
    },
    /**
     * True if the image should be hidden
     * @type {Boolean}
     */
    _hideImage: {
      type: Boolean,
      value: true
    },
    /**
     * Object when an error occurs fetching a person
     * @type {Object}
     */
    _fetchPersonError: {
      type: Object,
      observer: '_onFetchPersonError'
    },
    /**
     * Object when an error occurs fetching the view entries
     * @type {Object}
     */
    _fetchViewPersonError: {
      type: Object,
      observer: '_onFetchViewPersonError'
    },
    /**
     * Object when an error occurs fetching an image
     * @type {Object}
     */
    _fetchImageError: {
      type: Object,
      observer: '_onFetchImageError'
    }
  },
  listeners: {
    'tap': '_onTap'
  },
  /**
   * Produce an event when an error occurs fetching the image
   * @param  {Object} newVal The error
   * @param  {Object} oldVal The old error
   * @event now-avatar-image-error
   */
  _onFetchImageError: function(newVal, oldVal) {
    this.fire('now-avatar-image-error', newVal);
  },
  /**
   * Produce an event when an error occurs fetching the a person from the view
   * @param  {Object} newVal The error
   * @param  {Object} oldVal The old error
   * @event now-avatar-view-person-error
   */
  _onFetchViewPersonError: function(newVal, oldVal) {
    this.fire('now-avatar-view-person-error', newVal);
  },
  /**
   * Produce an event when an error occurs fetching a person document
   * @param  {Object} newVal The error
   * @param  {Object} oldVal The old error
   * @event now-avatar-person-error
   */
  _onFetchPersonError: function(newVal, oldVal) {
    this.fire('now-avatar-person-error', newVal);
  },
  /**
   * Sets which items are hidden and shown
   * @param {Boolean} hideIcon   True to hide the icon
   * @param {Boolean} hideLetter True to hide the letter
   * @param {Boolean} hideImage  True to hide the image
   */
  _setHiddenContent: function(hideIcon, hideLetter, hideImage) {
    this._hideIcon = hideIcon;
    this._hideLetter = hideLetter;
    this._hideImage = hideImage;
    this._setStyles();
  },
  /**
   * Fired when the name changes, sets the letter and and styles
   * @param {String} newVal The new value for the name
   */
  _onNameChange: function(newVal) {
    this.avatarLetter = this.name ? this.name.substring(4, 3).toUpperCase() : null;
    if (newVal) {
      this._setHiddenContent(true, false, true);
    }else {
      this._setHiddenContent(false, true, true);
    }
  },
  /**
   * Fires when the ddsViewUrl changes
   * @param  {String} newVal The new name
   * @param  {String} oldVal The old name
   */
  _onDdsViewUrlChange: function(newVal) {
    if (this.name && this.ddsViewUrl && this.ddsViewUrl !== '') {
      this._setHiddenContent(true, false, true);
      var ajax = this.$.viewPeopleAjax;
      ajax.params = {
        sortcolumn: 'FullName',
        keys: this.name
      };
      ajax.generateRequest();
    }else if (this.name && !this.ddsViewUrl) {
      this._setHiddenContent(true, false, true);
    }
  },
  /**
   * Fired when the person changes
   * @param  {Object} newVal The new person
   * @param  {Object} oldVal The old person
   */
  _onPersonChange: function(evt, detail) {
    //console.log(this.is, '_onPersonChange', arguments);
    var person = detail.response;
    this.person = person;
    if (person) {
      if (this.photoUrl) {
        this._photoUrl = this.photoUrl;
        this._setHiddenContent(true, true, false);
      }else if (this.photoField) {
        var mime = person[this.photoField];
        if (person[this.photoField]) {
          var content = person[this.photoField].content;
          for (var key in content) {
            var contentType = content[key].contentType;
            if (contentType.indexOf('image') > -1) {
              var contentTypeArr = contentType.split('"');
              var fileName = contentTypeArr[1];
              this._photoUrl = this.ddsHostName + '/names.nsf/0/' + person['@unid'] + '/$File/' + fileName;
              this._setHiddenContent(true, true, false);
              break;
            }
          }
        }
      }else {
        this._setHiddenContent(true, false, true);
      }
    }else {
      this._setHiddenContent(false, true, true);
    }
    if (this._hideIcon && !this._hideLetter && this._hideImage && this.name) {
      this._setHiddenContent(true, false, true);
    }
  },
  /**
   * Sets the background color, text color and line height
   */
  _setStyles: function() {
    if (this.name) {
      var bgColor = this.getBackgroundColor();
      var contrastColor = this.getContrastingColor(bgColor);
      this.style.background = bgColor;
      this.style.color = contrastColor;
    }
  },
  /**
   * Fires when the viewPerson property changes. Fetches the person
   * @param  {Array} newVal The new array from the query of the view
   * @param  {Array} oldVal The old array
   */
  _onViewPersonChange: function(newVal, oldVal) {
    //console.log(this.is, '_onViewPersonChange', arguments);
    if (newVal && newVal.length > 0) {
      var personEntry = newVal[0];
      this.ddsPersonUrl = this.ddsHostName + personEntry['@link'].href;
      var ajax = this.$.personAjax;
      ajax.generateRequest();
    }else {
      this._setHiddenContent(false, true, true);
    }
  },
  /**
   * Fired when the avatar is tapped. Provides a detail object with
   * person, this element and name
   * @param  {Event} evt The event object
   * @event now-avatar-tapped
   */
  _onTap: function(evt) {
    //console.log(this.is, '_onTap', arguments);
    if (this.clickUrl) {
      window.open(this.clickUrl, '_blank');
    }
    var detailObj = {
      person: this.person,
      elem: this,
      name: this.name
    };
    this.fire('now-avatar-tapped', detailObj);
  },
  /**
   * Get an avatar background color based on the canonical name
   * @return {String}      Hex color
   */
  getBackgroundColor: function() {
    var hash = 0;
    if (this.name) {
      var name = this.name;
      for (var i = 0; i < name.length; i++) {
        hash = name.charCodeAt(i) + ((hash << 5) - hash);
      }
    }
    var colour = '#';
    for (var j = 0; j < 3; j++) {
      var value = (hash >> (j * 8)) & 0xFF;
      var valueStr = value.toString(16);
      var hexColor = ('00' + valueStr).substr(-2);
      colour += hexColor;
    }
    return colour;
  },
  /**
   * Get a contrasting color that will show up on the avatar
   * @param  {String} colour The color to find the contrasint color from
   * @return {String}        css 'color' property value
   */
  getContrastingColor: function(colour) {
    return tinycolor(colour).isDark() ? 'white' : 'black';
  },
  /**
   * Fired when the photoUrl changes
   * @param  {String} newVal The photo url
   * @param  {String} oldVal The old photo url
   */
  _onPhotoUrlChange: function(newVal, oldVal) {
    if (newVal) {
      this._photoUrl = newVal;
      this._setHiddenContent(true,true,false);
    }
  }
});
  </script>
</dom-module>

Quite a bit going on here. The gist of this thing is to check what properties we have defined and make decisions based on those properties so we know what to show (i.e. A letter, icon or image). Also, perform any requests to the server we may need to determine what to show. So the flow through this is as follows:

  1. Look at the properties provided
  2. If no properties defined, just show an icon
  3. If only a name is provided
    1. display the avatar with a background color unique to that name along with the first letter of the common name
  4. If a name and DDS url information is provided
    1. Make a request to the server using the URL provided. We use the ($Users) view so we have to pass the name as the key in order to just return that entry
    2. Once we get that view entry, get the UNID of the document
      1. If we don’t find the person, show an icon
    3. Make another request to get the person document using the UNID from the view entry
    4. Set the “person” property to the response we got back from the server
      1. If the photoField property was defined, get the image from that field
      2. If the photoUrl property was defined, get the image from that url
      3. Show the relevant image in the avatar
  5. If no name is defined and only the photoUrl property is defined, show the photo from the url

The only really interesting bits here are the observers which fire when a value changes. In all my previous component examples I did not show an observer. This functionality is very powerful and allows you to make decisions when a value changes based on the actual value.

The other interesting bit is how the background color is determined. We make a hash from the name (that way it’s the same every time) and then we create a hexadecimal color string based on the hash. This ensures we get the same color for the same name every time, and in theory every name will be it’s own unique color. The biggest problem with this though is how do you select the color of text or of the icon so it’s visible on the auto-generated background color? I elected to use TinyColor. This is a small javascript library for working with colors that seems to be pretty performant.

Now to use this component we just define it in HTML.

<now-avatar
  dds-host-name="http://10.211.55.9"
  dds-view-url="http://10.211.55.9/names.nsf/api/data/collections/unid/912366901F00A457852561C20069B844"
  name="CN=Keith Strickland/O=REDPILL"
  photo-field="UserPhoto">
</now-avatar>

<now-avatar
  class="styledNowAvatar"
  name="CN=Keith Strickland/O=REDPILL">
</now-avatar>

<now-avatar
  photo-url="http://www.gravatar.com/avatar/2b14efcb21ba7256e13a981392832c84?d=404">
</now-avatar>

<now-avatar
  click-url="http://redpillnow.com">
</now-avatar>

There you have it, a rather robust avatar component with some smarts built into it so it know what to do with the information provided. If you wish to use this component visit it’s repository on GitHub and to see the docs and demo, visit my github host account. Now on the demo page, you’ll probably see failed requests because it can’t get to my domino directory, but I hope you’ll get the idea.

Until next time… Happy Coding!

 

 

Share This:



---------------------
http://keithstric.me/2016/10/12/a-polymer-avatar-component/
Oct 12, 2016
4 hits



Recent Blog Posts
4
Thoughts on TypeScript
Wed, Sep 6th 2017 9:25p   Keith Strickland
Over the past few months I’ve started working pretty extensively with TypeScript. For those of you who don’t know what TypeScript is: TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. It provides strong types to JavaScript. It allows for the creation of classes and enforces those classes in your code. If you define a Redpill.Widget class, you can then use that class in your code and the editor enforces the rules you define within that class. For example,
7
Replacing Lotus is…. complex
Fri, Sep 1st 2017 9:44p   Keith Strickland
If you followed Peter’s series on replacing Lotus he outlined some of the pitfalls, processes and decision points to undertake for success. I wanted to point out the technical side to a lot of those decisions. The short answer is that you need a tool to surface your domino data en-masse until such a time when decisions are made on each application. I have been working on that solution for quite some time now and I have to say, it’s complex. First you need to make a fundamental decisi
3
Web Component Thoughts….
Fri, Aug 25th 2017 8:55p   Keith Strickland
The past 1.5 years I’ve been working exclusively with Web Components and specifically Polymer. The more I use this technology the more convinced I am that this is the technology I should be using. Now, I’m not saying that Web Components and Polymer are hammers and every problem/project is a nail. However it’s quite refreshing that Polymer’s goal is to make itself irrelevant. What does that mean, Polymer is there temporarily until the browsers decide upon common standards
5
Just…. NO,NO,NO
Fri, Aug 11th 2017 7:45p   Keith Strickland
This week I attended MWLUG in Alexandria, VA. This was an awesome event, so many good speakers, good content and excellent camaraderie. I can’t say it enough, but Richard Moy and his team put on such a good event. So, I spoke to a couple of developers who are writing client JavaScript in Domino Designer. While Domino Designer is capable of allowing you to write JavaScript, all I can say is STOP! Stop torturing yourself, Stop making it harder on yourself to write good JavaScript. There are
4
Goodbye Evernote
Tue, Jan 17th 2017 5:22p   Keith Strickland
I’ve been using Evernote for a few years now and have enjoyed it’s feature set and the ability to plan and document a complex project (namely home/shop projects) with shopping lists, ideas, etc. But recently every time I attempt to use Evernote to create a quick note or maybe just jot something down, I’m presented with a request to upgrade to a pay plan, or to update or just general advertising. I can no longer just open it and create a note. Because of this, I have now backed
4
Merry Christmas!
Sun, Dec 25th 2016 10:57p   Keith Strickland
Merry Christmas!! I hope everyone is having a great holiday. I thought now might be a good time to look back over the year and review some of the technology I’ve dealt with. Surface Pro 4: Last year I got a Surface Pro 4 tablet. I started the process of switching to it instead of my aging MacBook Pro. While I REALLY like the hardware and how everything works there were just a few issues which got on my nerves so bad I couldn’t ever completely make the switch. As far as performance, I




Created and Maintained by Yancy Lent - About - Planet Lotus Blog - Advertising - Mobile Edition