Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to embed multiple Facebook videos (with React)

Reading from Facebook docs, it seems very easy to embed a Facebook video. However, the <div id="fb-root"></div> must be just before the script and the fb-video class div (from what I've observed). So if I want to embed a video I have to do something like:

<div className="card-video-wrapper">
    <div id="fb-root"></div>
    <script>(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/es_ES/sdk.js#xfbml=1&version=v2.8";
         fjs.parentNode.insertBefore(js, fjs);
    }(document, 'script', 'facebook-jssdk'));</script>
    <div className="fb-video" data-href="my-video-url" data-show-text="false" data-autoplay="true"></div>
</div>

I first thougth that the <div id="fb-root"></div> and the script should go just after the opening body tag, but in this way the video keeps hidden at the top of the page.

The problem comes when I want to embed multiple Facebook videos like:

<div className="card-video-wrapper">
    <div id="fb-root"></div>
    <script>(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/es_ES/sdk.js#xfbml=1&version=v2.8";
         fjs.parentNode.insertBefore(js, fjs);
    }(document, 'script', 'facebook-jssdk'));</script>
    <div className="fb-video" data-href="my-video-url-1" data-show-text="false" data-autoplay="true"></div>
</div>
<div className="card-video-wrapper">
    <div id="fb-root"></div>
    <script>(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/es_ES/sdk.js#xfbml=1&version=v2.8";
         fjs.parentNode.insertBefore(js, fjs);
    }(document, 'script', 'facebook-jssdk'));</script>
    <div className="fb-video" data-href="my-video-url-2" data-show-text="false" data-autoplay="true"></div>
</div>

In this case, the second one is not displayed.

Probably, <div id="fb-root"></div> is supoposed to be placed once, but as I've said, then the video is hidden. Let's see an example:

<body>
    <div id="fb-root"></div>
    <script>(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/es_ES/sdk.js#xfbml=1&version=v2.8";
         fjs.parentNode.insertBefore(js, fjs);
    }(document, 'script', 'facebook-jssdk'));</script>

    ...

    <div className="card-video-wrapper">
        <div className="fb-video" data-href="my-video-url-1" data-show-text="false" data-autoplay="true"></div>
    </div>
    <div className="card-video-wrapper">
        <div className="fb-video" data-href="my-video-url-2" data-show-text="false" data-autoplay="true"></div>
    </div>

    ...

</body>

Inspecting with the navigator console I see these styles applied to the childs of fb-root layer:

position: absolute;
top: -10000px;
height: 0px;
width: 0px;

What is the way to proceed to embed multiple Facebook videos?

Edit:

One would think that it has to be related to CSS. However, even removing all CSS rules I see the same problem, the videos are not shown (even just one video) when the <div id="fb-root"></div> and the script are placed after the starting body tag.

On the other hand, I've tested it with a simple page (as @hackerrdave suggested), and it works. Now I'm wondering what could be the problem. It's a React application, using some Framework7 components (I'm not sure if this could be relevant).

like image 945
Manolo Avatar asked Jan 10 '17 10:01

Manolo


3 Answers

The use of React is relevant, because the first DOM rendering does not contain any fb-video element.

The problem is that the fb-video element is rendered when the component receives the elements to render. Then, when the SDK loads, there are no videos to render. If I load the SDK once the elements are received, then the videos are shown.

Edit:

Thanks to @CBroe for the tip. A better solution is to use FB.XFBML.parse(); in the componentDidMount and/or componentDidUpdate function. In this way, I can put the <div id="fb-root"></div> and the script after the body tag.

componentDidMount() {
    FB.XFBML.parse();
}
componentDidUpdate() {
    FB.XFBML.parse();
}
like image 82
Manolo Avatar answered Oct 24 '22 09:10

Manolo


This is a workaround, because I could not find the problem exactly, but I am answering anyway since it leads to something workable. You have been warned.

I found that the Facebook SDK adds a span that somehow sits in the way. By creating your own iframe with exactly the same src and some other attributes, the video will display properly. If Facebook has a callback somewhere that indicates when it is done loading, we can use that to do exactly that. However, I could not find such a callback so I resolved to using MutationObserver.

The meat of the solution is in the onFacebookLoaded function below. Although I did not use React, I am pretty sure this should work there too. Obviously you can change which attributes you are interested in - there is allowfullscreen amongst others, that may be of importance.

The rest of the below code only waits until Facebook has loaded the videos and stops listening for changes after that. It is written in such a way that any number of videos on the page should work.

var onFacebookLoaded = function() {
      // replace .fb-video div with iframe
      var oldFrame = this.querySelector('iframe'),
          newFrame = document.createElement('iframe'),
          attrsWeWant = new Set();
      attrsWeWant.add('src').add('scrolling').add('title')
      .add('width').add('height').add('frameborder');
      if (oldFrame.hasAttributes()) {
        var attrs = oldFrame.attributes;
        for (let i = attrs.length - 1; i >= 0; --i) {
          if (attrsWeWant.has(attrs[i].name)) {
            newFrame.setAttribute(attrs[i].name, attrs[i].value);
          }
        }
      }
      this.parentNode.replaceChild(newFrame, this);
    },
    target = document.querySelectorAll('.fb-video'),
    count = target.length,
    observer = new MutationObserver(function(mutations) {
      mutations.forEach(function(mutation) {
        if (mutation.target.fbLoadedSeen !== true) {
          onFacebookLoaded.call(mutation.target);
          mutation.target.fbLoadedSeen = true;
          count--;
        }
      });

      // cleanup when all videos have loaded
      if (count === 0) {
        observer.disconnect();
      }
    });

// connect to updates
for (let i = 0; i < target.length; ++i) {
  observer.observe(target.item(i), {attributes: true});
}
<div id="fb-root"></div>
<script>
  (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 = "https://connect.facebook.net/en_US/sdk.js#xfbml=1&version=v2.8";
    fjs.parentNode.insertBefore(js, fjs);
  }(document, 'script', 'facebook-jssdk'));
</script>

<h1>Video 1</h1>
<div class="fb-video"
     data-href="https://www.facebook.com/facebook/videos/10153231379946729/"
     data-width="300"
     data-height="300"
     data-allowfullscreen="true"></div>

<h1>Video 2</h1>
<div class="fb-video"
     data-href="https://www.facebook.com/facebook/videos/10153231379946729/"
     data-width="500"
     data-height="400"
     data-allowfullscreen="true"></div>
like image 33
Just a student Avatar answered Oct 24 '22 11:10

Just a student


[from other answer by OP] The problem is that the fb-video element is rendered when the component receives the elements to render. Then, when the SDK loads, there are no videos to render. If I load the SDK once the elements are received, then the videos are shown.

Yes, the SDK goes through the document and looks for elements to parse once on initialization.

But is has a method specifically for the purpose of rendering content that was added to the document after SDK initialization: FB.XFBML.parse

It can be called without any argument to go look through the whole document again, or it can be passed a DOM element, so that it will only look through a particular DOM subtree.

like image 44
CBroe Avatar answered Oct 24 '22 10:10

CBroe