Introduction
This is the first article in a new blog series and is based around a project for the development of a SharePoint Client Web Part built using the SharePoint Framework (SPFx). The idea is to provide an alternative to the standard news and events web parts but this version will have some enhanced features and capabilities. What’s the problem with the standard web parts I hear you ask? Well great as they are, the standard web parts have one main missing feature that I think many customers will find very appealing and that is the ability to open the news page up in a pop-up dialog.
You see I think the standard Microsoft concept for the management of news is flawed as news pages are considered as site pages and stored in the Site Pages library. This means that static site pages on an intranet portal, say for the different departments or functions of the business, are all mixed up news pages as each Modern SharePoint site can only have a single Site Pages library. If you are not very careful the whole things end up a complete mess because portal pages and news pages are very different beasts and there all just thrown into the same container.
In my world, a portal page is a static entity which is essentially a permanent fixture of the portal. It’s static in the sense that it can be relied upon to be there and can be an integrated part of the portal’s navigation structure. Although the page may be static, the content surfaced on that page could (and probably should) be dynamic, it might be the place where new content is surfaced for example.
In contrast, a news page and event items are transient entities. They are a single-shot bullet of information pertaining to something specific. News articles and events go stale and whilst they may have some historical value to the business, their operational value is generally for a short time-window, after which they just become obsolete and are just noise.
My point is that these 2 types of page are very different and have very different lifecycles and are more than likely to have very different authors, creators and approvers. Throwing them into a single library is messy to the point of chaos.
The answer is to separate news channels from your portals and create one or more news and event sites as separate sites and then you can then surface the content created in these sites in the portal. The standard web parts let you do this very easily. This has been a feature of the standard News web part for quite some time and that same capability has now made its way to the Events web part as well. So, it is now entirely possible to separate out the management of news and events and maintain some order in the portal.
However, there are a couple of things that I don’t like about the standard web parts though. First of all, when a user accesses the news or event item, their browsers are redirected to news or event page and this can be very disorientating because one minute you are in the portal and then you’ve been whisked off to some news sites somewhere.
The other thing I have a concern about is that the filters applied in these web parts are all set in the web part itself. I want better governance that, with the ability to control the content being surfaced in the web part from the source library instead of settings in the web part. I might have a dozen web parts feeding from the sale source and these means I can change things in one place, the data source, rather than having to do it in a dozen places.
The first issue can be addressed if we can find a way to open the news article in a pop-up dialog rather than a full-page redirection and the second issue can be resolved if we drive the items to be displayed by using views on the source list/library and not by using filter parameters set in web parts themselves.
I should point out that there is a down-side to this approach. The standard web parts use the search API to retrieve content and this makes it possible to consume news and events from multiple sources i.e. different site pages libraries and event lists on multiple sites. With the view-based approach I am suggesting here, that won’t be possible as we will have to target a single, specific list or library in a single and specific web site. On the upside, this means we have more control and governance and it also means that we won’t have to wait for the search engine to index the content so there will be no enforced delay between content creation and availability, which is often confusing for users.
That then is my objective for this project and in this article, and ones to follow, I shall be guiding you through how I built such a web part.
Getting Started
If you’ve never created a SharePoint Client Web Part before then I suggest you do some basic groundwork first. Maybe start with my series on getting started with the SPFx or head off to Microsoft Docs.
I am assuming that your development environment is up and running and that you can safely get yourself to the point of creating and running the default web part that gets built for you by Yeoman. For this project I am using version 1.8 of the SPFx and my start point is a web part project that targets SharePoint Online only (not SP2016/19) and does not use an additional JavaScript framework i.e. React or Knockout are not required here.
I do however use the PnP JS library which makes accessing SharePoint data so much easier as it abstracts away most of those messy calls to the REST API. Head on over to the PnP JS getting started page for some guidance on how to include the library in your project but basically you can just run the following command from the PowerShell command line or from within Visual Studio Code.
npm install @pnp/logging @pnp/common @pnp/odata @pnp/sp –save
I’ve not bothered loading the Graph library for this project because all the data I am intending to access will be in SharePoint.
The main downside of using the PnP JS library is that it doesn’t natively support Internet Explorer (IE). I know that IE is a dying browser but it’s still very common place. You can however provide some support for IE 11 if you install the Polyfills package, which I did by running the following.
npm install –save @pnp/polyfill-ie11
Then we can add import statements to the web part TypeScript file as follows:
import "@pnp/polyfill-ie11"; import { sp } from "@pnp/sp"
Do a quick check that our project gulps and renders successfully and then we’re all set to begin.
I called my project Kaboodle Announce and the web part just simply – Announce.
Setting the Target Site URL
We’ll ignore the web part rendering for now and focus on the first task, which is build a UI in the web part properties which allows the user to select a target web site i.e. the location which will be used as the news or events source.
First up, we are going to need a web part property that stores the news site URL. I suppose I could have built some complex search interface that would allow users to type some value which then returns a list of matching sites and that’s what the standard web parts do of course. However, the intent here is to get the web part to consume data from a single, specific web site and it is not an unreasonable expectation that the user will be able to paste in the target site URL, so why over-complicate things.
To achieve this, I have replaced the default ‘description’ property (given to us my Yeoman) with a property called targetSiteURL, as shown below.
export interface IAnnounceWebPartProps { //description: string; targetSiteURL: string; }
I also updated the properties in the preconfiguredEntries element of the manifest.json file and set it to a blank string. The idea is that the web part will default to the current site unless a URL to a different site is provides in a web part property.
"preconfiguredEntries": [{ "groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other "group": { "default": "Other" }, "title": { "default": "Announce" }, "description": { "default": "The Announce web part from Kaboodle Software allows news and event pages to be displayed in a dialog" }, "officeFabricIconFontName": "Page", "properties": { "targetSiteURL": "", "contentSource" : 0 } }]
I then updated the getPropertyPaneConfiguration method so that it now provides a text box (actually a PropertyPaneTextField control) that hooks up to the new targetSiteURL property instead of the default ‘description’ property as before.
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { return { pages: [ { header: { description: "" }, groups: [ { groupName: "NEWS SITE AND VIEW", groupFields: [ PropertyPaneTextField('targetSiteURL', { label: "URL of source site (blank is current site)" }) ] } ] } ] }; }
I then replaced the web part render method with some simple markup which displays the text provided in the field control that links to the targetSiteURL property.
public render(): void { this.domElement.innerHTML = `${ styles.announce }"> ${ styles.label }"> ${escape(this.properties.targetSiteURL)}`; }
Then ‘gulp serve’ it to check all is well with the plumbing.
Accessing the Target Site
So far, we have just captured a text string provided by the user and displayed it back in the web part but what we need to do is to be able to access the actual site. To do that we can make our first use of the PnP JS library. The first thing we need to do is to set up a property that returns the URL of the current web site when the targetSiteURL property is empty or returns the value specified in the property i.e. the URL of a specific web site.
public get siteWebUrl(): string{ return this.properties.targetSiteURL || this.context.pageContext.web.absoluteUrl; }
Then we can modify the render method as follows:
import { Environment, EnvironmentType } from '@microsoft/sp-core-library'; public render(): void { if (Environment.type === EnvironmentType.Local) { this.domElement.innerHTML = "You need to debug this in a remote workbench"; } else { let newsWeb: Web = new Web(this.siteWebUrl); newsWeb.get().then( web => { this.domElement.innerHTML = web.Title; }); } }
The render method now does a quick check to make sure that we are using a real SharePoint web site and not a local workbench as we need some real data to work with here, so you’ll need to gulp server with the –nobrowser flag.
It then tries to instantiate a Web object from the siteWebUrl property and assuming all is well it gets the web site and renders the web site’s title, as can be seen below.
The web part is configured to update and call the render method every time a property is changed, so if a user is manually typing in the URL it will generate a number of errors when the site URL does not point to a valid web site, but I reckon that most users will be pasting in the URL and so it’s probably ok to leave things this way, even though it is a little inelegant!
News or Events?
This web part is supposed to handle both news article in the Site Pages library and Events in a calendar list. Now, every Modern site has one, and only one, Site Pages library which can be reliably located and accessed from code. However, web sites don’t necessarily come with an Events calendar already in place but what happens is that when you add a standard Events client web part on the page, SharePoint goes ahead and creates you a calendar called Events. Unlike the Site Pages library though, it is possible to add additional calendars to a site. However, to make things simple I’m just going to work with the SharePoint provisioned events list called Events or the Site Pages library depending on whether the web part is going to return news articles or event list items. What we need is a switch that will allows us to select one or the other.
I have implemented this switch using a PropertyPaneChoiceGroup control that connects with a new contentSource property I have set up in the web part. I chose to make this property an enumeration type and added that type definition to a separate project file called Enums.ts as shown below:
Then back in the interface of the web part I have imported this enumeration type and declared a new property as shown below:
Then (after making sure the PropertyPaneChoiceGroup class has been imported) I added a new control in the getPropertyPaneConfiguration method and hooked it up to the web part property as shown below.
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { return { pages: [ { header: { description: "" }, groups: [ { groupName: "NEWS SITE AND VIEW", groupFields: [ PropertyPaneTextField('targetSiteURL', { label: "URL of source site (blank is current site)" }), PropertyPaneChoiceGroup('contentSource', label: "Content Source", options: [ {key: 0, text: "News", checked : true}, {key: 1, text: 'Events'} ] }), ] } ] } ] }; }
Next, we need to update the logic in the render method.
public getSourceList(web: Web): List{ return (this.properties.contentSource == ContentSource.SitePages) ? web.getList(Utilities.ConvertToSiteRelativeUrl(this.siteWebUrl) + "/SitePages") : web.lists.getByTitle("Events"); } 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); contentSourceList.get().then(list => { this.domElement.innerHTML = list.Title; }).catch( e => { console.log(e); this.domElement.innerHTML = "List could not be accessed"; }); } }
This time we don’t need to load the web site as we can go straight to the target list by calling a little helper method called getSourceList. This method checks the switch and when its set to SitePages we get back a reference to the Site Page list by specifying a server relative URL. I’ve created a separate Utilities class with a ConvertToSiteRelativeUrl method that accepts an absolute URL and returns the site relative URL as shown below:
export abstract class Utilities { public static ConvertToSiteRelativeUrl(Url: string): string { let relUrl: string = Url; if (Url.toLowerCase().substr(0, 4) == "http") { let parts = Url.replace("://", "").split("/"); parts.shift(); relUrl = "/" + parts.join("/"); relUrl = this.TrimTrailingSlash(relUrl); } return relUrl; } public static TrimTrailingSlash(sourceString: string): string { let res = sourceString; if (sourceString.charAt(sourceString.length - 1) == "/") { res = sourceString.substr(0, sourceString.length - 1); } return res; } }
If the switch is set to Events then we get back a reference to the first list called “Events” which we can reasonably expect to be an events calendar list. If the site doesn’t contain a list called Events then an error will occur and we can report that to the user.
And there we have it, a web part that can be used to select either the Site Page library or an Events list on any site in your tenancy.
What’s Next
In the next post I’ll cover off how to create a view picker control so that we can use the settings of a selected view to control what items get returned to the web part for rendering.
2 comments