Master Module - Template Generator

View JSON output in console.

More Info...

Purpose

The "Master Module" empowers business users to effortlessly generate marketing content modules on demand. What used to require collaborative efforts from UX, Design, Engineering, and QE, spanning months of coordination and work, can now be accomplished instantly. The "Master Module" is designed to facilitate the creation of unique modules or establish a library of customizable templates.

Background

AEO faced challenges with its traditional CMS, particularly in adapting the brand's ever-evolving online presence. When presented with the chance to adopt a headless CMS, I suggested a dynamic solution that would instruct the UI on the placement and presentation of pre-defined marketing content. This solution streamlined a library of over 50 modules into a single, versatile module with the capability to generate countless layouts based on the team's creative ideas.

Overview

Grid System

Leveraging a custom adaptation of Bootstrap, the "Master Module" uses a responsive 12 column grid system to place dynamically sized blocks of content into rows. Expanding on the grid system, the "Master Module" also allows blocks to be "pushed" or "pulled" by 50% in order to better center content when needed.

Lockups

At the heart of the "Master Module" functionality lies the concept of the "lockup." A "lockup" is defined as marketing content incorporating various combinations of copy, media, and CTAs. Each "lockup" is represented by a numbered block within the "Master Module," providing users with a clear visual of the content's position and appearance across different breakpoints.

Dynamic Layouts

Example 1: Simple, Single Lockup Layout
Lockup
Layout
Mobile Module
Desktop Module
Example 2: Triple Lockup Layout with Breakpoint Changes
1st Lockup
2nd Lockup
3rd Lockup
Mobile Layout
Mobile Module
Desktop Layout
Desktop Module

Live Preview

The "Master Module" offers a straightforward yet effective preview of "lockup" settings. The square within the block denotes content placement, while lines represent copy attributes.

Justification
Settings
Anchoring

Similarly, animation settings can be displayed using these same elements.

Animation

Templates

The "Master Module" enables users to save their layouts using the CMS tool. When opened, it utilizes the CMS API to load all saved template entries. With no limitations on the number of templates, a naming convention was established to facilitate automatic filtering, aiding users in quickly locating their layouts.

Technical Details

Headless

Traditional CMS output generates fully-formed HTML, necessitating development efforts in both the CMS and UI. This not only significantly increases the payload but also establishes a dependency between the CMS and the platform. In contrast, the "Master Module" produces an efficient data object, delivering only the essential properties and values required for the UI to display content.

Open the console to view the live data output from the "Master Module" above!

Mobile First

Adhering to the industry standard of "mobile first," the "Master Module" collects all the data for the "small" breakpoint, followed by only the modified properties for higher breakpoints which will inherit any properties not explicitly overwritten. Furthermore, this minimizes the payload, enhancing overall efficiency and page speed.

sm: {
  anchor: "center",
  animate: "both",
  animation: "none",
  border: "none",
  hidden: "show",
  justify: "center",
  margin: "0 15px",
  offset: 2,
  padding: "0 15px",
  span: 10,
  split: "none"
}
            
md: {
  anchor: "tr",
  justify: "left"
}
            
lg: {
  border: "bottom",
  offset: 1,
  span: 11,
  split: "pull"
}
            

Loading Templates

function getTemplates(){
  var env = extensionField.stack._data.name || 'PROD';
  var xhr = new XMLHttpRequest();

  xhr.open('GET', extensionField.config.baseURL+'v3/content_types/'+extensionField.config.contentType+'/entries?environment='+extensionField.config.templateEnv, true);
  xhr.setRequestHeader("api_key", extensionField.config[env].apiKey);
  xhr.setRequestHeader("access_token", extensionField.config[env].deliveryToken);
  xhr.onreadystatechange = function () {
    if(xhr.readyState === XMLHttpRequest.DONE) {
      var status = xhr.status;
      if (status === 0 || (status >= 200 && status < 400)) {
        if(this.response){
          createTemplates(JSON.parse(this.response).entries);
        }else{
          return false;
        }
      } else {
        return false;
      }
    }
  };
  xhr.send();
}
          

Create Filter

for (var i = 0; i < templates.length; i++) {
  template = templates[i];
  title = template.title || '';
  result = title.match(/([^\-]+)\s-\s(.+)/);

  if (result && result.length === 3) {
    title = result[2] || '';
    category = result[1].replace(/\s/g, '-') || '';

    if (categories.indexOf(category) < 0) {
      categories.push(category);
    }
  } else {
    category = '';
  }

  html += '<option class="cat-' + category + '" value="' + i + '">' + title + '</option>';
}

html += '</select></div>';

if (categories.length > 0) {
  filterHtml = '<div class="w-45" style="display:inline-block;"><label for="filters" style="display:block">Filter</label><select id="filters" class="cs-text-box w-100" style="text-transform: capitalize;" name="filters" onchange="filterTemplates(this)"><option value="">All</option>';

  for (var x = 0; x < categories.length; x++) {
    filterHtml += '<option value="' + categories[x] + '">' + categories[x].replace(/\-/g, ' ') + '</option>';
  }

  filterHtml += '</select></div>';
}
      

Options Validation

function testTotalColsBlock(size){
  var ind = data.loops.breakpoints.indexOf(size);
  var offset = data.options[size].offset || data.options[data.loops.breakpoints[ind - 1] || size].offset || data.options[data.loops.breakpoints[ind - 2] || size].offset || 0;
  var span = data.options[size].span || data.options[data.loops.breakpoints[ind - 1] || size].span || data.options[data.loops.breakpoints[ind - 2] || size].span || 1;

  return (offset + span) <= 12 ? true : false;
}

function testAnimOpts(size){
  var animation = data.options[size].animation || false;
  var animate = data.options[size].animate || false;

  return (!animation || !animate) && !(!animation && !animate) && animation !== 'none' ? false : true;
}

function testHiddenBreakpoints(){
  var valid = false;

  for (var i = 0; i < data.loops.breakpoints.length; i++) {
    if(data.options[data.loops.breakpoints[i]].hidden === 'show'){
      valid = true;
    }
  }

  return valid;
}
      

Creating the Data Object

for (var i = 0; i < data.loops.areas.length; i++) {
  area = data.loops.areas[i];
  data.output.layout[area] = [];
  rows = [];
  rowElems = $('#'+(area !== 'content' ? 'row_'+area+'.row' : area+' .row'));

  for (var x = 0; x < rowElems.length; x++) {
    blocks = [];              
    blockElems = $(rowElems[x]).find('.block');

    for (var y = 0; y < blockElems.length; y++) {
      blockObj = {};
      optsObj = {};

      row_id = $(rowElems[x]).attr('id');
      block_id = $(blockElems[y]).attr('id');

      blockObj = data[area][row_id].blocks[block_id];
      blockObj.seq = iter;
      $(blockObj.elem).find('.num').text(iter++);

      for (var z = 0; z < data.loops.breakpoints.length; z++) {
        breakpoint = data.loops.breakpoints[z];
        optsObj[breakpoint] = {};

        for (var j = 0; j < data.loops.options.length; j++) {
          option = data.loops.options[j];
          val = blockObj.options[breakpoint][option];

          if(val !== ''){
            optsObj[breakpoint][option] = val;
          }
        }
      }
      blocks.push(optsObj);
    }
    rows.push(blocks);
  }
  data.output.layout[area] = rows;
}