Using a mktoImg LP element as a CSS background-image

As you probably already know, the best way to set background images is the CSS background-image property.

Real background images come with all the bells and whistles like background-position and background-size and background-repeat — they’re just the way to go. (You can hack a generic <img> tag somewhat with position: absolute, but it’s impossible to duplicate the feature set of a true background image.)

Unfortunately, on Marketo Guided LP Templates the mktoImg element — the only way to provide the invaluable Design Studio image browser to users — renders as an <img>. Yuck.[1]

But we can get the best of both worlds with a small amount of reusable JS. This’ll allow you to browse for image assets...

ss

... and have the selected image become a real background-image, here a hero image with heading text overlaid:

ss

Let’s see how it’s done.

The setup

The first thing we need is a mktoImg element, marked up with some special classes and attributes:

<div class="mktoImg imgBrowserOnly" id="heroImageBrowser" mktoName="Hero Image" data-background-for="#heroImage"></div>

I’ve decorated the usual div.mktoImg with the additional class imgBrowserOnly, meaning it’ll serve as an image asset browser in the LP Editor.

And I’ve added the data- attribute data-background-for: that’s the CSS selector for the visible element whose background will be set.

To be clear, neither the class nor the attribute have any meaning to the Marketo LP Editor. The editor will just leave them alone. Then we give them meaning using a tiny (truly, very little by my standards) bit of JS and a single CSS style.

The CSS

First, the style:

.imgBrowserOnly {
  display: none;
} 

Couldn’t be simpler. It just makes sure that anything tagged with that class (and of course all of its child elements) is not displayed. That’s because we’re using the mktoImg just to get the asset browser capability.

The JS

Then, the code:

/**
 * @version v1 2020-04-04
 * @copyright © 2020 Sanford Whiteman
 * @license Hippocratic 2.1
 */
(function(){
  var arrayify = getSelection.call.bind([].slice),
      srcSetters = {
        "data-background-for" : function(el,src){ el.style.backgroundImage = "url(\"" + src + "\")"; },
        "data-src-for" : function(el,src){ el.src = src; }
      };                  

  function bindImgBrowsers(){
    var imgBrowsers = document.querySelectorAll(".imgBrowserOnly");

    arrayify(imgBrowsers)
      .forEach(function(browseWrapper){
         var hasImgChild = !!browseWrapper.querySelector("img");

         if (hasImgChild) {
           pushSelected({currentTarget : browseWrapper});
         } else {
           browseWrapper.addEventListener("load", pushSelected, true);
         }
      });     
  }

  function pushSelected(e){
    var wrapperEl = e.currentTarget,
        selectedImgSrc = wrapperEl.querySelector("img").src;

    Object.keys(srcSetters)
      .forEach(function(setterName){
        if(wrapperEl.hasAttribute(setterName)){
          var targetEls = document.querySelectorAll(wrapperEl.getAttribute(setterName));

          arrayify(targetEls)
            .forEach(function(targetEl){
              srcSetters[setterName](targetEl,selectedImgSrc);
            });
        }
      });
  }

  document.addEventListener("DOMContentLoaded",bindImgBrowsers);
})();

I understand that may look like gobbledygook if you’re not a coder. But let me summarize what it does:

  • Loops over all elements with class imgBrowserOnly. For each item in the array:
    • Grabs the src attribute from the inner <img> — that’s the image URL — as soon as it’s ready
    • Reads the 2 supported CSS selector types, for background images and image URLs (see below).
    • Loops over all elements matching each CSS selector. For each:
      • Sets the appropriate property, either the background-image or the src

This highly expandable logic means you can add unlimited imgBrowserOnly elements without making a single change to the code.

Turning that around, it also means you can use a single imgBrowserOnly to set the (same) background for multiple elements. In the above example, I’m expecting only the single element with the ID heroImage, thus the selector is #heroImage. But if I had a few different parts on the same page with the class gradientBack then data-background-for=".gradientBack" would set the backgrounds on all of them.

You can also set the source (src) of other <img> tags, rather than backgrounds, which might be useful: data-src-for="#someImage".

And if you can think of other useful places to copy the image URL, you need only add a function to the srcSetters object. (That would require you to be a coder, though.)

Sample template

Here’s a complete Guided LP Template you can use to try out the code:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta class="mktoString" id="heroText" mktoName="Hero Text" allowHtml="true">
    <title>mktoImg to background-image Demo</title>
    <style>
     .imgBrowserOnly {
       display: none;
     }   
     #heroImage {
       height: 250px;
       background-size: cover;
     }
     #heroImage h1.heroText {
	   color: whitesmoke;
       background-color: darkmagenta;
       margin: 40px 20px;
       padding: 20px;
       display: inline-block;
       font-family: "Arial Black";
       transform: rotateZ(-2deg);
     }
    </style>
    <script>
    (function(){
      var arrayify = getSelection.call.bind([].slice),
          srcSetters = {
            "data-background-for" : function(el,src){ el.style.backgroundImage = "url(\"" + src + "\")"; },
            "data-src-for" : function(el,src){ el.src = src; }
     	  };                  

      function bindImgBrowsers(){
        var imgBrowsers = document.querySelectorAll(".imgBrowserOnly");

        arrayify(imgBrowsers)
          .forEach(function(browseWrapper){
             var hasImgChild = !!browseWrapper.querySelector("img");

             if (hasImgChild) {
               pushSelected({currentTarget : browseWrapper});
             } else {
               browseWrapper.addEventListener("load", pushSelected, true);
             }
          });     
      }

      function pushSelected(e){
        var wrapperEl = e.currentTarget,
            selectedImgSrc = wrapperEl.querySelector("img").src;

        Object.keys(srcSetters)
          .forEach(function(setterName){
            if(wrapperEl.hasAttribute(setterName)){
              var targetEls = document.querySelectorAll(wrapperEl.getAttribute(setterName));

              arrayify(targetEls)
                .forEach(function(targetEl){
                  srcSetters[setterName](targetEl,selectedImgSrc);
                });
            }
          });
      }
      
      document.addEventListener("DOMContentLoaded",bindImgBrowsers);
    })();
    </script>
  </head>
  <body>
    <div class="mktoImg imgBrowserOnly" id="heroImageBrowser" mktoName="Hero Image" data-background-for="#heroImage"></div>
    <div id="heroImage">
       <h1 class="heroText">${heroText}</h1>
    </div>
  </body>
</html>

Explaining the event model

If you’re peering closely at the code you might wonder why I use both DOMContentLoaded and a Load event on the selector <div>.

It’s because the feature needs to work both on the live LP and in the Marketo LP Editor’s Preview window. In Preview, the child images are injected after the DOM is ready. If you only cared about the live page, then DOMContentLoaded would suffice.


Notes

[1] You can use a mktoString variable, but that means copying-and-pasting from another tab... there’s a reason the finder/swapper popup exists!