Updated Dynamic Data sample to SPFx@1.7.0 (#669)

This commit is contained in:
Waldek Mastykarz 2018-11-09 08:36:29 +01:00 committed by Vesa Juvonen
parent 67a3279ef7
commit 0f78f7241b
22 changed files with 8055 additions and 8942 deletions

View File

@ -0,0 +1 @@
.npmrc

View File

@ -0,0 +1,11 @@
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.7.0",
"libraryName": "react-events-dynamicdata",
"libraryId": "dcd4558f-8ea7-4f77-b494-c2b8a4d51b1d",
"packageManager": "npm",
"componentType": "webpart"
}
}

View File

@ -5,7 +5,7 @@ Sample web parts illustrating using the SharePoint Framework Dynamic data capabi
![Web parts placed on a modern SharePoint page showing information about events](./assets/dynamic-data-webparts.png) ![Web parts placed on a modern SharePoint page showing information about events](./assets/dynamic-data-webparts.png)
## Used SharePoint Framework Version ## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-1.5.0--plusbeta-blue.svg) ![drop](https://img.shields.io/badge/drop-1.7.0-green.svg)
## Applies to ## Applies to
@ -22,6 +22,7 @@ react-events-dynamicdata|Waldek Mastykarz (MVP, Rencore, @waldekm)
Version|Date|Comments Version|Date|Comments
-------|----|-------- -------|----|--------
1.1|November 9, 2018|Updated sample to SPFx v1.7.0
1.0|June 5, 2018|Initial release 1.0|June 5, 2018|Initial release
## Disclaimer ## Disclaimer
@ -43,12 +44,14 @@ Version|Date|Comments
* edit a page * edit a page
* add the three web parts named: _Events_, _Event details_ and _Map_ * add the three web parts named: _Events_, _Event details_ and _Map_
* configure the _Event details_ web part: * configure the _Event details_ web part:
* as _Data source_, choose the _Events_ option * as _Connect to source_, choose the _Events_ option
* as _Data property_, choose the _Event_ option * as _Event's properties_, choose the _Event_ option
* configure the _Map_ web part: * configure the _Map_ web part:
* get a Bing maps API key (follow the link in the web part) * get a Bing maps API key (follow the link in the web part)
* as _Data source_, choose the _Events_ option * as _Connect to source_, choose the _Events_ option
* as _Data property_, choose the _Location_ option * as _Event's properties_, choose the _Location_ option
* as _Address_, choose the _address_ option
* as _City_, choose the _city_ option
## Features ## Features
@ -59,7 +62,6 @@ Web parts in this solution illustrate the following concepts on top of the Share
* making web part a dynamic data source * making web part a dynamic data source
* exposing multiple data properties from a single data source * exposing multiple data properties from a single data source
* subscribing to dynamic data source notifications from a web part * 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 * deploying list instances from a SharePoint Framework solution package
* using [PnPjs](https://github.com/pnp/pnpjs) to retrieve data from a SharePoint list * 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 * using [SharePoint Framework React Controls](https://github.com/SharePoint/sp-dev-fx-controls-react) in web parts

View File

@ -1,45 +0,0 @@
{
"$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
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -11,27 +11,27 @@
"test": "gulp test" "test": "gulp test"
}, },
"dependencies": { "dependencies": {
"@microsoft/sp-core-library": "1.5.0-plusbeta", "@microsoft/sp-core-library": "1.7.0",
"@microsoft/sp-lodash-subset": "1.5.0-plusbeta", "@microsoft/sp-lodash-subset": "1.7.0",
"@microsoft/sp-office-ui-fabric-core": "1.5.0-plusbeta", "@microsoft/sp-office-ui-fabric-core": "1.7.0",
"@microsoft/sp-webpart-base": "1.5.0-plusbeta", "@microsoft/sp-webpart-base": "1.7.0",
"@pnp/common": "^1.1.0", "@pnp/common": "^1.1.0",
"@pnp/logging": "^1.1.0", "@pnp/logging": "^1.1.0",
"@pnp/odata": "^1.1.0", "@pnp/odata": "^1.1.0",
"@pnp/sp": "^1.1.0", "@pnp/sp": "^1.1.0",
"@pnp/spfx-controls-react": "^1.4.0", "@pnp/spfx-controls-react": "^1.4.0",
"@types/es6-promise": "0.0.33", "@types/es6-promise": "0.0.33",
"@types/react": "15.6.6", "@types/react": "16.4.2",
"@types/react-dom": "15.5.6", "@types/react-dom": "16.0.5",
"@types/webpack-env": "1.13.1", "@types/webpack-env": "1.13.1",
"react": "15.6.2", "react": "16.3.2",
"react-dom": "15.6.2" "react-dom": "16.3.2"
}, },
"devDependencies": { "devDependencies": {
"@microsoft/generator-sharepoint": "1.5.0", "@microsoft/sp-build-web": "1.7.0",
"@microsoft/sp-build-web": "1.5.0-plusbeta", "@microsoft/sp-module-interfaces": "1.7.0",
"@microsoft/sp-module-interfaces": "1.5.0-plusbeta", "@microsoft/sp-webpart-workbench": "1.7.0",
"@microsoft/sp-webpart-workbench": "1.5.0-plusbeta", "@microsoft/sp-tslint-rules": "1.7.0",
"@types/chai": "3.4.34", "@types/chai": "3.4.34",
"@types/mocha": "2.2.38", "@types/mocha": "2.2.38",
"ajv": "~5.2.2", "ajv": "~5.2.2",

View File

@ -0,0 +1 @@
// A file is required to be in the root of the /src directory by the TypeScript compiler

View File

@ -20,6 +20,7 @@
"description": { "default": "Shows details of the selected event" }, "description": { "default": "Shows details of the selected event" },
"officeFabricIconFontName": "CustomList", "officeFabricIconFontName": "CustomList",
"properties": { "properties": {
"event": {},
"title": "Event details" "title": "Event details"
} }
}] }]

