Creating Shareable Widgets from Fieldable Panel Panes

Brian Nash
#Drupal | Posted

One of our current clients asked us to create a widget that could be placed on their site and external affiliated sites.  We are using Panelizer, Panels, Panopoly Magic and Fieldable Panel Panes to build the site.  Given that we are using Fieldable Panel Panes (FPP), the most straight forward solution was to create a new FPP and have the client create an instance of the FPP.  A menu callback could then be setup to essentially serve the rendered result of the FPP.  To help with this, the Web Widgets module was leveraged, providing some useful functionality.

Overall, there are only a few big steps that need to happen to get this to work.  First we need to define the FPP and then configure as needed.  After that we need to create a hook_menu implementation that will hook up the paths we want to use to serve the widget.  Then we need to hook up the callbacks to functions that will render out the specific FPP as well as provide an admin interface to retrieve the embed code that is used on external sites.  The JS and CSS needs to be filtered as well as adding in any custom JS or CSS files.  And finally, the embed code can be distributed.

The Implementation

The first step to creating a FPP widget is to create the pane itself.  Creating a new FPP is done via hook_entity_info_alter().  The important things to keep in mind in this code block are the label, pane category, and real path.  Access arguments are similar to hook_menu access arguments.  Once you have added this code into your module and cleared cache, browsing to admin/structure/fieldable-panels-panes will now show the new 'My Example Widget'.  Now the FPP fields can be customized (and exported via Features) to suit your needs.

  1. /**
  2. * Implementation of hook_entity_info_alter().
  3. */
  4. function example_widgets_entity_info_alter(&$entity_info) {
  5. // Example Widget.
  6. $entity_info['fieldable_panels_pane']['bundles']['example'] = array(
  7. 'label' => t('My Example Widget'),
  8. 'pane category' => t('Custom'),
  9. 'pane top level' => TRUE,
  10. 'admin' => array(
  11. 'path' => 'admin/structure/fieldable-panels-panes/manage/%fieldable_panels_panes_type',
  12. 'bundle argument' => 4,
  13. 'real path' => 'admin/structure/fieldable-panels-panes/manage/example',
  14. 'access arguments' => array('administer fieldable panels panes'),
  15. ),
  16. );
  17. }

At this point, you can try out the widget by placing it on the site via your preferred method (Panelizer, IPE, etc).  Now we need to make a hook_menu callback that will create a path to get the embed code and another path to provide the embedded widget.  The admin callback will provide the block of code that is to be added on another site which will pull in the widget.  For simplicity's sake, the access_callbacks are set to TRUE.  The admin callback should certainly be protected and the widgets/example path can be protected if needed.

  1. /**
  2. * Implements hook_menu().
  3. */
  4. function example_widgets_menu() {
  5. $items['admin/config/content/web_widgets'] = array(
  6. 'title' => 'Example Widgets',
  7. 'page callback' => 'example_widgets_example_embed_code',
  8. 'access callback' => TRUE,
  9. );
  10. $items['widgets/example/%'] = array(
  11. 'title' => 'Example Widget',
  12. 'page callback' => 'example_widgets_example_callback',
  13. 'page arguments' => array(2),
  14. 'access callback' => TRUE,
  15. );
  16. return $items;
  17. }

