Part 3. Creating MyDocument and MyDocumentCollection models extending Item and Items PnP JS Core classes

Post Series Index

This is a blog post in the series about working with Custom Business Objects, Parsers and Decorators in PnP JS Core:

  1. Introduction to Why do we should use Custom Business Objects (Models) in PnP JS Core
  2. Creating select and expand TypeScript Property Decorators to be used in PnP JS Core
  3. Creating MyDocument and MyDocumentCollection models extending Item and Items PnP JS Core classes (this article)
  4. How to consume our decorators, models and parsers from SPFx, the winning combination
  5. How to consume our decorators, models and parsers from SPFx, the winning combination
  6. Github project! Please remember to “star” if you liked it!

Introduction

In the previous posts of this series we explained why we should use Custom Business Objects in PnP JS Core and we implemented TypeScript decorators to help us to have more generic and maintainable code. In this article, we will see how to implement Custom Business Objects inheriting from Item and Items PnP JS Core generic classes and using the previously created TypeScript decorators internally.

What is the difference between Item and Items PnP JS Core classes?

Let’s do the comparison with Client Object Model, Item is ListItem and Items is ListItemCollection. PnP Core JS expose these two different classes Item and Items with different methods within them. For example, you can see different Item Methods and Items methods.

Imagine we are trying to get specific Item or Document from SharePoint using PnP Core JS then we will use this code (with no custom objects):

console.log("*************************************************************");
console.log("*** One document selecting all properties");
console.log("*************************************************************");
const myDocument: any = await pnp.sp
.web
.lists
.getByTitle(libraryName)
.items
.getById(1)
.get();
// query all item's properties
console.log(myDocument);

wit no custom objects

And similarly, to get Items or Document Collection we will use:

console.log("*************************************************************");
console.log("*** Document Collection selecting all properties");
console.log("*************************************************************");
const myDocumentCollection: any = await pnp.sp
.web
.lists
.getByTitle(libraryName)
.items
.get();
console.log(myDocumentCollection);

Here the result:

with no custom object result

Now, we already know the difference between Item and Items from PnP JS Core. Let’s implement our two custom classes inheriting both of them and combining TypeScript decorators.

Custom classes implementation inheriting from Item and Items

We are going to create two new classes called “MyDocument“ and “MyDocumentCollection“.

MyDocument

import { Item, ODataEntity, ODataParser, FetchOptions, Logger, LogLevel } from "sp-pnp-js";
// decorators implementation. See the page http://bit.ly/PnPJSCoreDecorators
import { select, expand } from "../utils/decorators";
// symbol emulation as it's not supported on IE
// consider using polyfill as well
import { getSymbol } from "../utils/symbol";
export class MyDocument extends Item {
// using @select decorator with no parameter will set the query property name to Title
@select()
public Title: string;
// using @select decorator with parameter will set the query property name to "FileLeafRef" but the Object property name to "Name"
@select("FileLeafRef")
public Name: string;
// we can even use combined @expand and @select decorators to query for "File/Length" property and later map the result to Size property using a custom parser
@select("File/Length")
@expand("File/Length")
public Size: number;
public CustomItemProps: string = "Custom Prop not interacting with SharePoint";
// override get to enforce select and expand for our fields to always optimize
public get(parser?: ODataParser<any>, getOptions?: FetchOptions): Promise<any> {
// public get(): Promise<MyDocument> {
this
._setCustomQueryFromDecorator("select")
._setCustomQueryFromDecorator("expand");
if (parser === undefined) {
parser = ODataEntity(MyDocument);
}
return super.get.call(this, parser, getOptions);
}
private _setCustomQueryFromDecorator(parameter: string): MyDocument {
const sym: string = getSymbol(parameter);
// get pre-saved select and expand props from decorators
const arrayprops: { propName: string, queryName: string }[] = this[sym];
let list: string = "";
if (arrayprops !== undefined && arrayprops !== null) {
list = arrayprops.map(i => i.queryName).join(",");
} else {
Logger.log({
level: LogLevel.Warning,
message: "[_setCustomQueryFromDecorator] - empty property: " + parameter + "."
});
}
// use apply and call to manipulate the request into the form we want
// if another select isn't in place, let's default to only ever getting our fields.
// implement method chain
return this._query.getKeys().indexOf("$" + parameter) > -1
? this
: this[parameter].call(this, list);
}
}

