11

Does anyone have a working example of how to send and receive window.postMessage() calls in angular? I found the ng-post-message module on github and ngmodules, but I look at that code and it doesn't make a whole lot of sense to me and the documentation is lacking a working example.

Edit: to add my failed attempt

The view

<div simulation-host element="thing in things"></div>
</div>
<div id="debugConsole">
    <h1>MESSAGE CONSOLE</h1>
    <p id="debugText"></p>
</div>

The model

$scope.things = 
[
    {
        "location"  :   "Foobar",   
        "resource"  :   $sce.trustAsResourceUrl("http://external.domain:14168/Foo/Bar"), 
        "title"     :   "Launch"    
    }
];

My attempt at a directive

var simulationFrameHost = angular.module('proxy.directives', []);
simulationFrameHost.config(function ($sceDelegateProvider){
    //URL Regex provided by Microsoft Patterns & Practices.
    $sceDelegateProvider.resourceUrlWhitelist(['^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&amp;%\$#_]*)?$','self']);
});

simulationFrameHost.directive('simulationHost', ['$window',function($window) {
    return {
        retrict: 'ACE',
        transclude: 'element',
        replace: true,
        scope: true,
        template: [
            '<ul>',
                '<li>',
                    '<span>',
                        '<a href="#">',
                            '{{thing.location}}',
                        '</a>',
                    '</span>',
                    '<messenger>',
                        '<iframe ng-src="{{thing.resource}}"  />',
                    '</messenger>',
                '</li>',
            '</ul>'
        ].join(''),
        compile: function (tElement, tAttrs, transclude) {
            var interval;            

            var show = function(msg)
            {
                var debugText = document.getElementById("debugText");
                if(debugText){
                    debugText.innerHTML += msg + "<br/>";
                }
            };
            var rpt = document.createAttribute('ng-repeat');
            rpt.value = tAttrs.element;
            console.log(tAttrs.element);
            tElement[0].children[0].attributes.setNamedItem(rpt);

            $(tElement[0].children[0].children[0].children[0]).on("click", function(event){

                console.log(event);
                var iframe = tElement[0].children[0].children[1].children[0].contentWindow;
                show("Initiating connection with: " + event.currentTarget.host);
                var message = {
                    action: 'syn',
                    param: 'connection'
                };

                interval = setInterval(function(){

                    //*****************************************
                    iframe.postMessage(JSON.stringify(message), 'http://'+ event.currentTarget.host);
                    //*****************************************

                }, 500);
                return false;               
            });



        }
    }
}]);

Working legacy code that I am trying to adapt to Angular

Note that this code uses a popup rather than an iframe; ran into the complication that IE's postMessage between windows is broken so have to fall back to iframe.

Markup

<body>
        <div id="debugConsole">
            <h1>MESSAGE CONSOLE</h1>
            <p id="debugText"></p>
        </div>
        <h1>This is a test</h1>

        <ul>
            <li>
                <a href= "http://external.domain:14168/Foo/Bar" target="_blank" ><p>Foobar</p></a>
            </li>
        </ul>
        <script src="js/jquery-1.10.2.min.js"></script> 
        <script src="bower_components/angular/angular.js"></script>
        <script src="bower_components/angular-animate/angular-animate.js"></script>
        <script src="bower_components/angular-route/angular-route.js"></script>
        <script src="bower_components/angular-resource/angular-resource.js"></script>
        <script src="js/SCORM_API_wrapper.js"></script>
        <script src="js/json2.js"></script>
        <script src="js/plugins.js"></script>
        <script src="js/index.js"></script>
    </body>

index.js

$('a').on("click",function(event){
    console.log(event);
    var pop = window.open(event.currentTarget.href, 'poop');
    show("Initiating connection with: " + event.currentTarget.host);
    var message = {
        action: 'syn',
        param: 'connection',
    };

    interval = setInterval(function(){
        pop.postMessage(JSON.stringify(message), 'http://'+ event.currentTarget.host);
    }, 500);
    return false;

});

$(window).on("message", function(e) {
    clearInterval(interval);

    var eventData = JSON.parse(e.originalEvent.data);
    show("Message received from: " + e.originalEvent.origin);
    if(eventData.action) {
        switch (eventData.action) {
            case 'syn-ack':
                ack(e.originalEvent, eventData.param, eventData.value);
                break;
            case "set":
                show("Set request received: " + e.originalEvent.data);
                set(eventData.param, eventData.value);
                break;
            case "get":
                show("Get request received: " + e.originalEvent.data);
                var value = get(eventData.param);
                var response = {
                    action: "response",
                    type: "get",
                    param: eventData.param,
                    value: value
                };
                e.originalEvent.source.postMessage(JSON.stringify(response), channel);
                break;
        }
    }
});

