Search code examples
twitterember.jsfacebook-javascript-sdkfacebook-likeember-cli

Ember-cli & Facebook-share buttons - how to?


Goal - Have share buttons for Facebook

I've had various problems, I've managed to solve most issues but never all at the same time.

  • ReferenceError: FB is not defined (error in the console)
  • "FB init function gives wrong version error" (error in the console)
  • My button does not render after transitioned to another route.
  • My button renders but there is no spacing around the button to adjacent stuff on the initial route

About that rendering issue (the - / hyphen is there to notice the spacing issue)

  • Rendering for the 1st time:

1st render

  • Rendering for the 2nd+ time:

2nd+ render

What I've learned:

  • Facebook wants a <div id="fb-root"></div> as the first element inside your <body>
  • FB.XFBML.parse() can be called after didInsertElement to render a component after a transition

I made a JSBin boilerplate attempt, it's currently stuck at a undefined FB error.

Partial answer I'm also interested in:

  • Understanding how complex the solution at least must me to achieve a good result ("it must include an initializer, and a view/component!" or "you can solve this by just having ...")

Parts that may be of use

A post about "after FB.init()"

An initializer

/* global FB */
export default {
  name: 'facebook',

  initialize: function() {
    var fbAsyncInit = function() {
      FB.init({
        appId      : 123,
        xfbml      : true,
        version    : 'v2.2'
      });
    };

    (function(d, s, id){
       var js, fjs = d.getElementsByTagName(s)[0];
       if (d.getElementById(id)) {return;}
       js = d.createElement(s); js.id = id;
       js.src = "//connect.facebook.net/en_US/sdk.js";
       fjs.parentNode.insertBefore(js, fjs);
     }(document, 'script', 'facebook-jssdk'));

    window.fbAsyncInit = fbAsyncInit;
  }
};

A component

/* global FB */
import Ember from 'ember';

export default Ember.Component.extend({
    tagName: 'div',
    classNames: 'fb-like',
    attributeBindings: [
        'data-href',
        'data-layout',
        'data-action',
        'data-show-faces',
        'data-share'
    ],
    
    onDidInsertElement: function() {
        Ember.run.schedule('afterRender', FB.XFBML.parse);
    }.on('didInsertElement'),

    init: function() {
        Ember.run.schedule('afterRender', FB.XFBML.parse);
    }
});

A script tag

<script type="text/javascript" src="//connect.facebook.net/en_US/sdk.js#xfbml=1&appId=123&version=v2.2"></script>

The root div facebook asks for

<div id="fb-root"></div>

Solution

  • Loading Facebook SDK

    import ENV from "my-app/config/environment";
    import setupOfflineMode from "my-app/utils/offline-mode";
    
    export function initialize(container, application) {
      // offline mode stubs `FB`
      if (ENV.offlineMode) { return setupOfflineMode(); }
    
      // Wait for Facebook to load before allowing the application
      // to fully boot. This prevents `ReferenceError: FB is not defined`
      application.deferReadiness();
    
      var fbAsyncInit = function() {
        initFacebook(window.FB);
        application.advanceReadiness();
      };
    
      loadFacebookSDK();
    
      window.fbAsyncInit = fbAsyncInit;
    }
    
    function loadFacebookSDK() {
      (function(d, s, id){
        var js, fjs = d.getElementsByTagName(s)[0];
        if (d.getElementById(id)) {return;}
        js = d.createElement(s); js.id = id;
        js.src = "//connect.facebook.net/en_US/sdk.js";
        fjs.parentNode.insertBefore(js, fjs);
      }(document, 'script', 'facebook-jssdk'));
    }
    
    function initFacebook(FB) {
      FB.init({
        appId: ENV.FB_APP_ID,
        status: true,
        cookie: true,
        xfbml: true,
        version: ENV.GRAPH_API_VERSION
      });
    }
    
    export default {
      name: 'facebook',
      initialize: initialize
    };
    

    Share Links

    I think this is all I needed to do; I hope I'm not forgetting something...

    I didn't set up a component, so this is just a regular view, but it should work about the same.

      <div class="fb-share-button" {{bind-attr data-href=link}} data-type="button"></div>
    
    export default Ember.View.extend({
      setupSocialNetworks: function() {
        Ember.run.scheduleOnce('afterRender', this, function() {
          FB.XFBML.parse();
        });
      }.on('didInsertElement')
    });
    

    UPDATE: Alternate solutions

    I think which solution you use really depends on your needs. I'm focusing on a faster time-to-first-render, so I've changed my project to not deferReadiness for the Facebook SDK.

    I've been playing with two solutions, again I think it totally depends on your needs.

    1. Load the Facebook SDK in an initializer, but set a global promise for access.

      This starts the loading on boot, but allows your application to continue booting without having to wait for Facebook. All calls to the Facebook API need to be accessed through the promise.

      I'll share the details of this one if requested, but for now I'll just focus on the next solution:

    2. Load the Facebook SDK only on demand in a service.

      As before, all access to the Facebook API will need to go through a promise, but this time it is nicely encapsulated in a service and is only loaded on demand:

    // app/services/facebook.js
    import Ember from 'ember';
    import ENV from "juniper/config/environment";
    
    var computed = Ember.computed;
    var RSVP = Ember.RSVP;
    
    var _facebookSDKDeferrable = Ember.RSVP.defer();
    
    var fbAsyncInit = function() {
      _initFacebook(window.FB);
      _facebookSDKDeferrable.resolve(window.FB);
    };
    
    window.fbAsyncInit = fbAsyncInit;
    
    export default Ember.Service.extend({
      // Resolves when the Facebook SDK is ready.
      //
      // Usage:
      //
      //     facebook: Ember.inject.service(),
      //     foo: function() {
      //       this.get('facebook.SDK').then(function(FB) {
      //         // Facebook SDK is ready and FB is a reference to the SDK
      //       });
      //     }
      SDK: computed.alias('FB'),
      FB: computed(function() {
        _loadFacebookSDK();
        return _facebookSDKDeferrable.promise;
      })
    
      // I've also put promisified helpers for Facebook SDK
      // methods here.
    });
    
    function _loadFacebookSDK() {
      (function(d, s, id){
        var js, fjs = d.getElementsByTagName(s)[0];
        if (d.getElementById(id)) {return;}
        js = d.createElement(s); js.id = id;
        js.src = "//connect.facebook.net/en_US/sdk.js";
        fjs.parentNode.insertBefore(js, fjs);
      }(document, 'script', 'facebook-jssdk'));
    
    }
    
    function _initFacebook(FB) {
      FB.init({
        appId: ENV.FB_APP_ID,
        status: true,
        cookie: true,
        xfbml: false,
        version: ENV.GRAPH_API_VERSION
      });
    }
    

    Note also that I've set xfbml to false in FB.init. If your share links have been rendered before the SDK gets loaded, FB.init({ xfbml: true }) will "parse" them, and your FB.XFBML.parse() call will do it again. By setting xfbml to false, you ensure that FB.XFBML.parse() will only get called once.