Running headless Drupal with a separate javascript framework on the front-end can provide amazing user experiences and easy theming. Although, working with content editors with this separation can prove to be a tricky situation.
Problem
User story As part of the content team, I need to be able to see a preview of the article on a separate front-end (Ember) application without ever saving the node.
As I started reading this user story I was tasked with, the wheels started turning as to how I would complete this. I had to get information not saved anywhere to an Ember front-end application with no POST abilities. I love challenges that take a lot of thought before jumping in. I talked with some of the other developers here and came up with a pretty nifty solution to the problem, which come to find out was just a mis-communication on the story… and when I was done the client was still excited at the possibilities it could open up; but keep in mind this was still in it’s early stages and there are still a lot of holes to fill.
Solution
The first thing I did was add a Preview link and attach a javascript file to the node (article type) edit page via a simple hook_form_FORM_ID_alter().
<?php
/**
* Implements hook_form_FORM_ID_alter().
*/
function mymodule_form_article_node_form_alter(&$form, &$form_state, $form_id) {
$form['#attached']['js'][] = drupal_get_path('module', 'mymodule') . '/js/preview.js'
$form['actions']['preview_new'] = [
'#weight' => '40',
'#markup' => '<a id="preview-new">Preview on front-end</a>'
];
}
Pretty simple. Since it was just the beginning stages I just threw it in an anchor tag to get the link there. So it wasn’t pretty. Now to start gathering the node’s data since it won’t be saved anywhere.
Next step: Gather node info in js
So obviously the next step was to gather up the node’s information from the node edit screen to prepare for shipping to the front-end application. Most time spent here was just trying to mimic the api that the Ember front-end was already expecting.
/**
* @file
* A JavaScript file to preview new site from node edit form.
*
*/
(function ($, Drupal) {
Drupal.behaviors.openNewSite = {
attach : function(context, settings) {
viewInNewSite.init();
}
}
})(jQuery, Drupal);
var viewInNewSite = (function($) {
// Get the media on the node
var getMedia = function() {...};
// Gather the fields... return as an object
var getMessage = function() {...};
// Attach link here
var attachPreviewClick = function(link) {
var $link = $(link);
$link.on('click', function() {
var newWindow = window.open('http://the-front-end.com/articles/preview/preview-article');
var message = getMessage();
// This is where the magic happens after click to send to a new window, more below.
setTimeout(function() {
newWindow.postMessage(JSON.stringify(message), 'http://the-front-end.com');
}, 1000);
});
};
// Attach link from click
var init = function() {
attachPreviewClick('#preview-new');
};
return {
init: init
}
})(jQuery);
Using window.open() to open what will be the route in the Ember application returns the newWindow for us to post a message to. (I used a setTimeout()here because the opening application took a while to get started and the message would get missed… this held me up for a while since I knew the message should be getting sent.) Then using postMessage() on newWindowto ship off a message (our json object) to the opening window, regardless of if it is another domain. Insert security concerns here… but now we’re ready to setup the front end Ember application route.
To Ember!
The next step was to set up the Ember front-end application to listen for the message from the original window. Set up the basic route:
// Set up the route that we're wanting to use for previewing articles.
this.resource('articles', function () {
this.route('preview', {path: '/preview/article-preview'});
this.route('article', { path: '/:article_id' });
});
The application that I was previewing articles into already had a way to view articles by id as you see in the above code. So I didn’t want to have to duplicate anything… I wanted to use the same template and model for articles that were already developed. Since that was taken care of for me, it was just time to create a model for this page and make sure that I use the correct template. So start by creating the ArticlesPreviewRoute:
App.ArticlesPreviewRoute = (function() {
return Ember.Route.extend({
// Tell Ember to use the right template.
renderTemplate: function() {
this.render('articles.article');
},
controllerName: 'articles.article',
// This was required since the message being shipped is not a DS.RecordArray
setupController: function(controller, model) {
controller.set('model', model);
},
model: function (params) {
return getArticle();
}
});
/**
* Adds event listener to the window for incoming messages.
*
* @return {Promise}
*/
var getArticle = function() {
return new Promise(function(resolve, reject) {
window.addEventListener('message', function(event) {
if (event.origin.indexOf('the-back-end.com') !== -1) {
var data = JSON.parse(event.data);
resolve(data.articles[0]);
}
}, false);
});
};
})();
The getArticle() function above returns a new Promise and adds an event listener that verifies that the message is coming from the correct origin. Clicking the link from Drupal would now take content to a new tab and load up the article. There would be some concerns that need to be resolved such as security measures and if a user visits the path directly.
To cover the latter concern, a second promise would either resolve the promise or reject it if the set amount of time has passed without a message coming from the origin window.
App.ArticlesPreviewRoute = (function() {
return Ember.Route.extend({
...
model: function (params) {
var self = this;
var article = previewWait(10, getArticle()).catch(function() {
self.transitionTo('not-found');
});
return article;
}
});
var getArticle = function() {
return new Promise(function(resolve, reject) {...}
};
/**
* Reject the promise if there is no message after 'X' seconds
* @param {integer} seconds The amount of seconds to wait
* before rejecting promise.
* @param {Promise} promise The promise to resolve or
* reject if seconds passes
*/
var previewWait = function(seconds, promise) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
reject({'Bad': 'No data Found'});
}, seconds * 1000);
promise.then(function(data) {
resolve(data);
}).catch(reject);
});
};
})();
There you have it! A way to preview an article from a Drupal backend to an Ember front-end application without ever saving the article. A similar approach could be used for any of your favorite Javascript frameworks. Plus, this can be advanced even further into an “almost live” updating front-end that constantly checks the state of the fields on the Drupal backend. There have been thoughts of turning this into a Drupal module with some extra bells and whistles for configuring the way the json object is structured to fit any front-end framework… Is there a need for this? Let us know in the comments below or tweet us! Now, go watch the video!
Bình luận (0)
Add Comment