Working with placeholders in a JSS Angular app
Placeholders are vital components in any application built with Sitecore JSS. To provide examples, this topic uses the Sitecore JSS Angular sample application.
The JSS Angular sample application comes with a root placeholder. The root placeholder is defined in src/app/routing/layout/layout.component.html
.
The sc-placeholder
component is imported from Sitecore JSS for Angular, @sitecorelabs/sitecore-jss-angular
.
<sc-placeholder name="jss-main" [rendering]="route" (loaded)="onPlaceholderLoaded($event)"></sc-placeholder><sc-placeholder name="jss-main" [rendering]="route"></sc-placeholder>
The sc-placeholder
Angular component has a name
attribute with the value jss-main
, indicating that it represents the Sitecore placeholder named jss-main
. The placeholder renders one or more components based on route data, as indicated by the data bound to the [rendering]
input of the placeholder component:
<sc-placeholder name="jss-main" [rendering]="route" (loaded)="onPlaceholderLoaded($event)"></sc-placeholder>
You can apply sc-placeholder
to an existing DOM element as an attribute.
Understanding the origin of route data
To understand how the placeholder component receives the route
data, you must understand how the various components, application modules, and services work together.
In production, the script for building the JSS Angular application builds a server bundle and a client bundle to support universal rendering. After running the script jss build
, you can find the bundles at the following locations:
-
Client bundle - located in
dist/browser/
. -
Server bundle - located at
dist/server.bundle.js
.
The two bundles cannot run independently. They must be served by a server. JSS comes with a JavaScript view engine for Node that can run the server bundle. The JSS JavaScript view engine expects a server bundle that exports a renderView
function.
To build and serve the two bundles, the JSS Angular app defines two entry points for the application, with associated app modules and data services.
The file src/main.server.ts
defines the entry point for the server bundle, bootstrapping the application module for the server AppServerModule
, defined in src/app/app.server.module.ts
. The AppServerModule
uses the JssContextServerSideService
, defined in src/app/jss-context.server-side.service.ts
, that runs on the server and stores the context data for the JSS app. The context data contains the data for the current route and Sitecore context data.
The file src/main.ts
defines the entry point for the client bundle, bootstrapping the application module for client rendering AppModule
, defined in src/app/app.module.ts
. The AppModule
uses the JssContextService
, defined in src/app/jss-context.service.ts
, that runs on the client (in the browser) and stores the context data for the JSS app.
When the application runs in the browser from the server bundle, the server invokes the renderView
function that renders the AppServerModule
, preparing the context data for the JSS app. For implementation details, see the file server.bundle.ts
and associated services. Both context services use the Angular TransferState
class. The class is a key-value store that is transferred from the application module on the server-side to the application module on the client-side, after normalizing the data:
renderModule(AppServerModule, {
document: template,
url: path,
extraProviders: [
// custom injection with the initial state that SSR should utilize
{ provide: 'JSS_SERVER_LAYOUT_DATA', useValue: transferState },
{ provide: 'JSS_SERVER_VIEWBAG', useValue: state.viewBag },
],
})
.then((html) => callback(null, { html }))
.catch((err) => callback(err, null));
The client-side AppModule
bootstraps the root application component AppComponent
, defined in src/app/app.component.ts
, and imports the RoutingModule
class, defined in the file src/app/routing/routing.module.ts
. The RoutingModule
is responsible for attaching the Angular router to the application root, allowing the JSS Angular app to use an Angular router outlet.
The RoutingModule
defines which component renders in the router outlet placeholder. This component is LayoutComponent
, defined in the src/app/routing/layout/layout.component.ts
file. The RoutingModule
uses the JssRouteResolver
provider that, when resolving a route, sets the state for the active route.
Finally, the LayoutComponent
, injected with both route
and layout data, can subscribe to the active route and extract the route data from the Sitecore context.
At this point, the placeholder component can render the HTML markup for the route in a production setting.
If you work in disconnected mode, the JSS application retrieves route data from locally defined files. The sample application includes some pre-defined local data in the folder src/data
. When loading a route, for example, the home page, the application fetches data from the file src/data/en.yml
. The response contains the JSON data for the route:
{
"context": {
"pageEditing": false,
"site": {
"name": "JssDisconnectedLayoutService"
},
"pageState": "normal",
"language": "en"
},
"route": {
"databaseName": "available-in-connected-mode",
"deviceId": "available-in-connected-mode",
"itemId": "home-page",
"itemLanguage": "en",
"itemVersion": 1,
"layoutId": "available-in-connected-mode",
"templateId": "available-in-connected-mode",
"templateName": "available-in-connected-mode",
"name": "home",
"fields": {
"pageTitle": {
"value": "Welcome to Sitecore JSS"
}
},
"placeholders": {
"jss-main": [
{
"uid": "{2C4A53CC-9DA8-5F51-9D79-6EE2FC671B2D}",
"componentName": "ContentBlock",
"dataSource": "available-in-connected-mode",
"params": {},
"fields": {
"heading": {
"value": "Welcome to Sitecore JSS"
},
"content": {
"value": "<p>Thanks for using JSS. Here are some resources to get you started:</p>"
}
}
}
]
}
}}
The JSON data structure, in the route
property, contains a list of placeholders
. Each placeholder name is associated with an array of components. We call the mapping of components to placeholders component placement rules.
Because the sc-placeholder
component has access to the placeholders
object within the route
property, it finds the matching placeholder by name (jss-main
) and renders the array of components within it. In this example, it renders the ContentBlockComponent
component, a regular Angular component, dynamically and provides the heading
and content
, fields through a dependency injection token:
import { Component, Input } from '@angular/core';
import { ComponentRendering } from '@sitecore-jss/sitecore-jss-angular';
@Component({
selector: 'app-content-block',
templateUrl: './content-block.component.html',
})
export class ContentBlockComponent {
@Input() rendering: ComponentRendering;
}
The rendering
property includes all the information regarding the ContentBlockComponent
component coming from the data:
"componentName": "ContentBlock",
"dataSource": "available-in-connected-mode",
"params": {},
"fields": {
"heading": {
"value": "Welcome to Sitecore JSS"
},
"content": {
"value": "<p>Thanks for using JSS. Here are some resources to get you started:</p>"
}
}
Nesting placeholders and components
Because components can contain their own sc-placeholder
components, you can define their contents with placeholders
in the component placement rules.
For example, the WelcomeComponent
component might contain the following:
@Component({
selector: 'app-welcome',
template: `
<sc-placeholder name="welcome" [rendering]="rendering"></sc-placeholder>
`
})
export class WelcomeComponent {
rendering: any;
}
In this case, it then defines its child components in its component placement rules:
{
componentName: 'Welcome',
fields: {
title: {
value: 'Sitecore Experience Platform + JSS',
},
text: {
value: '<p>...</p>',
},
logoImage: {
value: {
src: '/assets/img/sc_logo.png',
alt: 'Logo'
},
},
},
placeholders: {
welcome: [
{
componentName: 'AnotherComponent',
fields: {
/* etc */
}
}
]
}
}
Input and output binding
It is possible to bind custom properties on components using the [inputs]
and [outputs]
binding properties on sc-placeholder
. These properties allow input and output binding to properties on all components within the placeholder.
This allows for orchestration/interaction with child components, even though they are created dynamically by sc-placeholder
, and can be particularly useful in complex user flows, which you still want to manage/optimize through Sitecore XP.
The following is a simplified example of using [inputs]
and [outputs]
:
@Component({
selector: 'my-container-component',
template: `<sc-placeholder
name="placeholder"
[rendering]="rendering"
[inputs]="inputs"
[outputs]="outputs"
></sc-placeholder>`
})
class MyContainerComponent {
@Input() public rendering: any;
inputs = {
hello: 'world',
something: () => 'can be really complex'
};
outputs = {
onSomething: (type) => alert(type)
}
}
@Component({selector: 'my-rendering')
class MyRendering {
// standard on any placeholder-managed component
@Input() public rendering: any;
// additional properties for input/output binding
@Input() hello: string;
@Input() something: Function;
@Output() onSomething = new EventEmitter<string>();
}
Lazy loading placeholders
When lazy loading a Sitecore component, the placeholder appears empty by default. You can specify a temporary body for the component that shows while the component is loading. When the component is finished loading, the temporary body is replaced with the actual content. The following is a simplified example:
<sc-placeholder [rendering]="rendering">
<img *scPlaceholderLoading src="loading.gif">
</sc-placeholder>
Enhancing placeholders
The Placeholder
component supports several customizations that can enhance the developer experience.
Error components
If a rendering error occurs in a placeholder, the placeholder 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 React component using the errorComponent
prop.
Missing Component 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.component.ts
yet), the rendering is replaced with a MissingComponent
component defined in the missingComponentComponent
property of your Angular component. The default implementation is a simple message, but you can customize it by using your own Angular 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.