The primary callback function for rendering the widget, needs added css via drupal_add_css (this doesn't seem to work for all versions of IE) and then you can render the widget.  Drupal_add_js could optionally be used for adding add-on JS files for IE as well.   Originally this had been designed to only serve up a single instance of an FPP.  With the addition of the fpid parameter in the callback, we can define specific FPP instances to load.

  1. /**
  2. *Loads the example widget and renders the
  3. *widget then passes to the web_widgets api and
  4. *then prints the widget.
  5. */
  7. function example_widgets_example_callback($fpid) {
  8. drupal_add_css(drupal_get_path('module', 'example_widgets') . '/css/example.css');
  9. $widget = example_widgets_render_widget($fpid);
  11. if ($widget) {
  12. print $widget;
  13. }
  14. print '';
  15. }

The following helper function loads the FPP instance and passes it to the fieldable_panels_pane_view which creates the primary view for the widget.  To prevent the function from the possibility of serving up a panel that isn't our example widget type, we use the example_widgets_load_fieldable_panels_by_name function.  The EFQ will query for all FPP instances with that machine name.  The renderable array is setup and the content is set to the fieldable_panels_pane_view result and then rendered.  The rendered result gets passed to the Web Widgets render function and the result is returned.  Note that we are using the inline_async style here.  Using inline_async allows for a better, dynamic loading experience that is more graceful for cases where the widget fails to load.  As detailed at the end of this blog, the example_widgets_alter_widget_links will clean up internal links so they work on external sites.

  1. /**
  2. *Helper function that handles the loading of the FPP
  3. *and then renders it as a web widget and returns the widget.
  4. */
  5. function example_widgets_render_widget($fpid) {
  6. $fp = array();
  7. $panels = example_widgets_load_fieldable_panels_by_name('example');
  8. if (is_numeric($fpid) && array_key_exists($fpid, $panels)) {
  9. $fp = fieldable_panels_panes_load($fpid);
  10. }
  12. if (count($fp) > 0) {
  13. $title = $fp->title;
  14. $view = fieldable_panels_pane_view($fp, 'full', 'und');
  15. $view = example_widgets_alter_widget_links($view);
  16. $renderable = array(
  17. '#prefix' => "<div id='example-widget'><div >",
  18. 'title' => array(
  19. '#prefix' => '<h3 class="title">',
  20. '#markup' => 'Example Widget',
  21. '#suffix' => '</h3>',
  22. ),
  23. 'content' => $view,
  24. '#suffix' => "</div></div>");
  25. $content = render($renderable);
  27. $widget = web_widgets_render_widget('inline_async', $content, $title);
  28. return $widget;
  29. }
  30. else {
  31. return FALSE;
  32. }
  33. }
  35. /**
  36. * Loads fieldable panels pane based on machine name.
  37. */
  38. function example_widgets_load_fieldable_panels_by_name($name) {
  39. $query = new EntityFieldQuery();
  40. $result = $query
  41. ->entityCondition('entity_type', 'fieldable_panels_pane')
  42. ->propertyCondition('bundle', $name)
  43. ->execute();
  44. return $result['fieldable_panels_pane'];
  45. }

This example_widgets_example_embed_code callback generates the code which needs to be added to an external site so they can pull in the widget.  There are a couple of items to note here.  The call to web_widgets_render_embed_code takes in the style and the variables.  The style needs to match the one used in the actual rendering of the widget (as seen above).  Variables need to include the fully qualified path to the widget.  To help make the widget id's unique, we can use the str_replace function to change the ids to be more unique so as not to cause possible issues with external sites.  Likewise, in order to have IE compatibility, we must add the CSS link here via another str_replace function.  In order to make the formatting a bit cleaner, using a helper function (_example_widgets_example_renderable_embed_code) to create a render array to format the data.  Depending on your theme, this would likely need to be changed and styled.

  1. /**
  2. *This returns the embed code that is used to
  3. */
  4. function example_widgets_example_embed_code() {
  5. global $base_url;
  6. $embed = array(
  7. '#prefix' => '<h1>Example Widgets</h1>',
  8. );
  10. foreach ($panels as $panel) {
  11. $fpid = $panel->fpid;
  12. $variables = array('path' => $base_url . '/widgets/example/' . $fpid);
  13. $embed_code = web_widgets_render_embed_code('inline_async', $variables);
  14. //Since it spits out a whole, non-configurable form, we'll have to do a str_replace on the closing textarea tag.
  15. //This adds the CSS file in the embed so IE doesn't flip it's head.
  16. $embed_code = str_replace('</textarea>', '
  17. <link type="text/css" rel="stylesheet" href="' . $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'example_widgets') . '/css/example.css' . '" media="all"></textarea>', $embed_code);
  18. $embed[$fpid] = _example_widgets_example_renderable_embed_code($fpid, $embed_code);
  19. }
  20. return render($embed);
  21. }
  23. /**
  24. * Helper function that returns a render array for the embed code markup
  25. */
  26. function _example_widgets_example_renderable_embed_code($fpid, $embed_code) {
  27. $panel = fieldable_panels_panes_load($fpid);
  28. $title = $panel->title;
  29. $machine_name = $panel->machine_name;
  31. $renderable = array(
  32. '#prefix' => "<div class='embed-code' id='fpid-$fpid'>",
  33. 'header' => array(
  34. '#prefix' => "<div class='embed-header'>",
  35. 'title' => array(
  36. '#prefix' => "<h3>",
  37. '#markup' => "$title",
  38. '#suffix' => "</h3>",
  39. ),
  40. 'machine_name' => array(
  41. '#prefix' => "<div class='machine-name'>",
  42. '#markup' => "Machine Name - $machine_name",
  43. '#suffix' => "</div>",
  44. ),
  45. 'fpid' => array(
  46. '#prefix' => "<div class='fpid'>",
  47. '#markup' => "FPID - $fpid",
  48. '#suffix' => "</div>",
  49. ),
  50. '#suffix' => "</div>",
  51. ),
  52. 'embed' => array(
  53. '#markup' => $embed_code,
  54. ),
  55. '#suffix' => "</div>",
  56. );
  57. return $renderable;
  58. }

The Little Things

One of the last steps in creating the widget is to sanitize the JS, CSS and hrefs that are produced.  The following two hook alters will remove the JS and CSS as we don't want to include the origin sites CSS or JS in the widget, only specific JS and CSS files.  For this example, the hook_css_alter will also include a CSS file that has some default stylings.  This of course could be left out if you don't want to provide a user with default CSS (or have it as an option in the menu call).  In this case, we want to include a basic, easily over-rideable CSS file to provide a bare-bones look.  Likewise, if you need some JS action in the widget, you can include a JS file in the hook_js_alter function.

The example_widgets_alter_widget_links is used to clean the link tags href content.  In some cases this may not be needed, but links that are internal won't work correctly when rendered on other sites.  Thus, we need to alter the inline links and make sure they are fully qualified.  Depending on the widget, additional data from the widget may need to be checked.

  1. /**
  2. *Implements hook_js_alter
  3. */
  4. function example_widgets_js_alter(&$javascript) {
  5. if (preg_match('/widgets/example/d+/i', current_path())) {
  6. $javascript = array();
  7. }
  8. }
  1. /**
  2. *Implements hook_css_alter
  3. */
  4. function example_widgets_css_alter(&$css) {
  5. if (preg_match('/widgets/example/d+/i', current_path())) {
  6. $path = drupal_get_path('module', 'example_widgets') . '/css/example.css';
  7. $example_css = $css[$path];
  8. $css = array($path => $example_css);
  9. }
  10. }
  1. /**
  2. *Alters the internal link urls so they are fully qualified.
  3. */
  4. function example_widgets_alter_widget_links(&$view) {
  5. global $base_url;
  6. $links = $view['field_internal_links']['#items'];
  7. foreach ($links as $key => $link) {
  8. $view['field_internal_links'][$key]['#href'] = $base_url . "/" . $view['field_internal_links'][$key]['#href'];
  9. }
  10. $view['#contextual_links'] = array();
  11. return $view;
  12. }


That is about it.  Seems like a lot of code to simply generate a widget, but it's really powerful code.  Using FPP, the client can update items in the widget easily, and Web Widgets handles a lot of the back-end work.  In cases where a simple block or HTML code block needs to be rendered, the code would be much simpler.  As in most things, the more customizable (especially on the front end) the more complex we get in the back end.

To learn more about the power of Panes, check out Brian McMurray's blog post about how the Robin Hood Foundation used the power of Panes and Template Field for total content layout control.


Brian Nash