Build an Angular App Leveraging Sitefinity Headless Capabilities

January 30, 2019 Digital Experience, Sitefinity

In this tutorial, learn how to create an Angular app that consumes Sitefinity CMS OData services using Sitefinity’s easy-to-use OData SDK.

Sitefinity CMS provides powerful content management capabilities and robust APIs to consume content through OData services, enabling you to implement headless CMS use cases. In such scenarios you author and organize your content via Sitefinity CMS, and expose it using out of the box RESTFul API. This approach allows you to develop and deliver this content on various devices and leverage the most popular frontend frameworks.

This post demonstrates how to create an Angular app that consumes the Sitefinity CMS OData services. You don’t have to be too familiar with HTTP requests or the OData protocol, because we’ll be using Sitefinity’s OData SDK (also referred to below as WebServices SDK). The SDK is available through npm and has a simple and comprehensive API. We’ll be constantly evolving the SDK in 2019 with further functional and documentation improvements.

Prerequisites

  1. Our application consumes data from a Sitefinity CMS instance. In this example, we’re using a hosted trial Sitefinity website (Sandbox). If you don’t have one, creating it takes just a few clicks. Start here: https://www.progress.com/sitefinity-cms/try-now/sandbox.
  2. Node.js 8.10+
  3. Angular CLI (https://github.com/angular/angular-cli/wiki)
  4. The full source code of the application that we're discussing can be found here: https://github.com/Sitefinity/QuantumHeadlessAngularApp

Initial Setup

  1. Start by creating a new Angular application and giving it a name
     ng new QuantumHeadlessAngularApp
  2. Add sitefinity-webservices-sdk as a npm dependency and install it.

    npm install  sitefinity-webservices-sdk --save
  3. Add a reference to the sitefinity-webservices-sdk javascript in the angular.json.
    "scripts": [ 
      "node_modules/sitefinity-webservices-sdk/dist/sitefinity-webservices-sdk.js" 
    ] 
  4. Add the sitefinity-webservices-sdk as a provider.
    providers: [ 
      { provide: 'Sitefinity', useValue: window['Sitefinity'] }, 
    ] 

Create the Sitefinity Service

The Sitefinity service provides single instances of the Sitefinity SDK object and the SDK query object. We inject Sitefinity in the constructor of the service and initialize the instance. It takes a serviceUrl, pointing to the default OData service of your hosted Sitefinity project. The service URL is constructed by appending /appi/default to your Sitefinity website domain. For example:
http://site17899124552543.srv06.sandbox.sitefinity.com/api/default/

import {Inject, Injectable} from '@angular/core';
import {SettingsService} from './settings.service';

const endpoint = '/api/default/';

@Injectable({
  providedIn: 'root'
})
export class SitefinityService {
  private sitefinity: any;
  private queryInstance: any;

  get instance(): any {
    if(!this.sitefinity) {
      this.initializeInstance();
    }
    return this.sitefinity;
  }

  get query(): any {
    return this.queryInstance;
  }

  constructor(@Inject('Sitefinity') private sf, private settings: SettingsService) {}

  private initializeInstance(){
    const serviceUrl = `${this.settings.url}${endpoint}`;
    if(serviceUrl) {
      this.sitefinity = new this.sf({serviceUrl});
      this.queryInstance = new this.sf.Query();
    }
  }
}

Create the Application

The application consists of a navigation menu and two pages—News and Showcases.

The News page displays a list of news items. Additional functionality provides users with the ability to load more content and filter it by taxonomy (tags and categories).

The second page displays a list of showcases (where showcases is a dynamic content type we created in the hosted trial). Users will be able to search for news items, showcases and images.

The general layout of the app and its reusable parts are located in the layout.component. The template of the layout.component consists of a navigation, a search bar, a header image, and a container for different content across the pages. Here’s a brief outline of the functionality contained in each component:

  • The navigation uses the Angular routing mechanism
  • The search bar is a separate component
  • The container is an Angular router-outlet, displaying content, depending on the current route
  • Header images are dynamic as well

Displaying News Items

The NewsItemsComponent displays a list of news items. After the component is initialized, we call the getNewsItems method to load the list.

ngOnInit() { 
  this.getNewsItems(); 
} 

In the getNewsItems method of the NewsItems service you can see how to get newsItems from Sitefinity, using the WebServices SDK.

getNewsItems(): Observable<NewsItem[]> { 
  let query; 
  const newsReplaySubject = new ReplaySubject<NewsItem[]>(1); 
    query = this.sitefinity 
      .query 
      .select('Title', 'Id', 'Content', 'DateCreated', 'PublicationDate', 'Summary', 'UrlName', 'Author','Tags', 'Category', 'Featured') 
      .expand('Thumbnail') 
      .order('PublicationDate desc'); 
    this.sitefinity.instance.data(newsItemsDataOptions).get({ 
    query: query, 
    successCb: data => newsReplaySubject.next(data.value as NewsItem[]), 
    failureCb: data => console.log(data) 
  }); 
  return newsReplaySubject.asObservable(); 
}

OData supports various kinds of query options for querying data. Query parameters give you the option to modify your request with key-value pairs. The query implementation, coming from the Sitefinity WebServices SDK, simplifies this syntax, so you can use methods like select, expand, order, where, and so on. For example, in our getNewsItems method, we use the Sitefinity query to select the desired properties to be returned, fetch the related data items, and specify the sort order of the results. Since the Thumbnail property is a related field, we use the expand method to get its value populated.

The Sitefinity object instance can be configured using the data object, where we pass information about the content type, the provider and the culture.

export const newsItemsDataOptions = { 
  urlName: 'newsitems', 
  providerName: 'OpenAccessDataProvider', 
  cultureName: 'en' 
}; 

Going back to the NewsComponent, we inject the NewsService in the constructor. In the getNewsItems method implementation we subscribe to the getNewsItems method of the NewsService. We assign the fetched news items to a public property of the NewsItems class, so that we can use it in the template of the component.

this.subscription = this.newsService.getAllNews().subscribe((data: NewsItem[]) => { 
  if (data) { 
    this.newsItems.push(data); 
  } 
}); 

Adding the Load More Option

To implement a Load more functionality, we extend the query with skip and take methods.

query = this.sitefinity 
  .query 
  .select('Title', 'Id', 'Content', 'DateCreated', 'PublicationDate', 'Summary', 'UrlName', 'Author', 'Tags', 'Category', 'Featured') 
  .expand('Thumbnail') 
  .order('PublicationDate desc') 
  .skip(skip).take(take); 

Here’s what the final getNewsItems method implementation of our service should look like:

getNewsItems(take?: number, skip?: number): Observable<NewsItem[]> { 
  let query; 
  const newsReplaySubject = new ReplaySubject<NewsItem[]>(1); 
  if ((take !== null && take !== undefined) && (skip !== null && skip !== undefined)) { 
    query = this.sitefinity 
      .query 
      .select('Title', 'Id', 'Content', 'DateCreated', 'PublicationDate', 'Summary', 'UrlName', 'Author', 'Tags', 'Category', 'Featured') 
      .expand('Thumbnail') 
      .order('PublicationDate desc') 
      .skip(skip).take(take); 
  } else { 
    query = this.sitefinity 
      .query 
      .select('Title', 'Id', 'Content', 'DateCreated', 'PublicationDate', 'Summary', 'UrlName', 'Author','Tags', 'Category', 'Featured') 
      .expand('Thumbnail') 
      .order('PublicationDate desc'); 
  } 
    this.sitefinity.instance.data(newsItemsDataOptions).get({ 
    query: query, 
    successCb: data => newsReplaySubject.next(data.value as NewsItem[]), 
    failureCb: data => console.log(data) 
  }); 
  return newsReplaySubject.asObservable(); 
} 

Implement Getting a Single Item

Getting a single newsItem with the SDK is done using the getSingle method of the instance. Here’s how we get a newsItem by id in the NewsService.

getNewsItem(id: string): Observable<NewsItem> { 
  const newsReplaySubject = new ReplaySubject<any>(1); 
    this.sitefinity.instance.data(newsItemsDataOptions).getSingle({ 
        key: id, 
        query: this.sitefinity 
          .query 
          .select('Title', 'Id', 'Content', 'DateCreated', 'Summary', 'UrlName', 'Author', 'Tags') 
          .expand('Thumbnail') 
          .order('Title desc'), 
        successCb: (data: NewsItem) => {newsReplaySubject.next(data)}, 
        failureCb: data => console.log(data) 
      }); 
  return newsReplaySubject.asObservable(); 
} 

Add Filtering by Taxonomy

On the News page, we can also filter by tag and by category. We do this by implementing a getNewsByTaxa method of the NewsService, where propertyName is the taxonomy field name (for example “Tags” or “Category”) and taxaId is the Id of the taxonomy we are filtering by.

getNewsByTaxa(propertyName: string, taxaId: string): Observable<NewsItem[]> { 
  const newsSubject = new ReplaySubject<any>(1); 
  this.sitefinity.instance.data(newsItemsDataOptions).get({ 
    query: this.sitefinity 
      .query 
      .select('Title', 'Id', 'Content', 'DateCreated', 'Summary', 'UrlName', 'Author') 
      .expand('Thumbnail') 
      .order('Title desc') 
      .where() 
      .any() 
      .eq(propertyName, taxaId) 
      .done().done(), 
    successCb: data => newsSubject.next(data.value as NewsItem[]), 
    failureCb: data => console.log(data) 
  }); 
  return newsSubject.asObservable(); 
} 

Notice how we are using the where method of the Sitefinity query in the getNewsByTaxa implementation. The syntax is straightforward and support standard filtering operations.

Note the double use of the done() method, which is required because the last call to it ends the definition of the current query. Еach logical operator (for example, eq, and, or, contains, and so on) requires calling the done method again. This is specific to the current implementation of the Sitefinity OData SDK and is subject to refactoring in the future versions, where only one call to done() will be needed.

.where() 
.any() 
.eq(propertyName, taxaId) 
.done().done()

Getting the Total Item Count (Without Fetching the Items)

The last method we have in the News service is:

getAllNewsCount(): Observable<number> { 
  const newsReplaySubject = new ReplaySubject<any>(1); 
    this.sitefinity.instance.data(newsItemsDataOptions).get({ 
      query: this.sitefinity 
        .query 
        .count(false), 
      successCb: (data: number) => newsReplaySubject.next(data), 
      failureCb: data => console.log(data) 
    }); 
  return newsReplaySubject.asObservable(); 
} 

By default, the count method returns all items, as well as their count. However, when passing false as an argument, the method returns only the count. In our application we use this method to check when the Show more link should be displayed.

Implement the Search Functionality

Another important part of the application is the search functionality. We want to search in multiple content types—images, showcases and news items. For this we utilize the batch request functionality of the SDK, which allows us to combine multiple HTTP requests into one.

private getItemsBySearchWord(searchWord: string): void { 
  const batch = this.sitefinity.instance.batch(data => this._searchResults.next(this.mapSearchResults(data))); 
  batch.get({ entitySet: 'showcases', query: this.sitefinity 
      .query 
      .select('Title', 'Client', 'Challenge', 'Solution', 'Results', 'Id') 
      .order('Title asc') 
      .where() 
      .or() 
      .contains('Title', searchWord) 
      .or() 
      .contains('Client', searchWord) 
      .or() 
      .contains('Challenge', searchWord) 
      .or() 
      .contains('Solution', searchWord) 
      .or() 
      .contains('Results', searchWord) 
      .done().done().done().done().done().done()}); 
  batch.get({ entitySet: 'images', query: this.sitefinity 
      .query 
      .where() 
      .contains('Title', searchWord) 
      .done()}); 
  batch.get({ entitySet: 'newsitems', query: this.sitefinity 
      .query 
      .select('Title', 'Content', 'Summary', 'Id') 
      .order('Title asc') 
      .where() 
      .or() 
      .contains('Title', searchWord) 
      .or() 
      .contains('Content', searchWord) 
      .or() 
      .contains('Summary', searchWord) 
      .done().done().done().done()}); 
  batch.execute(); 
} 

To chain the requests we instantiate a new batch from the Sitefinity instance, passing a callback for the result. We then call the get method of the batch object for each content type we would like to request. To finalize the batch request, we use the batch.execute() method.

We hope this blog post helped you get started with the Sitefinity OData services, and you enjoyed working with the SDK. If you have any questions, please drop us a line in the comments section below. If you're new to Sitefinity, you can learn more here or jump right in and start your free 30-day trial today.

Need to get up to speed with Sitefinity? Sign up for our free course designed to help aspiring  developers leverage Sitefinity's full potential.

 

Zheyna Peleva

Jen Peleva was a Principal frontend developer for the Sitefinity CMS.

Read next What is GraphQL and How to Use It in Sitefinity DX