An example is available as a GitHub repository that linked at the end of the post.
When building SharePoint customizations, you might have used the PnPjs to communicate with SharePoint. We will see how to update different data types of SharePoint document library using PnPJs.
What is PnPjs?
PnPjs is a collection of fluent libraries for consuming SharePoint, Graph, and Office 365 REST APIs in a type-safe way. You can use it within SharePoint Framework, Nodejs, or any JavaScript project. This an open source initiative.
To work with PnPjs :
- Installation: npm install @pnp/sp
- Etablish Context: because the SharePoint Framework provides a local context for each component, we need to define that context in the library. This allows us to determine the request URLs.
import { sp } from '@pnp/sp/presets/all';
export default class UpdateDocPropertiesCommandSet extends BaseListViewCommandSet<IUpdateDocPropertiesCommandSetProperties> {
@override
public onInit(): Promise<void> {
//init context
return super.onInit().then((_) => {
sp.setup({
spfxContext: this.context
});
});
}
}
We are going to build a ListView Command Set extension that updates data of several types of the document library: date, choice, lookup, managed metadata. A click on a button triggers the updating of the document properties according to their names. The action consists of taking the name of the document and extracting its properties, the separator will be “_”.
Example file name: file1_Planning_EN_CCI London_Biologie_210518.docx.
- TypeDoc: Planning
- Langue: EN
- Lacalisation: CCI London
- Subject: Biologie
- PublicationDate: 05/18/2021
Required :
- Node.js
- gulp: CLI version: 2.3.0, and Local version: 4.0.2
Create ListView Command Set extension
First, we need to create a ListView Command Set type spfx extension. You can see the steps here.
Customize fields:
We add these fields to our document library:
- TypeDoc: Choice (Planning, Report, CompteRendu)
- DatePublication: Date and Time
- Langue: Lookup (information already on this site), this field will be linked to a languages list
- Localisation: Managed Metadata (set of termstore).
- Subject Single line of text
Select Data with PnPjs:
- Files to update: only select files updated after the last update. The last date time update is stored in property bag of root folder of the library, and it managed with JSOM API.
import { IItems, SPBatch, SPRest } from '@pnp/sp/presets/all';
export default class DocumentListService {
private sp: SPRest;
private batch: SPBatch;
private docItems: IItems;
constructor(sp: SPRest) {
this.sp = sp;
}
public initDocItems() {
this.docItems = this.sp.web.lists.getByTitle('Documents').items;
}
//FSObjType ne 1 => Do not take files
//Modified ge datetime'${lastUpdateDateTime}' => fields Modified >lastUpdateDateTime
public getFilesModified(lastUpdateDateTime): Promise<any> {
return this.docItems.select('Id', 'File/Name').expand('File/Name').
filter(`FSObjType ne 1 and Modified ge datetime'${lastUpdateDateTime}'`)
.get();
}
/*
...
...
...
*/
}
- Choice list: get the list of choices from the TypeDoc field, this allows us to check if the property “TypeDoc” extracted from the name of the file is valid.
export default class DocumentListService {
/*
...
*/
public getChoicesTypeDoc(): Promise<any> {
return this.sp.web.lists.getByTitle('Documents').fields.getByInternalNameOrTitle('TypeDoc').select('Choices').get();
}
/*
...
...
*/
}
- Language list: get the list of languages from web site, this allows us to check if the property “language” extracted from the name of the file is valid.
export default class DocumentListService {
/*
...
*/
public getLanguages(): Promise<any> {
return this.sp.web.lists.getByTitle('Langues').items.select('Title', 'Id').get();
}
/*
...
...
*/
}
Localisation (managed metadata): get managed metadata from the term store. You must enter the group ID and the set ID. You can find them in the “Term Store Management Tool” on your website. this allows us to check if the property “localisation” extracted from the name of the file is valid.
export default class DocumentListService {
/*
...
*/
public getLocalisationsTermeStore(): Promise<any> {
//UPDATE_HERE : ID du groupe CCI termeStore => 45f13976-c5f0-4f49-b7cf-004afa72d7b4 | ID du Set Localisation=> 7ee71116-9a06-40fe-be85-b66ee794847d
return this.sp.termStore.groups.getById("45f13976-c5f0-4f49-b7cf-004afa72d7b4").sets.getById("7ee71116-9a06-40fe-be85-b66ee794847d").children();
}
/*
...
...
*/
}
Update data with PnPjs:
To improve performance, queries are grouped together. Usefully, the library provides built-in support for batch processing REST requests. This involves creating a batch instance and using it with the inBatch method to group these requests. To run the queries, we call the execute method of the batch object.
- Choice Field: to update the choice field, we will just assign the value to the TypeDoc property, and if that value is not present in the choice list, the update will fail.
export default class DocumentListService {
/*
...
*/
public updateValueOfTypeDoc(id, value) {
this.docItems.getById(id).inBatch(this.batch).update({
TypeDoc: value,
});
}
/*
...
...
*/
}
- Lookup Field: to update the value of a Lookup type field, we assign the element ID to the field name + Id. Example: The value of english language id in languages list is 2, and the lookup Field in our library is named language, to update the value of field, we assign to languageId the value 2.
export default class DocumentListService {
/*
...
*/
public updatevalueOfLanguage(id, value) {
this.docItems.getById(id).inBatch(this.batch).update({
LangueId: value,
});
}
/*
...
...
*/
}
- Managed Metadata Field: to update the value of a managed metadata type field, we assign the element name and ID.
export default class DocumentListService {
/*
...
*/
public updateValueOfLocalisation(id, localisation, validLocalisationId) {
this.docItems.getById(id).inBatch(this.batch).validateUpdateListItem([
{ FieldName: 'Localisation', FieldValue: localisation + '|' + validLocalisationId + ';' },
]);
}
/*
...
...
*/
}
Date and Time Field: to update the value of a Date and Time type field, we assign a value with iso format without millisecondes. Exemple: “2021–09–01T14:48:00Z”. To get this format use the method: “Date.prototype.toISOString()”
export default class DocumentListService {
/*
...
*/
public updateValueOfPublicationDate(id, value) {
this.docItems.getById(id).inBatch(this.batch).update({
DatePublication: value,
});
}
/*
...
...
*/
}
- Single line of text Field: we assign the value of type string to the name of the field.
export default class DocumentListService {
/*
...
*/
public updateValueOfSubject(id, value) {
this.docItems.getById(id).inBatch(this.batch).update({
Subject: value,
});
}
/*
...
...
*/
}
Trigger the update script:
The method “onExecute” defines what happens when a command is executed. As for updating and retrieving the property bag, we use JSOM API, so we have to run the method “executeMDEcommand” in the context : “SP.ClientContext”.
executeFunc: ensures that the specified file that contains the specified function is loaded and then runs the specified callback function.
export default class UpdateDocPropertiesCommandSet extends BaseListViewCommandSet<IUpdateDocPropertiesCommandSetProperties> {
/*
...
...
*/
/*This method is triggered when a command is executed*/
@override
public onExecute(event: IListViewCommandSetExecuteEventParameters): void {
switch (event.itemId) {
case 'MDE':
//éxécuter la fonction updateItems dans le ccontext SP
SP.SOD.executeFunc('sp.js', 'SP.ClientContext', this.executeMDEcommand);
break;
default:
throw new Error('Unknown command');
}
}
/*
...
...
*/
}
After initializing the services, we call the “getProperties” method which retrieves the RootFolder properties from our list of documents. When the result is available, the onGetLastUpdateDateTimeSuccess callback is triggered. In this callback, we get our “LasteUpdateTime” property and update the items in our list.
private executeMDEcommand() {
//init services
let documentListService: DocumentListService = new DocumentListService(sp);
let propertyBagService: PropertyBagService = new PropertyBagService();
/*
...
...
*/
//CallBack which is executed when the Properties Bag is retrieved
let onGetLastUpdateDateTimeSuccess = (sender: any, args: SP.ClientRequestSucceededEventArgs) => {
let lastUpdateDateTime = propertyBagService.getPropertyLastUpdateDateTime();
//Update items of the document list
updateListItems(lastUpdateDateTime);
};
//Retrieve the properties of RootFolder from the documents list
propertyBagService.getProperties(onGetLastUpdateDateTimeSuccess);
}
The update and the data retrieval are done asynchronously, the “updateListItems” method is therefore declared as asynchronous. Before updating the items, we check the validity of the values extracted from their filenames. then we retrieve and update the items whose value in the Modified field is greater than the value of “LastUpdateDateTime” property.
private executeMDEcommand() {
/*
...
...
*/
let updateListItems = async lastUpdateDateTime => {
let validLanguages;
let validTypesDoc = [];
let validLocalisations: Localisation[] = [];
//Retrieve the elements of the Location Set
let getLocalisationsPromise = documentListService.getLocalisationsTermeStore().then(items => {
let element;
for (let i = 0; i < items.length; i++) {
element = items[i];
validLocalisations.push(new Localisation(element.labels[0].name, element.id));
}
});
//retrieve the choice list of the TypeDoc field
let getChoicesPromise = documentListService.getChoicesTypeDoc().then((fieldData: IChoiceFieldInfo) => {
validTypesDoc = fieldData.Choices;
});
//retrieve the Languages list from the Language control
let getLanguagesListPromise = documentListService.getLanguages().then(langues => {
validLanguages = langues;
});
//Wait for the ending Promess. NB: we wait for the slowest of the 3
await getLocalisationsPromise;
await getChoicesPromise;
await getLanguagesListPromise;
documentListService.initDocItems();
//Select the files modified since the last update, and modified their fields if the name of the file is in the correct format.
documentListService.getFilesModified(lastUpdateDateTime).then(response => {
if (response.length == 0) {
toast.show({ title: '', message: "All information is up to date", type: "info", newestOnTop: false });
}
else {
response.forEach((element, index, elements) => {
let fileName = element.File.Name;
let message = "";
let fileNameSplit = fileName.split("_");
if (fileNameSplit.length == 6) {
//Extract properties from file name
let typeDoc = fileNameSplit[1];
let language = fileNameSplit[2];
let localisation = fileNameSplit[3];
let subject = fileNameSplit[4];
let datePub = fileNameSplit[5].split(".")[0];
//Check the validity of the extracted properties
let languageId = DocumentListService.isValidLanguage(language, validLanguages);
let validTypeDoc = DocumentListService.isValidTypeDoc(typeDoc, validTypesDoc);
let validLocalisationId = DocumentListService.isValidLocalisation(localisation, validLocalisations);
let validDateFormat = DocumentListService.isValidDateFormat(datePub);
//Create update requests for properties in a Batch (Queue)
documentListService.initBatch();
//Update Localisation Field
if (validLocalisationId != -1) {
documentListService.updateValueOfLocalisation(element.Id, localisation, validLocalisationId);
}
else {
message = message + "Localisation";
}
//Update Langue Field
if (languageId != -1) {
documentListService.updatevalueOfLanguage(element.Id, languageId);
}
else {
let m = message == "" ? "Langue" : ", Langue";
message = message + m;
}
//Update TypeDoc Field
if (validTypeDoc) {
documentListService.updateValueOfTypeDoc(element.Id, typeDoc);
}
else {
let m = message == "" ? "TypeDoc" : ", TypeDoc";
message = message + m;
}
//Update DatePublication Field
if (validDateFormat != "") {
documentListService.updateValueOfPublicationDate(element.Id, validDateFormat);
}
else {
let m = message == "" ? "PublicationDate" : ", PublicationDate";
message = message + m;
}
//Update Subject Field
documentListService.updateValueOfSubject(element.Id, subject);
/*
...
...
*/
//run the Batch update requests
documentListService.executeBatch().then(() => {
//When all the properties of the last element in the list are modified, we update the LastUpdateDateTime property with the current date and time (now)
if (index === elements.length - 1) {
propertyBagService.setPropertyLastUpdateDateTime();
}
});
}
}
);
}
}
);
};
/*
...
...
*/
}
Deployment of the ListView Command Set extension:
I do not detail all the stages of deployment here, but I will give the points that are important to know.
- To create the solution with gulp, run these command: “gulp build — ship” and then type “gulp package-solution — ship” . You will find the solution in “./sharepoint/solution”
- In order for your extension to be available on the client once the solution package is installed in the client application catalog, you must set the “skipFeatureDeployment” and “isDomainIsolated” attributes to False in the “./config/package-solution.json” file.
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "mde-extension",
"id": "2dc9b02a-8ca4-44e3-bc90-1667e5b45eb2",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"skipFeatureDeployment": false,
"isDomainIsolated": false,
"features": [
{
"title": "Application Extension - Deployment of custom action.",
"description": "Deploys a custom action with ClientSideComponentId association",
"id": "44ffcfdd-648e-4b1a-83c5-1aa36b377092",
"version": "1.0.0.0",
"assets": {
"elementManifests": [
"elements.xml",
"ClientSideInstance.xml"
]
}
}
]
},
"paths": {
"zippedPackage": "solution/mde-extension.sppkg"
}
}
- To make the extension only appear in document lists, set the “RegistrationId” attribute to 101 in the “./sharepoint/assets/elements.xml” file.
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<CustomAction
Title="UpdateDocProperties"
RegistrationId="101"
RegistrationType="List"
Location="ClientSideExtension.ListViewCommandSet.CommandBar"
ClientSideComponentId="467f2217-c0cf-4603-a0f8-b92f991897f3"
ClientSideComponentProperties="">
</CustomAction>
</Elements>
I hope this has helped you. Good coding 😜 🤪
You find source code here: https://github.com/zinedineBenkhider/SpfxExtension
I am passionate about WEB and Mobile development. Very curious to learn and I like to share my knowledge.