View File

@ -4,28 +4,24 @@ import { Version } from '@microsoft/sp-core-library';
import { import {
BaseClientSideWebPart, BaseClientSideWebPart,
IPropertyPaneConfiguration, IPropertyPaneConfiguration,
PropertyPaneTextField, IWebPartPropertiesMetadata,
IPropertyPaneDropdownOption, PropertyPaneDynamicFieldSet,
PropertyPaneDropdown PropertyPaneDynamicField
} from '@microsoft/sp-webpart-base'; } from '@microsoft/sp-webpart-base';
import * as strings from 'EventDetailsWebPartStrings'; import * as strings from 'EventDetailsWebPartStrings';
import { EventDetails, IEventDetailsProps } from './components'; import { EventDetails, IEventDetailsProps } from './components';
import { IDynamicDataSource } from '@microsoft/sp-dynamic-data';
import { IEvent } from '../../data'; import { IEvent } from '../../data';
import { DynamicProperty } from '@microsoft/sp-component-base';
/** /**
* Represents properties of the Event details web part * Represents properties of the Event details web part
*/ */
export interface IEventDetailsWebPartProps { export interface IEventDetailsWebPartProps {
/** /**
* The ID of the dynamic data to which the web part is subscribed * Event to show the details for
*/ */
propertyId: string; event: DynamicProperty<IEvent>;
/**
* The dynamic data source ID to which the web part is subscribed
*/
sourceId: string;
/** /**
* Web part title * Web part title
*/ */
@ -37,20 +33,6 @@ export interface IEventDetailsWebPartProps {
* from the connected dynamic data source. * from the connected dynamic data source.
*/ */
export default class EventDetailsWebPart extends BaseClientSideWebPart<IEventDetailsWebPartProps> { 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 * Event handler for clicking the Configure button on the Placeholder
*/ */
@ -58,48 +40,14 @@ export default class EventDetailsWebPart extends BaseClientSideWebPart<IEventDet
this.context.propertyPane.open(); 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 { public render(): void {
let event: IEvent = undefined; const needsConfiguration: boolean = !this.properties.event.tryGetSource();
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( const element: React.ReactElement<IEventDetailsProps> = React.createElement(
EventDetails, EventDetails,
{ {
needsConfiguration: needsConfiguration, needsConfiguration: needsConfiguration,
event: event, event: this.properties.event,
onConfigure: this._onConfigure, onConfigure: this._onConfigure,
displayMode: this.displayMode, displayMode: this.displayMode,
title: this.properties.title, title: this.properties.title,
@ -116,47 +64,28 @@ export default class EventDetailsWebPart extends BaseClientSideWebPart<IEventDet
return Version.parse('1.0'); return Version.parse('1.0');
} }
protected get propertiesMetadata(): IWebPartPropertiesMetadata {
return {
'event': {
dynamicPropertyType: 'object'
}
};
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { 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 { return {
pages: [ pages: [
{ {
groups: [ groups: [
{ {
groupFields: [ groupFields: [
PropertyPaneDropdown('sourceId', { PropertyPaneDynamicFieldSet({
label: strings.SourceIdFieldLabel, label: 'Select event source',
options: sourceOptions, fields: [
selectedKey: this.properties.sourceId PropertyPaneDynamicField('event', {
}), label: 'Event source'
PropertyPaneDropdown('propertyId', { })
label: strings.PropertyIdFieldLabel, ]
options: propertyOptions,
selectedKey: this.properties.propertyId
}) })
] ]
} }
@ -165,27 +94,4 @@ export default class EventDetailsWebPart extends BaseClientSideWebPart<IEventDet
] ]
}; };
} }
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;
}
} }

View File

@ -4,12 +4,13 @@ import { IEventDetailsProps } from './IEventDetailsProps';
import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder"; import { Placeholder } from "@pnp/spfx-controls-react/lib/Placeholder";
import { Label } from 'office-ui-fabric-react/lib/Label'; import { Label } from 'office-ui-fabric-react/lib/Label';
import { Icon } from 'office-ui-fabric-react/lib/Icon'; import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { IEvent } from '../../../data';
import { WebPartTitle } from '@pnp/spfx-controls-react/lib/WebPartTitle'; import { WebPartTitle } from '@pnp/spfx-controls-react/lib/WebPartTitle';
import { IEvent } from '../../../data';
export class EventDetails extends React.Component<IEventDetailsProps, {}> { export class EventDetails extends React.Component<IEventDetailsProps, {}> {
public render(): React.ReactElement<IEventDetailsProps> { public render(): React.ReactElement<IEventDetailsProps> {
const { needsConfiguration, event, onConfigure, displayMode, title, updateProperty } = this.props; const { needsConfiguration, event, onConfigure, displayMode, title, updateProperty } = this.props;
const eventData: IEvent | undefined = event.tryGetValue();
return ( return (
<div className={styles.eventDetails}> <div className={styles.eventDetails}>
@ -24,8 +25,8 @@ export class EventDetails extends React.Component<IEventDetailsProps, {}> {
buttonLabel='Configure' buttonLabel='Configure'
onConfigure={onConfigure} />} onConfigure={onConfigure} />}
{!needsConfiguration && {!needsConfiguration &&
event && eventData &&
(!event.name || !event.address) && (!eventData.name || !eventData.address) &&
<Placeholder <Placeholder
iconName='Edit' iconName='Edit'
iconText='Configure your web part' iconText='Configure your web part'
@ -33,19 +34,19 @@ export class EventDetails extends React.Component<IEventDetailsProps, {}> {
buttonLabel='Configure' buttonLabel='Configure'
onConfigure={onConfigure} />} onConfigure={onConfigure} />}
{!needsConfiguration && {!needsConfiguration &&
!event && !eventData &&
<Placeholder <Placeholder
iconName='CustomList' iconName='CustomList'
iconText='Event details' iconText='Event details'
description='Select an event' />} description='Select an event' />}
{!needsConfiguration && {!needsConfiguration &&
event && eventData &&
<ul> <ul>
<li><Label>Event name</Label> {event.name}</li> <li><Label>Event name</Label> {eventData.name}</li>
<li><Label>City</Label> {event.city}</li> <li><Label>City</Label> {eventData.city}</li>
<li><Label>Address</Label> {event.address}</li> <li><Label>Address</Label> {eventData.address}</li>
<li><Label>Organizer</Label> <Icon iconName='Mail' /> <a href={`mailto:${event.organizerEmail}`}>{event.organizerName}</a></li> <li><Label>Organizer</Label> <Icon iconName='Mail' /> <a href={`mailto:${eventData.organizerEmail}`}>{eventData.organizerName}</a></li>
<li><Label>Date</Label> {new Date(event.date).toLocaleDateString()}</li> <li><Label>Date</Label> {new Date(eventData.date).toLocaleDateString()}</li>
</ul>} </ul>}
</div> </div>
); );

View File

@ -1,4 +1,5 @@
import { DisplayMode } from "@microsoft/sp-core-library"; import { DisplayMode } from "@microsoft/sp-core-library";
import { DynamicProperty } from "@microsoft/sp-component-base";
import { IEvent } from "../../../data"; import { IEvent } from "../../../data";
/** /**
@ -12,7 +13,7 @@ export interface IEventDetailsProps {
/** /**
* The currently selected event * The currently selected event
*/ */
event: IEvent; event: DynamicProperty<IEvent>;
/** /**
* Determines if the web part has been connected to a dynamic data source or * Determines if the web part has been connected to a dynamic data source or
* not * not

View File

@ -20,6 +20,7 @@
"description": { "default": "Shows upcoming events" }, "description": { "default": "Shows upcoming events" },
"officeFabricIconFontName": "Calendar", "officeFabricIconFontName": "Calendar",
"properties": { "properties": {
"event": "",
"title": "Upcoming events" "title": "Upcoming events"
} }
}] }]

View File

@ -3,8 +3,7 @@ import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library'; import { Version } from '@microsoft/sp-core-library';
import { import {
BaseClientSideWebPart, BaseClientSideWebPart,
IPropertyPaneConfiguration, IPropertyPaneConfiguration
PropertyPaneTextField
} from '@microsoft/sp-webpart-base'; } from '@microsoft/sp-webpart-base';
import * as strings from 'EventsWebPartStrings'; import * as strings from 'EventsWebPartStrings';
@ -12,7 +11,7 @@ import { Events } from './components';
import { IEventsProps } from './components/IEventsProps'; import { IEventsProps } from './components/IEventsProps';
import { sp } from '@pnp/sp'; import { sp } from '@pnp/sp';
import { IDynamicDataController, IDynamicDataPropertyDefinition } from '@microsoft/sp-dynamic-data'; import { IDynamicDataPropertyDefinition, IDynamicDataCallables } from '@microsoft/sp-dynamic-data';
import { IEvent, ILocation } from '../../data'; import { IEvent, ILocation } from '../../data';
/** /**
@ -28,7 +27,7 @@ export interface IEventsWebPartProps {
/** /**
* Events web part. Shows list of events retrieved from a SharePoint list * Events web part. Shows list of events retrieved from a SharePoint list
*/ */
export default class EventsWebPart extends BaseClientSideWebPart<IEventsWebPartProps> implements IDynamicDataController { export default class EventsWebPart extends BaseClientSideWebPart<IEventsWebPartProps> implements IDynamicDataCallables {
/** /**
* Currently selected event * Currently selected event
*/ */

View File

@ -1,18 +1,18 @@
import * as React from 'react'; import * as React from 'react';
import styles from './Events.module.scss'; import styles from './Events.module.scss';
import { IEventsProps, IEventsState } from '.'; import { IEventsProps, IEventsState } from '.';
import { IEvent, IEventItem } from "../../../data"; import { IEventItem } from "../../../data";
import { sp } from "@pnp/sp"; import { sp } from "@pnp/sp";
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner'; import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle"; import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
import { ListView, IViewField, SelectionMode, GroupOrder, IGrouping } from "@pnp/spfx-controls-react/lib/ListView"; import { ListView, SelectionMode } from "@pnp/spfx-controls-react/lib/ListView";
/** /**
* Events component * Events component
*/ */
export class Events extends React.Component<IEventsProps, IEventsState> { export class Events extends React.Component<IEventsProps, IEventsState> {
constructor() { constructor(props: IEventsProps) {
super(); super(props);
// set default state // set default state
this.state = { this.state = {
@ -38,31 +38,60 @@ export class Events extends React.Component<IEventsProps, IEventsState> {
loading: true loading: true
}); });
this.setState({
loading: false,
events: [{
name: 'Tampa Home Show',
city: 'Tampa, FL',
address: '333 S Franklin St',
organizerName: 'Grady Archie',
organizerEmail: 'GradyA@contoso.OnMicrosoft.com',
date: '2018-05-29T00:00:00Z'
},
{
name: 'Custom Electronic Design and Installation Association (CEDIA)',
city: 'San Diego, CA',
address: '111 W Harbor Dr',
organizerName: 'Megan Bowen',
organizerEmail: 'MeganB@contoso.OnMicrosoft.com',
date: '2018-06-15T00:00:00Z'
},
{
name: 'Design Automation Conference (DAC)',
city: 'San Francisco, CA',
address: '747 Howard St Fl 5',
organizerName: 'Irvin Sayers',
organizerEmail: 'IrvinSB@contoso.OnMicrosoft.com',
date: '2018-07-05T00:00:00Z'
}],
error: undefined
});
// load information about events from the SharePoint list // load information about events from the SharePoint list
sp.web // sp.web
.getList(`${this.props.siteUrl}/Lists/CompanyEvents`) // .getList(`${this.props.siteUrl}/Lists/CompanyEvents`)
.items.getAll() // .items.getAll()
.then((items: IEventItem[]): void => { // .then((items: IEventItem[]): void => {
this.setState({ // this.setState({
loading: false, // loading: false,
events: items.map(i => { // events: items.map(i => {
return { // return {
date: i.PnPEventDate, // date: i.PnPEventDate,
name: i.Title, // name: i.Title,
city: i.PnPCity, // city: i.PnPCity,
address: i.PnPAddress, // address: i.PnPAddress,
organizerName: i.PnPOrganizerName, // organizerName: i.PnPOrganizerName,
organizerEmail: i.PnPOrganizerEmail // organizerEmail: i.PnPOrganizerEmail
}; // };
}) // })
}); // });
}, (error: any): void => { // }, (error: any): void => {
// communicate error // // communicate error
this.setState({ // this.setState({
loading: false, // loading: false,
error: error // error: error
}); // });
}); // });
} }
public render(): React.ReactElement<IEventsProps> { public render(): React.ReactElement<IEventsProps> {

View File

@ -1,6 +1,6 @@
{ {
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json", "$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "44181a37-27e5-4802-8f0b-4f8766792ede", "id": "44181a37-27e5-4802-8f0b-4f8766792eda",
"alias": "MapWebPart", "alias": "MapWebPart",
"componentType": "WebPart", "componentType": "WebPart",
@ -22,6 +22,7 @@
"properties": { "properties": {
"address": "", "address": "",
"bingMapsApiKey": "", "bingMapsApiKey": "",
"city": "",
"title": "" "title": ""
} }
}] }]

View File

@ -5,16 +5,17 @@ import {
BaseClientSideWebPart, BaseClientSideWebPart,
IPropertyPaneConfiguration, IPropertyPaneConfiguration,
PropertyPaneTextField, PropertyPaneTextField,
PropertyPaneDropdown, PropertyPaneLink,
IPropertyPaneDropdownOption, IPropertyPaneConditionalGroup,
PropertyPaneLabel, PropertyPaneDynamicFieldSet,
PropertyPaneLink PropertyPaneDynamicField,
IWebPartPropertiesMetadata,
DynamicDataSharedDepth
} from '@microsoft/sp-webpart-base'; } from '@microsoft/sp-webpart-base';
import * as strings from 'MapWebPartStrings'; import * as strings from 'MapWebPartStrings';
import { Map, IMapProps } from './components'; import { Map, IMapProps } from './components';
import { IDynamicDataSource } from '@microsoft/sp-dynamic-data'; import { DynamicProperty } from '@microsoft/sp-component-base';
import { ILocation } from '../../data';
/** /**
* Map web part properties * Map web part properties
@ -23,19 +24,15 @@ export interface IMapWebPartProps {
/** /**
* The address to display on the map * The address to display on the map
*/ */
address: string; address: DynamicProperty<string>;
/** /**
* Bing maps API key to use with the Bing maps API * Bing maps API key to use with the Bing maps API
*/ */
bingMapsApiKey: string; bingMapsApiKey: string;
/** /**
* The ID of the dynamic data to which the web part is subscribed * The city where the address is located
*/ */
propertyId: string; city: DynamicProperty<string>;
/**
* The dynamic data source ID to which the web part is subscribed
*/
sourceId: string;
/** /**
* Web part title * Web part title
*/ */
@ -48,20 +45,6 @@ export interface IMapWebPartProps {
* source connection. * source connection.
*/ */
export default class MapWebPart extends BaseClientSideWebPart<IMapWebPartProps> { 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 * Event handler for clicking the Configure button on the Placeholder
*/ */
@ -69,50 +52,14 @@ export default class MapWebPart extends BaseClientSideWebPart<IMapWebPartProps>
this.context.propertyPane.open(); 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 { public render(): void {
// subscribe to dynamic data changes notifications // Get the location to show on the map. The location will be retrieved
// do this only once the first time the web part is rendered and only, // either from the event selected in the connected data source or from the
// if the dynamic data source ID and property ID are provided // address entered in web part properties
if (this.renderedOnce === false) { const address: string | undefined = this.properties.address.tryGetValue();
if (this.properties.sourceId && this.properties.propertyId) { const city: string | undefined = this.properties.city.tryGetValue();
try { const needsConfiguration: boolean = !this.properties.bingMapsApiKey || (!address && !this.properties.address.tryGetSource()) ||
this.context.dynamicDataProvider.registerPropertyChanged(this.properties.sourceId, this.properties.propertyId, this.render); (!city && !this.properties.city.tryGetSource());
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( const element: React.ReactElement<IMapProps> = React.createElement(
Map, Map,
@ -120,8 +67,8 @@ export default class MapWebPart extends BaseClientSideWebPart<IMapWebPartProps>
needsConfiguration: needsConfiguration, needsConfiguration: needsConfiguration,
httpClient: this.context.httpClient, httpClient: this.context.httpClient,
bingMapsApiKey: this.properties.bingMapsApiKey, bingMapsApiKey: this.properties.bingMapsApiKey,
dynamicAddress: dynamicAddress, dynamicAddress: !!this.properties.address.tryGetSource(),
address: address, address: `${address} ${city}`,
onConfigure: this._onConfigure, onConfigure: this._onConfigure,
width: this.domElement.clientWidth, width: this.domElement.clientWidth,
height: this.domElement.clientHeight, height: this.domElement.clientHeight,
@ -140,39 +87,21 @@ export default class MapWebPart extends BaseClientSideWebPart<IMapWebPartProps>
return Version.parse('1.0'); return Version.parse('1.0');
} }
protected get propertiesMetadata(): IWebPartPropertiesMetadata {
return {
// Denote the address web part property as a dynamic property of type
// object to allow the address information to be serialized by
// the SharePoint Framework.
'address': {
dynamicPropertyType: 'string'
},
'city': {
dynamicPropertyType: 'string'
}
} as any as IWebPartPropertiesMetadata;
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { 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 { return {
pages: [ pages: [
{ {
@ -190,62 +119,55 @@ export default class MapWebPart extends BaseClientSideWebPart<IMapWebPartProps>
}) })
] ]
}, },
// Web part properties group for specifying the information about
// the address to show on the map.
{ {
// Primary group is used to provide the address to show on the map
// in a text field in the web part properties
primaryGroup: {
groupName: strings.DataGroupName, groupName: strings.DataGroupName,
groupFields: [ groupFields: [
PropertyPaneTextField('address', { PropertyPaneTextField('address', {
label: strings.AddressFieldLabel label: strings.AddressFieldLabel
}),
PropertyPaneTextField('city', {
label: strings.CityFieldLabel
}) })
] ]
}, },
{ // Secondary group is used to retrieve the address from the
groupName: strings.ConnectionGroupName, // connected dynamic data source
secondaryGroup: {
groupName: strings.DataGroupName,
groupFields: [ groupFields: [
PropertyPaneDropdown('sourceId', { PropertyPaneDynamicFieldSet({
label: strings.SourceIdFieldLabel, label: 'Address',
options: sourceOptions, fields: [
selectedKey: this.properties.sourceId PropertyPaneDynamicField('address', {
label: strings.AddressFieldLabel
}), }),
PropertyPaneDropdown('propertyId', { PropertyPaneDynamicField('city', {
label: strings.PropertyIdFieldLabel, label: strings.CityFieldLabel
options: propertyOptions, })
selectedKey: this.properties.propertyId ],
sharedConfiguration: {
// because address and city come from the same data source
// the connection can share the selected dynamic property
depth: DynamicDataSharedDepth.Property
}
}) })
] ]
} },
// Show the secondary group only if the web part has been
// connected to a dynamic data source
showSecondaryGroup: !!this.properties.address.tryGetSource()
} as IPropertyPaneConditionalGroup
] ]
} }
] ]
}; };
} }
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 { protected get disableReactivePropertyChanges(): boolean {
// set property changes mode to reactive, so that the Bing Maps API is not // 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 // called on each keystroke when typing in the address to show on the map

View File

@ -1,5 +1,7 @@
import { HttpClient } from "@microsoft/sp-http"; import { HttpClient } from "@microsoft/sp-http";
import { DisplayMode } from "@microsoft/sp-core-library"; import { DisplayMode } from "@microsoft/sp-core-library";
import { DynamicProperty } from "@microsoft/sp-component-base";
import { ILocation } from "../../../data";
/** /**
* Map component properties * Map component properties
@ -8,7 +10,7 @@ export interface IMapProps {
/** /**
* The address to show on the map * The address to show on the map
*/ */
address: string; address: ILocation | string | undefined;
/** /**
* The Bing maps API key to use when communicating with the Bing maps API * The Bing maps API key to use when communicating with the Bing maps API
*/ */

View File

@ -10,8 +10,8 @@ import { WebPartTitle } from '@pnp/spfx-controls-react/lib/WebPartTitle';
* Map component. Renders map for the specified location * Map component. Renders map for the specified location
*/ */
export class Map extends React.Component<IMapProps, IMapState> { export class Map extends React.Component<IMapProps, IMapState> {
constructor() { constructor(props: IMapProps) {
super(); super(props);
// set default state // set default state
this.state = { this.state = {
@ -30,6 +30,10 @@ export class Map extends React.Component<IMapProps, IMapState> {
return; return;
} }
if (!this.props.address) {
return;
}
// indicate that the component will be loading its data // indicate that the component will be loading its data
this.setState({ this.setState({
error: undefined, error: undefined,
@ -68,14 +72,14 @@ export class Map extends React.Component<IMapProps, IMapState> {
}); });
} }
public componentWillMount(): void { public componentDidMount(): void {
// get coordinates for the current address after the component has been // get coordinates for the current address after the component has been
// instantiated // instantiated
this._resolveCoordinates(); this._resolveCoordinates();
} }
public componentDidUpdate?(prevProps: IMapProps, prevState: IMapState, prevContext: any): void { public componentDidUpdate?(prevProps: IMapProps, prevState: IMapState, snapshot: any): void {
if (this.props.address !== prevProps.address) { if (prevProps.address !== this.props.address) {
// get coordinates for the new address // get coordinates for the new address
this._resolveCoordinates(); this._resolveCoordinates();
} }

View File

@ -3,6 +3,7 @@ define([], function() {
"AddressFieldLabel": "Address", "AddressFieldLabel": "Address",
"BingMapsApiKeyFieldLabel": "Bing Maps API Key", "BingMapsApiKeyFieldLabel": "Bing Maps API Key",
"BingMapsGroupName": "Bing Maps configuration", "BingMapsGroupName": "Bing Maps configuration",
"CityFieldLabel": "City",
"ConnectionGroupName": "Connection", "ConnectionGroupName": "Connection",
"DataGroupName": "Data", "DataGroupName": "Data",
"ErrorText": "An error has occurred while connecting to the data source. Details: ", "ErrorText": "An error has occurred while connecting to the data source. Details: ",

View File

@ -2,6 +2,7 @@ declare interface IMapWebPartStrings {
AddressFieldLabel: string; AddressFieldLabel: string;
BingMapsApiKeyFieldLabel: string; BingMapsApiKeyFieldLabel: string;
BingMapsGroupName: string; BingMapsGroupName: string;
CityFieldLabel: string;
ConnectionGroupName: string; ConnectionGroupName: string;
DataGroupName: string; DataGroupName: string;
ErrorText: string; ErrorText: string;

View File

@ -9,6 +9,7 @@
"sourceMap": true, "sourceMap": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"skipLibCheck": true, "skipLibCheck": true,
"outDir": "lib",
"typeRoots": [ "typeRoots": [
"./node_modules/@types", "./node_modules/@types",
"./node_modules/@microsoft" "./node_modules/@microsoft"
@ -22,5 +23,12 @@
"dom", "dom",
"es2015.collection" "es2015.collection"
] ]
} },
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"lib"
]
} }

View File

@ -0,0 +1,30 @@
{
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
"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-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,
"variable-name": false,
"whitespace": false
}
}