SPFx webpart sample using Redux Async Actions and ImmutableJS (#262)
* added wp base * started on adding redux * removed immutable to start with * initial code working (redux, immutable) * get lists, basic add new list ready * basic code using redux and immutable js ready * comments * image and readme * readme update * updated image * appease the linter * added comments * changed folder name * updated gif * typo in layouts path * link to blog and telemetry
This commit is contained in:
parent
35fff34e69
commit
db3a0653a2
|
@ -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-redux-restapi",
|
||||
"libraryId": "b40fef70-090e-43b4-adc7-64b379287548",
|
||||
"environment": "spo"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
# SharePoint Framework webpart sample using React, Redux and ImmutableJS
|
||||
|
||||
## Summary
|
||||
SharePoint Framework webpart which uses [Redux](http://redux.js.org/) to maintain a single state for the entire application and [ImmutableJS](https://facebook.github.io/immutable-js/) to create performant state trees.
|
||||
|
||||
Redux AJAX actions are used together with the SharePoint REST API to display lists in your site. You can also add a new list to the site from this webpart.
|
||||
|
||||
More details in my post here: [Using Redux Async Actions and ImmutableJS in SharePoint Framework](http://www.vrdmn.com/2017/07/using-redux-async-actions-and.html)
|
||||
|
||||
![](https://raw.githubusercontent.com/vman/sp-dev-fx-webparts/master/samples/react-redux-async-immutablejs/assets/react-redux-immutable.gif)
|
||||
|
||||
## Used SharePoint Framework Version
|
||||
![drop](https://img.shields.io/badge/version-GA-green.svg)
|
||||
|
||||
## Applies to
|
||||
|
||||
* [SharePoint Framework](https:/dev.office.com/sharepoint)
|
||||
* [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment)
|
||||
|
||||
## Solution
|
||||
|
||||
Solution|Author(s)
|
||||
--------|---------
|
||||
React-Redux-RESTAPI | Vardhaman Deshpande [@vrdmn](https://twitter.com/vrdmn)
|
||||
|
||||
## Version history
|
||||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0|July 11, 2017|Initial release
|
||||
|
||||
## Disclaimer
|
||||
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
|
||||
|
||||
---
|
||||
|
||||
## Minimal Path to Awesome
|
||||
|
||||
- Clone this repository
|
||||
- in the command line run:
|
||||
- `npm install`
|
||||
- `gulp serve`
|
||||
- Open the SharePoint Online version of the workbench: /_layouts/15/workbench.aspx
|
||||
|
||||
## Why Redux and ImmutableJS
|
||||
Every [Redux](http://redux.js.org/) action creates a copy of the state, changes the required properties in the copy and then returns the copy as a new state. This prevents bugs where the state is changed unknowingly.
|
||||
|
||||
On the other hand, this can also be performance intensive as for changing only a single element, we have to copy the entire state tree in memory. Fortunately, [ImmutableJS](https://facebook.github.io/immutable-js/) comes to the rescue here.
|
||||
|
||||
Using ImmutableJS, we can create new state trees in memory without duplicating the elements which are unchanged. When you create a new state object using ImmutableJS, the new object still points to the previous memory locations of unchanged elements. Only the properties which are changed are allocated new memory locations.
|
||||
|
||||
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-redux-async-immutablejs" />
|
Binary file not shown.
After Width: | Height: | Size: 416 KiB |
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"entries": [
|
||||
{
|
||||
"entry": "./lib/webparts/demo/DemoWebPart.js",
|
||||
"manifest": "./src/webparts/demo/DemoWebPart.manifest.json",
|
||||
"outputPath": "./dist/demo.bundle.js"
|
||||
}
|
||||
],
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"demoStrings": "webparts/demo/loc/{locale}.js"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"workingDir": "./temp/deploy/",
|
||||
"account": "<!-- STORAGE ACCOUNT NAME -->",
|
||||
"container": "react-redux-restapi",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"solution": {
|
||||
"name": "react-redux-restapi-client-side-solution",
|
||||
"id": "b40fef70-090e-43b4-adc7-64b379287548",
|
||||
"version": "1.0.0.0"
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/react-redux-restapi.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);
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"name": "react-redux-restapi",
|
||||
"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/react": "0.14.46",
|
||||
"@types/react-addons-shallow-compare": "0.14.17",
|
||||
"@types/react-addons-test-utils": "0.14.15",
|
||||
"@types/react-addons-update": "0.14.14",
|
||||
"@types/react-dom": "0.14.18",
|
||||
"@types/webpack-env": ">=1.12.1 <1.14.0",
|
||||
"immutable": "^3.8.1",
|
||||
"react": "15.4.2",
|
||||
"react-dom": "15.4.2",
|
||||
"react-redux": "^5.0.5",
|
||||
"redux": "^3.7.1",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/sp-build-web": "~1.1.0",
|
||||
"@microsoft/sp-module-interfaces": "~1.1.0",
|
||||
"@microsoft/sp-webpart-workbench": "~1.1.0",
|
||||
"@types/chai": ">=3.4.34 <3.6.0",
|
||||
"@types/mocha": ">=2.2.33 <2.6.0",
|
||||
"@types/react-redux": "^4.4.45",
|
||||
"@types/redux-logger": "^3.0.0",
|
||||
"gulp": "~3.9.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
|
||||
|
||||
"id": "096e9abd-b73c-4b73-8292-621bf1257402",
|
||||
"alias": "DemoWebPart",
|
||||
"componentType": "WebPart",
|
||||
"version": "*", // The "*" signifies that the version should be taken from the package.json
|
||||
"manifestVersion": 2,
|
||||
|
||||
/**
|
||||
* This property should only be set to true if it is certain that the webpart does not
|
||||
* allow arbitrary scripts to be called
|
||||
*/
|
||||
"safeWithCustomScriptDisabled": false,
|
||||
|
||||
"preconfiguredEntries": [{
|
||||
"groupId": "096e9abd-b73c-4b73-8292-621bf1257402",
|
||||
"group": { "default": "Under Development" },
|
||||
"title": { "default": "Redux ImmutableJS Demo" },
|
||||
"description": { "default": "Webpart to Demo Redux and ImmutableJS" },
|
||||
"officeFabricIconFontName": "Page",
|
||||
"properties": {
|
||||
"description": "Demo"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
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 'demoStrings';
|
||||
import Demo from './components/Demo';
|
||||
import { IDemoWebPartProps } from './IDemoWebPartProps';
|
||||
|
||||
import configureStore from './store/configureStore';
|
||||
import { Provider } from 'react-redux';
|
||||
const store = configureStore();
|
||||
|
||||
export default class DemoWebPart extends BaseClientSideWebPart<IDemoWebPartProps> {
|
||||
|
||||
public render(): void {
|
||||
|
||||
const provider: React.ReactElement<Provider> = React.createElement(typeof Provider, null, React.createElement(
|
||||
Demo,
|
||||
{
|
||||
store: store, //Pass store explicitly to Demo as a prop
|
||||
description: this.properties.description,
|
||||
spHttpClient: this.context.spHttpClient,
|
||||
currentWebUrl: this.context.pageContext.web.serverRelativeUrl
|
||||
}));
|
||||
|
||||
|
||||
ReactDom.render(provider, 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 IDemoWebPartProps {
|
||||
description: string;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
export enum ActionTypes {
|
||||
ADD_LIST_REQUEST,// = "ADD_LIST_REQUEST" //String enums in TypeScript 2.4+
|
||||
ADD_LIST_SUCCESS,
|
||||
ADD_LIST_ERROR,
|
||||
GET_LISTS_REQUEST,
|
||||
GET_LISTS_SUCCESS,
|
||||
GET_LISTS_ERROR,
|
||||
UPDATE_TITLE,
|
||||
}
|
||||
|
||||
export type Action =
|
||||
{ type: ActionTypes.ADD_LIST_REQUEST } |
|
||||
{ type: ActionTypes.ADD_LIST_SUCCESS, payload: string } |
|
||||
{ type: ActionTypes.ADD_LIST_ERROR, payload: string } |
|
||||
{ type: ActionTypes.GET_LISTS_REQUEST } |
|
||||
{ type: ActionTypes.GET_LISTS_SUCCESS, payload: string[] } |
|
||||
{ type: ActionTypes.GET_LISTS_ERROR, payload: string } |
|
||||
{ type: ActionTypes.UPDATE_TITLE, payload: string };
|
|
@ -0,0 +1,80 @@
|
|||
import { ActionTypes, Action } from './actionTypes';
|
||||
import { SPHttpClient, ISPHttpClientOptions, SPHttpClientResponse } from '@microsoft/sp-http';
|
||||
import { IODataList } from '@microsoft/sp-odata-types';
|
||||
|
||||
|
||||
//Action Creators to create and return Actions
|
||||
export const updateTitle = (title: string): Action => ({
|
||||
type: ActionTypes.UPDATE_TITLE,
|
||||
payload: title
|
||||
});
|
||||
|
||||
//Each AJAX request ideally has 3 actions: request, success and error.
|
||||
//These can be used to modify the ui such as show a loading icon, show updated list, show error message etc.
|
||||
const addListRequest = (): Action => ({
|
||||
type: ActionTypes.ADD_LIST_REQUEST
|
||||
});
|
||||
const addListSuccess = (list: string): Action => ({
|
||||
type: ActionTypes.ADD_LIST_SUCCESS,
|
||||
payload: list
|
||||
});
|
||||
const addListError = (error: Error): Action => ({
|
||||
type: ActionTypes.ADD_LIST_ERROR,
|
||||
payload: error.message
|
||||
});
|
||||
|
||||
//Actions for getLists
|
||||
const getListsRequest = (): Action => ({
|
||||
type: ActionTypes.GET_LISTS_REQUEST
|
||||
});
|
||||
const getListsSuccess = (lists: string[]): Action => ({
|
||||
type: ActionTypes.GET_LISTS_SUCCESS,
|
||||
payload: lists
|
||||
});
|
||||
const getListsError = (error: Error): Action => ({
|
||||
type: ActionTypes.GET_LISTS_ERROR,
|
||||
payload: error.message
|
||||
});
|
||||
|
||||
export function addList(spHttpClient: SPHttpClient, currentWebUrl: string, listTitle: string) {
|
||||
return async (dispatch: any) => {
|
||||
|
||||
//Fire the 'request' action if you want to update the state to specify that an ajax request is being made.
|
||||
//This can be used to show a loading screen or a spinner.
|
||||
dispatch(addListRequest());
|
||||
|
||||
const spOpts: ISPHttpClientOptions = {
|
||||
body: `{ Title: '${listTitle}', BaseTemplate: 100 }`
|
||||
};
|
||||
|
||||
try {
|
||||
const response: SPHttpClientResponse = await spHttpClient.post(`${currentWebUrl}/_api/web/lists`, SPHttpClient.configurations.v1, spOpts);
|
||||
const list: IODataList = await response.json();
|
||||
|
||||
//Fire the 'success' action when you want to update the state based on a successfull request.
|
||||
dispatch(addListSuccess(list.Title));
|
||||
|
||||
} catch (error) {
|
||||
//Fire the 'error' action when you want to update the state based on an error request.
|
||||
dispatch(addListError(error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getLists(spHttpClient: SPHttpClient, currentWebUrl: string) {
|
||||
return async (dispatch: any) => {
|
||||
|
||||
dispatch(getListsRequest());
|
||||
|
||||
try {
|
||||
const response: SPHttpClientResponse = await spHttpClient.get(`${currentWebUrl}/_api/web/lists?$filter=Hidden eq false&$select=Title`, SPHttpClient.configurations.v1);
|
||||
const responseJSON = await response.json();
|
||||
const lists: IODataList[] = responseJSON.value;
|
||||
const listTitles: string[] = lists.map(list => list.Title);
|
||||
dispatch(getListsSuccess(listTitles));
|
||||
|
||||
} catch (error) {
|
||||
dispatch(getListsError(error));
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
.demo {
|
||||
.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,85 @@
|
|||
import * as React from 'react';
|
||||
import styles from './Demo.module.scss';
|
||||
import { IDemoProps } from './IDemoProps';
|
||||
import { escape } from '@microsoft/sp-lodash-subset';
|
||||
import { Dispatch } from 'redux';
|
||||
import { updateTitle, addList, getLists } from '../actions/listActions';
|
||||
import { connect } from 'react-redux';
|
||||
import { ListState } from '../state/ListState';
|
||||
import { SPHttpClient } from '@microsoft/sp-http';
|
||||
|
||||
import * as ReactDOM from 'react-dom';
|
||||
|
||||
interface IConnectedDispatch {
|
||||
updateTitle: (title: string) => void;
|
||||
getLists: (spHttpClient: SPHttpClient, currentWebUrl: string) => void;
|
||||
addList: (spHttpClient: SPHttpClient, currentWebUrl: string, listtitle: string) => void;
|
||||
}
|
||||
|
||||
interface IConnectedState {
|
||||
title: string;
|
||||
lists: string[];
|
||||
}
|
||||
|
||||
//Map the application state to the properties of the Components. Making them available in this.props inside the component.
|
||||
function mapStateToProps(state: ListState, ownProps: IDemoProps): IConnectedState {
|
||||
return {
|
||||
title: state.title,
|
||||
lists: state.lists
|
||||
};
|
||||
}
|
||||
|
||||
//Map the actions to the properties of the Component. Making them available in this.props inside the component.
|
||||
const mapDispatchToProps = (dispatch: Dispatch<ListState>): IConnectedDispatch => ({
|
||||
updateTitle: (title: string) => {
|
||||
dispatch(updateTitle(title));
|
||||
},
|
||||
getLists: (spHttpClient: SPHttpClient, currentWebUrl: string) => {
|
||||
dispatch(getLists(spHttpClient, currentWebUrl));
|
||||
},
|
||||
addList: (spHttpClient: SPHttpClient, currentWebUrl: string, listtitle: string) => {
|
||||
dispatch(addList(spHttpClient, currentWebUrl, listtitle));
|
||||
}
|
||||
});
|
||||
|
||||
//Components does not have a state of it's own. The state is is passed to the component as props from mapStateToProps
|
||||
class Demo extends React.Component<IDemoProps & IConnectedState & IConnectedDispatch, {}> {
|
||||
public render() {
|
||||
return (
|
||||
<div className={styles.demo}>
|
||||
<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">
|
||||
<span className="ms-font-xl ms-fontColor-white">Welcome to SharePoint!</span>
|
||||
<p className="ms-font-l ms-fontColor-white">{escape(this.props.description)}</p>
|
||||
<span>Add New List:</span>
|
||||
<div>
|
||||
<input type="text" value={this.props.title} onChange={this.handleChange.bind(this)} />
|
||||
<input type="submit" value="Add" onClick={this.handleSubmit.bind(this)} />
|
||||
</div>
|
||||
<ul>
|
||||
{this.props.lists.map(list => {
|
||||
return <li>{list}</li>;
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private handleChange(event: React.FormEvent<HTMLInputElement>){
|
||||
this.props.updateTitle(event.currentTarget.value);
|
||||
}
|
||||
|
||||
private handleSubmit(event: React.MouseEvent<HTMLInputElement>){
|
||||
this.props.addList(this.props.spHttpClient, this.props.currentWebUrl, this.props.title);
|
||||
}
|
||||
|
||||
private componentDidMount() {
|
||||
this.props.getLists(this.props.spHttpClient, this.props.currentWebUrl);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Demo);
|
|
@ -0,0 +1,10 @@
|
|||
import { ListState } from '../state/ListState';
|
||||
import { Store } from 'redux';
|
||||
import { SPHttpClient } from '@microsoft/sp-http';
|
||||
|
||||
export interface IDemoProps {
|
||||
store: Store<ListState>;
|
||||
description: string;
|
||||
spHttpClient: SPHttpClient;
|
||||
currentWebUrl: string;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"PropertyPaneDescription": "Description",
|
||||
"BasicGroupName": "Group Name",
|
||||
"DescriptionFieldLabel": "Description Field"
|
||||
}
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
declare interface IDemoStrings {
|
||||
PropertyPaneDescription: string;
|
||||
BasicGroupName: string;
|
||||
DescriptionFieldLabel: string;
|
||||
}
|
||||
|
||||
declare module 'demoStrings' {
|
||||
const strings: IDemoStrings;
|
||||
export = strings;
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { Action, ActionTypes } from '../actions/actionTypes';
|
||||
import { ListState } from '../state/ListState';
|
||||
import { Reducer } from 'redux';
|
||||
|
||||
const initState = new ListState();
|
||||
|
||||
//Reducer determines how the state should change after every action.
|
||||
const listsReducer: Reducer<ListState> = (state: ListState = initState, action: Action): ListState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.ADD_LIST_REQUEST:
|
||||
return state; //You can show a loading message here.
|
||||
case ActionTypes.ADD_LIST_SUCCESS:
|
||||
return state.addList(action.payload);
|
||||
case ActionTypes.ADD_LIST_ERROR:
|
||||
return state;
|
||||
case ActionTypes.GET_LISTS_REQUEST:
|
||||
return state;
|
||||
case ActionTypes.GET_LISTS_SUCCESS:
|
||||
return state.setLists(action.payload);
|
||||
case ActionTypes.GET_LISTS_ERROR:
|
||||
return state; //.setMessage(action.payload); //You can show an error message here
|
||||
case ActionTypes.UPDATE_TITLE:
|
||||
return state.setTitle(action.payload);
|
||||
default: return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default listsReducer;
|
|
@ -0,0 +1,34 @@
|
|||
import * as Immutable from 'immutable';
|
||||
|
||||
export interface IListState {
|
||||
title: string;
|
||||
lists: string[];
|
||||
}
|
||||
|
||||
export const initialState: IListState = {
|
||||
title: "",
|
||||
lists: []
|
||||
};
|
||||
|
||||
//Immutable State.
|
||||
export class ListState extends Immutable.Record(initialState) implements IListState {
|
||||
|
||||
//Getters
|
||||
public readonly title: string;
|
||||
public readonly lists: string[];
|
||||
|
||||
//Setters
|
||||
public setTitle(newTitle: string): ListState {
|
||||
return this.set("title", newTitle) as ListState;
|
||||
}
|
||||
|
||||
public addList(item: string): ListState {
|
||||
return this.update("lists", (lists: string[]) => {
|
||||
return lists.concat(item);
|
||||
}) as ListState;
|
||||
}
|
||||
|
||||
public setLists(items: string[]): ListState {
|
||||
return this.set("lists", items) as ListState;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { createStore, applyMiddleware, Store } from 'redux';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import listsReducer from '../reducers/listsReducer';
|
||||
import { ListState } from '../state/ListState';
|
||||
|
||||
const loggerMiddleware = createLogger();
|
||||
|
||||
export default function configureStore() {
|
||||
|
||||
//do not use loggerMiddleware in production
|
||||
const listSateStore: Store<ListState> = createStore<ListState>(listsReducer, applyMiddleware(thunkMiddleware, loggerMiddleware));
|
||||
|
||||
return listSateStore;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/// <reference types="mocha" />
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
describe('DemoWebPart', () => {
|
||||
it('should do something', () => {
|
||||
assert.ok(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "commonjs",
|
||||
"jsx": "react",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": 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