For a headless live preview setup, there should be a way to disable the iframe refresh to instead let the client poll for changes. Ideally, Craft could postMessage update triggers to the iframe but simply being able to disable the automatic refresh would be much appreciated.
Been thinking about this too, and pretty likely we will get this into 3.4 :)
Just implemented this for the next 3.4 release.
Sections’ preview targets now have a “Refresh” checkbox, which you can uncheck to disable auto-refreshing of the iframe when content changes.

Any preview targets that are programmatically defined can also opt out of auto-refreshing:
use craft\elements\Entry;
use craft\events\RegisterPreviewTargetsEvent;
use yii\base\Event;
Event::on(Entry::class, Entry::EVENT_REGISTER_PREVIEW_TARGETS, function(RegisterPreviewTargetsEvent $event) {
$event->previewTargets[] = [
'label' => 'Gatsby',
'url' => 'http://localhost:8000',
'refresh' => false,
];
});
I’ve also added a beforeUpdateIframe event to the Craft.Preview JavaScript class, which will get fired whenever the iframe _would_ get updated (regardless of whether refresh is enabled), so you can register an event listener for that to fire your postMessage or whatever else.
Garnish.on(Craft.Preview, 'beforeUpdateIframe', function(event) {
if (!event.refresh) {
event.$iframe[0].contentWindow.postMessage('content updated', '*');
}
});
You can register that JS code with a custom module:
public function init()
{
// ...
if (Craft::$app->request->getIsCpRequest()) {
Craft::$app->view->registerJs($js);
}
}
Or use the Control Panel JS plugin.
Many thanks for this, I'm not getting a token in the initial (and when refresh is false, only) load of the iframe though. Did I miss something?
When the event is fired for the initial load, there is no token yet, and the iframe itself hasn’t even been created yet. You can tell that it is the initial load if event.resetScroll is set to true.
Sorry, I'll be clearer. Unless a draft is already created when you open preview, the iframe src itself is never passed a token param, only the x-craft-live-preview, and since it's not being refreshed it will never get a token. Could be solved if a token was passed in the JS event object, but of course then the no-refresh live preview would always demand a postmessage integration.
Ahhh, got it. Just fixed that.
To get the fix early, change your craftcms/cms requirement in composer.json to:
"require": {
"craftcms/cms": "3.4.x-dev#0f0fc2091feab5b61444e30abbcd75c14e40ff4e as 3.4.0-RC1.1",
"...": "..."
}
Then run composer update.
Btw, in @brandonkelly's example above, I needed it to be this to work in 3.4.27:
Garnish.on(Craft.Preview, 'beforeUpdateIframe', function(event) {
if (!event.refresh) {
event.target.$iframe[0].contentWindow.postMessage('content updated', '*');
}
});
Note the event.target
I'm running into a problem with this solution and new entries that rely on a slug in the URL. If a content author opens the preview pane with no slug defined, the preview iframe loads with a temp slug. Then if they choose a title, which sets the slug, the temp slug used by the preview pane no longer works. Seems that in this case the preview pane should force a "refresh" regardless of user setting. Either that or the slug change could be included in the beforeUpdateIframe event.
I suppose I could check document.querySelector('#slug').value in the event, but that's not ideal. Anyone else run into this?
@jamealg That was fixed in 3.3.0 via 807e4916e1fcc35ecbdd7532f0335cb61ef2a7f8, however if you have disabled iframe refresh I can see how the issues would persist. Not sure what we could do about it though.
@brandonkelly Does it make sense for Craft to initiate that postMessage automatically when iframe refresh is disabled? That message could also contain the entry's URL which could be used by the target application to determine how to fetch the data associated with that entry.
This is the workaround I came up with based on your previous example:
Garnish.on(Craft.Preview, 'beforeUpdateIframe', function(event) {
if (!event.refresh) {
let embeddedIframe = event.target.\$iframe[0];
let src = embeddedIframe.src;
// If the slug has been set, try replacing any temporary tokens in the iframe src URL
let slug = document.querySelector('#slug').value;
if(slug) {
src = src.replace(/(__temp)[a-zA-Z_-]*/, slug);
}
// Let the iframe know that content has been updated and pass the path (whether updated or not)
let srcAsURL = new URL(src);
let path = srcAsURL.pathname + srcAsURL.search;
embeddedIframe.contentWindow.postMessage({
message: 'content updated',
path,
}, '*');
}
});
This is an essential feature IMO for headless + live preview. It would be wonderful if it were built directly into the CMS.
No I don’t think craft should call postMessage() itself – it wouldn’t know what the message should be.
Just realized you can fetch the updated preview target URL like this from your event handler:
event.target.draftEditor.settings.previewTargets[event.target.activeTarget].url
(event.target will be the Craft.Preview instance.)
I think it's @jamealg's example message works fine. In my implementation I use preview:change. I think you'd just pick _something_ and then developers would just standardize around that with their listeners. Currently, I'm installing Control Panel JS for each project that needs just for this functionality.
Well my code doesn’t require any guessing ;)
Most helpful comment
Just implemented this for the next 3.4 release.
Sections’ preview targets now have a “Refresh” checkbox, which you can uncheck to disable auto-refreshing of the iframe when content changes.
Any preview targets that are programmatically defined can also opt out of auto-refreshing:
I’ve also added a
beforeUpdateIframeevent to theCraft.PreviewJavaScript class, which will get fired whenever the iframe _would_ get updated (regardless of whetherrefreshis enabled), so you can register an event listener for that to fire yourpostMessageor whatever else.You can register that JS code with a custom module:
Or use the Control Panel JS plugin.