Merge pull request #872 from KEMiCZA/dev

MobX multiple stores sample
This commit is contained in:
Laura Kokkarinen 2019-06-10 21:41:53 +03:00 committed by GitHub
commit 1ba602962a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 18847 additions and 0 deletions

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,12 @@
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.8.2",
"libraryName": "react-mobx-multiple-stores",
"libraryId": "94924d67-e7b2-415f-9bd3-3f69b18b37c8",
"packageManager": "npm",
"isDomainIsolated": false,
"componentType": "webpart"
}
}

View File

@ -0,0 +1,52 @@
# Webpart with React and Mobx using multiple stores
## Summary
A sample webpart that uses the [Mobx](https://mobx.js.org/) library with multiple stores to keep track of the applications state.
<img src="assets/demo.gif"/>
## 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
```
## Features
* 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
<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

@ -0,0 +1,18 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"mobx-tutorial-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/mobxTutorial/MobxTutorialWebPart.js",
"manifest": "./src/webparts/mobxTutorial/MobxTutorialWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"MobxTutorialWebPartStrings": "lib/webparts/mobxTutorial/loc/{locale}.js"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-mobx-multiple-stores",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,13 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-mobx-multiple-stores-client-side-solution",
"id": "94924d67-e7b2-415f-9bd3-3f69b18b37c8",
"version": "1.0.0.0",
"includeClientSideAssets": true,
"isDomainIsolated": false
},
"paths": {
"zippedPackage": "solution/react-mobx-multiple-stores.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -0,0 +1,7 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
build.initialize(gulp);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
{
"name": "react-mobx-multiple-stores",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "1.8.2",
"@microsoft/sp-lodash-subset": "1.8.2",
"@microsoft/sp-office-ui-fabric-core": "1.8.2",
"@microsoft/sp-property-pane": "1.8.2",
"@microsoft/sp-webpart-base": "1.8.2",
"@types/es6-promise": "0.0.33",
"@types/react": "16.7.22",
"@types/react-dom": "16.8.0",
"@types/webpack-env": "1.13.1",
"mobx": "5.9.4",
"mobx-react": "5.4.4",
"office-ui-fabric-react": "6.143.0",
"react": "16.7.0",
"react-dom": "16.7.0"
},
"resolutions": {
"@types/react": "16.7.22"
},
"devDependencies": {
"@microsoft/rush-stack-compiler-3.3": "0.2.13",
"@microsoft/sp-build-web": "1.8.2",
"@microsoft/sp-module-interfaces": "1.8.2",
"@microsoft/sp-tslint-rules": "1.8.2",
"@microsoft/sp-webpart-workbench": "1.8.2",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2",
"gulp": "~3.9.1"
}
}

View File

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

View File

@ -0,0 +1,89 @@
import { action, computed, observable, runInAction } from "mobx";
import { RootStore } from "./RootStore";
export enum ApplicationStatus {
CreateList = "Create List",
CreateItems = "Create Items",
Completed = "Completed"
}
export interface IFakeItem {
title: string;
important: boolean;
}
export class AppStore {
@observable public isLoadingConfiguration: boolean;
@observable public isLoadingOtherStuff: boolean;
@observable public status: ApplicationStatus;
@observable public listTitle: string;
@observable public items: IFakeItem[];
constructor(private rootStore: RootStore) {
this.setInitialState();
}
@action
private setInitialState(): void {
this.status = ApplicationStatus.CreateList;
this.listTitle = null;
this.items = [];
this.isLoadingConfiguration = true;
this.isLoadingOtherStuff = false;
}
@computed
public get appStatus(): string {
let result: string = `The current status is '${this.status}'. `;
result += this.status === ApplicationStatus.CreateItems || this.items.length > 0 ? `List '${this.listTitle}' successfully created. ` : "";
result += this.status === ApplicationStatus.Completed ? `In total there were ${this.items.length} items added. ` : "";
return result + `${this.rootStore.configStore.allowImportantItems ? "" : "Adding important items is currently not allowed."}`;
}
@computed
public get importantItems(): IFakeItem[] {
return this.items.filter(x => x.important);
}
@computed
public get isInitializing(): boolean {
return this.isLoadingConfiguration || this.isLoadingOtherStuff;
}
@action
public async createList(listTitle: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
// Mock creating list
setTimeout(() => {
// Make sure we change our state in an action.
runInAction(() => {
this.status = ApplicationStatus.CreateItems;
this.listTitle = listTitle;
resolve();
});
}, 1000);
});
}
@action
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
public confirmItems(): void {
this.status = ApplicationStatus.Completed;
}
}

View File

@ -0,0 +1,41 @@
import { action, observable } from "mobx";
import { RootStore } from "./RootStore";
export class ConfigStore {
@observable public isLoading: boolean;
@observable public allowImportantItems: boolean;
@observable public applicationTitle: string;
constructor(private rootStore: RootStore) {
this.setInitialState();
// Mock REST call for fetching configuration data
setTimeout(() => {
this.loadConfigration();
}, 1000);
}
@action
public setInitialState(): void {
this.isLoading = true;
this.allowImportantItems = true;
this.applicationTitle = null;
}
@action
private loadConfigration() {
this.isLoading = false;
this.rootStore.appStore.isLoadingConfiguration = false;
}
@action
public setApplicationTitle(title: string): void {
this.applicationTitle = title;
}
@action
public setAllowImportantItems(allow: boolean): void {
this.allowImportantItems = allow;
}
}

View File

@ -0,0 +1,17 @@
import { AppStore } from "./AppStore";
import { ConfigStore } from "./ConfigStore";
export enum Stores {
AppStore = "appStore",
ConfigurationStore = "configStore"
}
export class RootStore {
public readonly appStore: AppStore;
public readonly configStore: ConfigStore;
constructor() {
this.configStore = new ConfigStore(this);
this.appStore = new AppStore(this);
}
}

View File

@ -0,0 +1,28 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "c2ebe6a8-5c9f-4199-bff0-4e0222926d4f",
"alias": "MobxTutorialWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"supportedHosts": ["SharePointWebPart"],
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "MobxTutorial" },
"description": { "default": "An example of a webpart using mobx for managing the application state" },
"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

@ -0,0 +1,85 @@
import { Version } from '@microsoft/sp-core-library';
import { IPropertyPaneConfiguration, PropertyPaneTextField } from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart, PropertyPaneCheckbox } from '@microsoft/sp-webpart-base';
import { configure } from "mobx";
import * as strings from 'MobxTutorialWebPartStrings';
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { RootStore } from '../../stores/RootStore';
import MobxTutorialProvider from './components/MobxTutorialProvider';
// State modification should always happen through actions
configure({ enforceActions: "always" });
export interface IMobxTutorialWebPartProps {
ApplicationTitle: string;
AllowImportantItems: boolean;
}
export default class MobxTutorialWebPart extends BaseClientSideWebPart<IMobxTutorialWebPartProps> {
private readonly dependencies = { rootStore: new RootStore() };
protected onInit() {
return new Promise<void>((resolve, reject) => {
const { configStore } = this.dependencies.rootStore;
configStore.setAllowImportantItems(this.properties.AllowImportantItems);
configStore.setApplicationTitle(this.properties.ApplicationTitle);
resolve();
});
}
public render(): void {
const element: React.ReactElement<{}> = React.createElement(
MobxTutorialProvider,
{
stores: { ...this.dependencies.rootStore }
}
);
ReactDom.render(element, this.domElement);
}
protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void {
const { configStore } = this.dependencies.rootStore;
if (propertyPath === "ApplicationTitle") {
configStore.setApplicationTitle(newValue);
}
else if (propertyPath === "AllowImportantItems") {
configStore.setAllowImportantItems(newValue);
}
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(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('ApplicationTitle', {
label: strings.AppTitleFieldLabel,
}),
PropertyPaneCheckbox('AllowImportantItems', {
text: strings.AllowImportantItemsLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,40 @@
import { toJS } from 'mobx';
import { DetailsList, DetailsListLayoutMode, IColumn } from 'office-ui-fabric-react/lib/DetailsList';
import * as React from 'react';
import { IFakeItem } from '../../../stores/AppStore';
type DetailedFakeItemViewerProps = {
items: IFakeItem[];
};
export class DetailedFakeItemViewer extends React.Component<DetailedFakeItemViewerProps, {}> {
private _columns: IColumn[];
public state = {
items: []
};
constructor(props: DetailedFakeItemViewerProps) {
super(props);
this._columns = [
{ key: 'Title', name: 'Title', fieldName: 'title', minWidth: 100, maxWidth: 200, isResizable: true },
{ key: 'Important', name: 'Important', fieldName: 'important', minWidth: 100, maxWidth: 150, isResizable: true, onRender: (item: IFakeItem) => { return item.important ? "Yes" : "No"; } }
];
}
public render(): React.ReactElement<DetailedFakeItemViewerProps> {
const { items } = this.props;
return (
<div>
<DetailsList
compact={true}
items={toJS<IFakeItem[]>(items)}
columns={this._columns}
setKey="set"
layoutMode={DetailsListLayoutMode.fixedColumns}
selectionPreservedOnEmptyClick={true}
/>
</div>
);
}
}

View File

@ -0,0 +1,47 @@
import { inject, observer } from "mobx-react";
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import * as React from 'react';
import { AppStore } from "../../../stores/AppStore";
import { Stores } from '../../../stores/RootStore';
import { DetailedFakeItemViewer } from "./DetailedFakeItemViewer";
import { FakeItemCreator } from "./FakeItemCreator";
import styles from "./MobxTutorial.module.scss";
type FakeItemContainerStoreProps = {
appStore: AppStore;
};
type FakeItemContainerOwnProps = {};
type FakeItemContainerProps = Partial<FakeItemContainerStoreProps> & FakeItemContainerOwnProps;
@inject(Stores.AppStore)
@observer
export class FakeItemContainer extends React.Component<FakeItemContainerProps, {}> {
public render(): React.ReactElement<FakeItemContainerProps> {
const { appStore } = this.props;
return (
<div className={styles.grid}>
<div className={styles.row}>
<div className={`${styles.columnCreateItems}`}>
<FakeItemCreator></FakeItemCreator>
</div>
<div className={`${styles.columnItemDetails}`}>
<p>Count items: {appStore.items.length} | Count important items: {appStore.importantItems.length}</p>
<DetailedFakeItemViewer items={appStore.items}></DetailedFakeItemViewer>
</div>
</div>
<div className={styles.row}>
<PrimaryButton
text="Confirm items"
onClick={() => { appStore.confirmItems(); }}
className={styles.inputElement}
/>
</div>
</div>
);
}
}

View File

@ -0,0 +1,98 @@
import { inject, observer } from "mobx-react";
import { DefaultButton } from 'office-ui-fabric-react/lib/Button';
import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox';
import { TextField } from "office-ui-fabric-react/lib/TextField";
import * as React from 'react';
import { AppStore } from "../../../stores/AppStore";
import { Stores } from '../../../stores/RootStore';
import styles from "./MobxTutorial.module.scss";
import { ConfigStore } from "../../../stores/ConfigStore";
type FakeItemCreatorStoreProps = {
appStore: AppStore;
configStore: ConfigStore;
};
type FakeItemCreatorOwnProps = {};
type FakeItemCreatorState = {
itemTitle: string;
isImportant: boolean;
requiredTitle: string;
isLoading: boolean;
};
type FakeItemCreatorProps = Partial<FakeItemCreatorStoreProps> & FakeItemCreatorOwnProps;
const initialState: FakeItemCreatorState = {
itemTitle: "",
isImportant: false,
requiredTitle: undefined,
isLoading: false
};
@inject(Stores.AppStore, Stores.ConfigurationStore)
@observer
export class FakeItemCreator extends React.Component<FakeItemCreatorProps, FakeItemCreatorState> {
public state = initialState;
public render(): React.ReactElement<FakeItemCreatorProps> {
const { configStore } = this.props;
return (
<>
<TextField
label="Title"
errorMessage={this.state.requiredTitle}
onChange={this._onChangeItemTitle}
value={this.state.itemTitle}
className={styles.inputElement}
required
/>
<Checkbox
label="Important?"
onChange={this._onIsImportantCheckboxChange}
className={styles.inputElement}
checked={configStore.allowImportantItems && this.state.isImportant}
disabled={!configStore.allowImportantItems}
/>
<DefaultButton
onClick={() => this._onAddFakeItem()}
iconProps={{ iconName: 'Add' }}
allowDisabledFocus={true}
className={styles.inputElement}
disabled={this.state.isLoading}
>{this.state.isLoading ? "Adding..." : "Add"}</DefaultButton>
</>
);
}
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;
this.setState({ ...this.state, isLoading: true });
await appStore.addListItem({
title: this.state.itemTitle,
important: configStore.allowImportantItems && this.state.isImportant
});
this.setState(initialState);
}
private _onChangeItemTitle = (ev: React.FormEvent<HTMLInputElement>, newValue?: string): void => {
if (newValue === "" && newValue.length === 0) {
this.setState({ ...this.state, itemTitle: newValue, requiredTitle: "Required" });
}
else {
this.setState({ ...this.state, itemTitle: newValue, requiredTitle: undefined });
}
}
private _onIsImportantCheckboxChange = (ev: React.FormEvent<HTMLElement>, isChecked: boolean): void => {
this.setState({ ...this.state, isImportant: isChecked });
}
}

View File

@ -0,0 +1,77 @@
import { inject, observer } from "mobx-react";
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import * as React from 'react';
import { AppStore } from '../../../stores/AppStore';
import { Stores } from "../../../stores/RootStore";
import styles from './MobxTutorial.module.scss';
type ListCreatorStoreProps = {
appStore: AppStore;
};
type ListCreatorProps = Partial<ListCreatorStoreProps>;
type ListCreatorState = {
loading: boolean;
errorMessage: string;
listTitle: string;
};
@inject(Stores.AppStore)
@observer
export class ListCreator extends React.Component<ListCreatorProps, ListCreatorState> {
public state = {
loading: false,
errorMessage: undefined,
listTitle: null
};
public render(): React.ReactElement<ListCreatorProps> {
const spinner = (<Spinner size={SpinnerSize.xSmall} label="Creating list ..." labelPosition="right" />);
return (
<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>
</div>
</div>
);
}
public async createList() {
if (this.state.listTitle && this.state.listTitle.length > 0) {
this.setState({ ...this.state, loading: true, errorMessage: undefined });
await this.props.appStore.createList(this.state.listTitle);
this.setState({ ...this.state, loading: false });
}
else {
this.setState({ ...this.state, errorMessage: "Required" });
}
}
private _onChangeListTitle = (ev: React.FormEvent<HTMLInputElement>, newValue?: string) => {
if (newValue === "" && newValue.length === 0) {
this.setState({ ...this.state, listTitle: newValue, errorMessage: "Required" });
}
else {
this.setState({ ...this.state, listTitle: newValue });
}
}
}

View File

@ -0,0 +1,41 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.mobxTutorial {
margin: 20px;
.grid {
@include ms-Grid;
padding: 10px;
}
.row {
@include ms-Grid-row;
}
.columnCreateItems {
@include ms-Grid-col;
@include ms-lg4;
@include ms-xl4;
@include ms-sm12;
padding-right: 50px;
}
.columnItemDetails {
@include ms-Grid-col;
@include ms-lg8;
@include ms-xl8;
@include ms-sm12;
}
.title {
@include ms-font-xl;
}
.subTitle {
@include ms-font-l;
}
.inputElement {
margin-top: 5px;
}
}

View File

@ -0,0 +1,63 @@
import { inject, observer } from 'mobx-react';
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
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';
import { ProgressIndicator } from './ProgressIndicator';
export type MobxTutorialStoreProps = {
appStore: AppStore;
configStore: ConfigStore;
};
export type MobxTutorialProps = Partial<MobxTutorialStoreProps>;
@inject(Stores.AppStore, Stores.ConfigurationStore)
@observer
export class MobxTutorial extends React.Component<MobxTutorialProps, {}> {
public render(): React.ReactElement<MobxTutorialProps> {
const { appStore, configStore } = this.props;
if (appStore.isInitializing)
return (<Spinner size={SpinnerSize.large} label="Initializing... please hodl" ariaLive="assertive" labelPosition="left" />);
return (
<div className={styles.mobxTutorial}>
<div className={styles.row}>
<div className={styles.title}>{configStore.applicationTitle}</div>
<ProgressIndicator></ProgressIndicator>
</div>
{appStore.status === ApplicationStatus.CreateList ?
<div className={styles.row}>
<div className={styles.subTitle}>1) Create List</div>
<ListCreator></ListCreator>
</div>
: null
}
{appStore.status === ApplicationStatus.CreateItems ?
<div className={styles.row}>
<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
}
</div>
);
}
}

View File

@ -0,0 +1,18 @@
import { Provider } from "mobx-react";
import * as React from 'react';
import { MobxTutorial } from './MobxTutorial';
type MobxTutorialProviderOwnProps = {
stores: {};
};
export default class MobxTutorialProvider extends React.Component<MobxTutorialProviderOwnProps, {}> {
public render(): React.ReactElement<{}> {
return (
<Provider {...this.props.stores}>
<MobxTutorial></MobxTutorial>
</Provider>
);
}
}

View File

@ -0,0 +1,20 @@
import { inject, observer } from "mobx-react";
import * as React from 'react';
import { AppStore } from "../../../stores/AppStore";
import { Stores } from '../../../stores/RootStore';
type ProgressIndicatorStoreProps = {
appStore: AppStore;
};
type ProgressIndicatorOwnProps = {};
type ProgressIndicatorProps = Partial<ProgressIndicatorStoreProps> & ProgressIndicatorOwnProps;
@inject(Stores.AppStore)
@observer
export class ProgressIndicator extends React.Component<ProgressIndicatorProps, {}> {
public render(): React.ReactElement<ProgressIndicatorProps> {
const { appStore } = this.props;
return (<p>{appStore.appStatus}</p>);
}
}

View File

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

View File

@ -0,0 +1,11 @@
declare interface IMobxTutorialWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
AppTitleFieldLabel: string;
AllowImportantItemsLabel: string;
}
declare module 'MobxTutorialWebPartStrings' {
const strings: IMobxTutorialWebPartStrings;
export = strings;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,38 @@
{
"extends": "./node_modules/@microsoft/rush-stack-compiler-3.3/includes/tsconfig-web.json",
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"outDir": "lib",
"inlineSources": false,
"strictNullChecks": false,
"noUnusedLocals": false,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
},
"include": [
"src/**/*.ts"
],
"exclude": [
"node_modules",
"lib"
]
}

View File

@ -0,0 +1,30 @@
{
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"variable-name": false,
"whitespace": false
}
}

View File

@ -112,6 +112,7 @@ Web part displaying hierarchical information from SharePoint list<br/>[react-dis
Webhooks Realtime<br/>[react-webhooks-realtime](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-webhooks-realtime)|This web part demonstrates how to leverage the capabilities of SharePoint Webhooks.The libraries used by this web part are Socket.io, sp pnp js, moment.|![react-webhooks-realtime](https://raw.githubusercontent.com/SharePoint/sp-dev-fx-webparts/master/samples/react-webhooks-realtime/assets/spfx-react-webhooks-realtime.gif)|![drop](https://img.shields.io/badge/version-GA-green.svg)
Webpart showing Url validation for SharePoint using Office Graph<br/>[react-graph-evalurl](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-graph-evalurl)|This sample contains a class that evaluates the url input of a text field against the Microsoft Graph. It is possible to evalute the existance of the following three SharePoint Elements:|![react-graph-evalurl](https://raw.githubusercontent.com/SharePoint/sp-dev-fx-webparts/master/samples/react-graph-evalurl/assets/evaluation-client-searching-for-site-collection.png)|![drop](https://img.shields.io/badge/drop-1.4.1-green.svg)
Webpart with React and Mobx<br/>[react-mobx](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-mobx)|Sample webpart implementation that uses Mobx to keep track of its state.|![react-mobx](https://i.gyazo.com/e6f1903b9a9c8201985cd25cc1fe28bc.gif)|![drop](https://img.shields.io/badge/drop-drop5-red.svg)
Webpart with React and Mobx (multiple stores)<br/> [react-mobx-multiple-stores](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-mobx-multiple-stores)|A sample webpart that uses the [Mobx](https://mobx.js.org/) library with multiple stores to keep track of the applications state.|![react-mobx-multiple-stores](https://raw.githubusercontent.com/KEMiCZA/sp-dev-fx-webparts/dev/samples/react-mobx-multiple-stores/assets/demo.gif)|![drop](https://img.shields.io/badge/version-1.8.2-green.svg)
Webpart with React and Redux<br/>[react-redux](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-redux)|Sample webpart implementation that uses Redux to keep track of its state.|![react-redux](https://i.gyazo.com/729c4addf6c992513f8eb91a3fa0e302.gif)|![drop](https://img.shields.io/badge/drop-drop5-red.svg)
Yammer REST API SPFx webpart<br/>[react-yammer-api](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-yammer-api)|This sample shows how Yammer REST APIs can be consumed by using SharePoint Framework React webpart and the Yammer JavaScript SDK. The SPFx webpart contains wrapper around the Yammer JavaScript SDK that can be extended for fluent typescript api experience.|![react-yammer-api](https://raw.githubusercontent.com/SharePoint/sp-dev-fx-webparts/master/samples/react-yammer-api/assets/spfx-yammer-api-webpart.jpg)|![drop](https://img.shields.io/badge/drop-GA-green.svg)
Youtube Web Part<br/>[react-youtube](https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples/react-youtube)|This web part allows to search and view the Youtube videos, across the Youtube API, directly on a SharePoint page, furthermore the property panel offers the possibility to specify the api key, the number of items to display and it is also possible specify a Youtube Channel Id.|![react-youtube](https://raw.githubusercontent.com/SharePoint/sp-dev-fx-webparts/master/samples/react-youtube/assets/Preview.gif)|![drop](https://img.shields.io/badge/version-GA-green.svg)