Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[angular-xmcloud] Introduce SXA layout component for angular #1873

Merged
merged 15 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Our versioning strategy is as follows:
* `nodeAppDestination` arg can be passed into `create-sitecore-jss` command to define path for proxy to be installed in
* `[create-sitecore-jss]``[template/angular-xmcloud]` Angular SXA components ([#1864](https://github.com/Sitecore/jss/pull/1864))
* `[sitecore-jss-angular]` Angular placeholder now supports SXA components ([#1870](https://github.com/Sitecore/jss/pull/1870))
* `[template/angular-xmcloud]` Angular SXA layout; plugin functionality for JssState ([#1873](https://github.com/Sitecore/jss/pull/1873))
addy-pathania marked this conversation as resolved.
Show resolved Hide resolved

### 🛠 Breaking Change

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Inject, Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { HTMLLink } from '@sitecore-jss/sitecore-jss-angular';

@Injectable()
export class JssLinkService {
document: Document;

constructor() {
this.document = Inject(DOCUMENT);
}

addHeadLinks(headLinks: HTMLLink[]) {
if (!headLinks || !headLinks.length) {
return;
}

headLinks.forEach((headLink: HTMLLink) => {
let link: HTMLLinkElement = this.document.createElement('link');
link.setAttribute('rel', headLink.rel);
link.setAttribute('href', headLink.href);
this.document.head.appendChild(link);
addy-pathania marked this conversation as resolved.
Show resolved Hide resolved
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { JssState } from '../../../JssState';
import { Plugin } from '..';
import { LayoutServiceData, getContentStylesheetLink } from '@sitecore-jss/sitecore-jss-angular';

class HeadLinksPlugin implements Plugin {
order = 2;

exec(jssState: JssState, layoutData: LayoutServiceData) {
// TODO: get contextId and edgeUrl properly
const sitecoreEdgeContextId = '';
const sitecoreEdgeUrl = '';
const contentStyles = getContentStylesheetLink(
layoutData,
sitecoreEdgeContextId,
sitecoreEdgeUrl
);
contentStyles && jssState.headLinks.push(contentStyles);

return jssState;
}
}

export const headLinksPlugin = new HeadLinksPlugin();
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<app-navigation></app-navigation>
<div class="{{ mainClassPageEditing }}">
<ng-container *ngIf="state === LayoutState.Layout">
<app-scripts></app-scripts>
<header>
<div id="header">
<sc-placeholder
name="headless-header"
[rendering]="route"
addy-pathania marked this conversation as resolved.
Show resolved Hide resolved
></sc-placeholder>
</div>
</header>
<main>
<div id="content">
<sc-placeholder
name="headless-main"
[rendering]="route"
(loaded)="onMainPlaceholderLoaded($event)"
addy-pathania marked this conversation as resolved.
Show resolved Hide resolved
></sc-placeholder>
</div>
</main>
<footer>
<div id="footer">
<sc-placeholder
name="headless-footer"
[rendering]="route"
addy-pathania marked this conversation as resolved.
Show resolved Hide resolved
></sc-placeholder>
</div>
</footer>
</ng-container>

<app-not-found
*ngIf="state === LayoutState.NotFound"
[errorContextData]="errorContextData"
></app-not-found>
<app-server-error *ngIf="state === LayoutState.Error"></app-server-error>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/* eslint-disable no-shadow, no-console */
import { Component, OnInit, OnDestroy } from '@angular/core';
import { RouteData, Field, LayoutServiceContextData } from '@sitecore-jss/sitecore-jss-angular';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { JssState } from '../../JssState';
import { JssMetaService } from '../../jss-meta.service';
import { JssLinkService } from '../../jss-link.service';

enum LayoutState {
Layout,
NotFound,
Error,
}

interface RouteFields {
[name: string]: unknown;
pageTitle: Field<string>;
}

@Component({
selector: 'app-layout',
templateUrl: './layout.component.html',
})
export class LayoutComponent implements OnInit, OnDestroy {
route: RouteData<RouteFields>;
state: LayoutState;
LayoutState = LayoutState;
subscription: Subscription;
errorContextData: LayoutServiceContextData;
mainClassPageEditing: string;

constructor(
private activatedRoute: ActivatedRoute,
private readonly meta: JssMetaService,
private linkService: JssLinkService
) {}

ngOnInit() {
// route data is populated by the JssRouteResolver
this.subscription = this.activatedRoute.data.subscribe(
(data: { jssState: JssState<RouteFields> }) => {
if (!data.jssState) {
this.state = LayoutState.NotFound;
return;
}

if (data.jssState.sitecore && data.jssState.sitecore.route) {
this.route = data.jssState.sitecore.route;
this.setMetadata(this.route.fields);
this.state = LayoutState.Layout;
this.mainClassPageEditing = data.jssState.sitecore.context.pageEditing
? 'editing-mode'
: 'prod-mode';

this.linkService.addHeadLinks(data.jssState.headLinks);
}

if (data.jssState.routeFetchError) {
if (
data.jssState.routeFetchError.status >= 400 &&
data.jssState.routeFetchError.status < 500
) {
this.state = LayoutState.NotFound;
} else {
this.state = LayoutState.Error;
}

this.errorContextData =
data.jssState.routeFetchError.data && data.jssState.routeFetchError.data.sitecore;
}
}
);
}

ngOnDestroy() {
// important to unsubscribe when the component is destroyed
this.subscription.unsubscribe();
}

setMetadata(routeFields: RouteFields) {
// set page title, if it exists
if (routeFields && routeFields.pageTitle) {
this.meta.setTitle(routeFields.pageTitle.value || 'Page');
}
}

onMainPlaceholderLoaded(_placeholderName: string) {
// you may optionally hook to the loaded event for a placeholder,
// which can be useful for analytics and other DOM-based things that need to know when a placeholder's content is available.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ const pluginDefinitions: PluginDefinition[] = [
rootPath: 'scripts/config/plugins',
moduleType: ModuleType.ESM,
},
{
distPath: 'src/app/temp/jss-state-factory-plugins.ts',
rootPath: 'src/app/lib/jss-state-factory/plugins',
moduleType: ModuleType.ESM,
relative: true,
},
];

pluginDefinitions.forEach((definition) => {
Expand Down
addy-pathania marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import {
RouteData,
LayoutServiceContextData,
} from '@sitecore-jss/sitecore-jss-angular';
import { RouteData, LayoutServiceContextData, HTMLLink } from '@sitecore-jss/sitecore-jss-angular';
import { LayoutServiceError } from './layout/jss-layout.service';

export class JssState<Fields = Record<string, unknown>> {
Expand All @@ -12,4 +9,5 @@ export class JssState<Fields = Record<string, unknown>> {
route: RouteData<Fields>;
};
viewBag: { [key: string]: unknown };
headLinks: HTMLLink[];
}
addy-pathania marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { map, shareReplay, catchError } from 'rxjs/operators';
import { Observable, of as observableOf, BehaviorSubject } from 'rxjs';
import { JssState } from './JssState';
import { JssLayoutService, LayoutServiceError } from './layout/jss-layout.service';
import { sitecoreJssStateFactory } from './lib/jss-state-factory';

export const jssKey = makeStateKey<JssState>('jss');

Expand Down Expand Up @@ -43,10 +44,10 @@ export class JssContextService {
map((routeData) => {
const lsResult = routeData as LayoutServiceData;

const result = new JssState();
result.sitecore = lsResult.sitecore ? lsResult.sitecore : null;
const result = sitecoreJssStateFactory.create(lsResult);
addy-pathania marked this conversation as resolved.
Show resolved Hide resolved
result.language = appLanguage;
result.serverRoute = route;

return result;
}),
catchError((error: LayoutServiceError) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { LayoutServiceData } from '@sitecore-jss/sitecore-jss-angular';
import { JssState } from './../../JssState';
import * as plugins from './../../../app/temp/jss-state-factory-plugins';
addy-pathania marked this conversation as resolved.
Show resolved Hide resolved

export interface Plugin {
/**
* Detect order when the plugin should be called, e.g. 0 - will be called first (can be a plugin which data is required for other plugins)
*/
order: number;
/**
* A function which will be called during page props generation
addy-pathania marked this conversation as resolved.
Show resolved Hide resolved
*/
exec(jssState: JssState, layoutData: LayoutServiceData): JssState;
}

export class SitecoreJssStateFactory {
addy-pathania marked this conversation as resolved.
Show resolved Hide resolved
/**
* Create JssState for given layout service data
* @param {LayoutServiceData} layoutData the layout service data
*/
public create(layoutData: LayoutServiceData): JssState {
const finalJssState = (Object.values(plugins) as Plugin[])
.sort((p1, p2) => p1.order - p2.order)
.reduce((result, plugin) => {
return plugin.exec(result, layoutData);
}, new JssState());

return finalJssState;
}
}

export const sitecoreJssStateFactory = new SitecoreJssStateFactory();
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { JssState } from '../../../JssState';
import { Plugin } from '..';
import { LayoutServiceData } from '@sitecore-jss/sitecore-jss-angular';

class LayoutDataPlugin implements Plugin {
order = 0;

exec(jssState: JssState, layoutData: LayoutServiceData) {
jssState.sitecore = layoutData.sitecore ? layoutData.sitecore : null;

return jssState;
}
}

export const layoutDataPlugin = new LayoutDataPlugin();
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!.gitignore
2 changes: 2 additions & 0 deletions packages/sitecore-jss-angular/src/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export {
ComponentRendering,
ComponentFields,
ComponentParams,
getContentStylesheetLink,
} from '@sitecore-jss/sitecore-jss/layout';
export {
RetryStrategy,
Expand All @@ -69,6 +70,7 @@ export {
HttpResponse,
enableDebug,
ClientError,
HTMLLink,
} from '@sitecore-jss/sitecore-jss';
export { isServer } from '@sitecore-jss/sitecore-jss/utils';
export {
Expand Down