The wiring to do this is mostly there, but lightning apps don't have access to Sfdc.canvas
at present, it seems. If they did have access to Sfdc.canvas
then this would work.
You can send a message from the canvas app, and it will reach the containing window, but it seems there's no way to listen for it at present. You can also subscribe to events in the canvas app, and if you construct and post a correctly formatted message from the top window, the canvas app will receive it. Funnily enough, onCanvasSubscribed
will fire when the canvas app subscribes, but I cannot for the life of my figure out why it would, when it seems impossible to publish
a message from the lightning component.
So, in short, the answer to this is "it's not possible". The long answer is "it's a very bad idea, because you need to partially reimplement the canvas event messaging protocol, which exposes you to the risk that the unpublished, proprietary protocol will change, thus breaking your app without warning."
A note on access to Sfdc.canvas
The Salesforce Spring ’16 Release Notes state that Sfdc JavaScript Global Is Removed for lightning components, however:
The sole exception is the official Force.com Canvas API, which is available using the Sfdc.canvas object. Canvas APIs as documented in the Canvas Developer Guide remain available and are fully supported.
This seems to suggest that Sfdc.canvas
ought to be available to lightning components, and the fact that it appears to be unavailable could be considered a defect.
Warning: rest of this post covers non-standard, probably unsupported techniques
So, for posterity and those unmotivated by pragmatic programming practices, this is how you can send and receive messages from a canvas app within lightning.
Sending messages from canvas to lightning
Send a message as normal from the canvas app:
var srClient = signedRequest.client;
Sfdc.canvas.client.publish(
srClient,
{name : "mynamespace.statusChanged", payload : {status : 'Completed'}});
You ought to be able to listen for this message using:
Sfdc.canvas.parent.subscribe('mynamespace.statusChanged', callbackFn);
And indeed, if you run this in the javascript console, you can listen for these messages. If only lightning components had access to Sfdc.canvas
! Since we don't, the only way to receive this message in the lightning component is by listening for it directly on the window:
window.addEventListener('message', function (event) {
var data = JSON.parse(event.data);
if (data.targetModule === 'Canvas' &&
data.body &&
data.body.event &&
data.body.event.name === 'mynamespace.statusChanged')
console.log('payload from canvas app', data.body.event.payload);
}, false);
Sending messages from the lightning component to canvas app
We can subscribe to messages as normal in the canvas app:
var srClient = signedRequest.client;
Sfdc.canvas.client.subscribe(
srClient,
{
name : 'johnny.begood',
onData : function (event) {
console.log("Subscribed to custom event ", event);
}
});
// at this point, onCanvasSubscribed
in the lightning component seems to fire
It's very odd that onCanvasSubscribed
fires at this point. That seems pointless if there's no way official way to publish. Perhaps there is a way, but if there is, I haven't been able to find it.
Ideally, you'd be able to publish an event from the lightning app this way:
Sfdc.canvas.parent.publish('mynamespace.statusChanged', payload);
As with subscribing, this is not possible, but there is a workaround. From the lightning component, post a correctly formatted message to the outer wrapper iframe:
// canvas apps contain both an inner and outer iframe. The canvas app is
// in the inner iframe, but we post our message to the outer iframe
var outerIframe = document.querySelector('.forceCanvasApp iframe');
// we need the correct target URL for the outer iframe. hard-coding this
// is a Bad Idea. How to do it otherwise left as an exercise for the reader
// Hint: listen for the subscribe message via
// `window.addEventListener('message', listener)`
var wrapperUrl = 'https://my-domain.my.salesforce.com';
// to correctly format this message, you'll need to listen on the window
// for the canvas app subscribe message
// and extract the instance id
var instanceIdFromClientSubscribeMessage = '_:test01:6:0:canvasapp';
var payload = { whatever: 'you want' };
var wrappedPayload = {
parentVersion: '39.0',
clientVersion: '34.0',
body: {
type: 'event',
config: {
client: {
instanceId: instanceIdFromClientSubscribeMessage
}
},
event: {
name: 'johnny.begood',
method: 'onData',
payload: payload
}
}
}
outerIframe.contentWindow
.postMessage(JSON.stringify(wrappedPayload), wrapperUrl);
It's Possible!
In a previous version of this answer, I stated that this was not possible. After working with some Lightning Developers at Salesforce (many thanks, JF & Jaswinder), we were able to work out how to get Lightning components to work with a component's custom renderer.
Note that this is not exactly canon, and we're working on the edge (maybe slightly beyond) of what's supported, so if you go this route, you are accepting the responsibility to maintain it in the future if some low-level change in Lightning suddenly breaks everything. Then again... isn't that the nature of software development? 😉
The example below was built with API version 41.
Container Component
The container component is the component that has my manually manipulated DOM elements. It's where I want to display other Lightning Components.
Component XML
<aura:component implements="flexipage:availableForAllPageTypes" access="global" >
<aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
<div aura:id="root"></div>
<div aura:id="renderBox" style="display: none;"></div>
</aura:component>
Helper
({
createComponent: function(component, componentType, componentAttributes, targetElement){
$A.getCallback(function(){
$A.createComponent(
"c:wrapper",
{
componentType: componentType,
componentAttributes: componentAttributes,
targetElement: targetElement,
},
function(wrapperComponent, status, errorMessage){
if (status === "INCOMPLETE") {
return;
}
else if (status === "ERROR") {
return;
}
var renderBox = component.find("renderBox");
var body = renderBox.get("v.body") || [];
body.push(wrapperComponent);
renderBox.set("v.body", body);
}
);
})();
},
})
Renderer
({
'afterRender' : function(component, helper) {
var mainElement = component.mainElement = document.createElement('div'),
span1Element = document.createElement('span'),
span2Element = document.createElement('span');
mainElement.style.border = 'solid red 8px';
mainElement.style.margin = '50px 20px 120px 90px';
mainElement.style.padding = '20px 130px 90px 10px';
mainElement.style.backgroundColor = 'aqua';
span1Element.innerText = 'I am not a ';
span2Element.innerText = 'Lightning Component';
span2Element.style.fontWeight = 'bold';
mainElement.appendChild( span1Element );
mainElement.appendChild( span2Element );
component.find('root').getElement().appendChild( mainElement );
// ************************************************************
// This is the key line of code. I've created DOM elements
// above, and now I'm going to render a Lightning component
// and attach it to those DOM elements:
helper.createComponent(
// A reference to the parent component:
component,
// The type of Lightning component to render:
'forceChatter:publisher',
// Attributes to send to the Lightning component to render:
{ context: "GLOBAL", type: "News" },
// The DOM element where the Lightning component should
// ultimately live:
mainElement
);
// ************************************************************
}
})
Wrapper Component
The documentation is pretty clear that you cannot access the getElement()
function of a component from another namespace, but I wondered if I might be able to wrap a Lightning Component within a wrapper component that's in my namespace. This would give me access to the element and might allow me to move things around. I could render the desired component within the "renderBox" div and then move it into my custom DOM.
Component XML
<aura:component >
<!-- Describes the component that we want to render -->
<aura:attribute name="componentType" type="String"/>
<aura:attribute name="componentAttributes" type="Object"/>
<!-- The target element where the component should be moved -->
<aura:attribute name="targetElement" type="Object"/>
<!-- This is where our component is initially rendered, and it's where the component "lives" in Lightning's DOM -->
<div aura:id="holdingPen"></div>
<aura:handler name="init" value="{!this}" action="{!c.init}"/>
</aura:component>
Controller
({
init: function(component) {
$A.createComponent(
component.type = component.get("v.componentType"),
component.attributes = component.get("v.componentAttributes"),
function(cmp){
component.find("holdingPen").set("v.body", [cmp]);
}
);
}
})
Renderer
({
afterRender: function(cmp) {
if (cmp.moveComplete) return;
var holdingPen = cmp.getElement(),
target = cmp.get("v.targetElement");
if (target && target.appendChild){
target.appendChild(holdingPen);
cmp.moveComplete = true;
}
}
})
Result
It works! Of course, there are still some issues here that need to be addressed. For example, you are now responsible for handling the dynamically generated component's lifecycle. If you decide to remove it or it's parent from the DOM, then you need to explicitly call destroy on the dynamic component. How exactly you handle that is going to depend on your architecture and isn't covered by the example above.
Here's a screenshot of my decidedly un-lovely page:
Best Answer
I think the canvas-all.js is only meant to be used in the app being canvassed, and not in any Aura/LWC component.
Is there anything keeping you from using the standard iframe postMessage API? If you see the documentation for force:canvasApp it states that the component takes a canvasId="someId" property that you should use for targeting events:
"An unique label within a page for the Canvas app window. This should be used when targeting events to this canvas app."
I havent tried this out but it might send you in the right direction:
Then in your app being canvassed: