Adding dynamic data sample react-events-dynamicdata
This commit is contained in:
parent
4385260f73
commit
22d9dbfaa3
|
@ -0,0 +1,67 @@
|
|||
# Dynamic data
|
||||
|
||||
Sample web parts illustrating using the SharePoint Framework Dynamic data capability.
|
||||
|
||||
![Web parts placed on a modern SharePoint page showing information about events](./assets/dynamic-data-webparts.png)
|
||||
|
||||
## Used SharePoint Framework Version
|
||||
![drop](https://img.shields.io/badge/drop-1.5.0--plusbeta-blue.svg)
|
||||
|
||||
## Applies to
|
||||
|
||||
* [SharePoint Framework](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview)
|
||||
* [Office 365 developer tenant](http://dev.office.com/sharepoint/docs/spfx/set-up-your-developer-tenant)
|
||||
|
||||
## Solution
|
||||
|
||||
Solution|Author(s)
|
||||
--------|---------
|
||||
react-events-dynamicdata|Waldek Mastykarz (MVP, Rencore, @waldekm)
|
||||
|
||||
## Version history
|
||||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0|June 5, 2018|Initial release
|
||||
|
||||
## Disclaimer
|
||||
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
|
||||
|
||||
---
|
||||
|
||||
## Minimal Path to Awesome
|
||||
|
||||
* clone this repo
|
||||
* move to right folder
|
||||
* in the command line run:
|
||||
* `npm install`
|
||||
* `gulp bundle --ship`
|
||||
* `gulp package-solution --ship`
|
||||
* from the _sharepoint/solution_ folder, deploy the .sppkg file to the App catalog in your tenant
|
||||
* in the site where you want to test this solution
|
||||
* add the app named _react-events-dynamicdata-client-side-solution_
|
||||
* edit a page
|
||||
* add the three web parts named: _Events_, _Event details_ and _Map_
|
||||
* configure the _Event details_ web part:
|
||||
* as _Data source_, choose the _Events_ option
|
||||
* as _Data property_, choose the _Event_ option
|
||||
* configure the _Map_ web part:
|
||||
* get a Bing maps API key (follow the link in the web part)
|
||||
* as _Data source_, choose the _Events_ option
|
||||
* as _Data property_, choose the _Location_ option
|
||||
|
||||
## Features
|
||||
|
||||
This sample contains a set of SharePoint Framework client-side web parts that illustrate the Dynamic data capability.
|
||||
|
||||
Web parts in this solution illustrate the following concepts on top of the SharePoint Framework:
|
||||
|
||||
* making web part a dynamic data source
|
||||
* exposing multiple data properties from a single data source
|
||||
* subscribing to dynamic data source notifications from a web part
|
||||
* persisting dynamic data subscription information in web part properties
|
||||
* deploying list instances from a SharePoint Framework solution package
|
||||
* using [PnPjs](https://github.com/pnp/pnpjs) to retrieve data from a SharePoint list
|
||||
* using [SharePoint Framework React Controls](https://github.com/SharePoint/sp-dev-fx-controls-react) in web parts
|
||||
|
||||
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-events-dynamicdata" />
|
Binary file not shown.
After Width: | Height: | Size: 197 KiB |
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
|
||||
"version": "2.0",
|
||||
"bundles": {
|
||||
"events": {
|
||||
"components": [
|
||||
{
|
||||
"entrypoint": "./lib/webparts/events/EventsWebPart.js",
|
||||
"manifest": "./src/webparts/events/EventsWebPart.manifest.json"
|
||||
},
|
||||
{
|
||||
"entrypoint": "./lib/webparts/eventDetails/EventDetailsWebPart.js",
|
||||
"manifest": "./src/webparts/eventDetails/EventDetailsWebPart.manifest.json"
|
||||
},
|
||||
{
|
||||
"entrypoint": "./lib/webparts/map/MapWebPart.js",
|
||||
"manifest": "./src/webparts/map/MapWebPart.manifest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"EventsWebPartStrings": "lib/webparts/events/loc/{locale}.js",
|
||||
"EventDetailsWebPartStrings": "lib/webparts/eventDetails/loc/{locale}.js",
|
||||
"MapWebPartStrings": "lib/webparts/map/loc/{locale}.js",
|
||||
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
|
||||
"workingDir": "./temp/deploy/",
|
||||
"account": "<!-- STORAGE ACCOUNT NAME -->",
|
||||
"container": "react-events-dynamicdata",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||
"solution": {
|
||||
"name": "react-events-dynamicdata-client-side-solution",
|
||||
"id": "57e5d6e5-04df-4810-b2bd-5b2751200ad9",
|
||||
"version": "1.0.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"features": [
|
||||
{
|
||||
"title": "Events - Deployment of data",
|
||||
"description": "Deploys data required by the solution",
|
||||
"id": "1b23d9fa-50ab-48e2-9b57-1b7cf18658a5",
|
||||
"version": "1.0.0.0",
|
||||
"assets": {
|
||||
"elementManifests": [
|
||||
"events_elements.xml"
|
||||
],
|
||||
"elementFiles": [
|
||||
"events_schema.xml"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/react-events-dynamicdata.sppkg"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
|
||||
"port": 4321,
|
||||
"https": true,
|
||||
"initialPage": "https://localhost:5432/workbench",
|
||||
"api": {
|
||||
"port": 5432,
|
||||
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/core-build/tslint.schema.json",
|
||||
// Display errors as warnings
|
||||
"displayAsWarning": true,
|
||||
// The TSLint task may have been configured with several custom lint rules
|
||||
// before this config file is read (for example lint rules from the tslint-microsoft-contrib
|
||||
// project). If true, this flag will deactivate any of these rules.
|
||||
"removeExistingRules": true,
|
||||
// When true, the TSLint task is configured with some default TSLint "rules.":
|
||||
"useDefaultConfigAsBase": false,
|
||||
// Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules
|
||||
// which are active, other than the list of rules below.
|
||||
"lintConfig": {
|
||||
// Opt-in to Lint rules which help to eliminate bugs in JavaScript
|
||||
"rules": {
|
||||
"class-name": false,
|
||||
"export-name": false,
|
||||
"forin": false,
|
||||
"label-position": false,
|
||||
"member-access": true,
|
||||
"no-arg": false,
|
||||
"no-console": false,
|
||||
"no-construct": false,
|
||||
"no-duplicate-case": true,
|
||||
"no-duplicate-variable": true,
|
||||
"no-eval": false,
|
||||
"no-function-expression": true,
|
||||
"no-internal-module": true,
|
||||
"no-shadowed-variable": true,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-unnecessary-semicolons": true,
|
||||
"no-unused-expression": true,
|
||||
"no-use-before-declare": true,
|
||||
"no-with-statement": true,
|
||||
"semicolon": true,
|
||||
"trailing-comma": false,
|
||||
"typedef": false,
|
||||
"typedef-whitespace": false,
|
||||
"use-named-parameter": true,
|
||||
"valid-typeof": true,
|
||||
"variable-name": false,
|
||||
"whitespace": false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const gulp = require('gulp');
|
||||
const build = require('@microsoft/sp-build-web');
|
||||
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
|
||||
|
||||
build.initialize(gulp);
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"name": "react-events-dynamicdata",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/sp-core-library": "1.5.0-plusbeta",
|
||||
"@microsoft/sp-lodash-subset": "1.5.0-plusbeta",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.5.0-plusbeta",
|
||||
"@microsoft/sp-webpart-base": "1.5.0-plusbeta",
|
||||
"@pnp/common": "^1.1.0",
|
||||
"@pnp/logging": "^1.1.0",
|
||||
"@pnp/odata": "^1.1.0",
|
||||
"@pnp/sp": "^1.1.0",
|
||||
"@pnp/spfx-controls-react": "^1.4.0",
|
||||
"@types/es6-promise": "0.0.33",
|
||||
"@types/react": "15.6.6",
|
||||
"@types/react-dom": "15.5.6",
|
||||
"@types/webpack-env": "1.13.1",
|
||||
"react": "15.6.2",
|
||||
"react-dom": "15.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/generator-sharepoint": "1.5.0",
|
||||
"@microsoft/sp-build-web": "1.5.0-plusbeta",
|
||||
"@microsoft/sp-module-interfaces": "1.5.0-plusbeta",
|
||||
"@microsoft/sp-webpart-workbench": "1.5.0-plusbeta",
|
||||
"@types/chai": "3.4.34",
|
||||
"@types/mocha": "2.2.38",
|
||||
"ajv": "~5.2.2",
|
||||
"gulp": "~3.9.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
|
||||
<Field ID="8f34b2cd-0a01-4a45-9a81-3d564d24fdf8"
|
||||
Name="PnPCity"
|
||||
DisplayName="City"
|
||||
Type="Text"
|
||||
Required="TRUE"
|
||||
Group="PnP Columns" />
|
||||
<Field ID="8b6ebc5d-2b1c-4170-9475-75105394034d"
|
||||
Name="PnPAddress"
|
||||
DisplayName="Address"
|
||||
Type="Note"
|
||||
RichText="FALSE"
|
||||
NumLines="2"
|
||||
Required="TRUE"
|
||||
Group="PnP Columns" />
|
||||
<Field ID="00292097-d535-43d5-a4de-91fbb8c9481b"
|
||||
Name="PnPOrganizerName"
|
||||
DisplayName="Organizer name"
|
||||
Type="Text"
|
||||
Group="PnP Columns" />
|
||||
<Field ID="d7e33d91-8743-4429-a96a-b70ffdbd0f35"
|
||||
Name="PnPOrganizerEmail"
|
||||
DisplayName="Organizer e-mail"
|
||||
Type="Text"
|
||||
Group="PnP Columns" />
|
||||
<Field ID="6d34055b-cafd-4f32-a1ab-c6db8a3c5c84"
|
||||
Name="PnPEventDate"
|
||||
DisplayName="Event date"
|
||||
Type="DateTime"
|
||||
Format="DateOnly"
|
||||
Group="PnP Columns" />
|
||||
|
||||
<ContentType ID="0x0100A73DD10A87FA4075B98FC1CBCAA2C775"
|
||||
Name="PnP Event"
|
||||
Group="PnP Content Types"
|
||||
Description="Event">
|
||||
<FieldRefs>
|
||||
<FieldRef ID="8f34b2cd-0a01-4a45-9a81-3d564d24fdf8" />
|
||||
<FieldRef ID="8b6ebc5d-2b1c-4170-9475-75105394034d" />
|
||||
<FieldRef ID="00292097-d535-43d5-a4de-91fbb8c9481b" />
|
||||
<FieldRef ID="d7e33d91-8743-4429-a96a-b70ffdbd0f35" />
|
||||
<FieldRef ID="6d34055b-cafd-4f32-a1ab-c6db8a3c5c84" />
|
||||
</FieldRefs>
|
||||
</ContentType>
|
||||
|
||||
<ListInstance
|
||||
CustomSchema="events_schema.xml"
|
||||
FeatureId="00bfea71-de22-43b2-a848-c05709900100"
|
||||
Title="Company events"
|
||||
Description="Company events"
|
||||
TemplateType="100"
|
||||
Url="Lists/CompanyEvents">
|
||||
<Data>
|
||||
<Rows>
|
||||
<Row>
|
||||
<Field Name="Title">Tampa Home Show</Field>
|
||||
<Field Name="PnPCity">Tampa, FL</Field>
|
||||
<Field Name="PnPAddress">333 S Franklin St</Field>
|
||||
<Field Name="PnPOrganizerName">Grady Archie</Field>
|
||||
<Field Name="PnPOrganizerEmail">GradyA@contoso.OnMicrosoft.com</Field>
|
||||
<Field Name="PnPEventDate">2018-05-29T00:00:00Z</Field>
|
||||
</Row>
|
||||
<Row>
|
||||
<Field Name="Title">Custom Electronic Design and Installation Association (CEDIA)</Field>
|
||||
<Field Name="PnPCity">San Diego, CA</Field>
|
||||
<Field Name="PnPAddress">111 W Harbor Dr</Field>
|
||||
<Field Name="PnPOrganizerName">Megan Bowen</Field>
|
||||
<Field Name="PnPOrganizerEmail">MeganB@contoso.OnMicrosoft.com</Field>
|
||||
<Field Name="PnPEventDate">2018-06-15T00:00:00Z</Field>
|
||||
</Row>
|
||||
<Row>
|
||||
<Field Name="Title">Design Automation Conference (DAC)</Field>
|
||||
<Field Name="PnPCity">San Francisco, CA</Field>
|
||||
<Field Name="PnPAddress">747 Howard St Fl 5</Field>
|
||||
<Field Name="PnPOrganizerName">Irvin Sayers</Field>
|
||||
<Field Name="PnPOrganizerEmail">IrvinSB@contoso.OnMicrosoft.com</Field>
|
||||
<Field Name="PnPEventDate">2018-07-05T00:00:00Z</Field>
|
||||
</Row>
|
||||
</Rows>
|
||||
</Data>
|
||||
</ListInstance>
|
||||
</Elements>
|
|
@ -0,0 +1,34 @@
|
|||
<List xmlns:ows="Microsoft SharePoint" Title="Redirections" EnableContentTypes="TRUE" FolderCreation="FALSE" Direction="$Resources:Direction;" Url="Lists/PnPRedirections" BaseType="0" xmlns="http://schemas.microsoft.com/sharepoint/">
|
||||
<MetaData>
|
||||
<ContentTypes>
|
||||
<ContentTypeRef ID="0x0100A73DD10A87FA4075B98FC1CBCAA2C775" />
|
||||
</ContentTypes>
|
||||
<Fields></Fields>
|
||||
<Views>
|
||||
<View BaseViewID="1" Type="HTML" WebPartZoneID="Main" DisplayName="$Resources:core,objectiv_schema_mwsidcamlidC24;" DefaultView="TRUE" MobileView="TRUE" MobileDefaultView="TRUE" SetupPath="pages\viewpage.aspx" ImageUrl="/_layouts/images/generic.png" Url="AllItems.aspx">
|
||||
<XslLink Default="TRUE">main.xsl</XslLink>
|
||||
<JSLink>clienttemplates.js</JSLink>
|
||||
<RowLimit Paged="TRUE">30</RowLimit>
|
||||
<Toolbar Type="Standard" />
|
||||
<ViewFields>
|
||||
<FieldRef Name="LinkTitle"></FieldRef>
|
||||
<FieldRef Name="PnPCity"></FieldRef>
|
||||
<FieldRef Name="PnPAddress"></FieldRef>
|
||||
<FieldRef Name="PnPOrganizerName"></FieldRef>
|
||||
<FieldRef Name="PnPOrganizerEmail"></FieldRef>
|
||||
<FieldRef Name="PnPEventDate"></FieldRef>
|
||||
</ViewFields>
|
||||
<Query>
|
||||
<OrderBy>
|
||||
<FieldRef Name="ID" />
|
||||
</OrderBy>
|
||||
</Query>
|
||||
</View>
|
||||
</Views>
|
||||
<Forms>
|
||||
<Form Type="DisplayForm" Url="DispForm.aspx" SetupPath="pages\form.aspx" WebPartZoneID="Main" />
|
||||
<Form Type="EditForm" Url="EditForm.aspx" SetupPath="pages\form.aspx" WebPartZoneID="Main" />
|
||||
<Form Type="NewForm" Url="NewForm.aspx" SetupPath="pages\form.aspx" WebPartZoneID="Main" />
|
||||
</Forms>
|
||||
</MetaData>
|
||||
</List>
|
|
@ -0,0 +1,23 @@
|
|||
import { ILocation } from ".";
|
||||
|
||||
/**
|
||||
* Represents event
|
||||
*/
|
||||
export interface IEvent extends ILocation {
|
||||
/**
|
||||
* The start date of the event
|
||||
*/
|
||||
date: string;
|
||||
/**
|
||||
* The name of the event
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The e-mail address of the person organizing the event
|
||||
*/
|
||||
organizerEmail: string;
|
||||
/**
|
||||
* The name of the person organizing the event
|
||||
*/
|
||||
organizerName: string;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* Represents a SharePoint list item where information about an event is stored
|
||||
*/
|
||||
export interface IEventItem {
|
||||
/**
|
||||
* The name of the event
|
||||
*/
|
||||
Title: string;
|
||||
/**
|
||||
* The city where the event is located
|
||||
*/
|
||||
PnPCity: string;
|
||||
/**
|
||||
* The address where the event is located
|
||||
*/
|
||||
PnPAddress: string;
|
||||
/**
|
||||
* The start date of the event
|
||||
*/
|
||||
PnPEventDate: string;
|
||||
/**
|
||||
* The name of the person organizing the event
|
||||
*/
|
||||
PnPOrganizerName: string;
|
||||
/**
|
||||
* The e-mail address of the person organizing the event
|
||||
*/
|
||||
PnPOrganizerEmail: string;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Represents a location
|
||||
*/
|
||||
export interface ILocation {
|
||||
/**
|
||||
* Location address
|
||||
*/
|
||||
address: string;
|
||||
/**
|
||||
* Location city
|
||||
*/
|
||||
city: string;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from './IEvent';
|
||||
export * from './IEventItem';
|
||||
export * from './ILocation';
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "d164f880-41df-4698-846d-6bae153d8258",
|
||||
"alias": "EventDetailsWebPart",
|
||||
"componentType": "WebPart",
|
||||
|
||||
// The "*" signifies that the version should be taken from the package.json
|
||||
"version": "*",
|
||||
"manifestVersion": 2,
|
||||
|
||||
// If true, the component can only be installed on sites where Custom Script is allowed.
|
||||
// Components that allow authors to embed arbitrary script code should set this to true.
|
||||
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
|
||||
"requiresCustomScript": false,
|
||||
|
||||
"preconfiguredEntries": [{
|
||||
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
|
||||
"group": { "default": "Other" },
|
||||
"title": { "default": "Event details" },
|
||||
"description": { "default": "Shows details of the selected event" },
|
||||
"officeFabricIconFontName": "CustomList",
|
||||
"properties": {
|
||||
"title": "Event details"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { Version } from '@microsoft/sp-core-library';
|
||||
import {
|
||||
BaseClientSideWebPart,
|
||||
IPropertyPaneConfiguration,
|
||||
PropertyPaneTextField,
|
||||
IPropertyPaneDropdownOption,
|
||||
PropertyPaneDropdown
|
||||
} from '@microsoft/sp-webpart-base';
|
||||
|
||||
import * as strings from 'EventDetailsWebPartStrings';
|
||||
import { EventDetails, IEventDetailsProps } from './components';
|
||||
import { IDynamicDataSource } from '@microsoft/sp-dynamic-data';
|
||||
import { IEvent } from '../../data';
|
||||
|
||||
/**
|
||||
* Represents properties of the Event details web part
|
||||
*/
|
||||
export interface IEventDetailsWebPartProps {
|
||||
/**
|
||||
* The ID of the dynamic data to which the web part is subscribed
|
||||
*/
|
||||
propertyId: string;
|
||||
/**
|
||||
* The dynamic data source ID to which the web part is subscribed
|
||||
*/
|
||||
sourceId: string;
|
||||
/**
|
||||
* Web part title
|
||||
*/
|
||||
title: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Events web part. Shows detail information about a selected event retrieved
|
||||
* from the connected dynamic data source.
|
||||
*/
|
||||
export default class EventDetailsWebPart extends BaseClientSideWebPart<IEventDetailsWebPartProps> {
|
||||
/**
|
||||
* The previous ID of the dynamic data source to which the web part is
|
||||
* subscribed. Used to unsubscribe from previously registered dynamic data
|
||||
* source notifications after changing web part configuration in the property
|
||||
* pane.
|
||||
*/
|
||||
private _lastSourceId: string = undefined;
|
||||
/**
|
||||
* The previous ID of the dynamic data to which the web part is subscribed.
|
||||
* Used to unsubscribe from previously registered dynamic data source
|
||||
* notifications after changing web part configuration in the property pane.
|
||||
*/
|
||||
private _lastPropertyId: string = undefined;
|
||||
|
||||
/**
|
||||
* Event handler for clicking the Configure button on the Placeholder
|
||||
*/
|
||||
private _onConfigure = (): void => {
|
||||
this.context.propertyPane.open();
|
||||
}
|
||||
|
||||
protected onInit(): Promise<void> {
|
||||
// bind render method to the current instance so that it can be correctly
|
||||
// invoked when dynamic data change notification is triggered
|
||||
this.render = this.render.bind(this);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
let event: IEvent = undefined;
|
||||
const needsConfiguration: boolean = !this.properties.sourceId || !this.properties.propertyId;
|
||||
|
||||
// subscribe to dynamic data changes notifications
|
||||
// do this only once the first time the web part is rendered and only,
|
||||
// if the dynamic data source ID and property ID are provided
|
||||
if (this.renderedOnce === false && !needsConfiguration) {
|
||||
try {
|
||||
this.context.dynamicDataProvider.registerPropertyChanged(this.properties.sourceId, this.properties.propertyId, this.render);
|
||||
// store current values for the dynamic data source ID and property ID
|
||||
// so that the web part can unsubscribe from notifications when the
|
||||
// web part configuration changes
|
||||
this._lastSourceId = this.properties.sourceId;
|
||||
this._lastPropertyId = this.properties.propertyId;
|
||||
}
|
||||
catch (e) {
|
||||
this.context.statusRenderer.renderError(this.domElement, `An error has occurred while connecting to the data source. Details: ${e}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// retrieve the current value of dynamic data only if the dynamic data
|
||||
// source ID and property ID have been provided
|
||||
if (!needsConfiguration) {
|
||||
const source: IDynamicDataSource = this.context.dynamicDataProvider.tryGetSource(this.properties.sourceId);
|
||||
event = source ? source.getPropertyValue(this.properties.propertyId) : undefined;
|
||||
}
|
||||
|
||||
const element: React.ReactElement<IEventDetailsProps> = React.createElement(
|
||||
EventDetails,
|
||||
{
|
||||
needsConfiguration: needsConfiguration,
|
||||
event: event,
|
||||
onConfigure: this._onConfigure,
|
||||
displayMode: this.displayMode,
|
||||
title: this.properties.title,
|
||||
updateProperty: (value: string): void => {
|
||||
this.properties.title = value;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
ReactDom.render(element, this.domElement);
|
||||
}
|
||||
|
||||
protected get dataVersion(): Version {
|
||||
return Version.parse('1.0');
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
// get all available dynamic data sources on the page
|
||||
const sourceOptions: IPropertyPaneDropdownOption[] =
|
||||
this.context.dynamicDataProvider.getAvailableSources().map(source => {
|
||||
return {
|
||||
key: source.id,
|
||||
text: source.metadata.title
|
||||
};
|
||||
});
|
||||
const selectedSource: string = this.properties.sourceId;
|
||||
|
||||
let propertyOptions: IPropertyPaneDropdownOption[] = [];
|
||||
if (selectedSource) {
|
||||
const source: IDynamicDataSource = this.context.dynamicDataProvider.tryGetSource(selectedSource);
|
||||
if (source) {
|
||||
// get the list of all properties exposed by the currently selected
|
||||
// data source
|
||||
propertyOptions = source.getPropertyDefinitions().map(prop => {
|
||||
return {
|
||||
key: prop.id,
|
||||
text: prop.title
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pages: [
|
||||
{
|
||||
groups: [
|
||||
{
|
||||
groupFields: [
|
||||
PropertyPaneDropdown('sourceId', {
|
||||
label: strings.SourceIdFieldLabel,
|
||||
options: sourceOptions,
|
||||
selectedKey: this.properties.sourceId
|
||||
}),
|
||||
PropertyPaneDropdown('propertyId', {
|
||||
label: strings.PropertyIdFieldLabel,
|
||||
options: propertyOptions,
|
||||
selectedKey: this.properties.propertyId
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
protected onPropertyPaneFieldChanged(propertyPath: string): void {
|
||||
if (propertyPath === 'sourceId') {
|
||||
// reset the selected property ID after selecting a different dynamic
|
||||
// data source
|
||||
this.properties.propertyId =
|
||||
this.context.dynamicDataProvider.tryGetSource(this.properties.sourceId).getPropertyDefinitions()[0].id;
|
||||
}
|
||||
|
||||
if (this._lastSourceId && this._lastPropertyId) {
|
||||
// unsubscribe from the previously registered dynamic data changes
|
||||
// notifications
|
||||
this.context.dynamicDataProvider.unregisterPropertyChanged(this._lastSourceId, this._lastPropertyId, this.render);
|
||||
}
|
||||
|
||||
// subscribe to the newly configured dynamic data changes notifications
|
||||
this.context.dynamicDataProvider.registerPropertyChanged(this.properties.sourceId, this.properties.propertyId, this.render);
|
||||
// store current values for the dynamic data source ID and property ID
|
||||
// so that the web part can unsubscribe from notifications when the
|
||||
// web part configuration changes
|
||||
this._lastSourceId = this.properties.sourceId;
|
||||
this._lastPropertyId = this.properties.propertyId;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
|
||||
|
||||
.eventDetails {
|
||||
.info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 1em;
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import * as React from 'react';
|
||||
import styles from './EventDetails.module.scss';
|
||||
import { IEventDetailsProps } from './IEventDetailsProps';
|
||||
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
|
||||
import { Label } from 'office-ui-fabric-react/lib/Label';
|
||||
import { Icon } from 'office-ui-fabric-react/lib/Icon';
|
||||
import { IEvent } from '../../../data';
|
||||
import { WebPartTitle } from '@pnp/spfx-controls-react/lib/WebPartTitle';
|
||||
|
||||
export class EventDetails extends React.Component<IEventDetailsProps, {}> {
|
||||
public render(): React.ReactElement<IEventDetailsProps> {
|
||||
const { needsConfiguration, event, onConfigure, displayMode, title, updateProperty } = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.eventDetails}>
|
||||
<WebPartTitle displayMode={displayMode}
|
||||
title={title}
|
||||
updateProperty={updateProperty} />
|
||||
{needsConfiguration &&
|
||||
<Placeholder
|
||||
iconName='Edit'
|
||||
iconText='Configure your web part'
|
||||
description='Please configure the web part.'
|
||||
buttonLabel='Configure'
|
||||
onConfigure={onConfigure} />}
|
||||
{!needsConfiguration &&
|
||||
event &&
|
||||
(!event.name || !event.address) &&
|
||||
<Placeholder
|
||||
iconName='Edit'
|
||||
iconText='Configure your web part'
|
||||
description='The selected data source is not providing events. Change the data source'
|
||||
buttonLabel='Configure'
|
||||
onConfigure={onConfigure} />}
|
||||
{!needsConfiguration &&
|
||||
!event &&
|
||||
<Placeholder
|
||||
iconName='CustomList'
|
||||
iconText='Event details'
|
||||
description='Select an event' />}
|
||||
{!needsConfiguration &&
|
||||
event &&
|
||||
<ul>
|
||||
<li><Label>Event name</Label> {event.name}</li>
|
||||
<li><Label>City</Label> {event.city}</li>
|
||||
<li><Label>Address</Label> {event.address}</li>
|
||||
<li><Label>Organizer</Label> <Icon iconName='Mail' /> <a href={`mailto:${event.organizerEmail}`}>{event.organizerName}</a></li>
|
||||
<li><Label>Date</Label> {new Date(event.date).toLocaleDateString()}</li>
|
||||
</ul>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import { DisplayMode } from "@microsoft/sp-core-library";
|
||||
import { IEvent } from "../../../data";
|
||||
|
||||
/**
|
||||
* Event details components properties
|
||||
*/
|
||||
export interface IEventDetailsProps {
|
||||
/**
|
||||
* Web part display mode. Used for inline editing of the web part title
|
||||
*/
|
||||
displayMode: DisplayMode;
|
||||
/**
|
||||
* The currently selected event
|
||||
*/
|
||||
event: IEvent;
|
||||
/**
|
||||
* Determines if the web part has been connected to a dynamic data source or
|
||||
* not
|
||||
*/
|
||||
needsConfiguration: boolean;
|
||||
/**
|
||||
* Event handler for clicking the Configure button on the Placeholder
|
||||
*/
|
||||
onConfigure: () => void;
|
||||
/**
|
||||
* The title of the web part
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* Event handler after updating the web part title
|
||||
*/
|
||||
updateProperty: (value: string) => void;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './EventDetails';
|
||||
export * from './IEventDetailsProps';
|
|
@ -0,0 +1,6 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"SourceIdFieldLabel": "Data source",
|
||||
"PropertyIdFieldLabel": "Data property"
|
||||
}
|
||||
});
|
9
samples/react-events-dynamicdata/src/webparts/eventDetails/loc/mystrings.d.ts
vendored
Normal file
9
samples/react-events-dynamicdata/src/webparts/eventDetails/loc/mystrings.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
declare interface IEventDetailsWebPartStrings {
|
||||
SourceIdFieldLabel: string;
|
||||
PropertyIdFieldLabel: string;
|
||||
}
|
||||
|
||||
declare module 'EventDetailsWebPartStrings' {
|
||||
const strings: IEventDetailsWebPartStrings;
|
||||
export = strings;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "a453b126-4b2d-4314-bf38-76d1e4416175",
|
||||
"alias": "EventsWebPart",
|
||||
"componentType": "WebPart",
|
||||
|
||||
// The "*" signifies that the version should be taken from the package.json
|
||||
"version": "*",
|
||||
"manifestVersion": 2,
|
||||
|
||||
// If true, the component can only be installed on sites where Custom Script is allowed.
|
||||
// Components that allow authors to embed arbitrary script code should set this to true.
|
||||
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
|
||||
"requiresCustomScript": false,
|
||||
|
||||
"preconfiguredEntries": [{
|
||||
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
|
||||
"group": { "default": "Other" },
|
||||
"title": { "default": "Events" },
|
||||
"description": { "default": "Shows upcoming events" },
|
||||
"officeFabricIconFontName": "Calendar",
|
||||
"properties": {
|
||||
"title": "Upcoming events"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { Version } from '@microsoft/sp-core-library';
|
||||
import {
|
||||
BaseClientSideWebPart,
|
||||
IPropertyPaneConfiguration,
|
||||
PropertyPaneTextField
|
||||
} from '@microsoft/sp-webpart-base';
|
||||
|
||||
import * as strings from 'EventsWebPartStrings';
|
||||
import { Events } from './components';
|
||||
import { IEventsProps } from './components/IEventsProps';
|
||||
|
||||
import { sp } from '@pnp/sp';
|
||||
import { IDynamicDataController, IDynamicDataPropertyDefinition } from '@microsoft/sp-dynamic-data';
|
||||
import { IEvent, ILocation } from '../../data';
|
||||
|
||||
/**
|
||||
* Properties of the Events web part
|
||||
*/
|
||||
export interface IEventsWebPartProps {
|
||||
/**
|
||||
* Web part title
|
||||
*/
|
||||
title: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Events web part. Shows list of events retrieved from a SharePoint list
|
||||
*/
|
||||
export default class EventsWebPart extends BaseClientSideWebPart<IEventsWebPartProps> implements IDynamicDataController {
|
||||
/**
|
||||
* Currently selected event
|
||||
*/
|
||||
private _selectedEvent: IEvent;
|
||||
|
||||
/**
|
||||
* Event handler for selecting an event in the list
|
||||
*/
|
||||
private _eventSelected = (event: IEvent): void => {
|
||||
// store the currently selected event in the class variable. Required
|
||||
// so that connected component will be able to retrieve its value
|
||||
this._selectedEvent = event;
|
||||
// notify subscribers that the selected event has changed
|
||||
this.context.dynamicDataSourceManager.notifyPropertyChanged('event');
|
||||
// notify subscribers that the selected location has changed
|
||||
this.context.dynamicDataSourceManager.notifyPropertyChanged('location');
|
||||
}
|
||||
|
||||
protected onInit(): Promise<void> {
|
||||
// setup PnPjs context
|
||||
sp.setup({
|
||||
spfxContext: this.context
|
||||
});
|
||||
// register this web part as dynamic data source
|
||||
this.context.dynamicDataSourceManager.initializeSource(this);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of dynamic data properties that this dynamic data source
|
||||
* returns
|
||||
*/
|
||||
public getPropertyDefinitions(): ReadonlyArray<IDynamicDataPropertyDefinition> {
|
||||
return [
|
||||
{
|
||||
id: 'event',
|
||||
title: 'Event'
|
||||
},
|
||||
{
|
||||
id: 'location',
|
||||
title: 'Location'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current value of the specified dynamic data set
|
||||
* @param propertyId ID of the dynamic data set to retrieve the value for
|
||||
*/
|
||||
public getPropertyValue(propertyId: string): IEvent | ILocation {
|
||||
switch (propertyId) {
|
||||
case 'event':
|
||||
return this._selectedEvent;
|
||||
case 'location':
|
||||
return this._selectedEvent ? { city: this._selectedEvent.city, address: this._selectedEvent.address } : undefined;
|
||||
}
|
||||
|
||||
throw new Error('Bad property id');
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
const element: React.ReactElement<IEventsProps> = React.createElement(
|
||||
Events,
|
||||
{
|
||||
displayMode: this.displayMode,
|
||||
onEventSelected: this._eventSelected,
|
||||
title: this.properties.title,
|
||||
updateProperty: (value: string): void => {
|
||||
this.properties.title = value;
|
||||
},
|
||||
siteUrl: this.context.pageContext.web.serverRelativeUrl
|
||||
}
|
||||
);
|
||||
|
||||
ReactDom.render(element, this.domElement);
|
||||
}
|
||||
|
||||
protected onDispose(): void {
|
||||
ReactDom.unmountComponentAtNode(this.domElement);
|
||||
}
|
||||
|
||||
protected get dataVersion(): Version {
|
||||
return Version.parse('1.0');
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
return {
|
||||
pages: []
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
|
||||
|
||||
.events {
|
||||
.error {
|
||||
color: $ms-color-red;
|
||||
|
||||
.msg {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
import * as React from 'react';
|
||||
import styles from './Events.module.scss';
|
||||
import { IEventsProps, IEventsState } from '.';
|
||||
import { IEvent, IEventItem } from "../../../data";
|
||||
import { sp } from "@pnp/sp";
|
||||
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
|
||||
import { ListView, IViewField, SelectionMode, GroupOrder, IGrouping } from "@pnp/spfx-controls-react/lib/ListView";
|
||||
|
||||
/**
|
||||
* Events component
|
||||
*/
|
||||
export class Events extends React.Component<IEventsProps, IEventsState> {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// set default state
|
||||
this.state = {
|
||||
loading: false,
|
||||
events: [],
|
||||
error: undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for selecting an event in the list
|
||||
*/
|
||||
private _getSelection = (event: any[]): void => {
|
||||
// since the list allows selecting only one item, pick the first selected
|
||||
// event and pass to the event handler specified through component
|
||||
// properties
|
||||
this.props.onEventSelected(event[0]);
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
// indicate that the component is loading data
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
|
||||
// load information about events from the SharePoint list
|
||||
sp.web
|
||||
.getList(`${this.props.siteUrl}/Lists/CompanyEvents`)
|
||||
.items.getAll()
|
||||
.then((items: IEventItem[]): void => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
events: items.map(i => {
|
||||
return {
|
||||
date: i.PnPEventDate,
|
||||
name: i.Title,
|
||||
city: i.PnPCity,
|
||||
address: i.PnPAddress,
|
||||
organizerName: i.PnPOrganizerName,
|
||||
organizerEmail: i.PnPOrganizerEmail
|
||||
};
|
||||
})
|
||||
});
|
||||
}, (error: any): void => {
|
||||
// communicate error
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: error
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IEventsProps> {
|
||||
const { loading, error, events } = this.state;
|
||||
const { displayMode, title, updateProperty } = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.events}>
|
||||
<WebPartTitle displayMode={displayMode}
|
||||
title={title}
|
||||
updateProperty={updateProperty} />
|
||||
{loading &&
|
||||
<Spinner size={SpinnerSize.large} label='Loading events...' />}
|
||||
{!loading &&
|
||||
error &&
|
||||
<div className={styles.error}>The following error occurred while loading events: <span className={styles.msg}>{error}</span></div>}
|
||||
{!loading &&
|
||||
!error &&
|
||||
events.length === 0 &&
|
||||
<div className={styles.info}>No events found</div>}
|
||||
{!loading &&
|
||||
events.length > 0 &&
|
||||
<ListView
|
||||
items={events}
|
||||
viewFields={[
|
||||
{
|
||||
name: 'name',
|
||||
displayName: 'Event',
|
||||
sorting: true
|
||||
},
|
||||
{
|
||||
name: 'city',
|
||||
displayName: 'Location',
|
||||
sorting: true,
|
||||
minWidth: 100
|
||||
}
|
||||
]}
|
||||
compact={true}
|
||||
selectionMode={SelectionMode.single}
|
||||
selection={this._getSelection} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import { WebPartContext } from "@microsoft/sp-webpart-base";
|
||||
import { DisplayMode } from "@microsoft/sp-core-library";
|
||||
import { IEvent } from "../../../data";
|
||||
|
||||
/**
|
||||
* Events components properties
|
||||
*/
|
||||
export interface IEventsProps {
|
||||
/**
|
||||
* Web part display mode. Used for inline editing of the web part title
|
||||
*/
|
||||
displayMode: DisplayMode;
|
||||
/**
|
||||
* Event handler for selecting an event in the list
|
||||
*/
|
||||
onEventSelected: (event: IEvent) => void;
|
||||
/**
|
||||
* The absolute URL of the current web
|
||||
*/
|
||||
siteUrl: string;
|
||||
/**
|
||||
* The title of the web part
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* Event handler after updating the web part title
|
||||
*/
|
||||
updateProperty: (value: string) => void;
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import { IEvent } from "../../../data";
|
||||
|
||||
/**
|
||||
* Events component state
|
||||
*/
|
||||
export interface IEventsState {
|
||||
/**
|
||||
* Error message that occurred while loading the data
|
||||
*/
|
||||
error: string;
|
||||
/**
|
||||
* The list of events
|
||||
*/
|
||||
events: IEvent[];
|
||||
/**
|
||||
* Indicates if the component is currently loading data or not
|
||||
*/
|
||||
loading: boolean;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export * from './Events';
|
||||
export * from './IEventsProps';
|
||||
export * from './IEventsState';
|
|
@ -0,0 +1,4 @@
|
|||
define([], function() {
|
||||
return {
|
||||
}
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
declare interface IEventsWebPartStrings {
|
||||
}
|
||||
|
||||
declare module 'EventsWebPartStrings' {
|
||||
const strings: IEventsWebPartStrings;
|
||||
export = strings;
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "44181a37-27e5-4802-8f0b-4f8766792ede",
|
||||
"alias": "MapWebPart",
|
||||
"componentType": "WebPart",
|
||||
|
||||
// The "*" signifies that the version should be taken from the package.json
|
||||
"version": "*",
|
||||
"manifestVersion": 2,
|
||||
|
||||
// If true, the component can only be installed on sites where Custom Script is allowed.
|
||||
// Components that allow authors to embed arbitrary script code should set this to true.
|
||||
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
|
||||
"requiresCustomScript": false,
|
||||
|
||||
"preconfiguredEntries": [{
|
||||
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
|
||||
"group": { "default": "Other" },
|
||||
"title": { "default": "Map" },
|
||||
"description": { "default": "Shows the specified location on a map" },
|
||||
"officeFabricIconFontName": "Globe2",
|
||||
"properties": {
|
||||
"address": "",
|
||||
"bingMapsApiKey": "",
|
||||
"title": ""
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,255 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { Version } from '@microsoft/sp-core-library';
|
||||
import {
|
||||
BaseClientSideWebPart,
|
||||
IPropertyPaneConfiguration,
|
||||
PropertyPaneTextField,
|
||||
PropertyPaneDropdown,
|
||||
IPropertyPaneDropdownOption,
|
||||
PropertyPaneLabel,
|
||||
PropertyPaneLink
|
||||
} from '@microsoft/sp-webpart-base';
|
||||
|
||||
import * as strings from 'MapWebPartStrings';
|
||||
import { Map, IMapProps } from './components';
|
||||
import { IDynamicDataSource } from '@microsoft/sp-dynamic-data';
|
||||
import { ILocation } from '../../data';
|
||||
|
||||
/**
|
||||
* Map web part properties
|
||||
*/
|
||||
export interface IMapWebPartProps {
|
||||
/**
|
||||
* The address to display on the map
|
||||
*/
|
||||
address: string;
|
||||
/**
|
||||
* Bing maps API key to use with the Bing maps API
|
||||
*/
|
||||
bingMapsApiKey: string;
|
||||
/**
|
||||
* The ID of the dynamic data to which the web part is subscribed
|
||||
*/
|
||||
propertyId: string;
|
||||
/**
|
||||
* The dynamic data source ID to which the web part is subscribed
|
||||
*/
|
||||
sourceId: string;
|
||||
/**
|
||||
* Web part title
|
||||
*/
|
||||
title: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map web part. Shows the map of the specified location. The location can be
|
||||
* specified either directly in the web part properties or via a dynamic data
|
||||
* source connection.
|
||||
*/
|
||||
export default class MapWebPart extends BaseClientSideWebPart<IMapWebPartProps> {
|
||||
/**
|
||||
* The previous ID of the dynamic data source to which the web part is
|
||||
* subscribed. Used to unsubscribe from previously registered dynamic data
|
||||
* source notifications after changing web part configuration in the property
|
||||
* pane.
|
||||
*/
|
||||
private _lastSourceId: string;
|
||||
/**
|
||||
* The previous ID of the dynamic data to which the web part is subscribed.
|
||||
* Used to unsubscribe from previously registered dynamic data source
|
||||
* notifications after changing web part configuration in the property pane.
|
||||
*/
|
||||
private _lastPropertyId: string;
|
||||
|
||||
/**
|
||||
* Event handler for clicking the Configure button on the Placeholder
|
||||
*/
|
||||
private _onConfigure = (): void => {
|
||||
this.context.propertyPane.open();
|
||||
}
|
||||
|
||||
protected onInit(): Promise<void> {
|
||||
// bind render method to the current instance so that it can be correctly
|
||||
// invoked when dynamic data change notification is triggered
|
||||
this.render = this.render.bind(this);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
// subscribe to dynamic data changes notifications
|
||||
// do this only once the first time the web part is rendered and only,
|
||||
// if the dynamic data source ID and property ID are provided
|
||||
if (this.renderedOnce === false) {
|
||||
if (this.properties.sourceId && this.properties.propertyId) {
|
||||
try {
|
||||
this.context.dynamicDataProvider.registerPropertyChanged(this.properties.sourceId, this.properties.propertyId, this.render);
|
||||
this._lastSourceId = this.properties.sourceId;
|
||||
this._lastPropertyId = this.properties.propertyId;
|
||||
}
|
||||
catch (e) {
|
||||
this.context.statusRenderer.renderError(this.domElement, `${strings.ErrorText}${e}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const dynamicAddress: boolean = !!this.properties.sourceId;
|
||||
|
||||
const needsConfiguration: boolean = !this.properties.bingMapsApiKey ||
|
||||
(!dynamicAddress && !this.properties.address) ||
|
||||
(dynamicAddress && !this.properties.propertyId);
|
||||
|
||||
let address: string = dynamicAddress ? undefined : this.properties.address;
|
||||
|
||||
// if the web part is set to retrieve its address from a dynamic data source
|
||||
// and the dynamic data source has been configured, try to retrieve the
|
||||
// currently selected location
|
||||
if (!needsConfiguration && dynamicAddress) {
|
||||
const source: IDynamicDataSource = this.context.dynamicDataProvider.tryGetSource(this.properties.sourceId);
|
||||
const location: ILocation = source ? source.getPropertyValue(this.properties.propertyId) : undefined;
|
||||
if (location) {
|
||||
address = `${location.address} ${location.city}`;
|
||||
}
|
||||
}
|
||||
|
||||
const element: React.ReactElement<IMapProps> = React.createElement(
|
||||
Map,
|
||||
{
|
||||
needsConfiguration: needsConfiguration,
|
||||
httpClient: this.context.httpClient,
|
||||
bingMapsApiKey: this.properties.bingMapsApiKey,
|
||||
dynamicAddress: dynamicAddress,
|
||||
address: address,
|
||||
onConfigure: this._onConfigure,
|
||||
width: this.domElement.clientWidth,
|
||||
height: this.domElement.clientHeight,
|
||||
title: this.properties.title,
|
||||
displayMode: this.displayMode,
|
||||
updateProperty: (value: string): void => {
|
||||
this.properties.title = value;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
ReactDom.render(element, this.domElement);
|
||||
}
|
||||
|
||||
protected get dataVersion(): Version {
|
||||
return Version.parse('1.0');
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
// get all available dynamic data sources on the page
|
||||
const sourceOptions: IPropertyPaneDropdownOption[] =
|
||||
this.context.dynamicDataProvider.getAvailableSources().map(source => {
|
||||
return {
|
||||
key: source.id,
|
||||
text: source.metadata.title
|
||||
};
|
||||
});
|
||||
// add an extra option on top to indicate, that the web part should get
|
||||
// its address information from the web part configuration rather than from
|
||||
// a dynamic data source
|
||||
sourceOptions.unshift({
|
||||
key: '',
|
||||
text: 'Web part configuration'
|
||||
});
|
||||
const selectedSource: string = this.properties.sourceId;
|
||||
|
||||
let propertyOptions: IPropertyPaneDropdownOption[] = [];
|
||||
if (selectedSource) {
|
||||
const source: IDynamicDataSource = this.context.dynamicDataProvider.tryGetSource(selectedSource);
|
||||
if (source) {
|
||||
// get the list of all properties exposed by the currently selected
|
||||
// data source
|
||||
propertyOptions = source.getPropertyDefinitions().map(prop => {
|
||||
return {
|
||||
key: prop.id,
|
||||
text: prop.title
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pages: [
|
||||
{
|
||||
groups: [
|
||||
{
|
||||
groupName: strings.BingMapsGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneTextField('bingMapsApiKey', {
|
||||
label: strings.BingMapsApiKeyFieldLabel
|
||||
}),
|
||||
PropertyPaneLink('', {
|
||||
href: 'https://www.bingmapsportal.com/',
|
||||
text: strings.GetBingMapsApiKeyLinkText,
|
||||
target: '_blank'
|
||||
})
|
||||
]
|
||||
},
|
||||
{
|
||||
groupName: strings.DataGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneTextField('address', {
|
||||
label: strings.AddressFieldLabel
|
||||
})
|
||||
]
|
||||
},
|
||||
{
|
||||
groupName: strings.ConnectionGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneDropdown('sourceId', {
|
||||
label: strings.SourceIdFieldLabel,
|
||||
options: sourceOptions,
|
||||
selectedKey: this.properties.sourceId
|
||||
}),
|
||||
PropertyPaneDropdown('propertyId', {
|
||||
label: strings.PropertyIdFieldLabel,
|
||||
options: propertyOptions,
|
||||
selectedKey: this.properties.propertyId
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
protected onPropertyPaneFieldChanged(propertyPath: string): void {
|
||||
if (!this.properties.sourceId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (propertyPath === 'sourceId') {
|
||||
// reset the selected property ID after selecting a different dynamic
|
||||
// data source
|
||||
this.properties.propertyId =
|
||||
this.context.dynamicDataProvider.tryGetSource(this.properties.sourceId).getPropertyDefinitions()[0].id;
|
||||
}
|
||||
|
||||
if (this._lastSourceId && this._lastPropertyId) {
|
||||
// unsubscribe from the previously registered dynamic data changes
|
||||
// notifications
|
||||
this.context.dynamicDataProvider.unregisterPropertyChanged(this._lastSourceId, this._lastPropertyId, this.render);
|
||||
}
|
||||
|
||||
// subscribe to the newly configured dynamic data changes notifications
|
||||
this.context.dynamicDataProvider.registerPropertyChanged(this.properties.sourceId, this.properties.propertyId, this.render);
|
||||
// store current values for the dynamic data source ID and property ID
|
||||
// so that the web part can unsubscribe from notifications when the
|
||||
// web part configuration changes
|
||||
this._lastSourceId = this.properties.sourceId;
|
||||
this._lastPropertyId = this.properties.propertyId;
|
||||
}
|
||||
|
||||
protected get disableReactivePropertyChanges(): boolean {
|
||||
// set property changes mode to reactive, so that the Bing Maps API is not
|
||||
// called on each keystroke when typing in the address to show on the map
|
||||
// in web part properties
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import { HttpClient } from "@microsoft/sp-http";
|
||||
import { DisplayMode } from "@microsoft/sp-core-library";
|
||||
|
||||
/**
|
||||
* Map component properties
|
||||
*/
|
||||
export interface IMapProps {
|
||||
/**
|
||||
* The address to show on the map
|
||||
*/
|
||||
address: string;
|
||||
/**
|
||||
* The Bing maps API key to use when communicating with the Bing maps API
|
||||
*/
|
||||
bingMapsApiKey: string;
|
||||
/**
|
||||
* Web part display mode. Used for inline editing of the web part title
|
||||
*/
|
||||
displayMode: DisplayMode;
|
||||
/**
|
||||
* Indicates if the component retrieves its address from a dynamic data source
|
||||
* or not. Used to display different message to the user if no address data
|
||||
* is available.
|
||||
*/
|
||||
dynamicAddress: boolean;
|
||||
/**
|
||||
* Web part height. Used to render the map
|
||||
*/
|
||||
height: number;
|
||||
/**
|
||||
* Instance of the HttpClient. Used to communicate with the Bing maps API to
|
||||
* get coordinates for the specified address
|
||||
*/
|
||||
httpClient: HttpClient;
|
||||
/**
|
||||
* Determines if the web part has been connected to a dynamic data source or
|
||||
* not
|
||||
*/
|
||||
needsConfiguration: boolean;
|
||||
/**
|
||||
* Event handler for clicking the Configure button on the Placeholder
|
||||
*/
|
||||
onConfigure: () => void;
|
||||
/**
|
||||
* The title of the web part
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* Event handler after updating the web part title
|
||||
*/
|
||||
updateProperty: (value: string) => void;
|
||||
/**
|
||||
* Web part width. Used to render the map
|
||||
*/
|
||||
width: number;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* Map component state
|
||||
*/
|
||||
export interface IMapState {
|
||||
/**
|
||||
* Coordinates for the specified address
|
||||
*/
|
||||
coordinates: number[];
|
||||
/**
|
||||
* Error message that occurred while loading the data
|
||||
*/
|
||||
error: string;
|
||||
/**
|
||||
* Indicates if the component is currently loading data or not
|
||||
*/
|
||||
loading: boolean;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
|
||||
|
||||
.map {
|
||||
color: inherit;
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
import * as React from 'react';
|
||||
import styles from './Map.module.scss';
|
||||
import { IMapProps, IMapState } from './';
|
||||
import { HttpClient } from '@microsoft/sp-http';
|
||||
import { Placeholder } from '@pnp/spfx-controls-react/lib/Placeholder';
|
||||
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
|
||||
import { WebPartTitle } from '@pnp/spfx-controls-react/lib/WebPartTitle';
|
||||
|
||||
/**
|
||||
* Map component. Renders map for the specified location
|
||||
*/
|
||||
export class Map extends React.Component<IMapProps, IMapState> {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// set default state
|
||||
this.state = {
|
||||
error: undefined,
|
||||
loading: false,
|
||||
coordinates: []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves coordinates for the address set through component's properties
|
||||
*/
|
||||
private _resolveCoordinates(): void {
|
||||
// nothing to do if the parent web part hasn't been configured
|
||||
if (this.props.needsConfiguration) {
|
||||
return;
|
||||
}
|
||||
|
||||
// indicate that the component will be loading its data
|
||||
this.setState({
|
||||
error: undefined,
|
||||
loading: true,
|
||||
coordinates: []
|
||||
});
|
||||
|
||||
// get coordinates for the address specified through component's properties
|
||||
this.props.httpClient
|
||||
.get(`https://dev.virtualearth.net/REST/v1/Locations?q=${this.props.address}&key=${this.props.bingMapsApiKey}`, HttpClient.configurations.v1)
|
||||
.then((res) => {
|
||||
return res.json();
|
||||
})
|
||||
.then((data): void => {
|
||||
if (data &&
|
||||
data.statusCode === 200) {
|
||||
// store coordinates and indicate that loading data is finished
|
||||
this.setState({
|
||||
loading: false,
|
||||
coordinates: data.resourceSets[0].resources[0].point.coordinates
|
||||
});
|
||||
}
|
||||
else {
|
||||
// communicate error from Bing maps API
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: data.statusDescription
|
||||
});
|
||||
}
|
||||
}, (error: any): void => {
|
||||
// communicate generic errors
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: error
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public componentWillMount(): void {
|
||||
// get coordinates for the current address after the component has been
|
||||
// instantiated
|
||||
this._resolveCoordinates();
|
||||
}
|
||||
|
||||
public componentDidUpdate?(prevProps: IMapProps, prevState: IMapState, prevContext: any): void {
|
||||
if (this.props.address !== prevProps.address) {
|
||||
// get coordinates for the new address
|
||||
this._resolveCoordinates();
|
||||
}
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IMapProps> {
|
||||
const { needsConfiguration, address, onConfigure, dynamicAddress, width, height, displayMode, title, updateProperty } = this.props;
|
||||
const { coordinates, loading } = this.state;
|
||||
|
||||
return (
|
||||
<div className={styles.map}>
|
||||
<WebPartTitle displayMode={displayMode}
|
||||
title={title}
|
||||
updateProperty={updateProperty} />
|
||||
{needsConfiguration &&
|
||||
<Placeholder
|
||||
iconName='Edit'
|
||||
iconText='Configure your web part'
|
||||
description='Please configure the web part.'
|
||||
buttonLabel='Configure'
|
||||
onConfigure={onConfigure} />
|
||||
}
|
||||
{!needsConfiguration &&
|
||||
!loading &&
|
||||
!address &&
|
||||
dynamicAddress &&
|
||||
<Placeholder
|
||||
iconName='Globe2'
|
||||
iconText='Map'
|
||||
description='Select a location' />
|
||||
}
|
||||
{!needsConfiguration &&
|
||||
loading &&
|
||||
<Spinner size={SpinnerSize.large} label='Loading map...' />}
|
||||
{!needsConfiguration &&
|
||||
address &&
|
||||
coordinates.length > 0 &&
|
||||
<iframe width={width} height={height} frameBorder="0" src={`https://www.bing.com/maps/embed?h=400&w=500&cp=${coordinates[0]}~${coordinates[1]}&lvl=15&typ=d&sty=r&src=SHELL&FORM=MBEDV8`} scrolling="no">
|
||||
</iframe>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from './IMapProps';
|
||||
export * from './IMapState';
|
||||
export * from './Map';
|
|
@ -0,0 +1,14 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"AddressFieldLabel": "Address",
|
||||
"BingMapsApiKeyFieldLabel": "Bing Maps API Key",
|
||||
"BingMapsGroupName": "Bing Maps configuration",
|
||||
"ConnectionGroupName": "Connection",
|
||||
"DataGroupName": "Data",
|
||||
"ErrorText": "An error has occurred while connecting to the data source. Details: ",
|
||||
"GetBingMapsApiKeyLinkText": "Get Bing Maps API key",
|
||||
"SourceIdFieldLabel": "Data source",
|
||||
"PropertyIdFieldLabel": "Data property",
|
||||
"PropertyPaneDescription": "Web part configuration"
|
||||
}
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
declare interface IMapWebPartStrings {
|
||||
AddressFieldLabel: string;
|
||||
BingMapsApiKeyFieldLabel: string;
|
||||
BingMapsGroupName: string;
|
||||
ConnectionGroupName: string;
|
||||
DataGroupName: string;
|
||||
ErrorText: string;
|
||||
GetBingMapsApiKeyLinkText: string;
|
||||
PropertyIdFieldLabel: string;
|
||||
PropertyPaneDescription: string;
|
||||
SourceIdFieldLabel: string;
|
||||
}
|
||||
|
||||
declare module 'MapWebPartStrings' {
|
||||
const strings: IMapWebPartStrings;
|
||||
export = strings;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"./node_modules/@microsoft"
|
||||
],
|
||||
"types": [
|
||||
"es6-promise",
|
||||
"webpack-env"
|
||||
],
|
||||
"lib": [
|
||||
"es5",
|
||||
"dom",
|
||||
"es2015.collection"
|
||||
]
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue