added readme and demo gif & smaller code cleanup

This commit is contained in:
Kemal Sinanagic 2019-05-24 19:34:58 +02:00
parent c26e0113bd
commit 333d6936d5
13 changed files with 134 additions and 82 deletions

View File

@ -1,26 +1,52 @@
## react-mobx-multiple-stores
# Webpart with React and Mobx using multiple stores
This is where you include your WebPart documentation.
## Summary
A sample webpart that uses the [Mobx](https://mobx.js.org/) library with multiple stores to keep track of the applications state.
### Building the code
<img src="assets/demo.gif"/>
```bash
git clone the repo
npm i
npm i -g gulp
gulp
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-1.8.2-green.svg)
## Applies to
* [SharePoint Framework](https://dev.office.com/sharepoint)
* [SharePoint Framework Webpart Samples](https://github.com/SharePoint/sp-dev-fx-webparts)
* [Office 365 developer tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-developer-tenant)
## Solution
Solution|Author(s)
--------|---------
react-mobx-multiple-stores | Kemal Sinanagic / [@kemicza](http://twitter.com/kemicza) / kemicza@gmail.com
## Version history
Version|Date|Comments
-------|----|--------
1.0|May 24, 2019|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
```sh
$ git clone https://github.com/SharePoint/sp-dev-fx-webparts
$ cd sp-dev-fx-webparts/samples/react-mobx-multiple-stores
$ npm install
$ gulp serve
```
This package produces the following:
## Features
* lib/* - intermediate-stage commonjs build artifacts
* dist/* - the bundled script, along with other resources
* deploy/* - all resources which should be uploaded to a CDN.
* Enforces that the state always needs be updated in **actions**, using the <em>always</em> flag for <em>enforceActions</em>.
* Demonstrates the **toJS** method to convert an observable array to a javascript structure. This is used to render the items in a DetailsList.
* Out-of-the-box MobX **decorators** to keep the code clean.
* **Asynchronous** actions
* MobX **computed** values
* **Typescript** version 3.3.4 using <em>@microsoft/rush-stack-compiler-3.3</em> for compatibility with the latest MobX version and typings
### Build options
gulp clean - TODO
gulp test - TODO
gulp serve - TODO
gulp bundle - TODO
gulp package-solution - TODO
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-mobx-multiple-stores" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

View File

@ -7025,7 +7025,8 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
@ -7440,7 +7441,8 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@ -7496,6 +7498,7 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -7539,12 +7542,14 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
}
}
},

View File

@ -2,7 +2,6 @@ import { action, computed, observable, runInAction } from "mobx";
import { RootStore } from "./RootStore";
export enum ApplicationStatus {
Loading = "Loading",
CreateList = "Create List",
CreateItems = "Create Items",
Completed = "Completed"
@ -69,8 +68,18 @@ export class AppStore {
}
@action
public addListItem(item: IFakeItem): void {
this.items.push(item);
public async addListItem(item: IFakeItem): Promise<void> {
return new Promise<void>((resolve, reject) => {
// Mock adding item a new item
setTimeout(() => {
// Make sure we change our state in an action.
runInAction(() => {
this.items.push(item);
resolve();
});
}, 500);
});
}
@action

View File

@ -19,7 +19,7 @@
"group": { "default": "Other" },
"title": { "default": "MobxTutorial" },
"description": { "default": "An example of a webpart using mobx for managing the application state" },
"officeFabricIconFontName": "Page",
"iconImageUrl": "data:image\/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM\/rhtAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAnwSURBVFhHzVjbq1xnFV\/7cmbmzJzL5NySKhQMpVh90odSEaWCCkWE1AZRqtLWW0Ef9Kn1glTESqVWBR98EEWUViESY+ODCqK0vliw4EsrCMZYJTbnNsnJmZyZffH3+62195mT+Ad05cxe61vfuvy+9V32t5PUIHsNUwtw8ydfsqt\/fMaSJLUGcd1KJMj+h0dtFYT+fGIVhUQqf4ScwLKCKoFcQYYaej7REo\/oeDivraxKW+h1bfXdD1jvvseodYBbALf5zOOWLw2lVBJGb4KQPFO0oYfQ7SaWQi9w8ole4YDecaAfj5mYesoHNtKZlXVpvU7X1oZLVuxcsu4Hvmz90191gBc+edLKvW1L8o5AKJgCU\/bxM5c6K1RNACrL88Tm5lAryCi92yNjHXICuYKMEOCVorgp9AjCKtcAWZU14uS2sTK0skBfeWBJ\/5gNn3rZUqZN0kzOMEeDSZRP5NCYmeQyi4nSWYnA0goQ9Oj2wUQc9kFmJRM8GF82kNNULYXO88zBlQDXVAhLjeRPkUfUtAS1wSAHFHm4CarhShk2bgmAs3ryQRINBp2NBWNKISLgdAYcjDkQssh4CBBSKnToaFgTR+EbFyiRlYlZOaxr1wqYasIW\/CMOHvKFzPiMAlMNnPGPry5rowkciscp90xydoAuglgJ9AlnEIMoWLRl7X+SMXARobkttAQLnVfRY7qDxxEE6Fg5rkm2E20qKJuNFrunraAc8WgnNJinwo8gEVU7kp0IRh+s6TYIK0pbPrWGYCvQ0MgX645xWLGNlSWBJDgK3EwCB87TgFlIEVshIzD+wpH\/iiubVu1uWrG9aeX2lpVXthwk7WgB0fe5A\/HpQ2ByhfMG+8vRCHFGtppcs3r3v1Zduezg2ItSavdrmyuYSMfMxU\/fjmNmy5Isk3FzLJQ7m3b7+bCcoZfvSSxfWYOEoLDlUZNnSMQqE1gcOypIHDvF9o7d9uzNsbbun7N0ecNBalrhN51Ysrhma99+yStIwE4cKaoCBRd02uvZ1s+\/Zju\/+q5tPe0ne3l1W9zJPcuYGvlSy0qgMl5F6CFTX6EIpPHZr9v133zP9n\/5DUu6\/aPgFAtirEFV8F9RwTqbo8mRKtbjPauxE4rNid3xHF5HAPj3D65aigoqDqz51+\/jFYnK3VhFAivL0gaTkR3\/IQY3OGZbp2BzbBH98OktwJhWCBO8LqaWLKzY+nf+FmvQM4nxqUqgh1VM5hcsGyxb2ldnS221wkvHDQDxTS49dzIrB6B9zMR8r6O2qIf+eWyS7o3gYE8fcalnAAbTlHgTBBmGnPIbiapq\/woqvwsQU59mavlHH1St2tuxTnVgC\/2u3jpNHEEBAg2SgCiLR2\/DQQ6QMp3BnUUl0NseOxG8oWp\/zzYeetJe9+gZm9t4gxUTLGzo+fawqrB0MLTjj5y19QefsmLvCqO5IynyedUggPs6RBujoG1j7QBJAcBZNEgQWZFGJRFeNeY0Hx63xbedsuW777diPHYDUD0dW\/\/OU\/rVi+uwLdwxiJXUZmBQbIZmk6im3BzoYkVJhwBJBMFOsdSriIZXcYZo05m30W9\/oObC209bfVDorcLASTGx5Xd8SH0Hf\/gRzqEeXVryDRugwHUUCSStWEEMgIYgzxzAxA8ZiOuxAXmUMtzd9v78O8mdEyctWxhYURTKu9TvWXbbXeqbvvhrq\/Nug0o6EjcPwfGAbqcXpGnXT80GoG8M2VAD7iw2DB6a5lmCT9pN7eqffqHm8nsesmJ\/bEud1PI775Nu8sJZqzt9uMcaC\/JCoY1p1iuOCiRod3L7nJ1inqqhVCz6RENVPLRs+7P5gQ5x0vCeh20OF81yfNU6d39cOh7GXApthZp5I6HdXg4IjrdgcF5M2uMIpLQOxDGqLxRkbRVnqenH1I1ffA6bdmrdW9+ET4Y1m0xKm3vzu3DMFDb56+9jelkPAnE\/5lAbgHW94u2aLwW\/qrOzBTlbFwTwiVWXowt2uE0ihwTapYOOjc48LtXCez9l6V0fljw+h9fYYNH9CdDziSSiUhUrqAWOdagKCrFXNqgFqED4+UxDIgc5iwaIkkwiSA8fWnvnviV58X2ftcX3f07y+Nkn8Z4dxLoiHSalyK0hcJQacOACza4wP1pBEqooAJJdIDtC0DEAP3TSLLfq4Lrtv3AeN5xbrHvyLTb5y3mchdeRh574wdgvvZ5VT4ELfUwzB+0XV3a57QzAeHtQandW0xNBSezCrwNgWZr6Gse7eufHj4SB2einj+pdq52K46XZqSiME3yYyTFwg4CpctzVkBuQIAfYZg9CQPYrAC3Am5FQlaORZQ5OGoAtNl+x\/3z+rfYKfuNLF1FeXA7irEvIaRrkbnzG0UJwHAAH0oCL0XheoaEQbw+q4tipcGxU+yOrrrHfY+UA56NnXEwLIqa9eZtevmjFqxeszOeVSBVUVcDDnsRY1bWRjiQlJzjyGJCu\/EwPCoDRCuJa4c6163s2PP0FW3ngCVt72C+sJOYiQL43CU67kPa4kVuOOyVGXxCgqgLjNqGjXP7EY7b80W\/a4r1fRAF4kUCV47XHJcFQSgDShfXfn7kD3xqbqNqcdyIdE5Y7l+zkOTdsiK2Lp\/ChP9wIcBw9Ozgoryarys8A\/ryCuOhuv2qvP3M0Fumf9yaWDVfbGKQK7\/J0Yc1u\/f4\/GoBvxE0Z3yRJjoSC7\/9gzw+bCp9uc1nswTS3ZGn1CDheELjKHJx780gd4FWIWyGC0Ba3cnx81fzfCP\/T\/KVLKxiD5yRpTZZTAFwFwAsxxSAdAmzJzjcJKVtat976Lbjin8BITwic5gvgBITg8BNgJuYiZcUKB+22ftal8M2G+FQ4hk8GVI2xmuOHVefPZbjxAWoBOnlixaUjmhnXFd10TkFEn84tCiCBox+aWm9y9jgFQLI9+5aQDbjHYjAfoAicS1d6xgQJYMwqGu4Y5jrnSM05xd3lpm5xeOlswKFmTK6rFTYKr\/nou\/kgpgM7ZmOw4s6puel\/FiIzGAQE4DnHlvszkVfKX3HgSgANGAfFr0GCaxJQyQoS3P876+jbxIAA8+CMTXtwkgNs7CRjregMhCgFOdxYGUyTArQHsMxBWGMtOLeVFv3N\/90IHJO2Z91RLj37IylbpADITACJFmdVlULbf4dnnQdgQDUjxMzUoEoaiNQMiHWoXSvYaAYY5hM42qANPntxVQXjv80EcIDvh3J0GYtmimNgqvsdP57rcgJdIV7hfodPN93zbEqd91fgNoWMT89WD1\/qaVseuE8N39kY\/O8NPHCpgF6+HqMCL3bHtvjOjwigzkEKu09\/xfaf\/xk0fI2xChy5LNqKOOejYXjiT7Vt9FJFgxKKNd\/8Zzvb6o8+CXjA5vCaVQrc+seekFUL8LVJZv8DKLJX7WNRDjkAAAAASUVORK5CYII=",
"properties": {
"ApplicationTitle": "Mobx Tutorial Title (change in webpart properties) 😎",
"AllowImportantItems": true

View File

@ -3,7 +3,7 @@ import { DetailsList, DetailsListLayoutMode, IColumn } from 'office-ui-fabric-re
import * as React from 'react';
import { IFakeItem } from '../../../stores/AppStore';
export type DetailedFakeItemViewerProps = {
type DetailedFakeItemViewerProps = {
items: IFakeItem[];
};

View File

@ -7,12 +7,12 @@ import { DetailedFakeItemViewer } from "./DetailedFakeItemViewer";
import { FakeItemCreator } from "./FakeItemCreator";
import styles from "./MobxTutorial.module.scss";
export type FakeItemContainerStoreProps = {
type FakeItemContainerStoreProps = {
appStore: AppStore;
};
export type FakeItemContainerOwnProps = {};
export type FakeItemContainerProps = Partial<FakeItemContainerStoreProps> & FakeItemContainerOwnProps;
type FakeItemContainerOwnProps = {};
type FakeItemContainerProps = Partial<FakeItemContainerStoreProps> & FakeItemContainerOwnProps;
@inject(Stores.AppStore)
@observer
@ -22,14 +22,6 @@ export class FakeItemContainer extends React.Component<FakeItemContainerProps, {
return (
<div className={styles.grid}>
<div className={styles.row}>
<PrimaryButton
text="Confirm items"
onClick={() => { appStore.confirmItems(); }}
className={styles.inputElement}
/>
</div>
<div className={styles.row}>
<div className={`${styles.columnCreateItems}`}>
<FakeItemCreator></FakeItemCreator>
@ -41,6 +33,14 @@ export class FakeItemContainer extends React.Component<FakeItemContainerProps, {
</div>
</div>
<div className={styles.row}>
<PrimaryButton
text="Confirm items"
onClick={() => { appStore.confirmItems(); }}
className={styles.inputElement}
/>
</div>
</div>
);
}

View File

@ -8,27 +8,27 @@ import { Stores } from '../../../stores/RootStore';
import styles from "./MobxTutorial.module.scss";
import { ConfigStore } from "../../../stores/ConfigStore";
export type FakeItemCreatorStoreProps = {
type FakeItemCreatorStoreProps = {
appStore: AppStore;
configStore: ConfigStore;
};
export type FakeItemCreatorOwnProps = {
};
type FakeItemCreatorOwnProps = {};
export type FakeItemCreatorState = {
type FakeItemCreatorState = {
itemTitle: string;
isImportant: boolean;
requiredTitle: string;
isLoading: boolean;
};
export type FakeItemCreatorProps = Partial<FakeItemCreatorStoreProps> & FakeItemCreatorOwnProps;
type FakeItemCreatorProps = Partial<FakeItemCreatorStoreProps> & FakeItemCreatorOwnProps;
const initialState: FakeItemCreatorState = {
itemTitle: "",
isImportant: false,
requiredTitle: undefined
requiredTitle: undefined,
isLoading: false
};
@inject(Stores.AppStore, Stores.ConfigurationStore)
@ -39,8 +39,7 @@ export class FakeItemCreator extends React.Component<FakeItemCreatorProps, FakeI
public render(): React.ReactElement<FakeItemCreatorProps> {
const { configStore } = this.props;
return (
<div className={styles.row}>
<>
<TextField
label="Title"
errorMessage={this.state.requiredTitle}
@ -59,24 +58,28 @@ export class FakeItemCreator extends React.Component<FakeItemCreatorProps, FakeI
/>
<DefaultButton
onClick={this._onAddFakeItem}
onClick={() => this._onAddFakeItem()}
iconProps={{ iconName: 'Add' }}
allowDisabledFocus={true}
className={styles.inputElement}
>Add</DefaultButton>
</div>
disabled={this.state.isLoading}
>{this.state.isLoading ? "Adding..." : "Add"}</DefaultButton>
</>
);
}
private _onAddFakeItem = (): void => {
private async _onAddFakeItem(): Promise<void> {
if (this.state.itemTitle === "" && this.state.itemTitle.length === 0) {
this.setState({ ...this.state, requiredTitle: "Required" });
return;
}
const { appStore, configStore } = this.props;
appStore.addListItem({ title: this.state.itemTitle, important: configStore.allowImportantItems && this.state.isImportant });
this.setState({ ...this.state, isLoading: true });
await appStore.addListItem({
title: this.state.itemTitle,
important: configStore.allowImportantItems && this.state.isImportant
});
this.setState(initialState);
}

View File

@ -7,12 +7,12 @@ import { AppStore } from '../../../stores/AppStore';
import { Stores } from "../../../stores/RootStore";
import styles from './MobxTutorial.module.scss';
export type ListCreatorStoreProps = {
type ListCreatorStoreProps = {
appStore: AppStore;
};
export type ListCreatorProps = Partial<ListCreatorStoreProps>;
export type ListCreatorState = {
type ListCreatorProps = Partial<ListCreatorStoreProps>;
type ListCreatorState = {
loading: boolean;
errorMessage: string;
listTitle: string;
@ -31,23 +31,25 @@ export class ListCreator extends React.Component<ListCreatorProps, ListCreatorSt
const spinner = (<Spinner size={SpinnerSize.xSmall} label="Creating list ..." labelPosition="right" />);
return (
<div className={styles.row}>
<TextField
label="List title"
errorMessage={this.state.errorMessage}
onChange={this._onChangeListTitle}
value={this.state.listTitle}
disabled={this.state.loading}
required
/>
<div className={styles.grid}>
<div className={styles.row}>
<TextField
label="List title"
errorMessage={this.state.errorMessage}
onChange={this._onChangeListTitle}
value={this.state.listTitle}
disabled={this.state.loading}
required
/>
<PrimaryButton
onClick={() => this.createList()}
disabled={this.state.loading}
className={styles.inputElement}
>
{this.state.loading ? spinner : "Create List"}
</PrimaryButton>
<PrimaryButton
onClick={() => this.createList()}
disabled={this.state.loading}
className={styles.inputElement}
>
{this.state.loading ? spinner : "Create List"}
</PrimaryButton>
</div>
</div>
);
}

View File

@ -4,6 +4,7 @@ import * as React from 'react';
import { ApplicationStatus, AppStore } from '../../../stores/AppStore';
import { ConfigStore } from '../../../stores/ConfigStore';
import { Stores } from '../../../stores/RootStore';
import { DetailedFakeItemViewer } from './DetailedFakeItemViewer';
import { FakeItemContainer } from './FakeItemContainer';
import { ListCreator } from './ListCreator';
import styles from './MobxTutorial.module.scss';
@ -37,8 +38,7 @@ export class MobxTutorial extends React.Component<MobxTutorialProps, {}> {
<div className={styles.subTitle}>1) Create List</div>
<ListCreator></ListCreator>
</div>
:
null
: null
}
{appStore.status === ApplicationStatus.CreateItems ?
@ -46,6 +46,13 @@ export class MobxTutorial extends React.Component<MobxTutorialProps, {}> {
<div className={styles.subTitle}>2) Create Items</div>
<FakeItemContainer></FakeItemContainer>
</div>
: null
}
{appStore.status === ApplicationStatus.Completed ?
<div className={styles.row}>
<DetailedFakeItemViewer items={appStore.items}></DetailedFakeItemViewer>
</div>
:
null
}

View File

@ -2,7 +2,7 @@ import { Provider } from "mobx-react";
import * as React from 'react';
import { MobxTutorial } from './MobxTutorial';
export type MobxTutorialProviderOwnProps = {
type MobxTutorialProviderOwnProps = {
stores: {};
};

View File

@ -3,12 +3,12 @@ import * as React from 'react';
import { AppStore } from "../../../stores/AppStore";
import { Stores } from '../../../stores/RootStore';
export type ProgressIndicatorStoreProps = {
type ProgressIndicatorStoreProps = {
appStore: AppStore;
};
export type ProgressIndicatorOwnProps = {};
export type ProgressIndicatorProps = Partial<ProgressIndicatorStoreProps> & ProgressIndicatorOwnProps;
type ProgressIndicatorOwnProps = {};
type ProgressIndicatorProps = Partial<ProgressIndicatorStoreProps> & ProgressIndicatorOwnProps;
@inject(Stores.AppStore)
@observer

View File

@ -1,8 +1,8 @@
define([], function() {
define([], function () {
return {
"PropertyPaneDescription": "Application configuration",
"BasicGroupName": "Group Name",
"AppTitleFieldLabel": "Application Title Field",
"BasicGroupName": "Global Settings",
"AppTitleFieldLabel": "Application Title",
"AllowImportantItemsLabel": "Allow important items"
}
});