Skip to content

Commit e3edb08

Browse files
waldekmastykarzVesaJuvonen
authored andcommitted
Updated tutorial on using SP PnP JS with SPFx (SharePoint#933)
1 parent 1cae27d commit e3edb08

File tree

1 file changed

+107
-117
lines changed

1 file changed

+107
-117
lines changed

docs/spfx/web-parts/guidance/use-sp-pnp-js-with-spfx-web-parts.md

Lines changed: 107 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,16 @@ yo @microsoft/sharepoint
3838
Enter the following values when prompted during the setup of the new project:
3939

4040
- **spfx-sp-pnp-js-example** as solution name (keep default)
41+
- **SharePoint Online only (latest)** as the baseline packages version
4142
- **Current Folder** as the solution ___location
42-
- **Knockout** as the framework
43+
- **Y** as allow tenant admin to deploy solution to all sites
44+
- **WebPart** as component to create
4345
- **SPPnPJSExample** as the name of the web part
4446
- **Example of using sp-pnp-js within SPFx** as the description
47+
- **Knockout** as the framework
4548

4649
![Completed Project Scaffolding](../../../images/sp-pnp-js-guide-completed-setup.png)
4750

48-
4951
Once the scaffolding completes, open the project in the code editor of your choosing. The screenshots shown here demonstrate [Visual Studio Code](https://code.visualstudio.com/). To open the directory within Visual Studio Code, enter the following in the console:
5052

5153
```sh
@@ -115,8 +117,8 @@ The takeaway is that by using sp-pnp-js, we write much less code to handle reque
115117
```TypeScript
116118
import * as ko from 'knockout';
117119
import styles from './SpPnPjsExample.module.scss';
118-
import { ISpPnPjsExampleWebPartProps } from './ISpPnPjsExampleWebPartProps';
119-
import pnp, { List, ListEnsureResult, ItemAddResult } from "sp-pnp-js";
120+
import { ISpPnPjsExampleWebPartProps } from './SpPnPjsExampleWebPart';
121+
import pnp, { List, ListEnsureResult, ItemAddResult, FieldAddResult } from "sp-pnp-js";
120122

121123
export interface ISpPnPjsExampleBindingContext extends ISpPnPjsExampleWebPartProps {
122124
shouter: KnockoutSubscribable<{}>;
@@ -131,15 +133,16 @@ export interface OrderListItem {
131133
OrderNumber: string;
132134
}
133135

134-
export default class SpPnPjsExampleViewModel {
136+
const LIST_EXISTS: string = 'List exists';
135137

138+
export default class SpPnPjsExampleViewModel {
136139
public description: KnockoutObservable<string> = ko.observable('');
137140
public newItemTitle: KnockoutObservable<string> = ko.observable('');
138141
public newItemNumber: KnockoutObservable<string> = ko.observable('');
139142
public items: KnockoutObservableArray<OrderListItem> = ko.observableArray([]);
140143

141144
public labelClass: string = styles.label;
142-
public helloWorldClass: string = styles.helloWorld;
145+
public spPnPjsExampleClass: string = styles.spPnPjsExample;
143146
public containerClass: string = styles.container;
144147
public rowClass: string = `ms-Grid-row ms-bgColor-themeDark ms-fontColor-white ${styles.row}`;
145148
public buttonClass: string = `ms-Button ${styles.button}`;
@@ -153,8 +156,7 @@ export default class SpPnPjsExampleViewModel {
153156
}, this, 'description');
154157

155158
// Load the items
156-
this.getItems().then(items => {
157-
159+
this.getItems().then((items: OrderListItem[]): void => {
158160
this.items(items);
159161
});
160162
}
@@ -163,9 +165,7 @@ export default class SpPnPjsExampleViewModel {
163165
* Gets the items from the list
164166
*/
165167
private getItems(): Promise<OrderListItem[]> {
166-
167-
return this.ensureList().then(list => {
168-
168+
return this.ensureList().then((list: List): Promise<OrderListItem[]> => {
169169
// Here we are using the getAs operator so that our returned value will be typed
170170
return list.items.select("Id", "Title", "OrderNumber").getAs<OrderListItem[]>();
171171
});
@@ -175,28 +175,24 @@ export default class SpPnPjsExampleViewModel {
175175
* Adds an item to the list
176176
*/
177177
public addItem(): void {
178-
179178
if (this.newItemTitle() !== "" && this.newItemNumber() !== "") {
180-
181-
this.ensureList().then(list => {
182-
179+
this.ensureList().then((list: List): Promise<ItemAddResult> => {
183180
// Add the new item to the SharePoint list
184-
list.items.add({
181+
return list.items.add({
185182
Title: this.newItemTitle(),
186183
OrderNumber: this.newItemNumber(),
187-
}).then((iar: ItemAddResult) => {
188-
189-
// Add the new item to the display
190-
this.items.push({
191-
Id: iar.data.Id,
192-
OrderNumber: iar.data.OrderNumber,
193-
Title: iar.data.Title,
194-
});
195-
196-
// Clear the form
197-
this.newItemTitle("");
198-
this.newItemNumber("");
199184
});
185+
}).then((iar: ItemAddResult) => {
186+
// Add the new item to the display
187+
this.items.push({
188+
Id: iar.data.Id,
189+
OrderNumber: iar.data.OrderNumber,
190+
Title: iar.data.Title,
191+
});
192+
193+
// Clear the form
194+
this.newItemTitle("");
195+
this.newItemNumber("");
200196
});
201197
}
202198
}
@@ -205,85 +201,84 @@ export default class SpPnPjsExampleViewModel {
205201
* Deletes an item from the list
206202
*/
207203
public deleteItem(data): void {
208-
209-
if (confirm("Are you sure you want to delete this item?")) {
210-
this.ensureList().then(list => {
211-
list.items.getById(data.Id).delete().then(_ => {
212-
this.items.remove(data);
213-
});
214-
}).catch((e: Error) => {
215-
alert(`There was an error deleting the item: ${e.message}`);
216-
});
204+
if (!confirm("Are you sure you want to delete this item?")) {
205+
return;
217206
}
207+
208+
this.ensureList().then((list: List): Promise<void> => {
209+
return list.items.getById(data.Id).delete();
210+
}).then(_ => {
211+
this.items.remove(data);
212+
}).catch((e: Error) => {
213+
alert(`There was an error deleting the item: ${e.message}`);
214+
});
218215
}
219216

220217
/**
221218
* Ensures the list exists. If not, it creates it and adds some default example data
222219
*/
223220
private ensureList(): Promise<List> {
224-
225-
return new Promise<List>((resolve, reject) => {
226-
221+
return new Promise<List>((resolve: (list: List) => void, reject: (err: string) => void): void => {
222+
let listEnsureResults: ListEnsureResult;
227223
// Use lists.ensure to always have the list available
228-
pnp.sp.web.lists.ensure("SPPnPJSExampleList").then((ler: ListEnsureResult) => {
224+
pnp.sp.web.lists.ensure("SPPnPJSExampleList")
225+
.then((ler: ListEnsureResult): Promise<FieldAddResult> => {
226+
listEnsureResults = ler;
229227

230-
if (ler.created) {
228+
if (!ler.created) {
229+
// resolve main promise
230+
resolve(ler.list);
231+
// break promise chain
232+
return Promise.reject(LIST_EXISTS);
233+
}
231234

232235
// We created the list on this call, so let's add a column
233-
ler.list.fields.addText("OrderNumber").then(_ => {
234-
235-
// And we will also add a few items so we can see some example data
236-
// Here we use batching
237-
238-
// Create a batch
239-
let batch = pnp.sp.web.createBatch();
240-
241-
ler.list.getListItemEntityTypeFullName().then(typeName => {
242-
243-
ler.list.items.inBatch(batch).add({
244-
Title: "Title 1",
245-
OrderNumber: "4826492"
246-
}, typeName);
247-
248-
ler.list.items.inBatch(batch).add({
249-
Title: "Title 2",
250-
OrderNumber: "828475"
251-
}, typeName);
252-
253-
ler.list.items.inBatch(batch).add({
254-
Title: "Title 3",
255-
OrderNumber: "75638923"
256-
}, typeName);
257-
258-
// Excute the batched operations
259-
batch.execute().then(_ => {
260-
// All of the items have been added within the batch
261-
262-
resolve(ler.list);
263-
264-
}).catch(e => reject(e));
265-
266-
}).catch(e => reject(e));
267-
268-
}).catch(e => reject(e));
269-
270-
} else {
271-
272-
resolve(ler.list);
273-
}
274-
275-
}).catch(e => reject(e));
236+
return ler.list.fields.addText("OrderNumber");
237+
}).then((): Promise<string> => {
238+
console.warn('Adding items...');
239+
// And we will also add a few items so we can see some example data
240+
// Here we use batching
241+
return listEnsureResults.list.getListItemEntityTypeFullName();
242+
}).then((typeName: string): Promise<void> => {
243+
// Create a batch
244+
const batch = pnp.sp.web.createBatch();
245+
listEnsureResults.list.items.inBatch(batch).add({
246+
Title: "Title 1",
247+
OrderNumber: "4826492"
248+
}, typeName);
249+
250+
listEnsureResults.list.items.inBatch(batch).add({
251+
Title: "Title 2",
252+
OrderNumber: "828475"
253+
}, typeName);
254+
255+
listEnsureResults.list.items.inBatch(batch).add({
256+
Title: "Title 3",
257+
OrderNumber: "75638923"
258+
}, typeName);
259+
260+
// Execute the batched operations
261+
return batch.execute();
262+
}).then((): void => {
263+
// All of the items have been added within the batch
264+
resolve(listEnsureResults.list);
265+
}).catch((e: any): void => {
266+
if (e !== LIST_EXISTS) {
267+
reject(e);
268+
}
269+
});
276270
});
277271
}
278272
}
279273
```
274+
280275
## Update the Template
281276

282277
Finally, we need to update the template to match the functionality we have added into the ViewModel. Copy the code below into the **SpPnPjsExample.template.html** file. We have added a title row, a foreach repeater
283278
for the items collection, and a form allowing you to add new items to the list.
284279

285280
```html
286-
<div data-bind="attr: {class:helloWorldClass}">
281+
<div data-bind="attr: {class:spPnPjsExampleClass}">
287282
<div data-bind="attr: {class:containerClass}">
288283

289284
<div data-bind="attr: {class:rowClass}">
@@ -339,6 +334,7 @@ for the items collection, and a form allowing you to add new items to the list.
339334
</div>
340335
</div>
341336
```
337+
342338
## Run the Example
343339

344340
Start the sample and add the web part to your SharePoint hosted workbench (/_layouts/workbench.aspx) to can see it in action.
@@ -359,7 +355,7 @@ The sp-pnp-js library contains a great range of functionality and extensibility.
359355

360356
When you are ready to deploy your solution and want to build using the `--ship` flag you need to mark sp-pnp-js as an external library in the configuration. This is done by updating the SPFx **config/config.js** file to include this line in the externals section:
361357

362-
```
358+
```json
363359
"sp-pnp-js": "https://cdnjs.cloudflare.com/ajax/libs/sp-pnp-js/2.0.1/pnp.min.js"
364360
```
365361

@@ -376,7 +372,7 @@ Add a new file named **MockSpPnPjsExampleViewModel.ts** alongside the other web
376372
```TypeScript
377373
import * as ko from 'knockout';
378374
import styles from './SpPnPjsExample.module.scss';
379-
import { ISpPnPjsExampleWebPartProps } from './ISpPnPjsExampleWebPartProps';
375+
import { ISpPnPjsExampleWebPartProps } from './SpPnPjsExampleWebPart';
380376
import pnp, { List, ListEnsureResult, ItemAddResult } from "sp-pnp-js";
381377
import { ISpPnPjsExampleBindingContext, OrderListItem } from './SpPnPjsExampleViewModel';
382378

@@ -388,7 +384,7 @@ export default class MockSpPnPjsExampleViewModel {
388384
public items: KnockoutObservableArray<OrderListItem> = ko.observableArray([]);
389385

390386
public labelClass: string = styles.label;
391-
public helloWorldClass: string = styles.helloWorld;
387+
public spPnPjsExampleClass: string = styles.spPnPjsExample;
392388
public containerClass: string = styles.container;
393389
public rowClass: string = `ms-Grid-row ms-bgColor-themeDark ms-fontColor-white ${styles.row}`;
394390
public buttonClass: string = `ms-Button ${styles.button}`;
@@ -397,13 +393,12 @@ export default class MockSpPnPjsExampleViewModel {
397393
this.description(bindings.description);
398394

399395
// When the web part description is updated, change this view model's description.
400-
bindings.shouter.subscribe((value: string) => {
396+
bindings.shouter.subscribe((value: string): void => {
401397
this.description(value);
402398
}, this, 'description');
403399

404400
// Load the items
405-
this.getItems().then(items => {
406-
401+
this.getItems().then((items: OrderListItem[]): void => {
407402
this.items(items);
408403
});
409404
}
@@ -433,9 +428,7 @@ export default class MockSpPnPjsExampleViewModel {
433428
* Simulates adding an item to the list
434429
*/
435430
public addItem(): void {
436-
437431
if (this.newItemTitle() !== "" && this.newItemNumber() !== "") {
438-
439432
// Add the new item to the display
440433
this.items.push({
441434
Id: this.items.length,
@@ -453,47 +446,45 @@ export default class MockSpPnPjsExampleViewModel {
453446
* Simulates deleting an item from the list
454447
*/
455448
public deleteItem(data): void {
456-
457449
if (confirm("Are you sure you want to delete this item?")) {
458450
this.items.remove(data);
459451
}
460452
}
461453
}
462454
```
455+
463456
### Update Webpart
464457

465458
Finally, we need to update the webpart to use the mock data when appropriate. Open the **SpPnPjsExampleWebPart.ts** file. Start by importing the mock ViewModel web just created:
466459

467460
```TypeScript
468461
import MockSpPnPjsExampleViewModel from './MockSpPnPjsExampleViewModel';
469462
```
463+
464+
Next, import the `Environment` and `EnvironmentType` types that you will use to detect the type of
465+
environment the web part is running in:
466+
467+
```ts
468+
import { Environment, EnvironmentType } from '@microsoft/sp-core-library';
469+
```
470+
470471
Then, locate the `_registerComponent` method and update it as shown below:
471472

472473
```TypeScript
473474
private _registerComponent(tagName: string): void {
474-
475-
if (Environment.type === EnvironmentType.Local) {
476-
console.log("here I am.")
477-
ko.components.register(
478-
tagName,
479-
{
480-
viewModel: MockSpPnPjsExampleViewModel,
481-
template: require('./SpPnPjsExample.template.html'),
482-
synchronous: false
483-
}
484-
);
485-
} else {
486-
ko.components.register(
487-
tagName,
488-
{
489-
viewModel: SpPnPjsExampleViewModel,
490-
template: require('./SpPnPjsExample.template.html'),
491-
synchronous: false
492-
}
493-
);
494-
}
475+
ko.components.register(
476+
tagName,
477+
{
478+
viewModel: Environment.type === EnvironmentType.Local ?
479+
MockSpPnPjsExampleViewModel :
480+
SpPnPjsExampleViewModel,
481+
template: require('./SpPnPjsExample.template.html'),
482+
synchronous: false
483+
}
484+
);
495485
}
496486
```
487+
497488
Finally, type `gulp serve` in the console to bring up the local workbench, which now will work with the mock data. (If you already have the server running, stop it using Ctrl+C and then restart it):
498489

499490
```sh
@@ -502,7 +493,6 @@ gulp serve
502493

503494
![Project as it appears running in the local workbench with mock data](../../../images/sp-pnp-js-guide-with-mock-data.png)
504495

505-
506496
## Download Full Example Code
507497

508498
Remember you can find the full sample [here](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/knockout-sp-pnp-js).

0 commit comments

Comments
 (0)