Creating an Announcement Client Web Part: Part 3 – Rendering Items

This is the 3rd part of my blog mini-series, based around a project to create an Announcement client web part using the SPFx.

The aim of this project is to build a web part that can hook up with views defined in the Events list or the Site Pages library, either on the local site or on any other site within an SharePoint online tenancy.

Here is where we are up to:

  • Part 1: Project setup and accessing web sites and lists. In this post, I explained how to set up the project and how to connect with the Events list and Site Pages library, on both the local site or any other site in the tenancy.
  • Part 2: Loading Views and List Items. In this post, I demonstrated how to load a drop-down picker in the web part properties pane with a list of views defined on the specified list or library and then how to use that view to generate a CAML query that would return the items specified by the view’s settings for filter, sort, folders and item count.

We can now get data back from a specific view from either the Events list of Site Pages library but so far, we have just rendered the titles of the items returned by the view’s query.  We need to do better than that.

Rendering Items

We need to find a way that converts the data we get back from the view query into a form that is more palatable for consuming users. Basically, we have to generate some HTML that wraps around each item.

View Fields

The first thing I am going to do is to extend the view fields that we get returned by the view query as shown below:

public static getViewFields(): string{return
    `<ViewFields>
 	<FieldRef Name="Title"></FieldRef>
	<FieldRef Name="Description"></FieldRef>
	<FieldRef Name="BannerImageUrl"></FieldRef>
	<FieldRef Name="BannerUrl"></FieldRef>
 	<FieldRef Name="EventDate"></FieldRef>
	<FieldRef Name="EndDate"></FieldRef>
	<FieldRef Name="Category"></FieldRef>
	<FieldRef Name="FileRef"></FieldRef>
	<FieldRef Name="Modified"></FieldRef>
	<FieldRef Name="Editor"></FieldRef>
  </viewFields>`;
}

The set of requested view fields is defined in the ViewUtilities class that I presented in Part 2 of this series.

Remember that the selected view is only the basis for the CAML Query we use to fetch items from the list. Whilst we want to use the filter and sort criteria as well as item limits and whether items in sub-folders are included or excluded, we can’t rely on the view to return us the columns that we might want to render in the web part. As such, we need to explicitly specify any view fields that we are relying on being available.

Now, it doesn’t seem to matter if the view fields element contains definitions that don’t actually exist on the list and so we can safely include all field definitions required by both of the different list/library types.

Note that some fields are not consistently named either. For example, in the Site Pages library the BannerImageUrl will give use access to the image used in the page header but in an events list, this same property is accessed via the BannerUrl field.

I’ve also added the EndDate, EventDate and Category fields as these might prove useful when rendering event list items, although these will not normally exist in a standard Site Pages library of course.

Item Interfaces

We need the web part to cope with two different item types, namely a Site Page and an Event list item and in order to get some intellisence in VS Code going and some stronger typing I have decided to define an interface for each type of item. You don’t need to do this but its is better programming practice than just using the ‘any’ type.

I have defined these interfaces in a separate project file, the content of which is presented below:

import { Guid } from '@microsoft/sp-core-library';
export interface IBannerUrl{
  Url: string;
  Description: string;
}

export interface IFile{
 ServerRelativeUrl: string;
 TimeCreated: Date;
 TimeLastModified: Date;
}
export interface INewsItem {
 Title: string;
 Description: string;
 File: IFile;
 BannerImageUrl: IBannerUrl;
 ContentTypeId: string;
 Created: Date;
 GUID: Guid;
 Id: number;
 Modified: Date;
}

export interface IEventItem {
 Title: string;
 Description: string;
 Category: string;
 BannerUrl: IBannerUrl;
 ContentTypeId: string;
 Created: Date;
 GUID: Guid;
 Id: number;
 Modified: Date;
 EventDate: Date;
 EndDate: Date;
}

A news items are actually web pages in the Site Pages library (and therefore essentially just a document in the library) they will have a child File object, for which I have defined an additional interface. The banner URLs are actually objects as well, both in Site Pages and Events. Although they have a different property name (so much for consistency) they have the same property signature and so can share the same IBannerUrl interface.

You can see that the IEventItem interface includes the newer properties now returned by the updated set of view fields.

Render Methods

We can now update the render method of the web part to call upon some helper methods to return formatted HTML instead of just the item title we output previously:

public render(): void {
  if (Environment.type === EnvironmentType.Local) {
    this.domElement.innerHTML = "You need to debug this in a remote workbench";
   }
  else {
    this.domElement.innerHTML = "";
    let newsWeb: Web = new Web(this.siteWebUrl);
    let contentSourceList : List = this.getSourceList(newsWeb);
    let itemMarkup: string = "";
    ViewUtilities.getViewQueryForList(contentSourceList, this.properties.viewId).then((view) => {
      ViewUtilities.getItemsByViewQuery(contentSourceList, view).then((items: any) => {
         items.forEach(item => {
           itemMarkup += this.getItemMarkup(item);
      });     
      this.domElement.innerHTML = itemMarkup;
    });
   });
 }
}

The only significant change here is that we are now assembling a string that will contain the HTML mark-up for each item returned by the view query by calling the getItemMarkup method for each item.

public getItemMarkup(item: any): string{
  return (this.properties.contentSource == ContentSource.SitePages) ? 
    this.getNewsItemMarkup(item) : this.getEventItemMarkup(item);
}

This is a very simple method which calls either the getNewsItemMarkup method or the getEventItemMarkup method depending on whether the contentSource property tells us we are hooked up to a Site Pages library or an Events list respectively.

These methods simply return an HTML table with data items inserted into appropriate cells.

public getNewsItemMarkup (item: INewsItem): string{
  let titleDiv : string = item.Title ? `${item.Title}` : "";
  let descriptionDiv: string = item.Description ? `${item.Description}` : "";
  let img: string = (item.BannerImageUrl) ? 
    `<img src="${Utilities.AppendResolutionParam(item.BannerImageUrl.Url)}" 
        style="height:60px; width:100px"/>` : "";
  return
    `<table>
       <tr>
         <td valign="top">${img}</td>
         <td valign="top">${titleDiv}${descriptionDiv}</td>
       </tr>
     </table>`;
}

public getEventItemMarkup (item: IEventItem): string{
  let titleDiv : string = item.Title ? `${item.Title}` : "";
  let descriptionDiv: string = item.Description ? 
    `${{Utilities.ExtractInnerText(item.Description)}` : "";
  let eventDate: string = item.EventDate ? 
    `Start: ${Utilities.ToFormattedDateTimeString(item.EventDate)}` : "";
  let endDate: string = item.EndDate ? 
    `End: ${Utilities.ToFormattedDateTimeString(item.EndDate)}` : "";
  let img = (item.BannerUrl) ? 
    `<img src="${Utilities.AppendResolutionParam(item.BannerUrl.Url)}" 
       style="height:60px; width:100px"/>` : ""
return
  `<table>
    <tr>
      <td valign="top">${img}</td>
      <td valign="top">${titleDiv}${descriptionDiv}${eventDate}${endDate}</td>
    </tr>
  </table>`;
}

Note that we need to check each property to make sure it is not null before inserting data. Missing data is simply inserted as an empty string.

Custom Formatting Utilities

In a couple of places, I call out to support methods defined in the Utilities class as follows:

public static AppendResolutionParam(sourceString : string): string{
  if (sourceString) {
    if (sourceString.indexOf("?") == -1) {
      return sourceString + "?Resolution=1";
    } else {
      return sourceString + "&Resolution=1";
   }
 } else {
  return  "";
 }
}

The AppendResolutionParam method simply adds a Resolution parameter to the URL for the banner image. This tells SharePoint to give us a lower resolution image, which will reduce the page payload and hence improve performance.

The second method allows us to control the format of date objects.

public static ToFormattedDateTimeString(sourceDate: Date): string{
  if (sourceDate){
    return moment(sourceDate).format('MMMM Do YYYY, h:mm a');
  }
  else {
    return "";
  }
}

This method makes use of the moment.js library which I added to the project by calling:

npm install moment –save

at the Visual Studio code terminal and then by adding a reference to the page at the head of the utilities package:

import * as moment from ‘moment’;

The ExtractInnerText method is only required when processing event list items. This is because the description of a site page gets returned as a truncated text string (chopped to 255 characters or so) but the description of an event gets returned as a non-truncated HTML string complete with markup tags (don’t you just love consistency).

public static ExtractInnerText(sourceString: string): string{
  if (sourceString){
    let text = (new DOMParser).parseFromString(sourceString, "text/html").documentElement.textContent;
    if (text.length < 255) {
      return text;
    } else {
      let lastSpace = text.lastIndexOf(" ");
      return (lastSpace == 0) ? text.substring(0, 255) + "..." : 
          text.substring(0, lastSpace) + "...";
    }
  }
  else {
    return "";
  }
}

The ExtractInnerText method first parses the string as an HTML element to fish out the actual text and then returns a string chopped to 255 characters or there about, appending a 3-dot ellipsis to indicate that the text has been truncated.

In both site pages and the event list items, the source of the description text is the first Text Web Part on the page if the description property is empty.

The Reveal

Gulp serve everything to a remote workbench and configure a web part instance to reveal the results:

Or

It’s not a bad start. We can retrieve the banner image and the item’s title and description and in the case of Events we can also get the start and end date of the Event. However, there is obviously room for improvement as we’d like to:

  • Improve the formatting by applying different styles for headers, description text and dates.
  • Have more flexible layout options.
  • Allow items to be opened in a dialog rather than just redirecting to a new page or opening things up in a new browser tab.
  • Make it responsive so that the layout adapts to the available column width.
  • Consider what to do if the user doesn’t set a custom image.
  • Use the group-by feature of views to show items in different news Channels which may be accessed via a set of tabs or expandable panels.

Kaboodle Announce

In fact, there are a myriad of improvements that can be made but at this point I’m just going to jump to the finished product, which we have called Kaboodle Announce.

This is a beta offering and its’ free, for now at least. So please head over to the product page where you can review the features, download the solution package and access the product documentation. All we ask is that you leave us some constructive feedback and if you like it, pass on the link through your own blog posts or social media.

Obviously, there is a lot of ground that hasn’t been covered between this mini-series of blog posts and the finished product but that’s way too much to cover here and in my defence, these posts were aimed at how to extract data from a view on a source list and not about how to manipulate that data once it has been sucked up from SharePoint.

However, in my journey to come to grips with the SPFx I plan on bring you more tips and tricks. Or maybe, if you take a look at the product and want to know, ‘how did you do that’ about anything, what I’ll do is respond by writing other posts that seek to provide some answers.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: