Working with placeholders in a JSS Vue.js sample app

Version: 21.x

Placeholders are vital components in any application built with JSS.

Note

Refer to Page composition in JSS apps using Sitecore data for more information about how Sitecore layouts, placeholders, and renderings work and how JSS leverages them.

This topic describes the techniques you can use to work with placeholders when developing Vue.js applications with JSS.

Basic placeholder technique

The most basic and most common way to add a placeholder is to use the Placeholder component:

RequestResponse
<template>
  <div>
    <h1>My App</h1>
    <sc-placeholder name="jss-main" :rendering="route" />
  </div>
</template>
<script>
import { Placeholder } from '@sitecore-jss/sitecore-jss-vue';

export default{
  name: 'MyComponent',
  props: {
    route: {
      type: Object
    }
  },
  components: {
    ScPlaceholder: Placeholder,
  }
}
</script>

The name is the key of the placeholder you are exposing, and the rendering is the current Sitecore-provided route/layout data or parent component data if you are exposing a placeholder from within another component.

The Placeholder component does not render a wrapper around child components. This is because of Vue's fragment technique.

SitecoreJssPlaceholderPlugin technique

This technique allows you to get an array of the Vue components in a placeholder attached as a computed property to the component instance that exposes the placeholder.

This technique enables very powerful manipulation techniques that would be nearly impossible in traditional Sitecore development.

To use this technique, you must install the SitecoreJssPlaceholderPlugin Vue plugin provided in the sitecore-jss-vue package, as follows:

RequestResponse
import { createApp, createSSRApp } from 'vue';
import { SitecoreJssPlaceholderPlugin } from '@sitecore-jss/sitecore-jss-vue';
import componentFactory from './path/to/your/componentFactory.js';

const app = isSSR ? createSSRApp(vueOptions) : createApp(vueOptions);

// The plugin uses a "global" componentFactory, but you can override
// the `componentFactory` at the component level if needed.
app.use(SitecoreJssPlaceholderPlugin, { componentFactory });

To use the plugin in your component, add the withPlaceholder property on the component:

RequestResponse
<template>
  <div>
    <component v-for="(tab, index) in tabs" :is="tab" :key="`tab${index}`" />
  </div>
</template>

<script>
export default {
  name: 'MyComponent',
  props: {
    rendering: {
      type: Object,
    },
  },
  withPlaceholder: {
    placeholders: 'tabs',
  },
};
</script>

In the preceding example, the SitecoreJssPlaceholderPlugin plugin uses a mixin that attaches to the beforeCreate hook to look for the withPlaceholder property on a component definition. The plugin then uses the value provided by the withPlaceholder property to find the specified placeholder data, for example, tabs, in the rendering property data. The plugin then creates a computed property on the component, using the name of the placeholder as the property name by default, and assigns an array of all the Vue components for that placeholder to the computed property. This allows you to use the dynamic component feature in Vue.js to render the placeholder components in your template.

When you iterate over the component array in your template, Vue renders the components where you emit them, or you can achieve this using the Placeholder component. If you emit the placeholder contents with this technique, the placeholder contents have no wrapping component and render inline. This is very useful when you are using Vue libraries that are based on a specific component hierarchy, such as vue-carousel. For example, consider the following code sample:

RequestResponse
<carousel>
  <slide>Slide 1 Content</div>
  <slide>Slide 2 Content</div
  <slide>Slide 3 Content</div
</carousel>

In the preceding sample, it is expected that the component hierarchy is carousel -> slide. If you want to add the slide components using a placeholder, so that Sitecore could define them, you can use the Placeholder component sc-placeholder to achieve the same hierarchy.

RequestResponse
<carousel>
  <sc-placeholder name="jss-slides" :rendering="rendering" />
</carousel>

Component transformation

If you want a completely flat component hierarchy such as Carousel->Slide in the previous example, you can take advantage of the computed property being an array to transform the child components with a wrapper using an inline template. When using this technique, the child Sitecore components can be completely unaware of the wrapping and render only their content for a clean separation of concerns:

RequestResponse
<template>
  <carousel>
    <template v-for="(slide, index) in $options.computed.slidesPlaceholder">
      <!-- this `v-if` is important, as it helps prevents breakage of the carousel markup when using Sitecore Experience Editor -->
      <component v-if="slide.isxEditorComponent" :is="slide" />

      <!-- wraps _all_ child components in a <slide> component -->
      <slide v-else :key="`slide${index}`">
        <component :is="slide" />
      </slide>
    </template>
  </carousel>
</template>

<script>
export default {
  name: 'ContainerComponent',
  props: {
    rendering: {
      type: Object,
    },
  },
  withPlaceholder: {
    // you can alias the computed prop name for the placeholder or pass an array of placeholders
    placeholders: {
      placeholder: 'slides',
      computedPropName: 'slidesPlaceholder',
    },
  },
};
</script>

Slot API

If you prefer the slot pattern instead of the dynamic computed property offered by the SitecoreJssPlaceholderPlugin plugin, JSS placeholders have a default slot scope. Using the <Placeholder> component's default scoped slot, you can take over the rendering of the placeholder contents in the same way as with the dynamically computed property.

The following example illustrates how to render the components array using the default scoped slot:

RequestResponse
<template>
  <sc-placeholder :rendering="rendering" name="jss-main">
    <div v-slot="{components, isEmpty}">
      <!--
        The placeholder is considered "empty" if it contains no assigned presentation components.
        However, if in Experience Editor (EE) mode, the placeholder may still contain EE-generated components.
      -->
      <template v-if="!isEmpty">
        <template v-for="(component, index) in components">
          <!--
            If the component is a presentation component, i.e. not an EE-generated component,
            we may want to wrap it in another component or markup.
          -->
          <div v-if="!component.isxEditorComponent" :key="index">
            <component :is="component" />
          </div>
          <!-- Else the component is an EE-generated component, render it as-is -->
          <component v-else :is="component" :key="index" />
        </template>
      </template>
      <template v-else>
        <!--
          In EE-mode, we still want to render "empty" placeholders so that content authors can
          assign components to them.
        -->
        <component v-for="(component, index) in components" :is="component" :key="index" />
      </template>
    </div>
  </sc-placeholder>
</template>

<script>
import { Placeholder } from '@sitecore-jss/sitecore-jss-vue';

export default {
  name: 'MyComponent',
  props: {
    rendering: {
      type: Object,
    }
  },
  components: {
    ScPlaceholder: Placeholder,
  },
};
</script>

Direct placeholder introspection

If you want to have complete control over the rendering of a placeholder, you can access the placeholder's data, as opposed to its components, and traverse it using $props.rendering to discover child component data and fields, because the placeholder hierarchy is a JavaScript object. You can render this data however you choose.

A good use case for this is components such as tabs that you might want to render pieces of in multiple places (the tab title, and the tab contents). For example:

RequestResponse
<template>
  <div class="tabs-container">
    <div class="tabs-heading">
      <ul>
        <li v-for="(tab, index) in rendering.placeholders['jss-tabs']" :key="`tab${index}`">
          <button :class="`${index === activeTabIndex ? 'active' : null}`" v-on:click="activeTabIndex = index">
            <sc-text :field="tab.fields.title" />
          </button>
        </li>
      </ul>
    </div>
    <div class="tabs-content">
      <component :is="activeTab" />
    </div>
  </div>
</template>
<script>
export default {
  name: 'Tabs',
  props: {
    rendering: {
      type: Object,
    }
  },
  data() {
    return {
      activeTabIndex: 0,
    };
  },
  withPlaceholder() {
    return {
      placeholders: [
        {
          placeholder: 'jss-tabs',
          computedPropName: 'tabsPlaceholder',
        },
      ],
    };
  },
  computed: {
    activeTab() {
      return this.$options.computed.tabsPlaceholder && this.$options.computed.tabsPlaceholder[this.activeTabIndex];
    },
  },
}
</script>

Multiple placeholders

You can render multiple placeholders in your components using any of the previously described techniques. For example:

  • Using the basic technique:

    RequestResponse
    <template>
      <div>
        <h1>My App</h1>
        <sc-placeholder name="jss-column-1" :rendering="rendering" />
        <sc-placeholder name="jss-column-2" :rendering="rendering" />
      </div>
    </template>
    <script>
    import { Placeholder } from '@sitecore-jss/sitecore-jss-vue';
    export default{
      name: 'MyComponent',
      props: {
        rendering: {
          type: Object
        }
      },
      components: {
        ScPlaceholder: Placeholder,
      }
    }
    </script>
  • Using SitecoreJssPlaceholderPlugin:

    RequestResponse
    <template>
      <div>
        <div class="col-1">
          <component v-for="(comp, index) in this.$options.computed.phColumn1" :is="comp" :key="`comp${index}`" />
        </div>
        <div class="col-2">
          <component v-for="(comp, index) in this.$options.computed.phColumn2" :is="comp" :key="`comp${index}`" />
        </div>
      </div>
    </template>
    
    <script>
    export default {
      name: 'MyComponent',
      props: {
        rendering: {
          type: Object,
        },
      },
      withPlaceholder: {
        placeholders: [
          {
            placeholder: 'jss-column-1',
            computedPropName: 'phColumn1',
          },
          {
            placeholder: 'jss-column-2',
            computedPropName: 'phColumn2',
          },
        ],
      },
    };
    </script>

Enhancing placeholders

The Placeholder component supports a number of customizations that can enhance your experience.

Error components

If a rendering error (that is, an exception) occurs in a placeholder, it displays an error component instead of the placeholder contents and logs the error details to the console. You can customize this component by substituting your own Vue component using the errorComponent property.

Dealing with missing components

If a placeholder contains a rendering name that is unknown to the componentFactory (for example, a back-end developer creates a Foo rendering and adds it to a page, but there is no Foo.js file yet), the rendering is replaced with a MissingComponent component defined in the missingComponentComponent property of your Vue component. The default implementation is a simple message, but you can customize it by using your own Vue component on the missingComponentComponent property.

Handling hidden renderings

If a rendering is hidden by personalization rules, the rendering is replaced by a HiddenRendering component to prevent error messages when the rendering is missing a JSS implementation. You can create your own component and provide it to the placeholder using the hiddenRenderingComponent property.

Do you have some feedback for us?

If you have suggestions for improving this article,