Two samples on communication between web parts through broadcasting events utilizing the Publish–subscribe pattern (#290)
* Sample added that shows how we can use the SPFx Event Aggregator to communicate between web parts * Sample added that shows how we can use the ReactiveX (RxJs) library with the SharePoint Framework to communicate between web parts through broadcasting events utilizing the Publish–subscribe pattern * Event Emitter sample README.md updated
This commit is contained in:
parent
5107f2c5d8
commit
8450d7855b
|
@ -0,0 +1,25 @@
|
||||||
|
# EditorConfig helps developers define and maintain consistent
|
||||||
|
# coding styles between different editors and IDEs
|
||||||
|
# editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
|
||||||
|
[*]
|
||||||
|
|
||||||
|
# change these settings to your own preference
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# we recommend you to keep these unchanged
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[{package,bower}.json]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
|
@ -0,0 +1 @@
|
||||||
|
* text=auto
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Build generated files
|
||||||
|
dist
|
||||||
|
lib
|
||||||
|
solution
|
||||||
|
temp
|
||||||
|
*.sppkg
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# OSX
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Visual Studio files
|
||||||
|
.ntvs_analysis.dat
|
||||||
|
.vs
|
||||||
|
bin
|
||||||
|
obj
|
||||||
|
|
||||||
|
# Resx Generated Code
|
||||||
|
*.resx.ts
|
||||||
|
|
||||||
|
# Styles Generated Code
|
||||||
|
*.scss.ts
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Folders
|
||||||
|
.vscode
|
||||||
|
coverage
|
||||||
|
node_modules
|
||||||
|
sharepoint
|
||||||
|
src
|
||||||
|
temp
|
||||||
|
|
||||||
|
# Files
|
||||||
|
*.csproj
|
||||||
|
.git*
|
||||||
|
.yo-rc.json
|
||||||
|
gulpfile.js
|
||||||
|
tsconfig.json
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"@microsoft/generator-sharepoint": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"libraryName": "react-events-aggregator",
|
||||||
|
"libraryId": "0443cb2b-2068-4b30-9893-ef6eeabe2a01",
|
||||||
|
"environment": "spo"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
# SPFx Event Aggregator Sample #
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This sample shows how we can use the SPFx Event Aggregator to communicate between web parts through broadcasting events utilizing the [Publish–subscribe pattern](https://en.wikipedia.org/wiki/Publish–subscribe_pattern). It enables a webpart or component to raise event (broadcast message) through the SPFx event aggregator and that event is received by other web parts or components that have been subscribed to receive it. Please note the SPFx Event Aggregator is still in Alpha, but this sample will be updated in case there are changes from the Alpha version to GA.
|
||||||
|
|
||||||
|
![SPFx eventAggregator](./assets/spfx-event-aggregator.gif)
|
||||||
|
|
||||||
|
|
||||||
|
## Used SharePoint Framework Version
|
||||||
|
![drop](https://img.shields.io/badge/drop-GA-green.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)
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Office 365 subscription with SharePoint Online.
|
||||||
|
- SharePoint Framework [development environment](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment) already set up.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
Solution|Author(s)
|
||||||
|
--------|---------
|
||||||
|
react-events-aggregator | Miguel Rabaca ( [Spanish Point](http://www.spanishpoint.ie) ), Velin Georgiev ( [@VelinGeorgiev](https://twitter.com/velingeorgiev) ), Austin Breslin ( [@AustinBreslin](https://www.linkedin.com/in/austin-breslin-84b4a74b/) )
|
||||||
|
|
||||||
|
## Version history
|
||||||
|
|
||||||
|
Version|Date|Comments
|
||||||
|
-------|----|--------
|
||||||
|
0.0.1|August 22, 2017 | Initial commit
|
||||||
|
|
||||||
|
## 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 repository.
|
||||||
|
- Open the command line, navigate to the web part folder and execute:
|
||||||
|
- `npm i`
|
||||||
|
- `gulp test` (optional)
|
||||||
|
- `gulp serve`
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
This Web Part illustrates the following concepts on top of the SharePoint Framework:
|
||||||
|
|
||||||
|
- Using React for building SharePoint Framework client-side web parts.
|
||||||
|
- Using Office UI Fabric React styles for building user experience consistent with SharePoint and Office.
|
||||||
|
- The use the SPFx event aggregator to broadcast messages (events).
|
||||||
|
- The use the SPFx event aggregator to subscribe and receive broadcasted messages (events).
|
||||||
|
- Unit tests including spies, mocks and faking class methods and properties.
|
||||||
|
|
||||||
|
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-events-aggregator" />
|
||||||
|
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 357 KiB |
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"entry": "./lib/webparts/broadcaster/BroadcasterWebPart.js",
|
||||||
|
"manifest": "./src/webparts/broadcaster/BroadcasterWebPart.manifest.json",
|
||||||
|
"outputPath": "./dist/broadcaster.bundle.js"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"entry": "./lib/webparts/receiver/ReceiverWebPart.js",
|
||||||
|
"manifest": "./src/webparts/receiver/ReceiverWebPart.manifest.json",
|
||||||
|
"outputPath": "./dist/receiver.bundle.js"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"externals": {},
|
||||||
|
"localizedResources": {
|
||||||
|
"broadcasterStrings": "webparts/broadcaster/loc/{locale}.js",
|
||||||
|
"receiverStrings": "webparts/receiver/loc/{locale}.js"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"deployCdnPath": "temp/deploy"
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"workingDir": "./temp/deploy/",
|
||||||
|
"account": "<!-- STORAGE ACCOUNT NAME -->",
|
||||||
|
"container": "react-events-aggregator",
|
||||||
|
"accessKey": "<!-- ACCESS KEY -->"
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"solution": {
|
||||||
|
"name": "react-events-aggregator-client-side-solution",
|
||||||
|
"id": "0443cb2b-2068-4b30-9893-ef6eeabe2a01",
|
||||||
|
"version": "1.0.0.0"
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"zippedPackage": "solution/react-events-aggregator.sppkg"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"port": 4321,
|
||||||
|
"initialPage": "https://localhost:5432/workbench",
|
||||||
|
"https": true,
|
||||||
|
"api": {
|
||||||
|
"port": 5432,
|
||||||
|
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
// 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-unused-imports": 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,3 @@
|
||||||
|
{
|
||||||
|
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const gulp = require('gulp');
|
||||||
|
const build = require('@microsoft/sp-build-web');
|
||||||
|
|
||||||
|
build.initialize(gulp);
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"name": "react-events-aggregator",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@microsoft/sp-core-library": "~1.1.0",
|
||||||
|
"@microsoft/sp-webpart-base": "~1.1.1",
|
||||||
|
"@types/webpack-env": ">=1.12.1 <1.14.0",
|
||||||
|
"react": "15.4.2",
|
||||||
|
"react-dom": "15.4.2",
|
||||||
|
"@types/react": "0.14.46",
|
||||||
|
"@types/react-dom": "0.14.18",
|
||||||
|
"@types/react-addons-shallow-compare": "0.14.17",
|
||||||
|
"@types/react-addons-update": "0.14.14",
|
||||||
|
"@types/react-addons-test-utils": "0.14.15"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@microsoft/sp-build-web": "~1.1.0",
|
||||||
|
"@microsoft/sp-module-interfaces": "~1.1.0",
|
||||||
|
"@microsoft/sp-webpart-workbench": "~1.1.0",
|
||||||
|
"gulp": "~3.9.1",
|
||||||
|
"@types/chai": ">=3.4.34 <3.6.0",
|
||||||
|
"@types/mocha": ">=2.2.33 <2.6.0",
|
||||||
|
"@types/sinon": "^2.3.3",
|
||||||
|
"chai-enzyme": "^0.8.0",
|
||||||
|
"enzyme": "^2.9.1",
|
||||||
|
"react-addons-test-utils": "^15.6.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "gulp bundle",
|
||||||
|
"clean": "gulp clean",
|
||||||
|
"test": "gulp test"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export class EventData {
|
||||||
|
public currentNumber: number;
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
|
||||||
|
|
||||||
|
"id": "d0c6977d-b995-46ff-a246-14ff43495e0e",
|
||||||
|
"alias": "BroadcasterWebPart",
|
||||||
|
"componentType": "WebPart",
|
||||||
|
"version": "*",
|
||||||
|
"manifestVersion": 2,
|
||||||
|
"safeWithCustomScriptDisabled": false,
|
||||||
|
|
||||||
|
"preconfiguredEntries": [{
|
||||||
|
"groupId": "d0c6977d-b995-46ff-a246-14ff43495e0e",
|
||||||
|
"group": { "default": "Under Development" },
|
||||||
|
"title": { "default": "Broadcaster" },
|
||||||
|
"description": { "default": "Broadcasts an event to a channel" },
|
||||||
|
"officeFabricIconFontName": "Page",
|
||||||
|
"properties": {
|
||||||
|
"description": "Broadcaster"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
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 'broadcasterStrings';
|
||||||
|
import { IBroadcasterWebPartProps } from './IBroadcasterWebPartProps';
|
||||||
|
import { IBroadcasterProps } from './components/IBroadcasterProps';
|
||||||
|
import Broadcaster from './components/Broadcaster';
|
||||||
|
|
||||||
|
export default class BroadcasterWebPart extends BaseClientSideWebPart<IBroadcasterWebPartProps> {
|
||||||
|
|
||||||
|
public render(): void {
|
||||||
|
|
||||||
|
const element: React.ReactElement<IBroadcasterProps> = React.createElement(
|
||||||
|
Broadcaster,
|
||||||
|
{
|
||||||
|
eventAggregator: this.context.eventAggregator
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
ReactDom.render(element, this.domElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected get dataVersion(): Version {
|
||||||
|
return Version.parse('1.0');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||||
|
return {
|
||||||
|
pages: [
|
||||||
|
{
|
||||||
|
header: {
|
||||||
|
description: strings.PropertyPaneDescription
|
||||||
|
},
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
groupName: strings.BasicGroupName,
|
||||||
|
groupFields: [
|
||||||
|
PropertyPaneTextField('description', {
|
||||||
|
label: strings.DescriptionFieldLabel
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface IBroadcasterWebPartProps {
|
||||||
|
description: string;
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
.broadcaster {
|
||||||
|
.container {
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0px auto;
|
||||||
|
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listItem {
|
||||||
|
max-width: 715px;
|
||||||
|
margin: 5px auto 5px auto;
|
||||||
|
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
// Our button
|
||||||
|
text-decoration: none;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
// Primary Button
|
||||||
|
min-width: 80px;
|
||||||
|
background-color: #0078d7;
|
||||||
|
border-color: #0078d7;
|
||||||
|
color: #ffffff;
|
||||||
|
|
||||||
|
// Basic Button
|
||||||
|
outline: transparent;
|
||||||
|
position: relative;
|
||||||
|
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
border-width: 0;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 16px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
margin: 0 4px;
|
||||||
|
vertical-align: top;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import styles from './Broadcaster.module.scss';
|
||||||
|
import { IBroadcasterProps } from './IBroadcasterProps';
|
||||||
|
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
|
||||||
|
import { EventData } from "../../../sharedLibs/EventData";
|
||||||
|
import { IEvent } from "@microsoft/sp-webpart-base/lib";
|
||||||
|
import { IBroadcasterState } from "./IBroadcasterState";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publisher (Observable) React component that broadcasts messages to Subscribers (Observers).
|
||||||
|
*/
|
||||||
|
export default class Broadcaster extends React.Component<IBroadcasterProps, IBroadcasterState> {
|
||||||
|
|
||||||
|
constructor(props: IBroadcasterProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = { eventNumber: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): React.ReactElement<IBroadcasterProps> {
|
||||||
|
return (
|
||||||
|
<div className={styles.broadcaster}>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={`ms-Grid-row ms-bgColor-themeDark ms-fontColor-white ${styles.row}`}>
|
||||||
|
<div className="ms-Grid-col ms-u-lg10 ms-u-xl8 ms-u-xlPush2 ms-u-lgPush1">
|
||||||
|
<h2>Event Message: {this.state.eventNumber}</h2>
|
||||||
|
<PrimaryButton onClick={this.broadcastData.bind(this)} id="BroadcastButton">
|
||||||
|
Broadcast message
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast data to all the subscribers.
|
||||||
|
*/
|
||||||
|
protected broadcastData(): void {
|
||||||
|
|
||||||
|
let eventNumber: number = this.state.eventNumber + 1;
|
||||||
|
|
||||||
|
this.setState((previousState: IBroadcasterState, props: IBroadcasterProps): IBroadcasterState => {
|
||||||
|
previousState.eventNumber = eventNumber;
|
||||||
|
return previousState;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.props.eventAggregator.raiseEvent(
|
||||||
|
"myCustomEvent:start",
|
||||||
|
{
|
||||||
|
data: { currentNumber: eventNumber },
|
||||||
|
sourceId: "BroadcasterWebPart",
|
||||||
|
targetId: "ReceiverWebPart"
|
||||||
|
} as IEvent<EventData>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { IEventAggregator } from "@microsoft/sp-webpart-base/lib";
|
||||||
|
|
||||||
|
export interface IBroadcasterProps {
|
||||||
|
eventAggregator: IEventAggregator;
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface IBroadcasterState {
|
||||||
|
eventNumber: number;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
define([], function() {
|
||||||
|
return {
|
||||||
|
"PropertyPaneDescription": "Description",
|
||||||
|
"BasicGroupName": "Group Name",
|
||||||
|
"DescriptionFieldLabel": "Description Field"
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,10 @@
|
||||||
|
declare interface IBroadcasterStrings {
|
||||||
|
PropertyPaneDescription: string;
|
||||||
|
BasicGroupName: string;
|
||||||
|
DescriptionFieldLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'broadcasterStrings' {
|
||||||
|
const strings: IBroadcasterStrings;
|
||||||
|
export = strings;
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
/// <reference types="mocha" />
|
||||||
|
/// <reference types="sinon" />
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { mount, ReactWrapper } from 'enzyme';
|
||||||
|
import { IEventAggregator, IEvent } from "@microsoft/sp-webpart-base/lib";
|
||||||
|
import EventAggregator from "@microsoft/sp-webpart-base/lib/core/events/EventAggregator";
|
||||||
|
import Broadcaster from "../components/Broadcaster";
|
||||||
|
import { EventData } from "../../../sharedLibs/EventData";
|
||||||
|
|
||||||
|
declare const sinon: any;
|
||||||
|
|
||||||
|
describe('BroadcasterWebPart', () => {
|
||||||
|
|
||||||
|
let raiseEventSpy: any;
|
||||||
|
let broadcastDataSpy: any;
|
||||||
|
let broadcaster: ReactWrapper<Broadcaster, any>;
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
|
||||||
|
// new instance of the event aggregator to be used with the react broadcaster.
|
||||||
|
let eventAggregator: IEventAggregator = new EventAggregator();
|
||||||
|
|
||||||
|
// create spies so we test if event is triggered.
|
||||||
|
raiseEventSpy = sinon.spy(EventAggregator.prototype, "raiseEvent");
|
||||||
|
broadcastDataSpy = sinon.spy(Broadcaster.prototype, "broadcastData");
|
||||||
|
|
||||||
|
// mount the Broadcaster so we can test it.
|
||||||
|
broadcaster = mount(<Broadcaster eventAggregator={eventAggregator} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should broadcast message with number 1', () => {
|
||||||
|
|
||||||
|
let currentEventNumber: number = 1;
|
||||||
|
|
||||||
|
// we click on the button to raise new event.
|
||||||
|
broadcaster.find("#BroadcastButton").simulate("click");
|
||||||
|
|
||||||
|
// check if event is broadcasted.
|
||||||
|
expect(broadcastDataSpy.calledOnce).to.be.true;
|
||||||
|
expect(broadcaster.state().eventNumber).to.be.eq(currentEventNumber);
|
||||||
|
expect(raiseEventSpy.calledOnce).to.be.true;
|
||||||
|
expect(raiseEventSpy.calledWith("myCustomEvent:start",
|
||||||
|
{
|
||||||
|
data: { currentNumber: currentEventNumber },
|
||||||
|
sourceId: "BroadcasterWebPart",
|
||||||
|
targetId: "ReceiverWebPart"
|
||||||
|
} as IEvent<EventData>)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should broadcast message with number 2', () => {
|
||||||
|
|
||||||
|
let currentEventNumber: number = 2;
|
||||||
|
|
||||||
|
// broadcast second event and check the data send.
|
||||||
|
broadcaster.find("#BroadcastButton").simulate("click");
|
||||||
|
|
||||||
|
expect(broadcastDataSpy.calledTwice).to.be.true;
|
||||||
|
expect(broadcaster.state().eventNumber).to.be.eq(currentEventNumber);
|
||||||
|
expect(raiseEventSpy.calledTwice).to.be.true;
|
||||||
|
expect(raiseEventSpy.calledWith("myCustomEvent:start",
|
||||||
|
{
|
||||||
|
data: { currentNumber: currentEventNumber },
|
||||||
|
sourceId: "BroadcasterWebPart",
|
||||||
|
targetId: "ReceiverWebPart"
|
||||||
|
} as IEvent<EventData>)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should fail on wrong message', () => {
|
||||||
|
|
||||||
|
let currentEventNumber: number = 3;
|
||||||
|
|
||||||
|
// broadcast third event and check the data send.
|
||||||
|
broadcaster.find("#BroadcastButton").simulate("click");
|
||||||
|
|
||||||
|
expect(broadcastDataSpy.calledThrice).to.be.true;
|
||||||
|
expect(broadcaster.state().eventNumber).to.be.eq(currentEventNumber);
|
||||||
|
expect(raiseEventSpy.calledThrice).to.be.true;
|
||||||
|
expect(raiseEventSpy.calledWith("myCustomEvent:start",
|
||||||
|
{
|
||||||
|
data: { currentNumber: currentEventNumber },
|
||||||
|
sourceId: "<Wrong_Message>",
|
||||||
|
targetId: "ReceiverWebPart"
|
||||||
|
} as IEvent<EventData>)).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface IReceiverWebPartProps {
|
||||||
|
description: string;
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
|
||||||
|
|
||||||
|
"id": "891b997e-67da-4da3-a6e1-3caf8bb264be",
|
||||||
|
"alias": "ReceiverWebPart",
|
||||||
|
"componentType": "WebPart",
|
||||||
|
"version": "*",
|
||||||
|
"manifestVersion": 2,
|
||||||
|
"safeWithCustomScriptDisabled": false,
|
||||||
|
|
||||||
|
"preconfiguredEntries": [{
|
||||||
|
"groupId": "891b997e-67da-4da3-a6e1-3caf8bb264be",
|
||||||
|
"group": { "default": "Under Development" },
|
||||||
|
"title": { "default": "Receiver" },
|
||||||
|
"description": { "default": "Receives events from the broadcaster web part" },
|
||||||
|
"officeFabricIconFontName": "Page",
|
||||||
|
"properties": {
|
||||||
|
"description": "Receiver"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
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 'receiverStrings';
|
||||||
|
import Receiver from './components/Receiver';
|
||||||
|
import { IReceiverProps } from './components/IReceiverProps';
|
||||||
|
import { IReceiverWebPartProps } from './IReceiverWebPartProps';
|
||||||
|
|
||||||
|
export default class ReceiverWebPart extends BaseClientSideWebPart<IReceiverWebPartProps> {
|
||||||
|
|
||||||
|
public render(): void {
|
||||||
|
|
||||||
|
const element: React.ReactElement<IReceiverProps> = React.createElement(
|
||||||
|
Receiver,
|
||||||
|
{
|
||||||
|
eventAggregator: this.context.eventAggregator,
|
||||||
|
subscriberId: this.context.instanceId
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
ReactDom.render(element, this.domElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected get dataVersion(): Version {
|
||||||
|
return Version.parse('1.0');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||||
|
return {
|
||||||
|
pages: [
|
||||||
|
{
|
||||||
|
header: {
|
||||||
|
description: strings.PropertyPaneDescription
|
||||||
|
},
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
groupName: strings.BasicGroupName,
|
||||||
|
groupFields: [
|
||||||
|
PropertyPaneTextField('description', {
|
||||||
|
label: strings.DescriptionFieldLabel
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { IEventAggregator } from "@microsoft/sp-webpart-base/lib";
|
||||||
|
|
||||||
|
export interface IReceiverProps {
|
||||||
|
eventAggregator: IEventAggregator;
|
||||||
|
subscriberId: string;
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface IReceiverState {
|
||||||
|
eventsList: Array<{ index: number, data: number }>;
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
.receiver {
|
||||||
|
.container {
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0px auto;
|
||||||
|
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listItem {
|
||||||
|
max-width: 715px;
|
||||||
|
margin: 5px auto 5px auto;
|
||||||
|
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
// Our button
|
||||||
|
text-decoration: none;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
// Primary Button
|
||||||
|
min-width: 80px;
|
||||||
|
background-color: #0078d7;
|
||||||
|
border-color: #0078d7;
|
||||||
|
color: #ffffff;
|
||||||
|
|
||||||
|
// Basic Button
|
||||||
|
outline: transparent;
|
||||||
|
position: relative;
|
||||||
|
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
border-width: 0;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 16px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
margin: 0 4px;
|
||||||
|
vertical-align: top;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import styles from './Receiver.module.scss';
|
||||||
|
import { IReceiverProps } from './IReceiverProps';
|
||||||
|
import { IEvent } from "@microsoft/sp-webpart-base/lib";
|
||||||
|
import { EventData } from "../../../sharedLibs/EventData";
|
||||||
|
import { IReceiverState } from './IReceiverState';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscriber (Observer) React component that Receives broadcasted messages from Publisher (Observable).
|
||||||
|
*/
|
||||||
|
export default class Receiver extends React.Component<IReceiverProps, IReceiverState> {
|
||||||
|
|
||||||
|
constructor(props: IReceiverProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = { eventsList: [] };
|
||||||
|
|
||||||
|
// subscribe for event by event name.
|
||||||
|
// another option would be to subscribe by SourceId (IEventAggregator.subscribeBySourceId). Check the IEventAggregator definitions.
|
||||||
|
this.props.eventAggregator.subscribeByEventName("myCustomEvent:start", this.props.subscriberId, this.receivedEvent.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): React.ReactElement<IReceiverProps> {
|
||||||
|
return (
|
||||||
|
<div className={styles.receiver}>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={`ms-Grid-row ms-bgColor-themeDark ms-fontColor-white ${styles.row}`}>
|
||||||
|
<div className="ms-Grid-col ms-u-lg10 ms-u-xl8 ms-u-xlPush2 ms-u-lgPush1">
|
||||||
|
<h2>Received events:</h2>
|
||||||
|
{
|
||||||
|
this.state.eventsList.map((item: { index: number, data: number }) => {
|
||||||
|
return <div key={item.index}>Received Event Message: {item.data}</div>;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that handles Received event from the eventAggregator.
|
||||||
|
* Changes the state of the eventsList by adding the new event data to the array.
|
||||||
|
* @param eventName the event name of the raised event.
|
||||||
|
* @param eventObject the event object of the raised event.
|
||||||
|
*/
|
||||||
|
protected receivedEvent(eventName: string, eventObject: IEvent<EventData>): void {
|
||||||
|
|
||||||
|
// update the events list with the newly received data from the event subscriber.
|
||||||
|
this.state.eventsList.push(
|
||||||
|
{
|
||||||
|
index: this.state.eventsList.length,
|
||||||
|
data: eventObject.data.currentNumber
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// set new state.
|
||||||
|
this.setState((previousState: IReceiverState, props: IReceiverProps): IReceiverState => {
|
||||||
|
previousState.eventsList = this.state.eventsList;
|
||||||
|
return previousState;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
define([], function() {
|
||||||
|
return {
|
||||||
|
"PropertyPaneDescription": "Description",
|
||||||
|
"BasicGroupName": "Group Name",
|
||||||
|
"DescriptionFieldLabel": "Description Field"
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,10 @@
|
||||||
|
declare interface IReceiverStrings {
|
||||||
|
PropertyPaneDescription: string;
|
||||||
|
BasicGroupName: string;
|
||||||
|
DescriptionFieldLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'receiverStrings' {
|
||||||
|
const strings: IReceiverStrings;
|
||||||
|
export = strings;
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
/// <reference types="mocha" />
|
||||||
|
/// <reference types="sinon" />
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { mount, ReactWrapper } from 'enzyme';
|
||||||
|
import { IEventAggregator, IEvent } from "@microsoft/sp-webpart-base/lib";
|
||||||
|
import EventAggregator from "@microsoft/sp-webpart-base/lib/core/events/EventAggregator";
|
||||||
|
import Receiver from "../components/Receiver";
|
||||||
|
import { EventData } from "../../../sharedLibs/EventData";
|
||||||
|
|
||||||
|
declare const sinon: any;
|
||||||
|
|
||||||
|
describe('ReceiverWebPart', () => {
|
||||||
|
|
||||||
|
let subscribeByEventNameSpy: any;
|
||||||
|
let subscribeBySourceIdSpy: any;
|
||||||
|
let receivedEventSpy: any;
|
||||||
|
let receiver: ReactWrapper<Receiver, any>;
|
||||||
|
let eventAggregator: IEventAggregator = new EventAggregator();
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
|
||||||
|
// create spies so we test if event is triggered.
|
||||||
|
subscribeByEventNameSpy = sinon.spy(EventAggregator.prototype, "subscribeByEventName");
|
||||||
|
subscribeBySourceIdSpy = sinon.spy(EventAggregator.prototype, "subscribeBySourceId");
|
||||||
|
receivedEventSpy = sinon.spy(Receiver.prototype, "receivedEvent");
|
||||||
|
|
||||||
|
// mount the Receiver so we can test it.
|
||||||
|
receiver = mount(<Receiver eventAggregator={eventAggregator} subscriberId="123" />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should Receiver be subscribed by event name', () => {
|
||||||
|
|
||||||
|
expect(subscribeByEventNameSpy.calledOnce).to.be.true;
|
||||||
|
expect(subscribeByEventNameSpy.calledWith("myCustomEvent:start", "123", sinon.match.any)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should Receive an event and call the receivedEvent function', () => {
|
||||||
|
|
||||||
|
let eventName: string = "myCustomEvent:start";
|
||||||
|
|
||||||
|
let eventObject: IEvent<EventData> = {
|
||||||
|
data: { currentNumber: 1 },
|
||||||
|
sourceId: "BroadcasterWebPart",
|
||||||
|
targetId: "ReceiverWebPart"
|
||||||
|
};
|
||||||
|
|
||||||
|
// raiseEvent so we can test if it has been received by the receivedEvent function.
|
||||||
|
eventAggregator.raiseEvent(eventName, eventObject);
|
||||||
|
|
||||||
|
expect(receivedEventSpy.calledOnce).to.be.true;
|
||||||
|
expect(receivedEventSpy.calledWith(eventName, eventObject)).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "commonjs",
|
||||||
|
"jsx": "react",
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"types": [
|
||||||
|
"es6-promise",
|
||||||
|
"es6-collections",
|
||||||
|
"webpack-env"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
# EditorConfig helps developers define and maintain consistent
|
||||||
|
# coding styles between different editors and IDEs
|
||||||
|
# editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
|
||||||
|
[*]
|
||||||
|
|
||||||
|
# change these settings to your own preference
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# we recommend you to keep these unchanged
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[{package,bower}.json]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
|
@ -0,0 +1 @@
|
||||||
|
* text=auto
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Build generated files
|
||||||
|
dist
|
||||||
|
lib
|
||||||
|
solution
|
||||||
|
temp
|
||||||
|
*.sppkg
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# OSX
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Visual Studio files
|
||||||
|
.ntvs_analysis.dat
|
||||||
|
.vs
|
||||||
|
bin
|
||||||
|
obj
|
||||||
|
|
||||||
|
# Resx Generated Code
|
||||||
|
*.resx.ts
|
||||||
|
|
||||||
|
# Styles Generated Code
|
||||||
|
*.scss.ts
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Folders
|
||||||
|
.vscode
|
||||||
|
coverage
|
||||||
|
node_modules
|
||||||
|
sharepoint
|
||||||
|
src
|
||||||
|
temp
|
||||||
|
|
||||||
|
# Files
|
||||||
|
*.csproj
|
||||||
|
.git*
|
||||||
|
.yo-rc.json
|
||||||
|
gulpfile.js
|
||||||
|
tsconfig.json
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"@microsoft/generator-sharepoint": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"libraryName": "react-rxjs-event-emitter",
|
||||||
|
"libraryId": "323ec3b8-b5de-4184-99a6-7e0f9c276412",
|
||||||
|
"environment": "spo"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
# SPFx ReactiveX (RxJs) Event Emitter Sample #
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This sample shows how we can use the [ReactiveX (RxJs)](http://reactivex.io/) library with the SharePoint Framework to communicate between web parts through broadcasting events utilizing the [Publish–subscribe pattern](https://en.wikipedia.org/wiki/Publish–subscribe_pattern). It enables a webpart or component to emit event (broadcast message) and that event is received by other web parts or components that have been subscribed to receive it. Please note this is custom implementation of the [Publish–subscribe pattern](https://en.wikipedia.org/wiki/Publish–subscribe_pattern) by using the [ReactiveX (RxJs)](http://reactivex.io/) library. The SPFx will nativelly support this in future without the need of custom implementation through new SPFx api called Event Aggregator, but it is still in Alpha.
|
||||||
|
|
||||||
|
![SPFx ReactiveX (RxJs) Event Emitter Sample](./assets/spfx-event-emitter.gif)
|
||||||
|
|
||||||
|
## Used SharePoint Framework Version
|
||||||
|
![drop](https://img.shields.io/badge/drop-GA-green.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)
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Office 365 subscription with SharePoint Online.
|
||||||
|
- SharePoint Framework [development environment](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment) already set up.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
Solution|Author(s)
|
||||||
|
--------|---------
|
||||||
|
react-rxjs-event-emitter | Velin Georgiev ( [@VelinGeorgiev](https://twitter.com/velingeorgiev) )
|
||||||
|
|
||||||
|
## Version history
|
||||||
|
|
||||||
|
Version|Date|Comments
|
||||||
|
-------|----|--------
|
||||||
|
0.0.1|August 22, 2017 | Initial commit
|
||||||
|
|
||||||
|
## 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 repository.
|
||||||
|
- Open the command line, navigate to the web part folder and execute:
|
||||||
|
- `npm i`
|
||||||
|
- `gulp test` (optional)
|
||||||
|
- `gulp serve`
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
This Web Part illustrates the following concepts on top of the SharePoint Framework:
|
||||||
|
|
||||||
|
- Using React for building SharePoint Framework client-side web parts.
|
||||||
|
- Using Office UI Fabric React styles for building user experience consistent with SharePoint and Office.
|
||||||
|
- The use the ReactiveX (RxJs) Event Emitter to broadcast messages (events).
|
||||||
|
- The use the ReactiveX (RxJs) Event Emitter to subscribe and receive broadcasted messages (events).
|
||||||
|
- Unit tests including spies, mocks and faking class methods and properties.
|
||||||
|
|
||||||
|
## Making the RxJsEventEmitter external SPFx library.
|
||||||
|
|
||||||
|
The RxJsEventEmitter library can be turned into external library so it can be used by multiple SPFx solutions.
|
||||||
|
There is a very good [blog post](https://blog.mastykarz.nl/dll-code-sharepoint-framework/) by @waldekmastykarz how this can be done.
|
||||||
|
|
||||||
|
|
||||||
|
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-rxjs-event-emitter" />
|
||||||
|
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 396 KiB |
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"entry": "./lib/libraries/rxJsEventEmitter/RxJsEventEmitter.js",
|
||||||
|
"manifest": "./src/libraries/rxJsEventEmitter/RxJsEventEmitter.manifest.json",
|
||||||
|
"outputPath": "./dist/rxjs-event-emitter.bundle.js"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"entry": "./lib/webparts/broadcaster/BroadcasterWebPart.js",
|
||||||
|
"manifest": "./src/webparts/broadcaster/BroadcasterWebPart.manifest.json",
|
||||||
|
"outputPath": "./dist/broadcaster.bundle.js"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"entry": "./lib/webparts/receiver/ReceiverWebPart.js",
|
||||||
|
"manifest": "./src/webparts/receiver/ReceiverWebPart.manifest.json",
|
||||||
|
"outputPath": "./dist/receiver.bundle.js"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"externals": {},
|
||||||
|
"localizedResources": {
|
||||||
|
"broadcasterStrings": "webparts/broadcaster/loc/{locale}.js",
|
||||||
|
"receiverStrings": "webparts/receiver/loc/{locale}.js"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"deployCdnPath": "temp/deploy"
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"workingDir": "./temp/deploy/",
|
||||||
|
"account": "<!-- STORAGE ACCOUNT NAME -->",
|
||||||
|
"container": "react-rxjs-event-emitter",
|
||||||
|
"accessKey": "<!-- ACCESS KEY -->"
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"solution": {
|
||||||
|
"name": "react-rxjs-event-emitter-client-side-solution",
|
||||||
|
"id": "323ec3b8-b5de-4184-99a6-7e0f9c276412",
|
||||||
|
"version": "1.0.0.0",
|
||||||
|
"skipFeatureDeployment": false
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"zippedPackage": "solution/react-rxjs-event-emitter.sppkg"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"port": 4321,
|
||||||
|
"initialPage": "https://localhost:5432/workbench",
|
||||||
|
"https": true,
|
||||||
|
"api": {
|
||||||
|
"port": 5432,
|
||||||
|
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
// 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-unused-imports": 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,3 @@
|
||||||
|
{
|
||||||
|
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const gulp = require('gulp');
|
||||||
|
const build = require('@microsoft/sp-build-web');
|
||||||
|
|
||||||
|
build.initialize(gulp);
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"name": "react-rxjs-event-emitter",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@microsoft/sp-core-library": "~1.1.0",
|
||||||
|
"@microsoft/sp-webpart-base": "~1.1.1",
|
||||||
|
"@types/webpack-env": ">=1.12.1 <1.14.0",
|
||||||
|
"react": "15.4.2",
|
||||||
|
"react-dom": "15.4.2",
|
||||||
|
"@types/react": "0.14.46",
|
||||||
|
"@types/react-dom": "0.14.18",
|
||||||
|
"@types/react-addons-shallow-compare": "0.14.17",
|
||||||
|
"@types/react-addons-update": "0.14.14",
|
||||||
|
"@types/react-addons-test-utils": "0.14.15",
|
||||||
|
"sp-pnp-js":"^2.0.7",
|
||||||
|
"@types/rx-lite":"4.0.4",
|
||||||
|
"rx-lite":"^4.0.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@microsoft/sp-build-web": "~1.1.0",
|
||||||
|
"@microsoft/sp-module-interfaces": "~1.1.1",
|
||||||
|
"@microsoft/sp-webpart-workbench": "~1.1.0",
|
||||||
|
"gulp": "~3.9.1",
|
||||||
|
"@types/chai": ">=3.4.34 <3.6.0",
|
||||||
|
"@types/mocha": ">=2.2.33 <2.6.0",
|
||||||
|
"@types/sinon": "^2.3.3",
|
||||||
|
"chai-enzyme": "^0.8.0",
|
||||||
|
"enzyme": "^2.9.1",
|
||||||
|
"react-addons-test-utils": "^15.6.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "gulp bundle",
|
||||||
|
"clean": "gulp clean",
|
||||||
|
"test": "gulp test"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export class EventData {
|
||||||
|
public currentNumber: number;
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
|
||||||
|
|
||||||
|
"id": "0ef3f066-d4d8-41b8-a683-aeb4e3a7848a",
|
||||||
|
"alias": "RxJsEventEmitter",
|
||||||
|
"componentType": "Library",
|
||||||
|
|
||||||
|
// The "*" signifies that the version should be taken from the package.json
|
||||||
|
"version": "*",
|
||||||
|
"manifestVersion": 2
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { Subject } from "rx-lite";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of EventEmitter that utilizes the ReactiveX (RxJs) Subject.
|
||||||
|
* See more at:
|
||||||
|
* https://github.com/Reactive-Extensions/RxJS/blob/master/doc/howdoi/eventemitter.md
|
||||||
|
*/
|
||||||
|
export class RxJsEventEmitter {
|
||||||
|
public subjects: Object;
|
||||||
|
public readonly hasOwnProp: any = {}.hasOwnProperty;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
|
||||||
|
this.subjects = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// tslint:disable:no-string-literal
|
||||||
|
/**
|
||||||
|
* Singleton for the page so we capture all the Observers and Observables in one global array;
|
||||||
|
*/
|
||||||
|
public static getInstance(): RxJsEventEmitter {
|
||||||
|
|
||||||
|
if(! window["RxJsEventEmitter"]) {
|
||||||
|
|
||||||
|
window["RxJsEventEmitter"] = new RxJsEventEmitter();
|
||||||
|
}
|
||||||
|
return window["RxJsEventEmitter"];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitts (broadcasts) event to Observers (Subscribers).
|
||||||
|
* @param name name of the event
|
||||||
|
* @param data event data
|
||||||
|
*/
|
||||||
|
public emit(name: string, data: Object): void {
|
||||||
|
let fnName: string = this._createName(name);
|
||||||
|
|
||||||
|
if (!this.subjects[fnName]) {
|
||||||
|
|
||||||
|
this.subjects[fnName] = new Subject();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.subjects[fnName].onNext(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes for event stream.
|
||||||
|
* If the event is broadcasted then handler (method)
|
||||||
|
* would be triggered and would receive data from the broadcasted event as method param.
|
||||||
|
* @param name name of the event
|
||||||
|
* @param handler event handler (method)
|
||||||
|
*/
|
||||||
|
public on(name: string, handler: any): void {
|
||||||
|
let fnName: string = this._createName(name);
|
||||||
|
|
||||||
|
if (!this.subjects[fnName]) {
|
||||||
|
|
||||||
|
this.subjects[fnName] = new Subject();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.subjects[fnName].subscribe(handler);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribes Observer (Subscriber) from event.
|
||||||
|
* @param name name of the event
|
||||||
|
*/
|
||||||
|
public off(name: string): void {
|
||||||
|
let fnName: string = this._createName(name);
|
||||||
|
|
||||||
|
if (this.subjects[fnName]) {
|
||||||
|
|
||||||
|
this.subjects[fnName].dispose();
|
||||||
|
delete this.subjects[fnName];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not tested.
|
||||||
|
*/
|
||||||
|
public dispose(): void {
|
||||||
|
|
||||||
|
let subjects: Object = this.subjects;
|
||||||
|
|
||||||
|
for (let prop in subjects) {
|
||||||
|
if (this.hasOwnProp.call(subjects, prop)) {
|
||||||
|
subjects[prop].dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.subjects = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
private _createName(name: string): string {
|
||||||
|
return `$${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
|
||||||
|
|
||||||
|
"id": "d0c6977d-b995-46ff-a246-14ff43495e0e",
|
||||||
|
"alias": "BroadcasterWebPart",
|
||||||
|
"componentType": "WebPart",
|
||||||
|
"version": "*",
|
||||||
|
"manifestVersion": 2,
|
||||||
|
"safeWithCustomScriptDisabled": false,
|
||||||
|
|
||||||
|
"preconfiguredEntries": [{
|
||||||
|
"groupId": "d0c6977d-b995-46ff-a246-14ff43495e0e",
|
||||||
|
"group": { "default": "Under Development" },
|
||||||
|
"title": { "default": "Broadcaster" },
|
||||||
|
"description": { "default": "Broadcasts an event to a channel" },
|
||||||
|
"officeFabricIconFontName": "Page",
|
||||||
|
"properties": {
|
||||||
|
"description": "Broadcaster"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
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 'broadcasterStrings';
|
||||||
|
import { IBroadcasterWebPartProps } from './IBroadcasterWebPartProps';
|
||||||
|
import { IBroadcasterProps } from './components/IBroadcasterProps';
|
||||||
|
import Broadcaster from './components/Broadcaster';
|
||||||
|
|
||||||
|
export default class BroadcasterWebPart extends BaseClientSideWebPart<IBroadcasterWebPartProps> {
|
||||||
|
|
||||||
|
public render(): void {
|
||||||
|
|
||||||
|
const element: React.ReactElement<IBroadcasterProps> = React.createElement(Broadcaster);
|
||||||
|
|
||||||
|
ReactDom.render(element, this.domElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected get dataVersion(): Version {
|
||||||
|
return Version.parse('1.0');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||||
|
return {
|
||||||
|
pages: [
|
||||||
|
{
|
||||||
|
header: {
|
||||||
|
description: strings.PropertyPaneDescription
|
||||||
|
},
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
groupName: strings.BasicGroupName,
|
||||||
|
groupFields: [
|
||||||
|
PropertyPaneTextField('description', {
|
||||||
|
label: strings.DescriptionFieldLabel
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface IBroadcasterWebPartProps {
|
||||||
|
description: string;
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
.broadcaster {
|
||||||
|
.container {
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0px auto;
|
||||||
|
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listItem {
|
||||||
|
max-width: 715px;
|
||||||
|
margin: 5px auto 5px auto;
|
||||||
|
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
// Our button
|
||||||
|
text-decoration: none;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
// Primary Button
|
||||||
|
min-width: 80px;
|
||||||
|
background-color: #0078d7;
|
||||||
|
border-color: #0078d7;
|
||||||
|
color: #ffffff;
|
||||||
|
|
||||||
|
// Basic Button
|
||||||
|
outline: transparent;
|
||||||
|
position: relative;
|
||||||
|
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
border-width: 0;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 16px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
margin: 0 4px;
|
||||||
|
vertical-align: top;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import styles from './Broadcaster.module.scss';
|
||||||
|
import { IBroadcasterProps } from './IBroadcasterProps';
|
||||||
|
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
|
||||||
|
import { IBroadcasterState } from './IBroadcasterState';
|
||||||
|
|
||||||
|
import { RxJsEventEmitter } from '../../../libraries/rxJsEventEmitter/RxJsEventEmitter';
|
||||||
|
import { EventData } from '../../../libraries/rxJsEventEmitter/EventData';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publisher (Observable) React component that broadcasts messages to Subscribers (Observers).
|
||||||
|
*/
|
||||||
|
export default class Broadcaster extends React.Component<IBroadcasterProps, IBroadcasterState> {
|
||||||
|
|
||||||
|
private readonly _eventEmitter: RxJsEventEmitter = RxJsEventEmitter.getInstance();
|
||||||
|
|
||||||
|
constructor(props: IBroadcasterProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = { eventNumber: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): React.ReactElement<IBroadcasterProps> {
|
||||||
|
return (
|
||||||
|
<div className={styles.broadcaster}>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={`ms-Grid-row ms-bgColor-themeDark ms-fontColor-white ${styles.row}`}>
|
||||||
|
<div className="ms-Grid-col ms-u-lg10 ms-u-xl8 ms-u-xlPush2 ms-u-lgPush1">
|
||||||
|
<h2>ReactiveX Event Emitter</h2>
|
||||||
|
<h2>Event Message: {this.state.eventNumber}</h2>
|
||||||
|
<PrimaryButton onClick={this.broadcastData.bind(this)} id="BroadcastButton">
|
||||||
|
Broadcast message
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast data to all the subscribers.
|
||||||
|
*/
|
||||||
|
protected broadcastData(): void {
|
||||||
|
|
||||||
|
let eventNumber: number = this.state.eventNumber + 1;
|
||||||
|
|
||||||
|
this.setState((previousState: IBroadcasterState, props: IBroadcasterProps): IBroadcasterState => {
|
||||||
|
previousState.eventNumber = eventNumber;
|
||||||
|
return previousState;
|
||||||
|
});
|
||||||
|
|
||||||
|
this._eventEmitter.emit("myCustomEvent:start", { currentNumber: eventNumber } as EventData);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export interface IBroadcasterProps {
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface IBroadcasterState {
|
||||||
|
eventNumber: number;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
define([], function() {
|
||||||
|
return {
|
||||||
|
"PropertyPaneDescription": "Description",
|
||||||
|
"BasicGroupName": "Group Name",
|
||||||
|
"DescriptionFieldLabel": "Description Field"
|
||||||
|
}
|
||||||
|
});
|
10
samples/react-rxjs-event-emitter/src/webparts/broadcaster/loc/mystrings.d.ts
vendored
Normal file
10
samples/react-rxjs-event-emitter/src/webparts/broadcaster/loc/mystrings.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
declare interface IBroadcasterStrings {
|
||||||
|
PropertyPaneDescription: string;
|
||||||
|
BasicGroupName: string;
|
||||||
|
DescriptionFieldLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'broadcasterStrings' {
|
||||||
|
const strings: IBroadcasterStrings;
|
||||||
|
export = strings;
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
/// <reference types="mocha" />
|
||||||
|
/// <reference types="sinon" />
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { mount, ReactWrapper } from 'enzyme';
|
||||||
|
import Broadcaster from "../components/Broadcaster";
|
||||||
|
|
||||||
|
import { EventData } from "../../../libraries/rxJsEventEmitter/EventData";
|
||||||
|
import { RxJsEventEmitter } from "../../../libraries/rxJsEventEmitter/RxJsEventEmitter";
|
||||||
|
|
||||||
|
declare const sinon: any;
|
||||||
|
|
||||||
|
describe('BroadcasterWebPart', () => {
|
||||||
|
|
||||||
|
let rxJsEventEmitterEmitSpy: any;
|
||||||
|
let broadcastDataSpy: any;
|
||||||
|
let broadcaster: ReactWrapper<Broadcaster, any>;
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
|
||||||
|
// create spies so we test if event is triggered.
|
||||||
|
rxJsEventEmitterEmitSpy = sinon.spy(RxJsEventEmitter.prototype, "emit");
|
||||||
|
broadcastDataSpy = sinon.spy(Broadcaster.prototype, "broadcastData");
|
||||||
|
|
||||||
|
// mount the Broadcaster so we can test it.
|
||||||
|
broadcaster = mount(<Broadcaster />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should broadcast message with number 1', () => {
|
||||||
|
|
||||||
|
let currentEventNumber: number = 1;
|
||||||
|
|
||||||
|
// we click on the button to raise new event.
|
||||||
|
broadcaster.find("#BroadcastButton").simulate("click");
|
||||||
|
|
||||||
|
// check if event is broadcasted.
|
||||||
|
expect(broadcastDataSpy.calledOnce).to.be.true;
|
||||||
|
expect(broadcaster.state().eventNumber).to.be.eq(currentEventNumber);
|
||||||
|
expect(rxJsEventEmitterEmitSpy.calledOnce).to.be.true;
|
||||||
|
expect(rxJsEventEmitterEmitSpy.calledWith("myCustomEvent:start",
|
||||||
|
{ currentNumber: currentEventNumber } as EventData)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should broadcast message with number 2', () => {
|
||||||
|
|
||||||
|
let currentEventNumber: number = 2;
|
||||||
|
|
||||||
|
// broadcast second event and check the data send.
|
||||||
|
broadcaster.find("#BroadcastButton").simulate("click");
|
||||||
|
|
||||||
|
expect(broadcastDataSpy.calledTwice).to.be.true;
|
||||||
|
expect(broadcaster.state().eventNumber).to.be.eq(currentEventNumber);
|
||||||
|
expect(rxJsEventEmitterEmitSpy.calledTwice).to.be.true;
|
||||||
|
expect(rxJsEventEmitterEmitSpy.calledWith("myCustomEvent:start",
|
||||||
|
{ currentNumber: currentEventNumber } as EventData)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should fail on wrong message', () => {
|
||||||
|
|
||||||
|
let currentEventNumber: number = 3;
|
||||||
|
let wrongEventNumber: number = 99999;
|
||||||
|
|
||||||
|
// broadcast third event and check the data send.
|
||||||
|
broadcaster.find("#BroadcastButton").simulate("click");
|
||||||
|
|
||||||
|
expect(broadcastDataSpy.calledThrice).to.be.true;
|
||||||
|
expect(broadcaster.state().eventNumber).to.be.eq(currentEventNumber);
|
||||||
|
expect(rxJsEventEmitterEmitSpy.calledThrice).to.be.true;
|
||||||
|
expect(rxJsEventEmitterEmitSpy.calledWith("myCustomEvent:start",
|
||||||
|
{ currentNumber: wrongEventNumber } as EventData)).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface IReceiverWebPartProps {
|
||||||
|
description: string;
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
|
||||||
|
|
||||||
|
"id": "891b997e-67da-4da3-a6e1-3caf8bb264be",
|
||||||
|
"alias": "ReceiverWebPart",
|
||||||
|
"componentType": "WebPart",
|
||||||
|
"version": "*",
|
||||||
|
"manifestVersion": 2,
|
||||||
|
"safeWithCustomScriptDisabled": false,
|
||||||
|
|
||||||
|
"preconfiguredEntries": [{
|
||||||
|
"groupId": "891b997e-67da-4da3-a6e1-3caf8bb264be",
|
||||||
|
"group": { "default": "Under Development" },
|
||||||
|
"title": { "default": "Receiver" },
|
||||||
|
"description": { "default": "Receives events from the broadcaster web part" },
|
||||||
|
"officeFabricIconFontName": "Page",
|
||||||
|
"properties": {
|
||||||
|
"description": "Receiver"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
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 'receiverStrings';
|
||||||
|
import Receiver from './components/Receiver';
|
||||||
|
import { IReceiverProps } from './components/IReceiverProps';
|
||||||
|
import { IReceiverWebPartProps } from './IReceiverWebPartProps';
|
||||||
|
|
||||||
|
export default class ReceiverWebPart extends BaseClientSideWebPart<IReceiverWebPartProps> {
|
||||||
|
|
||||||
|
public render(): void {
|
||||||
|
|
||||||
|
const element: React.ReactElement<IReceiverProps> = React.createElement(Receiver);
|
||||||
|
|
||||||
|
ReactDom.render(element, this.domElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected get dataVersion(): Version {
|
||||||
|
return Version.parse('1.0');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||||
|
return {
|
||||||
|
pages: [
|
||||||
|
{
|
||||||
|
header: {
|
||||||
|
description: strings.PropertyPaneDescription
|
||||||
|
},
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
groupName: strings.BasicGroupName,
|
||||||
|
groupFields: [
|
||||||
|
PropertyPaneTextField('description', {
|
||||||
|
label: strings.DescriptionFieldLabel
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export interface IReceiverProps {
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface IReceiverState {
|
||||||
|
eventsList: Array<{ index: number, data: number }>;
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
.receiver {
|
||||||
|
.container {
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0px auto;
|
||||||
|
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listItem {
|
||||||
|
max-width: 715px;
|
||||||
|
margin: 5px auto 5px auto;
|
||||||
|
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
// Our button
|
||||||
|
text-decoration: none;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
// Primary Button
|
||||||
|
min-width: 80px;
|
||||||
|
background-color: #0078d7;
|
||||||
|
border-color: #0078d7;
|
||||||
|
color: #ffffff;
|
||||||
|
|
||||||
|
// Basic Button
|
||||||
|
outline: transparent;
|
||||||
|
position: relative;
|
||||||
|
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
border-width: 0;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 16px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
margin: 0 4px;
|
||||||
|
vertical-align: top;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import styles from './Receiver.module.scss';
|
||||||
|
import { IReceiverProps } from './IReceiverProps';
|
||||||
|
import { IReceiverState } from './IReceiverState';
|
||||||
|
|
||||||
|
import { RxJsEventEmitter } from "../../../libraries/rxJsEventEmitter/RxJsEventEmitter";
|
||||||
|
import { EventData } from "../../../libraries/rxJsEventEmitter/EventData";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscriber (Observer) React component that Receives broadcasted messages from Publisher (Observable).
|
||||||
|
*/
|
||||||
|
export default class Receiver extends React.Component<IReceiverProps, IReceiverState> {
|
||||||
|
|
||||||
|
private readonly _eventEmitter: RxJsEventEmitter = RxJsEventEmitter.getInstance();
|
||||||
|
|
||||||
|
constructor(props: IReceiverProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = { eventsList: [] };
|
||||||
|
|
||||||
|
// subscribe for event by event name.
|
||||||
|
this._eventEmitter.on("myCustomEvent:start", this.receivedEvent.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): React.ReactElement<IReceiverProps> {
|
||||||
|
return (
|
||||||
|
<div className={styles.receiver}>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={`ms-Grid-row ms-bgColor-themeDark ms-fontColor-white ${styles.row}`}>
|
||||||
|
<div className="ms-Grid-col ms-u-lg10 ms-u-xl8 ms-u-xlPush2 ms-u-lgPush1">
|
||||||
|
<h2>ReactiveX Event Receiver</h2>
|
||||||
|
<h2>Received events:</h2>
|
||||||
|
{
|
||||||
|
this.state.eventsList.map((item: { index: number, data: number }) => {
|
||||||
|
return <div key={item.index}>Received Event Message: {item.data}</div>;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that handles Received event from the RxJsEventEmitter.
|
||||||
|
* Changes the state of the eventsList by adding the new event data to the array.
|
||||||
|
* @param data the event object of the raised event.
|
||||||
|
*/
|
||||||
|
protected receivedEvent(data: EventData): void {
|
||||||
|
|
||||||
|
// update the events list with the newly received data from the event subscriber.
|
||||||
|
this.state.eventsList.push(
|
||||||
|
{
|
||||||
|
index: this.state.eventsList.length,
|
||||||
|
data: data.currentNumber
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// set new state.
|
||||||
|
this.setState((previousState: IReceiverState, props: IReceiverProps): IReceiverState => {
|
||||||
|
previousState.eventsList = this.state.eventsList;
|
||||||
|
return previousState;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
define([], function() {
|
||||||
|
return {
|
||||||
|
"PropertyPaneDescription": "Description",
|
||||||
|
"BasicGroupName": "Group Name",
|
||||||
|
"DescriptionFieldLabel": "Description Field"
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,10 @@
|
||||||
|
declare interface IReceiverStrings {
|
||||||
|
PropertyPaneDescription: string;
|
||||||
|
BasicGroupName: string;
|
||||||
|
DescriptionFieldLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'receiverStrings' {
|
||||||
|
const strings: IReceiverStrings;
|
||||||
|
export = strings;
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
/// <reference types="mocha" />
|
||||||
|
/// <reference types="sinon" />
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { mount, ReactWrapper } from 'enzyme';
|
||||||
|
import Receiver from "../components/Receiver";
|
||||||
|
|
||||||
|
import { EventData } from "../../../libraries/rxJsEventEmitter/EventData";
|
||||||
|
import { RxJsEventEmitter } from "../../../libraries/rxJsEventEmitter/RxJsEventEmitter";
|
||||||
|
|
||||||
|
declare const sinon: any;
|
||||||
|
|
||||||
|
describe('ReceiverWebPart', () => {
|
||||||
|
|
||||||
|
let rxJsEventEmitterOnSpy: any;
|
||||||
|
let receivedEventSpy: any;
|
||||||
|
let receiver: ReactWrapper<Receiver, any>;
|
||||||
|
let eventEmitter: RxJsEventEmitter = RxJsEventEmitter.getInstance();
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
|
||||||
|
// create spies so we test if event is triggered.
|
||||||
|
rxJsEventEmitterOnSpy = sinon.spy(RxJsEventEmitter.prototype, "on");
|
||||||
|
receivedEventSpy = sinon.spy(Receiver.prototype, "receivedEvent");
|
||||||
|
|
||||||
|
// mount the Receiver so we can test it.
|
||||||
|
receiver = mount(<Receiver />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should Receiver be subscribed by event name', () => {
|
||||||
|
|
||||||
|
expect(rxJsEventEmitterOnSpy.calledOnce).to.be.true;
|
||||||
|
expect(rxJsEventEmitterOnSpy.calledWith("myCustomEvent:start", sinon.match.any)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should Receive an event and call the receivedEvent function', () => {
|
||||||
|
|
||||||
|
let data: EventData = { currentNumber: 1 };
|
||||||
|
|
||||||
|
// emit the data so we can test if it has been received by the receivedEvent function.
|
||||||
|
eventEmitter.emit("myCustomEvent:start", data);
|
||||||
|
|
||||||
|
expect(receivedEventSpy.calledOnce).to.be.true;
|
||||||
|
expect(receivedEventSpy.calledWith(data)).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "commonjs",
|
||||||
|
"jsx": "react",
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"types": [
|
||||||
|
"es6-promise",
|
||||||
|
"es6-collections",
|
||||||
|
"webpack-env"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Type definitions for Microsoft ODSP projects
|
||||||
|
// Project: ODSP
|
||||||
|
|
||||||
|
/* Global definition for UNIT_TEST builds
|
||||||
|
Code that is wrapped inside an if(UNIT_TEST) {...}
|
||||||
|
block will not be included in the final bundle when the
|
||||||
|
--ship flag is specified */
|
||||||
|
declare const UNIT_TEST: boolean;
|
||||||
|
|
||||||
|
/* Global defintion for SPO builds */
|
||||||
|
declare const DATACENTER: boolean;
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference path="@ms/odsp.d.ts" />
|
Loading…
Reference in New Issue