MyDocumentCollection

import { Items, ODataEntityArray, ODataParser, FetchOptions, Logger, LogLevel } from "sp-pnp-js";
// symbol emulation as it's not supported on IE
// consider using polyfill as well
import { getSymbol } from "../utils/symbol";
// import MyDocument to specify the ItemTemplate
import { MyDocument } from "./MyDocument";
export class MyDocumentCollection extends Items {
// we create an ItemTemplate property in order to get our Item model as part of our ItemCollection
private ItemTemplate: MyDocument = new MyDocument("");
public CustomCollectionProps: string = "Custom Collection Prop with no interaction with SharePoint";
// override get to enforce select and expand for our fields to always optimize
public get(parser?: ODataParser<any>, getOptions?: FetchOptions): Promise<any> {
// public get(): Promise<MyDocument> {
this
._setCustomQueryFromDecorator("select")
._setCustomQueryFromDecorator("expand");
if (parser === undefined) {
// default parser
parser = ODataEntityArray(MyDocument);
}
return super.get.call(this, parser, getOptions);
}
// this method is slightly different from the MyDocument method as it is using this.ItemTemplate as a reference from which getting the properties
private _setCustomQueryFromDecorator(parameter: string): MyDocumentCollection {
const sym: string = getSymbol(parameter);
// get pre-saved select and expand props from decorators
const arrayprops: { propName: string, queryName: string }[] = this.ItemTemplate[sym];
let list: string = "";
if (arrayprops !== undefined && arrayprops !== null) {
list = arrayprops.map(i => i.queryName).join(",");
} else {
Logger.log({
level: LogLevel.Warning,
message: "[_setCustomQueryFromDecorator] - empty property: " + parameter + "."
});
}
// use apply and call to manipulate the request into the form we want
// if another select isn't in place, let's default to only ever getting our fields.
// implement method chain
return this._query.getKeys().indexOf("$" + parameter) > -1
? this
: this[parameter].call(this, list);
}
}

Note you can see the full code in this github project.

Ideally we will create a folder in our solution to create all our models:

folder models

How to use MyDocument Custom Business Object from our PnP JS Core code

We can easily use the class with the following code:

How to consume MyDocument:

// import model
import { MyDocument } from "../model/MyDocument";
// import pnp js
import pnp from "sp-pnp-js";
console.log("*************************************************************");
console.log("*** One document using as(MyDocument) and get() with Default Parser");
console.log("*************************************************************");
const myDocumentWithCustomObjectGet: MyDocument = await pnp.sp
.web
.lists
.getByTitle(libraryName)
.items
.getById(1)
// using as("Model") overrides select and expand queries
.as(MyDocument)
.get();
// query only selected properties, using our Custom Model properties
// but only those that have the proper @select and @expand decorators
console.log(myDocumentWithCustomObjectGet);

consume MyDocument

How to use MyDocumentCollection:

// import models
import { MyDocument } from "../model/MyDocument";
import { MyDocumentCollection } from "../model/MyDocumentCollection";
// import pnp js
import pnp from "sp-pnp-js";
console.log("*************************************************************");
console.log("*** Document Collection using as(MyDocumentCollection) and get()");
console.log("*************************************************************");
const myDocumentsWithCustomObjectAsDocuments: MyDocument[] = await pnp.sp
.web
.lists
.getByTitle(libraryName)
.items
// using as("Model") overrides select and expand queries
.as(MyDocumentCollection)
.get();
// query only selected properties, using our Custom Model properties
// but only those that have the proper @select and @expand decorators
console.log(myDocumentsWithCustomObjectAsDocuments);

consume MyDocumentCollection

Conclusion

We can notice how both custom objects and decorators work well because queries to SP only brings the right dataTitle, FileLeafRef and File/Length“, but still we need a parser in order to correctly do the mapping between the query properties into our TypeScript objectsTitle, Name and Size“ properties.

In the next post we are going to implement a custom Parser and Array Parser to solve this specific issue.

 

Author: José Quinto
Link: https://blog.josequinto.com/2017/06/15/creating-mydocument-and-mydocumentcollection-models-extending-item-and-items-pnp-js-core-classes/
Copyright Notice: All articles in this blog are licensed under CC BY-SA 4.0 unless stating additionally.