Cross Framework Messaging
With the proliferation of Javascript frameworks, I found myself in the situation where I needed two different UI elements — built with two different Javascript frameworks — to communicate with each other.
Rather than choosing to migrate one onto a different framework, I looked at a way of allowing them to send messages to each other using the Mediator Design Pattern.
Using this pattern, I have an example of an Angular app, a React app, and a plain Javascript app communicating.
The Mediator Pattern
The Mediator pattern defines an object which encapsulates how a set of objects interact. For our purposes, it can be thought of as similar to the Pub/Sub pattern. In a conventional Pub/Sub pattern, any object can implement it — allowing other objects to subscribe to events, and publishing out events to subscribers. This allows decoupled, agent to agent, distributed communication.
Our Mediator pattern implements the same Pub/Sub pattern, but does so using a central object with which all objects communicate — i.e. that object mediates the communications.
For the purposes of this work, I will use a mediator module from github (https://github.com/ajacksified/Mediator.js).
A Demonstration Application
The following screenshot shows the application I will be building to demonstrate this pattern:
It consists of three panels, one with an Angular app, one with a React app and one with a plain Javascript app. Each panel has an active input field into which I can type, plus two read-only fields which I am looking to populate based on content typed into the other apps. So, for example, I want the Angular Input field in the React panel to echo text typed into the Angular Input field in the Angular panel.
I needed to build this demonstrator quickly, so the code works rather than being perfect, but it is fine for illustrating the principles.
The first thing I did was to make a mediator object available to any Javascript app. I wrote this directly in the HTML, but it would be better to do this using proper modularisation:
<!-- Mediator for cross-framework comms -->
<script src="js/mediator.min.js"></script>
<script>
window.mediator = new Mediator();
</script>
<!-- End of mediator -->
Using the Mediator in Plain Javascript
I’ll start with the plain Javascript use of the mediator, since that will be
the simplest to understand. Firstly, I want to publish to the mediator
whenever anything is typed into our Javascript input text field (so the other
apps can see it). The input field has an ID of j.javascriptInput
, so I add
an event listener for input events, check to see whether the input event is
on my j.javascriptInput
element and, if it is, publish a javascript event
to the mediator with a message containing the content of that input field:
Next, I want to subscribe to angular and react events from the mediator.
When I receive such an event, I will update the read-only j.angularInput
and
j.reactInput
fields.
Here is the full code for that:
// Emit an event when the "Javascript" input field is updated
window.addEventListener("input", function (e) {
if (e.srcElement.id === "j.javascriptInput") {
mediator.publish("javascript", e.srcElement.value);
}
});
// Watch for changes from Angular
mediator.subscribe("angular", function (data) {
document.getElementById("j.angularInput").value = data;
});
// Watch for changes from React
mediator.subscribe("react", function (data) {
document.getElementById("j.reactInput").value = data;
});
This gives us all the functionality we need for our plain Javascript app, but it doesn’t have anything else to talk to, so we can’t test it. Let’s build our second app.
Using the Mediator in Angular
If I were writing an actual Angular application, I would create a mediator service and then inject it into the components which needed to use it. However, for the purposes of this example, I will just use a variable in the controller.
The Angular app has the same basic structure as the plain Javascript, but sprinkled with additional Angular goodness. Let’s take a quick look at the HTML first. I’ve defined the Angular app and controller on the panel containing all the fields. Then I’m binding my input fields to input.angular, input.react and input.javascript:
<div class="panel panel-primary" ng-app="app" ng-controller="appController">
<div class="panel-heading">
<h3 class="panel-title">Angular</h3>
</div>
<div class="panel-body">
<form>
<div class="form-group has-success">
<label for="a.angularInput">Angular Input</label>
<input type="text" class="form-control" id="a.angularInput"
ng-model="input.angular">
</div>
<div class="form-group">
<label for="a.reactInput">React Input</label>
<input type="text" class="form-control" id="a.reactInput"
ng-model="input.react" readonly="readonly">
</div>
<div class="form-group">
<label for="a.javascriptInput">Javascript Input</label>
<input type="text" class="form-control" id="a.javascriptInput"
ng-model="input.javascript" readonly="readonly">
</div>
</form>
</div>
</div>
The Angular code itself starts by creating our $scope and putting input.angular, input.react and input.javascript into it so it can be used in the above HTML.
Next we create the mediator object I talked about earlier.
We then want to be able to publish changes to the mediator when the Angular input field changes. To achieve this, we $watch the input.angular variable. When there are changes, our $watch function publishes the new value to the mediator as an angular event.
To see changes to other fields, we subscribe to their events. When our subscribe detects an event, it simple sets input.react or input.javascript (on the $scope) to the new value. Note that I had to wrap that in a $apply so that Angular is aware of the change.
Here is all the code to make this happen:
angular.module("app", []).
controller("appController", function ($scope) {
var mediator;
// Variables to bind to our inputs
$scope.input = {
angular: "",
react: "",
javascript: ""
};
// Our shared mediator
mediator = window.mediator;
// Look for changes in the angular input field
$scope.$watch("input.angular", function (newValue) {
mediator.publish("angular", newValue);
});
// Handle updates from the mediator
mediator.subscribe("react", function (data) {
$scope.$apply(function () {
$scope.input.react = data;
});
});
mediator.subscribe("javascript", function (data) {
$scope.$apply(function () {
$scope.input.javascript = data;
});
});
});
Now we have two applications, we can test that our communication is working:
Using the Mediator in React
I’ll discuss the React code in two parts, starting with the React createClass. I’m defining an InputField class and it will contain the three input fields from the previous examples, this time with the React field as the one which takes data input.
I set the value for each field from the React props for the class. I also bind the onChange for the React Input field to an onChange function, which simply publishes the field value to the mediator:
// Create our input field class
var InputField = React.createClass({
render: function () {
return (
<form>
<div className="form-group">
<label htmlFor="r_angularInput">Angular Input</label>
<input type="text" className="form-control" id="r_angularInput" readOnly="readonly" value={this.props.angular} />
</div>
<div className="form-group has-success">
<label htmlFor="r_reactInput">React Input</label>
<input type="text" className="form-control" id="r_reactInput" onChange={this.onChange} />
</div>
<div className="form-group">
<label htmlFor="r_javascriptInput">Javascript Input</label>
<input type="text" className="form-control" id="r_javascriptInput" readOnly="readonly" value={this.props.javascript} />
</div>
</form>
);
},
onChange: function (e) {
mediator.publish("react", e.target.value);
}
});
Our React Input field is already built into this, but we need some way of populating the Angular and Plain Javascript fields. We do this by providing them as props into our InputField tag in the ReactDOM render function. Then, whenever we need to update them, we can just call render again.
I then added two subscriptions to the mediator for angular and javascript which, when triggered, update the content of the inputs and call render again to refresh the display:
// This is where we externalise our Angular and Javascript inputs
var inputs = {
angular: "",
javascript: ""
};
// Render the react elements
function render() {
ReactDOM.render(
<InputField angular={inputs.angular} javascript={inputs.javascript} />,
document.getElementById("react-root")
);
}
// Subscribe to angular events
mediator.subscribe("angular", function (data) {
inputs.angular = data;
render();
});
// Subscribe to javascript events
mediator.subscribe("javascript", function (data) {
inputs.javascript = data;
render();
});
// Now actually render
render();
This then brings the React panel into the communications and whichever panel we type into, the output is reflected in the other panels: