Extending with Custom Business Objects - SharePoint/PnP-JS-Core GitHub Wiki
When developing an application you often have defined entities representing the data you are manipulating. In client side development these are often models or view models depending on what framework you are using - but generally a set of data and the operations you can perform on that data. You can see a working example of this in the knockoutjs sample SPFx web part.
The sp-pnp-js library is designed to provide a base for you to build your custom objects and this article outlines that process. First let's take the simplest example, getting our list data back as a typed object instead of "any". To start we will define a class. You could use an interface here if you are only interested in retrieving data, but we will be adding to our class throughout this example so we have chosen a class.
class MyItem {
public Id: number;
public Title: string;
public ProductNumber: string;
public OrderDate: Date;
public OrderAmount: number;
}
And next we will retrieve items from a list using the getAs method. This is covered in more detail in the article on response parsers. Also a list is already created with the appropriate name and some sample data, this is covered later in this article.
import pnp from "sp-pnp-js";
// here we type our result as an array of MyItem instances. This uses our class like an interface
// and new instance of MyItem is not created, we are just using it for typing the results
pnp.sp.web.lists.getByTitle("ExtensibilityExample").items.getAs<MyItem[]>().then(items => {
console.log(items[0].OrderAmount);
});
The next step is to update MyItem to extend the base Item class from the library. To do that we add the extends declaration. This will grant all the methods of the Item base class to our MyItem class. We also need to import some supporting classes in addition to the pnp default.
import pnp,
{
Queryable,
Item,
ODataEntityArray
} from "sp-pnp-js";
class MyItem extends Item {
public Id: number;
public Title: string;
public ProductNumber: string;
public OrderDate: Date;
public OrderAmount: number;
}
The next step is to update our call to get the items to use the ODataEntityArray parser which will merge the data coming back from SharePoint with a new instance of our MyItem class. Meaning we will have an object that has all of the methods of Item, properties of MyItem, and the ability to chain additional operations.
// here we type our result using the ODataEntityArray parser which will merge the data and object instance
// and type the result appropriately.
pnp.sp.web.lists.getByTitle("ExtensibilityExample").items.getAs(ODataEntityArray(MyItem)).then(items => {
console.log(items[0].OrderAmount);
});
The upside is that we can now add functionality to our MyItem class and make use of it when binding our model. For example we will add a save method that wraps the library's update method.
class MyItem extends Item {
public Id: number;
public Title: string;
public ProductNumber: string;
public OrderDate: Date;
public OrderAmount: number;
// here we mask the update method to update our properties so we can set the values of our
// objects and save them without having to build the plain objects.
// this would also be an opportunity to manipulate any special field values
public save(): Promise<MyItem> {
// we could do a check here to see if we needed to update or create this item
// within the save method
return this.update({
Title: this.Title,
ProductNumber: this.ProductNumber,
OrderDate: this.OrderDate,
OrderAmount: this.OrderAmount
}).then(result => {
// using the as method to cast the result item as MyItem, which merges the
// data and a new instance of MyItem
return result.item.as(MyItem);
});
}
}
Now that we have covered the basics below you will find the full example. This example can be run as a debug module following the debugging guide. The full example shows a custom Web, List, and Item implementation with a few different options for how to construct your objects. There a many ways you can organize the objects and place the methods - the below is just to show what is possible. You should do what fits best with your project and other frameworks to which you are binding. The Example method is just there to run the code when you are debugging and shows the various methods in action.
// use of relative paths to the modules when working in debug folder
// switch to from "sp-pnp-js" when working in your project outside of
// the debug folder
import pnp,
{
Queryable,
Logger,
LogLevel,
ListEnsureResult,
ODataEntityArray,
Web,
List,
Item,
ODataEntity
} from "../src/pnp"
class MyWeb extends Web {
// an example method to show how you can define a list at runtime in your custom objects and it will
// be created if it doesn't exist or get returned if it does
public getMyList(): Promise<MyList> {
// we could configure other options here
return pnp.sp.web.lists.ensure(MyList.ListTitle).then((ler: ListEnsureResult) => {
// here we can do things like add fields or setup views, etc when we first create the list.
if (ler.created) {
// create some extra fields in the list as an example and update our default view
// to include those fields, all in a batch
let batch = pnp.sp.web.createBatch();
ler.list.fields.inBatch(batch).addText("ProductNumber");
ler.list.fields.inBatch(batch).addDateTime("OrderDate");
ler.list.fields.inBatch(batch).addCurrency("OrderAmount");
return batch.execute().then(_ => {
return ler.list.getListItemEntityTypeFullName().then(name => {
let batch2 = pnp.sp.web.createBatch();
// now we need to add some items, just for ExtensibilityExample
ler.list.items.inBatch(batch2).add({ Title: "Item 1", ProductNumber: "1234", OrderDate: new Date(), OrderAmount: 123.45 }, name);
ler.list.items.inBatch(batch2).add({ Title: "Item 2", ProductNumber: "1234", OrderDate: new Date(), OrderAmount: 123.45 }, name);
ler.list.items.inBatch(batch2).add({ Title: "Item 3", ProductNumber: "1234", OrderDate: new Date(), OrderAmount: 123.45 }, name);
ler.list.items.inBatch(batch2).add({ Title: "Item 4", ProductNumber: "1234", OrderDate: new Date(), OrderAmount: 123.45 }, name);
ler.list.items.inBatch(batch2).add({ Title: "Item 5", ProductNumber: "1234", OrderDate: new Date(), OrderAmount: 123.45 }, name);
ler.list.items.inBatch(batch2).add({ Title: "Item 6", ProductNumber: "1234", OrderDate: new Date(), OrderAmount: 123.45 }, name);
return batch2.execute().then(_ => {
// using the as method to "cast" the list as type MyList
return ler.list.as(MyList);
});
});
});
} else {
// using the as method to "cast" the list as type MyList
return ler.list.as(MyList);
}
});
}
}
class MyList extends List {
public static ListTitle = "ExtensibilityExample";
public getTop5Items(): Promise<MyItem[]> {
// casting the web to our web type
return pnp.sp.web.as(MyWeb).getMyList().then(list => {
return list.items.orderBy("Created").top(5).select.call(list.items, MyItem.Fields).getAs(ODataEntityArray(MyItem));
});
}
public addMyItem(title: string, productNumber: string, orderDate: Date, orderAmount: number): Promise<MyItem> {
return pnp.sp.web.as(MyWeb).getMyList().then(list => {
return list.items.add({
Title: title,
ProductNumber: productNumber,
OrderDate: orderDate,
OrderAmount: orderAmount
}).then(result => {
return result.item.as(MyItem);
});
});
}
}
class MyItem extends Item {
public static Fields = ["Id", "Title", "ProductNumber", "OrderDate", "OrderAmount"];
public Id: number;
public Title: string;
public ProductNumber: string;
public OrderDate: Date;
public OrderAmount: number;
// this is an example of using a static method to begin a chain and act as short hand for getting a list
// and getting an item from that list by id. See exmple below for usage
public static byId(id: number): Promise<MyItem> {
return pnp.sp.web.as(MyWeb).getMyList().then(list => {
return list.items.getById(id).getAs(ODataEntity(MyItem));
});
}
// override get to enfore select for our fields to always optimize
// but allow it to be overridden by any supplied values
public get(): Promise<MyItem> {
// 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.
const query = this._query.getKeys().indexOf("$select") > -1 ? this : this.select.apply(this, MyItem.Fields);
// call the base get, but in our case pass the appropriate ODataEntity def so we are returning
// a MyItem instance
return super.get.call(query, ODataEntity(MyItem), arguments[1] || {});
}
// here we wrap the update method so we can set the value on our
// object and save them without having to build the plain objects.
// this would also be an opportunity to manipulate any special field values
public save(): Promise<MyItem> {
// we could do a check here to see if we needed to update or create this item
// within the save method
return this.update({
Title: this.Title,
ProductNumber: this.ProductNumber,
OrderDate: this.OrderDate,
OrderAmount: this.OrderAmount
}).then(result => {
return result.item.as(MyItem);
});
}
}
// this is just a debug method to run our code
export function Example() {
Logger.activeLogLevel = LogLevel.Warning;
// inject our type into the method chain
pnp.sp.web.as(MyWeb).getMyList().then((list: MyList) => {
list.getTop5Items().then((items: MyItem[]) => {
Logger.write(`The array had ${items.length} items.`, LogLevel.Warning);
Logger.write(`The first item is: ${items[0].Title} for $${items[0].OrderAmount}`, LogLevel.Warning);
// in this case items[0] will be of type MyItem
// here using the standard update method and passing in the plain object
items[0].update({
OrderAmount: 4323.05,
}).then(iur => {
// calling our custom get method override when we get an item's details.
iur.item.as(MyItem).get().then(result => {
Logger.write(`After update and get we have ${result.Title} for $${result.OrderAmount}`, LogLevel.Warning);
// now we update the object via the properties and our custom save method
// to compare the two approaches
result.Title = "Now I am updating the title";
result.ProductNumber = "9876";
result.save();
});
});
// here we will use the static byId method of MyItem and chain from that
MyItem.byId(2).then(item => {
item.Title = "And this is item id 2 updated.";
item.save();
});
// use our custom create method to add a new item
pnp.sp.web.as(MyWeb).getMyList().then(list => {
list.addMyItem("Created Item", "1234", new Date(), 34.99).then(myNewItem => {
console.log(JSON.stringify(myNewItem));
});
});
});
});
}
Using this example you can extend the library to match your business objects and allowing you to easily bind to your framework of choice. For next steps check out the knockoutjs sample and the article on parsers to better understand the mechanics. Happy coding!