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.
ng new QuantumHeadlessAngularApp
npm install sitefinity-webservices-sdk --save
"scripts": [
"node_modules/sitefinity-webservices-sdk/dist/sitefinity-webservices-sdk.js"
]
providers: [
{ provide: 'Sitefinity', useValue: window['Sitefinity'] },
]
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();
}
}
}
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 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);
}
});
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();
}
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();
}
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()
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.
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.
Let our experts teach you how to use Sitefinity's best-in-class features to deliver compelling digital experiences.
Learn MoreSubscribe to get all the news, info and tutorials you need to build better business apps and sites