In my directive's compile, I'm trying to wire up a click event to the generated anchor tag. I'm trying to get the click to post a message to the iframe, but iframe.postMessage is doing nothing. It just goes off into the nether, and I've been working on this since 10 this morning. My eyes are starting to glaze over : p

Edit: Adding an extension requirement (now that I have functioning code) for a general messaging directive between separate containers, regardless of the container type:

1)iframe to parent

2)window to window (<=yes, I already know this doesn't work in IE)

I had legacy code working that performed window to window messaging by having the window that spawned the second post a "syn" message to it immediately after creating it. The second window then received the message as a "syn" and stored the sender as a messageHandle so that it could maintain a channel to post return messages then returned a "syn-ack." The originator followed up with an "ack" and the secondary window received the ack and proceeded with its work. (If the ack did not return before the timeout, I logged that the connection had failed and then the secondary window polled on an interval to attempt to restore the connection)

K. Alan Bates
  • 3,104
  • 5
  • 30
  • 54
  • Could you post a sample of the code you tried, or maybe the equivalent plain js code you're trying to emulate in angular? – Austin Mullins Jul 28 '14 at 02:25
  • 1
    It would be difficult to refactor my current code to isolate only the attempts to postMessage. I can try though. – K. Alan Bates Jul 28 '14 at 02:30
  • 1
    I cheated. $(window).on("message",function(){}); in the controller. I spent way too much time fooling with trying to get that working the "Angular Way." Took about 10 minutes to get it working the "Practical Way." I'll refactor if I have time. – K. Alan Bates Jul 29 '14 at 12:49
  • Practical works. Go ahead and post that as your answer. – Austin Mullins Jul 29 '14 at 12:57

2 Answers2

6

Just came across this post, this may help you.

(function() {
    'use strict';

    angular
    .module('postmessage')
    .factory('iFrameMessagingService', iFrameMessagingService);

iFrameMessagingService.$inject = ['$window', '$location', '$rootScope', '$log'];

function iFrameMessagingService($window, $location, $rootScope, $log) {
    var service = {
        sendMessage : sendMessage
    };

    activate();
    return service;

    function activate(){
        activateIncomingMessageHandler();
    }

    function activateIncomingMessageHandler(){
        $window.addEventListener('message', function(event){
            if (typeof(event.data) !== 'undefined'){

               // handle message
            }
        });

    }

    function sendMessage(message){
        // Dispatch the event.
        $log.debug(message);
        if($window.parent !== $window){
            $window.parent.postMessage(message, '*');
        }
    }

}
})();
Matt
  • 364
  • 4
  • 10
4

I couldn't get this working with an Angular directive. I pulled a wasted all nighter trying to get this done "The Right Way" and wish I had ejected that idea sooner because my requirements didn't really warrant it. This thing doesn't have to scale because it is purpose build software for providing a messaging proxy between X-Domain systems.

'use strict'
var app = angular.module('domain.system.controllers', ['services.proxy.mine']);
app.controller('ProxyCtrl', ['$scope', '$routeParams', '$window', '$sce', 'MyService',
                function    ( $scope,   $routeParams,   $window,   $sce,   MyService)
               {
                    $($window).on("message", function(e){
                       var message = JSON.parse(e.originalEvent.data);
                       if(message.recipient){
                            switch(message.recipient){
                                case: "ProxyCtrl":
                                       //handle message;
                                       break;
                            }
                       }
                    }
              }
]);

I am 100% interested in a detailed explanation of how to convert this code into a functioning directive.

K. Alan Bates
  • 3,104
  • 5
  • 30
  • 54
  • +1 this! I've just spent about 3 hours myself trying the angular directive before reluctantly falling back to jQuery. Similarly would be interested if somebody could tell us what we're missing. – Gavin Gilmour Jan 28 '15 at 11:31
  • I wrote this question quite a while back. I haven't had a need to revisit this, but I've gotten quite a bit better with directives. The next time I'm working in one of my angular clients, I'll try to find a few minutes to see if I can throw something together that might help you out. – K. Alan Bates Feb 04 '15 at 20:00
  • 2
    Could you maybe replace `$($window)` with `angular.element($window)`? – jeerbl Feb 09 '16 at 14:58
  • Don't forget to remove the listener in `$scope.$on("$destroy", ...)` – Etan Apr 05 '16 at 19:12