This is the 2nd part of a mini series of blog posts that explain how I implemented an Announcement Client Web Part using the SPFx. The context of what I want to achieve by building this web part has been explained in the 1st part of this series, but in summary:
- I want the items to be displayed in the web part to be driven by views on a source list or library rather than by applying complex filter settings in the web part itself.
- I want to have the option to open items in a pop-up dialog rather than redirecting the user’s browser to a completely different page, potentially on a different site.
So far, we have created the project and been able to connect with a source (Events) list or a Site Pages library on either the local site in which the web part has been added or in some other web site within the tenancy.
In this post I am going to explain how we can load the views defined in the source list/library into the web part properties so that the user can select which items are to be rendered in the web part as being those returned by the view of their choice.
However, before we go there, I just want to conduct a small critique of design philosophy that underpins most Modern web parts and then explain why I have chosen to the take the approach that I have.
Modern and Classic Design Philosophies
I could probably write a book on this but I’m not going to go all-out here. However, I feel it is important to understand that there has been a seed change in the design philosophy which underpins the SharePoint Modern UX when compared with the traditional presentation layer of Classic.
List View Web Parts
Every view you create generates a new view page with a List View Web Part (LVWP) suitably configured to present the items returned by the view in exactly the way you specify, irrespective of whether we are talking Modern of Classic.
The User Experience (UX) is considerably different however. Many view settings, such as view styles simply get ignored because they have no place in a Modern world. And even columns, like the incredibly useful property edit button, get passed over.
You do get some really cool new enhancements in exchange though, which I won’t go into here except to call out my favourite which is that multi-line text columns now get shown in a consistent height in a LVWP, such that if someone writes War and Peace in a multi-line text column, the grid layout is no longer wrecked – wonderful!
I could also throw a tantrum over the change in conditional formatting technologies. If you want to colour code a column in Classic then display templates were often the way to go but Modern has brought us an entirely different JSON-based method. Both technologies are incredibly cumbersome to work with to the extent of being completely unusable by any “normal” end user. My point of frustration is that display templates don’t work in Modern and the new column formatting method doesn’t work in Classic and so we now have to implement 2 solutions for the same end result – crazy!
I’m in danger of ranting here, so I need to rapidly move on to the main point that I am trying to make and that is that Classic gives us a reusable LVWP for every list and library in the site that we can happily add to any site page. So, you can use a page to assemble a business context by sucking up documents and list items from various lists and libraries and gluing them all in a mosaic of web parts, configured with views to present an extract of information relevant to your desired context.
However, in Modern we can no longer do this, well not in the same way. In fact, most lists don’t have a reusable LVWP in Modern at all! The design philosophy has changed. This change has been silent but it has also been significant because it forces us to think about how we present information to users, in a very different way.
Views vs Web Part Filter Properties
Most of the standard Modern Web Parts have steered away from using Views to drive content in favour of using a set of filter criteria specified in the web part itself. The design concept for many of these Modern web parts is to use the search API to retrieve content.
This has a few advantages:
- It means that data can be vacuumed up from multiple content sources (maybe several lists and libraries in numerous web sites) and presented in a single aggregated view point, the web part.
- Search is security trimmed and this means we can have some confidence that content is not being made accessible to users who should not have access to it (although it should be pointed out that the content returned by views is also security trimmed).
- It means we can have a logical separation from where content is stored and managed (say on team sites) and where it might be consumed (say on an intranet portal page)
This last one is the golden ticket in my Wonka bar, because traditional LVWPs where limited to presenting content in lists and libraries within the same site. SharePoint Classic fails to provide us a LVWP that could work outside of the confines of the site in which the list it was partnered with, was defined.
There were of course efforts to remedy this, most notably, the Content Query Web Part (CQWP) and the Content Search Web Part (CSWP) but these were cumbersome to work with and just plain ugly on the page.
But its not all good news though:
- This approach means that web part filters and settings tend to need to be complex (though to be fair not as a complex as the CQWP or CSWP of old). You’ve got to control the content presented in these web parts somehow and if you don’t do it on the source list at the backend (via a view), then you can only do it via configuration settings on the front end i.e. in the web part’s properties.
- It makes governance more challenging. For example, if my intranet homepage news is fed by a dozen team sites then it becomes difficult to control content. And you can’t really prioritise news to say that anything from Site X is high priority, but anything in Site Y is a “nice to know” and you also can’t control the time-to-live either; you can’t set an Expires date like you can on an Announcements list, for example.
- Because content is driven by search you can’t know for sure when it will show up in the web part, suffice it to say that you can be 100% sure that it won’t show up instantly as there will be an inevitable delay before the search indexer will know of an item’s existence.
It is my assertion that all of these downsides can be resolved by using a design philosophy based on views rather than search and if we can find a way to access lists on remote sites then we can also solve the same-site-only constraint imposed by standard LVWPs.
Dynamic Views
When you add a Classic LVWP to a page you get to select the list or library view presented through the web part (the default view on the list is chosen for you as the start point). But the view configuration is actually saved in the web part itself, as a snapshot of the view, rather than just referencing the view, as defined in the list, when the web part is rendered.
This is both a blessing and a curse. It does mean that you can modify the web part’s view settings without having to first create a custom view but it also led to confusion because it meant that changing a view setting had no effect on existing LVWPs as these are immediately decoupled from any source view on which they might be based.
The approach I am advocating will not seek to cache view settings but rather reference a view, and dynamically draw the content based on the view settings at the time content is rendered.
In Conclusion
So, whilst I applaud the Modern in its objectives, I don’t think that is necessarily offers the best approach in all circumstances. Sometimes it will be more appropriate to access content from a single content source as in the traditional LVWP model in SharePoint Classic. That then forms the rationale for why I am proposing and exploring an alternative model, one based on views but one which will:
- Dynamically look-up the settings of a linked view at render time, rather than saving a snapshot of those configuration settings within the web part itself.
- Be able to work across site and site collection boundaries, freeing us from the shackles of a co-hosting dependency and allowing us to separate the content creation and management space from the content consumption space, should there be a need to do so.
Enough with side-shown, let’s get back to the main event!
Building a View Picker Control
As the content to be displayed in the web part is to be driven by a view, we need to provide a way for the user to select that view. We could just provide a view name text property and let the user add some free text which we could then match to a view on the list. However, that would require the user to know the view names in advance and it would also break the link if someone was to rename the view in the list. A more elegant solution is to load a drop-down box with the list of views and get the user to select one, so that’s what we’ll be doing here.
First of all, we have to decide where in the UI this view picker to be located. We could make it a dynamic picker and display it in the markup of the web part itself. In this scenario the user could select a different view at run time without switching into page edit mode. Appealing as that might be, it’s not really what I am after here so instead I am going to make the view picker part of the web part configuration properties.
To build our view picker we need 3 key elements:
- We need a new web part property in which to store the unique ID of the selected view
- We need a field control (the drop-down picker) to allow the user to select a view
- We need a means to query the source list/library to enumerate the view and add the items to the field control, for which the user will select one
The View Web Part Property
To provide a web part property in which to store the id of the view we can simply add an appropriate property in the web part’s interface.
export interface IAnnounceWebPartProps { targetSiteURL: string; contentSource: ContentSource, viewId: string }
Adding the Field Control
To add the field control, we need to first make sure that the web part imports the PropertyPaneDropdown class and the I PropertyPaneDropdown interface.
import { IPropertyPaneConfiguration, PropertyPaneTextField, PropertyPaneChoiceGroup, PropertyPaneDropdown, IPropertyPaneDropdownOption } from '@microsoft/sp-property-pane';
Then we add a new PropertyPaneDropdown control to the JSON returned by the getPropertyPaneConfiguration method and link it to the new viewId property.
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'} ] }), PropertyPaneDropdown('viewId',{ label: "Selected view", options : this.viewOptions }), ] } ] } ] }; }
The options to be presented in the drop-down picker are specified in the viewOptions property of the web part, as described in the next paragraph.
Setting the View Options
The options property of the PropertyPaneDropdown class expects an array of items that comply with the IPropertyPaneDropdownOption interface, so that’s what we need to set up as a web part property.
public viewOptions: IPropertyPaneDropdownOption[];
But this is just a reference to an empty array and so we need to set this property with an actual collection of elements that represent the views on the list and which conform with the IPropertyPaneDropdownOption interface. We can do that in the onPropertyPaneConfigurationStart method which gets called whenever the property pane is rendered, with a call to a little helper method, as shown below:
protected onPropertyPaneConfigurationStart(): void{ this.loadListViewOptions(); } private loadListViewOptions(): void{ this.viewOptions = new Array<IPropertyPaneDropdownOption>(); let newsWeb: Web = new Web(this.siteWebUrl); this.getSourceList(newsWeb).views.select("Title", "Id").get().then (views => { views.forEach((view: any) =>{ this.viewOptions.push({key: view.Id, text: view.Title}); }); this.context.propertyPane.refresh(); }) }
The loadListViewOptions method accesses the source list or library as before (see Part 1) but now retrieves the set of defined views. To be able to create a view option, we just need 2 elements, namely the Title and ID of the view, so those are the only properties of the views that I need to request, specified in the view.select statement and this makes the query request as light and as nimble as it possibly can be.
Then it’s just a simple case of looping round the views and pushing a new item for each view into the options array.
Don’t miss the call to propertyPane.refresh() as without it the options won’t appear in the drop-down control.
So that takes care of loading the views as options into the drop-down control and the web part itself takes care of looking up the matched key value based on the value stored as web part property and associated with the control i.e. the viewId property.
We do still have one problem however. Currently when we use the other web part property controls, we may end up changing the source list. We might change the site URL for example or switch the web part between the Events list or Site Pages library. When we do this, the list of views does not currently change dynamically as users might expect. This is because we have only plumbed things up so that the view options are loaded when we first access the web property configuration pane.
To fix this, so that the view options get changed whenever other values which influence the set of view that should be shown, we need to provide a onPropertyPaneFieldChanged method. This method gets called whenever a web part property is updated and it usefully provides us with the name of the property that was changed and both the old and new values of that property.
protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void { if (this.requiresUpdate(propertyPath)){ this.loadListViewOptions(); } } private requiresUpdate(propertyPath : string) : boolean{ switch (propertyPath){ case ("contentSource"): return true; case ("targetSiteURL"): return true; default: return false; } }
We don’t need to update the set of view options whenever any property is changed and that then is the point of the requiresUpdate helper method. Basically, this method returns true if the site URL is changed or if the content source is switched between Events and Site Pages as it is only when something has changes with these properties that we need to invalidate the view options and load them afresh.
So now we have a web part with a configuration pane that allows the user to:
- Select a site (via the targetSiteUrl property) or leave that property blanks to use the local site as the content source
- Select either the Events list or the Site Pages library for that site
- Select a specific view, defined with the selected list/library
The above screen shot shows that the name of the selected view is displayed on the part’s content area. This was achieved by simply updating the web parts render method as shown below:
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.views.getById(this.properties.viewId).get().then(view => { this.domElement.innerHTML = view.Title; console.log(view); }).catch( e => { console.log(e); this.domElement.innerHTML = "The list or view could not be accessed"; }); } }
All that’s really different here from the render method I showed in Part 1, is that I am spitting out the title of the selected view as the HTML to be rendered in the web part, rather than the title of the selected list as we had done before.
Fetching View Items
Selecting a site is a stepping stone to selecting a list, which is itself a stepping stone to selecting a view but our end goal here is not to display the list of views but rather the list items that are returned by a view. So, the selection of a view is yet another (but final) stepping stone to getting use access to list items.
ViewUtilities Class
In order to may the web part svelte, I have created a separate View Utilities class (saved in ViewUtilities.ts type script), shown in its entirety below:
import { List, View } from "@pnp/sp"; export class ViewUtilities { public static getViewFields(): string{ return `<ViewFields> <FieldRef Name="Title"></FieldRef> <FieldRef Name="Description"></FieldRef> <FieldRef Name="BannerImageUrl"></FieldRef> <FieldRef Name="FileRef"></FieldRef> </viewFields>`; } public static getScope(view: any): string{ switch (view.Scope){ case 1: return `Scope="Recursive"`; case 2: return `Scope="RecursiveAll"`; case 3: return `Scope="FilesOnly"`; default: return ""; } } public static getRowLimit(view: any): string{ return `<RowLimit>${view.RowLimit}</RowLimit>`; } public static getItemsByViewQuery(list: List, view: any):Promise<any> { const xml = `<View ${this.getScope(view)}>${this.getViewFields()} <Query>${view.ViewQuery}</Query>${this.getRowLimit(view)} </View>`; return list.getItemsByCAMLQuery({'ViewXml':xml},"File").then((res) => { return res; }); } public static getViewQueryForList(list: List, viewId: string):Promise<View> { if(viewId){ return list.getView(viewId).select("ViewQuery", "Scope", "RowLimit").get().then(view => { return view; }); } else { return list.defaultView.select("ViewQuery", "Scope", "RowLimit").get().then(view => { return view; }); } }
Let’s start with the getViewQueryForList method. This method returns the view on the content source list specified by the viewId parameter. If the viewId is null then the default view is used. Note that we only need the ViewQuery, Scope and RowLimit properties of the view and so that’s all we ask for by using the select statement.
The job of the getItemsByViewQuery method is to return all the list items that result from the query specified in the view. It does this by first assembling the query XML based on the properties we have asked for from the specified view. To fetch the list items, we make a call to the getItemsByCAMLQuery method of the list using the XML query that that we have assembled.
Note that the ViewUtilities class provides some helper methods which convert view properties into text that can be used as part of the XML query string, as follows:
-
The getViewFields method specifies the fields that we need to bring back as part of the query. Note that here I have deliberately not using the columns that might be defined selected view and by doing so I am essentially using the filter, scope and item limits set in the view but removing any need for the view to include the right set of the columns that we will need to render list items in the web part.
-
The getScope method allows the support of views that might exclude content in folders.
- The getRowLimit method will honour the item limit settings specified in the view.
The Updated render method
First make sure that the new ViewUtilties is accessible in the web part.
import { ViewUtilities } from './ViewUtilities';
Then update the render method as follows:
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); ViewUtilities.getViewQueryForList(contentSourceList, this.properties.viewId).then((view) => { ViewUtilities.getItemsByViewQuery(contentSourceList, view).then((items: any) => { items.forEach(item => { this.domElement.innerHTML += `${item.Title}`;}) }) }) } }
After accessing the web site and list as before, we then get the query based on the view selected by the user in the web part property by calling the getViewQueryForList method. When we get back the query we then feed it to the getItemsByViewQuery method which then returns all the items that match the view query settings. Finally, we loop round all these items and output their title value inside the web part.
The Reveal
Gulp serve the web part using a remote workbench and then hook it up to a web site, list and view and we can see that the results are returned as expected.
In Summary
In this post I have explained why I believe there to be value in building web parts that can be fed by views defined on source lists and libraries. I then described how to add a view picker control and load it with the set of views defined on the selected list or library. Finally, I showed how to use that view to construct an CAML query in XML that can then be used to return the items that would be returned by the selected view.
What’s Next
So far, we’ve been able to select a site, a list within a site and a view within the list and successfully access the items returned by that view. However, all we’ve done is return the item titles and output that information in the web part’s mark-up. In the next post we’ll look at how we can render these list items in a meaningful way, including making the items pop-up in a dialog box rather than loading as a full page in the current browser window or in a new tab.
1 comment