added design patterns samples, abstract factory, factory method, builder, singleton (#524)

This commit is contained in:
Luis Valencia 2018-06-11 11:44:26 +02:00 committed by Vesa Juvonen
parent 88cbbf2300
commit 9c7e33b521
154 changed files with 90579 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,8 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.4.1",
"libraryName": "abstract-factory",
"libraryId": "bf29a897-ff82-4d9d-86c2-d13df911c3ef",
"environment": "spo"
}
}

View File

@ -0,0 +1,381 @@
# Abstract Factory Design Pattern
## Summary
The abstract factory pattern will allow to define an interface for the creation of objects without specifying their concrete classes. The objective of this pattern is that a class depends on the behavior of the abstract factory, which in turn will be implemented by different concrete classes that are changed at runtime based on some kind of configuration or predefined parameter.
## 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)
## Prerequisites
> N/A
## Solution
Solution|Author(s)
--------|---------
designpatterns-typescript\abstractfactory | [@levalencia](https://www.twitter.com/levalencia)
## Version history
Version|Date|Comments
-------|----|--------
1.0|May 15, 2018|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.**
---
### Abstract factory pattern
A very good real life scenario where this pattern can be used is in Data Application Layers scenario, more than often developers and architects are faced with requirements where an application needs to be able to access different databases or event different database servers which have different drivers, but the users want to to that without changing a lot of code, something that can be switched from an easy parameter somewhere.
For the sake of simplicity lets suppose you work at Company A, and company A acquired company B, at company A you have a webpart developed that brings Customer Information from Sharepoint List, but at Company B which was acquired and in the process of merging, they have Product Information in their own CRM which exposes data via REST APIs or just a JSON file.
The users wants to see their products in the same Sharepoint page using the same webpart, meaning that the webpart needs to be added twice with different parameters to the same page and users can search for customers information on both data sources, with the same source code.
### Project Structure
![](http://www.luisevalencia.com/content/images/2018/01/2018-01-04-12_33_49-TypescriptDesignPatterns02AbstractFactoryWebPart.ts---TypescriptDesignPatterns02.png)
As seen above we have a Factory component and in there we have all files that our project needs, lets discuss them one by one.
##### Customer.ts
Our model or data access object, nothing to fancy, the idea is to show the pattern, not complex Data Transfer Objects.
```typescript
class Customer{
public id: string;
public firstName: string;
public lastName: string;
}
export default Customer;
```
#### DatasourcesEnum.ts
Yay!, we have Enums on typescript, and this will allows to ease work with selections on dropdowns, checkboxlists, etc. In this case is just a dropdown list with 2 options, but I guess you see the benefit here.
```typescript
enum DataSources {
SharepointList = "SharepointList",
JsonData = "JsonData"
}
export default DataSources;
```
##### DaoFactory.ts
This is the abstract class DAO Factory that would need to be implemented, for the ease of sake, I am doing only one DAO, Customers, but you can use the same pattern for many different DTOs as well on the same class.
```typescript
import ICustomerDAO from "./ICustomerDAO";
import DataSources from "./DatasourcesEnum";
abstract class DAOFactory {
public abstract getCustomerDAO(): ICustomerDAO;
public static getDAOFactory(whichFactory: DataSources): DAOFactory {
switch (whichFactory) {
case DataSources.SharepointList:
return new SharepointListDAOFactory();
case DataSources.JsonData:
return new JsonDAOFactory();
default :
return null;
}
}
}
export default DAOFactory;
import SharepointListDAOFactory from "./SharepointListDAOFactory";
import JsonDAOFactory from "./JsonDAOFactory";
```
##### JsoDAOFactory.ts
This class is just the implementation of the factory method
```typescript
import DAOFactory from "./DaoFactory";
import JsonCustomerDAO from "./JsonCustomerDAO";
import ICustomerDao from "./ICustomerDao";
class JsonDAOFactory extends DAOFactory {
public getCustomerDAO(): ICustomerDao{
return new JsonCustomerDAO();
}
}
export default JsonDAOFactory;
```
##### SharepointListDAOFactory.ts
This class is just the implementation of the factory method
```typescript
import DaoFactory from "./DaoFactory";
import ICustomerDao from "./ICustomerDao";
import SharepointCustomerDao from "./SharepointCustomerDAO";
class SharepointListDAOFactory extends DaoFactory {
public getCustomerDAO(): ICustomerDao{
return new SharepointCustomerDao();
}
}
export default SharepointListDAOFactory;
```
##### ICustomerDao.ts
Now, this is the customer interface which defines the methods that would need to be implemented and that depends on the data source endpoint, database or driver, or whatever.
```typescript
import Customer from "./Customer";
interface ICustomerDao {
insertCustomer(): number;
deleteCustomer(): boolean;
findCustomer(): Customer;
updateCustomer(): boolean;
listCustomers(): Customer[];
}
export default ICustomerDao;
```
##### JsonCustomerDAO.ts
Implementation on these methods are left to the reader, but the main idea here is to implement based on the datasource the Data Access Logic here and return the strongly typed objects where needed.
```typescript
import ICustomerDao from "./ICustomerDao";
import Customer from "./Customer";
class JsonCustomerDAO implements ICustomerDao{
public insertCustomer(): number {
// implementation to be done by reader
return 1;
}
public deleteCustomer(): boolean {
// implementation to be done by reader
return true;
}
public findCustomer(): Customer {
// implementation to be done by reader
return new Customer();
}
public updateCustomer(): boolean {
// implementation to be done by reader
return true;
}
public listCustomers(): Customer[] {
// implementation to be done by reader
let c1: Customer= new Customer();
let c2: Customer= new Customer();
let list: Array<Customer> = [c1, c2 ];
return list;
}
}
export default JsonCustomerDAO;
```
##### SharepointCustomerDAO.ts
Implementation on these methods are left to the reader, but the main idea here is to implement based on the datasource the Data Access Logic here and return the strongly typed objects where needed.
```typescript
import ICustomerDao from "./ICustomerDao";
import Customer from "./Customer";
class SharepointCustomerDao implements ICustomerDao {
public insertCustomer(): number {
// implementation to be done by reader
return 1;
}
public deleteCustomer(): boolean {
// implementation to be done by reader
return true;
}
public findCustomer(): Customer {
// implementation to be done by reader
return new Customer();
}
public updateCustomer(): boolean {
// implementation to be done by reader
return true;
}
public listCustomers(): Customer[] {
// implementation to be done by reader
let c1: Customer = new Customer();
let c2: Customer = new Customer();
let list: Array<Customer> = [c1, c2 ];
return list;
}
}
export default SharepointCustomerDao;
```
##### The component
This is where we actually see the entire benefit of the abstract factory pattern, as you can see the code is really short here and easy to read, no custom business logic, and everything so easy to maintain.
We create a private property of type ICustomerDao to be instantiated on the setDaos method based on the input of the user in the property pane. This method is only called in the constructor once.
And then in the render method we just get the Customer items from the datasource, and as you can see, its totally generic, no custom logic based on the datasource selected.
```typescript
import * as React from 'react';
import { IAbstractfactoryProps } from "./IAbstractFactoryProps";
import { IAbstractFactoryState } from "./IAbstractFactoryState";
import styles from './Abstractfactory.module.scss';
import { escape } from '@microsoft/sp-lodash-subset';
import DaoFactory from "./DaoFactory";
import ICustomerDao from "./ICustomerDao";
import DataSources from "./DatasourcesEnum";
export default class Abstractfactory extends React.Component<IAbstractfactoryProps, {}> {
private customerDao: ICustomerDao;
constructor(props: IAbstractfactoryProps, state: IAbstractFactoryState) {
super(props);
this.setInitialState();
this.setDaos(props.datasource);
}
public render(): React.ReactElement<IAbstractfactoryProps> {
this.state = {
items: this.customerDao.listCustomers(),
};
return null;
}
public setInitialState(): void {
this.state = {
items: []
};
}
private setDaos(datasource: string): void {
const data: any = datasource === "Sharepoint" ? DataSources.SharepointList : DataSources.JsonData;
this.customerDao = DaoFactory.getDAOFactory(data).getCustomerDAO();
}
}
```
And just for your understanding, I show below the props and states clases
##### IAbstractfactoryProps.ts
```typescript
export interface IAbstractfactoryProps {
datasource: string;
}
```
##### IAbstractFactoryState.ts
```typescript
import Customer from "./Customer";
export interface IAbstractFactoryState {
items: Customer[];
}
```
And finally the webpart code
##### AbstractFactoryWebPart.ts
```typescript
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneDropdown
} from "@microsoft/sp-webpart-base";
import * as strings from 'AbstractfactoryWebPartStrings';
import Abstractfactory from './components/Abstractfactory';
import { IAbstractfactoryProps } from './components/IAbstractfactoryProps';
import { IAbstractfactoryWebPartProps } from "./IAbstractfactoryWebPartProps";
export default class AbstractfactoryWebPart extends BaseClientSideWebPart<IAbstractfactoryWebPartProps> {
public render(): void {
const element: React.ReactElement<IAbstractfactoryProps > = React.createElement(
Abstractfactory,
{
datasource: this.properties.datasource
}
);
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: [
PropertyPaneDropdown("datasource", {
label: "DataSource",
options: [
{ key: "1", text: "Sharepoint"},
{ key: "2", text: "JSON" }
],
selectedKey: "1",
})
]
}
]
}
]
};
}
}
```
>Conclusion:
We all know that Sharepoint Framework Projects are transpiled and bundled into one single JS file, however regardless of that for those of us who have worked in huge projects and are only User Interface Developers, we know that we can do better than what the standard samples show us in the standard documentation, with the above post I wanted to show you how simple is to create maintenable code, code that anyone can read, and later modify.
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/designpatterns-typescript/abstractfactory" />

View File

@ -0,0 +1,18 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"abstractfactory-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/abstractfactory/AbstractfactoryWebPart.js",
"manifest": "./src/webparts/abstractfactory/AbstractfactoryWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"AbstractfactoryWebPartStrings": "lib/webparts/abstractfactory/loc/{locale}.js"
}
}

View File

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

View File

@ -0,0 +1,7 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "abstract-factory",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,12 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "abstract-factory-client-side-solution",
"id": "bf29a897-ff82-4d9d-86c2-d13df911c3ef",
"version": "1.0.0.0",
"includeClientSideAssets": true
},
"paths": {
"zippedPackage": "solution/abstract-factory.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://dev.office.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,45 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/tslint.schema.json",
// Display errors as warnings
"displayAsWarning": true,
// The TSLint task may have been configured with several custom lint rules
// before this config file is read (for example lint rules from the tslint-microsoft-contrib
// project). If true, this flag will deactivate any of these rules.
"removeExistingRules": true,
// When true, the TSLint task is configured with some default TSLint "rules.":
"useDefaultConfigAsBase": false,
// Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules
// which are active, other than the list of rules below.
"lintConfig": {
// Opt-in to Lint rules which help to eliminate bugs in JavaScript
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-case": true,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"valid-typeof": true,
"variable-name": false,
"whitespace": false
}
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://dev.office.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,33 @@
{
"name": "abstract-factory",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"react": "15.6.2",
"react-dom": "15.6.2",
"@types/react": "15.6.6",
"@types/react-dom": "15.5.6",
"@microsoft/sp-core-library": "~1.4.1",
"@microsoft/sp-webpart-base": "~1.4.1",
"@microsoft/sp-lodash-subset": "~1.4.1",
"@microsoft/sp-office-ui-fabric-core": "~1.4.1",
"@types/webpack-env": ">=1.12.1 <1.14.0"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.4.1",
"@microsoft/sp-module-interfaces": "~1.4.1",
"@microsoft/sp-webpart-workbench": "~1.4.1",
"gulp": "~3.9.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0",
"ajv": "~5.2.2"
}
}

View File

@ -0,0 +1,26 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "dad2e81a-54ce-48c4-bb02-29e52ab9a785",
"alias": "AbstractfactoryWebPart",
"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,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "abstractfactory" },
"description": { "default": "abstractfactory description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "abstractfactory"
}
}]
}

View File

@ -0,0 +1,77 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneDropdown
} from "@microsoft/sp-webpart-base";
import * as strings from 'AbstractfactoryWebPartStrings';
import Abstractfactory from './components/Abstractfactory';
import { IAbstractFactoryProps } from './components/IAbstractFactoryProps';
import { IAbstractfactoryWebPartProps } from "./IAbstractfactoryWebPartProps";
export default class AbstractfactoryWebPart extends BaseClientSideWebPart<IAbstractfactoryWebPartProps> {
public render(): void {
const element: React.ReactElement<IAbstractFactoryProps > = React.createElement(
Abstractfactory,
{
datasource: this.properties.datasource
}
);
ReactDom.render(element, this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void {
this.properties[this.properties.datasource] = newValue;
this.render();
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
}
protected get disableReactivePropertyChanges(): boolean {
return true;
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneDropdown("datasource", {
label: "DataSource",
options: [
{ key: "1", text: "Sharepoint"},
{ key: "2", text: "JSON" }
],
selectedKey: "1",
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,5 @@
export interface IAbstractfactoryWebPartProps {
datasource: string;
}

View File

@ -0,0 +1,74 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.abstractfactory {
.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 {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// 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: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,67 @@
import * as React from 'react';
import { IAbstractFactoryProps } from "./IAbstractFactoryProps";
import { IAbstractFactoryState } from "./IAbstractFactoryState";
import styles from './Abstractfactory.module.scss';
import { escape } from '@microsoft/sp-lodash-subset';
import DaoFactory from "./DaoFactory";
import ICustomerDao from "./ICustomerDao";
import DataSources from "./DatasourcesEnum";
export default class Abstractfactory extends React.Component<IAbstractFactoryProps, IAbstractFactoryState> {
//Private instance of customerDao, please note it returns ICustomerDao, an Interface,
//not a concrete type
private customerDao: ICustomerDao;
constructor(props: IAbstractFactoryProps, state: IAbstractFactoryState) {
super(props);
this.setInitialState();
// We set the Dao depending on the selected data source
this.setDaos(props.datasource);
//Then we set the list of customers and note, we dont care if they come from Sharepoint
//Rest API or anything else.
this.state = {
items: this.customerDao.listCustomers(),
};
}
public render(): React.ReactElement<IAbstractFactoryProps> {
return (
<div className={ styles.abstractfactory }>
<div className={ styles.container }>
<div className={ styles.row }>
<div className={ styles.column }>
{this.state.items.map( i => (<div key={i.id}>{i.firstName}</div>))}
</div>
</div>
</div>
</div>
);
}
public setInitialState(): void {
this.state = {
items: []
};
}
public componentWillReceiveProps(nextProps: IAbstractFactoryProps): void {
if(nextProps.datasource !== this.props.datasource) {
this.setDaos(nextProps.datasource);
}
}
private setDaos(datasource: string): void {
const data: DataSources = datasource === "1" ? DataSources.SharepointList : DataSources.JsonData;
this.customerDao = DaoFactory.getDAOFactory(data).getCustomerDAO();
this.state = {
items: this.customerDao.listCustomers(),
};
//Now, its transparent for us a UI developers what datasource was selected
//this.customerDao.
}
}

View File

@ -0,0 +1,7 @@
class Customer{
public id: string;
public firstName: string;
public lastName: string;
}
export default Customer;

View File

@ -0,0 +1,27 @@
import ICustomerDAO from "./ICustomerDAO";
import DataSources from "./DatasourcesEnum";
abstract class DAOFactory {
//For each entity we will need to implement getCustomerDAO, this will make it easily replaceable
//when another datasource comes in
public abstract getCustomerDAO(): ICustomerDAO;
//Static method that receives a parameter depending on the datasource and will return the specifc
//factory
public static getDAOFactory(whichFactory: DataSources): DAOFactory {
switch (whichFactory) {
case DataSources.SharepointList:
return new SharepointListDAOFactory();
case DataSources.JsonData:
return new JsonDAOFactory();
default :
return null;
}
}
}
export default DAOFactory;
import SharepointListDAOFactory from "./SharepointListDAOFactory";
import JsonDAOFactory from "./JsonDAOFactory";

View File

@ -0,0 +1,6 @@
enum DataSources {
SharepointList = "SharepointList",
JsonData = "JsonData"
}
export default DataSources;

View File

@ -0,0 +1,4 @@
export interface IAbstractFactoryProps {
datasource: string;
}

View File

@ -0,0 +1,5 @@
import Customer from "./Customer";
export interface IAbstractFactoryState {
items?: Customer[];
}

View File

@ -0,0 +1,11 @@
import Customer from "./Customer";
interface ICustomerDao {
insertCustomer(): number;
deleteCustomer(): boolean;
findCustomer(): Customer;
updateCustomer(): boolean;
listCustomers(): Customer[];
}
export default ICustomerDao;

View File

@ -0,0 +1,42 @@
import ICustomerDao from "./ICustomerDao";
import Customer from "./Customer";
class JsonCustomerDAO implements ICustomerDao{
public insertCustomer(): number {
// implementation to be done by reader
return 1;
}
public deleteCustomer(): boolean {
// implementation to be done by reader
return true;
}
public findCustomer(): Customer {
// implementation to be done by reader
return new Customer();
}
public updateCustomer(): boolean {
// implementation to be done by reader
return true;
}
public listCustomers(): Customer[] {
// implementation to be done by reader
let c1: Customer= new Customer();
let c2: Customer= new Customer();
c1.id="3";
c1.firstName="Andrew";
c1.lastName="Valencia";
c2.id="4";
c2.firstName="Charles";
c2.lastName="Smith";
let list: Array<Customer> = [c1, c2 ];
return list;
}
}
export default JsonCustomerDAO;

View File

@ -0,0 +1,11 @@
import DAOFactory from "./DaoFactory";
import JsonCustomerDAO from "./JsonCustomerDAO";
import ICustomerDao from "./ICustomerDao";
class JsonDAOFactory extends DAOFactory {
public getCustomerDAO(): ICustomerDao{
return new JsonCustomerDAO();
}
}
export default JsonDAOFactory;

View File

@ -0,0 +1,40 @@
import ICustomerDao from "./ICustomerDao";
import Customer from "./Customer";
class SharepointCustomerDao implements ICustomerDao {
public insertCustomer(): number {
// implementation to be done by reader
return 1;
}
public deleteCustomer(): boolean {
// implementation to be done by reader
return true;
}
public findCustomer(): Customer {
// implementation to be done by reader
return new Customer();
}
public updateCustomer(): boolean {
// implementation to be done by reader
return true;
}
public listCustomers(): Customer[] {
// implementation to be done by reader
let c1: Customer = new Customer();
c1.id="1";
c1.firstName="Luis";
c1.lastName="Valencia";
let c2: Customer = new Customer();
c2.id="2";
c2.firstName="John";
c2.lastName="Smith";
let list: Array<Customer> = [c1, c2 ];
return list;
}
}
export default SharepointCustomerDao;

View File

@ -0,0 +1,13 @@
import DaoFactory from "./DaoFactory";
import ICustomerDao from "./ICustomerDao";
import SharepointCustomerDao from "./SharepointCustomerDAO";
// We extend the abstract class and implement the abstract method which returns a new instance of the
// needed factory class
class SharepointListDAOFactory extends DaoFactory {
public getCustomerDAO(): ICustomerDao{
return new SharepointCustomerDao();
}
}
export default SharepointListDAOFactory;

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field"
}
});

View File

@ -0,0 +1,10 @@
declare interface IAbstractfactoryWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
}
declare module 'AbstractfactoryWebPartStrings' {
const strings: IAbstractfactoryWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
}
}

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,8 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.4.1",
"libraryName": "builder",
"libraryId": "364e2bef-24fe-4408-b37d-6930333fd2b5",
"environment": "spo"
}
}

View File

@ -0,0 +1,480 @@
# Builder Design Pattern
## Summary
Builder pattern builds a complex object using simple objects and using a step by step approach. This type of design pattern comes under creational pattern as this pattern provides one of the best ways to create an object.
## 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)
## Prerequisites
> N/A
## Solution
Solution|Author(s)
--------|---------
designpatterns-typescript\builder | [@levalencia](https://www.twitter.com/levalencia)
## Version history
Version|Date|Comments
-------|----|--------
1.0|May 15, 2018|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.**
---
## Builder pattern
A Builder class builds the final object step by step. This builder is independent of other objects.
For this pattern, we have taken an existing example https://www.tutorialspoint.com/design_pattern/builder_pattern.htm and translated it to Typescript. Data Access implementation details are left to the reader.
The idea on this example is to show how you can build a Complex object from single objects, a Meal from (burger, fries, soda). Suppose you have a Sharepoint List for Burgers, another list for Sodas, another one for desserts, and you want to build different Meals (Menus), so this would be a perfect sample.
### UML
This is more or less the diagram of the classes were are coding below.
![](http://www.luisevalencia.com/content/images/2018/02/builder_pattern_uml_diagram.jpg/content/images/2018/02/builder_pattern_uml_diagram.jpg)
### Project structure
We have created a component with all the needed class, lets discuss them one by one.
![](http://www.luisevalencia.com/content/images/2018/02/builder_pattern_uml_diagram.jpg/content/images/2018/02/typescript.png)
### IItem.ts
This interface is the one that every item needs to implement to come with a common structure for all products.
```typescript
import IPacking from "./IPacking";
interface IItem {
name(): string;
packing(): IPacking;
price(): number;
}
export default IItem;
```
### IPacking.ts
This interface is the one that all packaging will use, eg: Bottle, Wrapper, etc, its the way to define common behavior and properties for each product packing.
```typescript
interface IPacking {
pack(): string;
}
export default IPacking;
```
### Bottle.ts
This is one type of packing, it implements the IPacking interface.
```typescript
import IPacking from "./IPacking";
class Bottle implements IPacking {
public pack(): string {
return "Bottle";
}
}
export default Bottle;
```
### Wrapper.ts
```typescript
import IPacking from "./IPacking";
class Wrapper implements IPacking {
public pack(): string {
return "Wrapper";
}
}
export default Wrapper;
```
### Burger.ts
This is an abstract class from which all our specific burgers need to implement, its there to have a common structure for name, packing and pricing.
```typescript
import IItem from "./IItem";
import Wrapper from "./Wrapper";
import IPacking from "./IPacking";
abstract class Burger implements IItem {
public name(): string {
throw new Error("Method not implemented.");
}
public packing(): IPacking {
return new Wrapper();
}
public abstract price(): number ;
}
export default Burger;
```
### ChickenBurger.ts
```typescript
import Burger from "./Burger";
class ChickenBurger extends Burger {
public price(): number {
return 15;
}
public name(): string {
return "Chicken Burger";
}
}
export default ChickenBurger;
```
### VegBurger.ts
```typescript
import Burger from "./Burger";
class VegBurger extends Burger {
public price(): number {
return 11;
}
public name(): string {
return "Veg Burger";
}
}
export default VegBurger;
```
### Colddrink.ts
```typescript
import IItem from "./IItem";
import IPacking from "./IPacking";
import Bottle from "./Bottle";
abstract class ColdDrink implements IItem {
public name(): string {
throw new Error("Method not implemented.");
}
public packing(): IPacking {
return new Bottle();
}
public abstract price(): number ;
}
export default ColdDrink;
```
### Coke.ts
```typescript
import ColdDrink from "./ColdDrink";
class Coke extends ColdDrink {
public price(): number {
return 2.5;
}
public name(): string {
return "Coca Cola";
}
}
export default Coke;
```
### Pepsi.ts
```typescript
import ColdDrink from "./ColdDrink";
class Pepsi extends ColdDrink {
public price(): number {
return 1.5;
}
public name(): string {
return "Pepsi Cola";
}
}
export default Pepsi;
```
### Meal.ts
This class will represent a full meal behavior, here we have the methods to add items to the Meal, get the cost and show the items belonging to the Meal.
```typescript
import IItem from "./IItem";
class Meal {
private items: IItem[] = [];
public addItem(item: IItem): void {
this.items.push(item);
}
public getCost(): number {
let cost: number = 0;
for(let item of this.items) {
cost+= item.price();
}
return cost;
}
public showItems(): string {
let returnStr: string = "";
for(let item of this.items) {
returnStr +="Item:" + item.name();
returnStr +=", Packing:" + item.packing().pack();
returnStr +=", Price: " + item.price();
}
returnStr += ", Total: " + this.getCost();
return returnStr;
}
}
export default Meal;
```
### MealBuilder.ts
Mealbuilder its just the class that uses the classes explained before to construct any type of meal, for sake of simplicity, we created only 2 meals here.
```typescript
import Meal from "./Meal";
import VegBurger from "./VegBurger";
import Coke from "./Coke";
import ChickenBurger from "./ChickenBurger";
class MealBuilder {
public prepareVegMeal(): Meal {
let meal: Meal= new Meal();
meal.addItem(new VegBurger());
meal.addItem(new Coke());
return meal;
}
public prepareNonVegMeal(): Meal {
let meal: Meal= new Meal();
meal.addItem(new ChickenBurger());
meal.addItem(new Coke());
return meal;
}
}
export default MealBuilder;
```
### IBuilderProps.ts
We created a selectedMeal string property to take the decision on which meal to build.
```typescript
export interface IBuilderProps {
selectedMeal: string;
}
```
### Builder.tsx
This is our component class, here we have a constructor and in the constructor we call the setMeal method, with the selected meal option as a parameter, and then we can define which meal to prepare. Once the meal is prepared, in the render method we can use the showItems method
```typescript
import * as React from 'react';
import styles from './Builder.module.scss';
import { IBuilderProps } from './IBuilderProps';
import { escape } from '@microsoft/sp-lodash-subset';
import MealBuilder from "./MealBuilder";
import Meal from "./Meal";
import { IPropertyPaneConfiguration } from "@microsoft/sp-webpart-base";
import {
PropertyPaneDropdown
} from "@microsoft/sp-webpart-base";
import {Version} from "@microsoft/sp-core-library";
import { IBuilderState } from './IBuilderState';
export default class Builder extends React.Component<IBuilderProps, IBuilderState> {
private mealBuilder: MealBuilder ;
private items: string;
private meal: Meal;
constructor(props: IBuilderProps, state: IBuilderState) {
super(props);
this.setInitialState();
//this.setMeal = this.setMeal.bind(this);
this.mealBuilder = new MealBuilder();
this.setMeal(props.selectedMeal);
}
public setInitialState(): void {
this.state = {
items: ""
};
}
public componentWillReceiveProps(nextProps: IBuilderProps): void {
if(nextProps.selectedMeal !== this.props.selectedMeal) {
this.setMeal(nextProps.selectedMeal);
}
}
public render(): React.ReactElement<IBuilderProps> {
return (
<div className={styles.builder}>
<div className={styles.container}>
<div className={`ms-Grid-row ms-bgColor-themeDark ms-fontColor-white ${styles.row}`}>
<div className="ms-Grid-col ms-lg10 ms-xl8 ms-xlPush2 ms-lgPush1">
<span className="ms-font-xl ms-fontColor-white">Welcome to Mac Luis!</span>
<p className="ms-font-l ms-fontColor-white">You have selected the following.</p>
<div> {this.state.items}</div>
</div>
</div>
</div>
</div>
);
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
private setMeal(selectedMeal: string): void {
if(selectedMeal===undefined){
this.meal = this.mealBuilder.prepareVegMeal();
}
if(selectedMeal === "0") {
this.meal = this.mealBuilder.prepareVegMeal();
}
if(selectedMeal === "1") {
this.meal = this.mealBuilder.prepareNonVegMeal();
}
this.state = {
items: this.meal.showItems(),
};
}
}
```
And finally
### BuilderWebPart.ts
Here what we do is just to use our component and sending the parameter of the selected meal, which is just a normal dropdown with 2 hardcoded values.
```typescript
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneDropdown
} from "@microsoft/sp-webpart-base";
import * as strings from 'BuilderWebPartStrings';
import Builder from './components/Builder';
import { IBuilderProps } from './components/IBuilderProps';
export interface IBuilderWebPartProps {
selectedMeal: string;
}
export default class BuilderWebPart extends BaseClientSideWebPart<IBuilderWebPartProps> {
public render(): void {
const element: React.ReactElement<IBuilderProps > = React.createElement(
Builder,
{
selectedMeal: this.properties.selectedMeal
}
);
ReactDom.render(element, this.domElement);
}
protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void {
this.properties[this.properties.selectedMeal] = newValue;
this.render();
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
protected get disableReactivePropertyChanges(): boolean {
return true;
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: "Header"
},
groups: [
{
groupName: "Group",
groupFields: [
PropertyPaneDropdown("selectedMeal", {
label: "Select meal",
options: [
{ key: "0", text: "Veg" },
{ key: "1", text: "Nonveg" }
],
selectedKey: 0
})
]
}
]
}
]
};
}
}
```
Data source implementation is left to the reader
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/designpatterns-typescript/builder" />

View File

@ -0,0 +1,18 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"builder-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/builder/BuilderWebPart.js",
"manifest": "./src/webparts/builder/BuilderWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"BuilderWebPartStrings": "lib/webparts/builder/loc/{locale}.js"
}
}

View File

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

View File

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

View File

@ -0,0 +1,12 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "builder-client-side-solution",
"id": "364e2bef-24fe-4408-b37d-6930333fd2b5",
"version": "1.0.0.0",
"includeClientSideAssets": true
},
"paths": {
"zippedPackage": "solution/builder.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://dev.office.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,45 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/tslint.schema.json",
// Display errors as warnings
"displayAsWarning": true,
// The TSLint task may have been configured with several custom lint rules
// before this config file is read (for example lint rules from the tslint-microsoft-contrib
// project). If true, this flag will deactivate any of these rules.
"removeExistingRules": true,
// When true, the TSLint task is configured with some default TSLint "rules.":
"useDefaultConfigAsBase": false,
// Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules
// which are active, other than the list of rules below.
"lintConfig": {
// Opt-in to Lint rules which help to eliminate bugs in JavaScript
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-case": true,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"valid-typeof": true,
"variable-name": false,
"whitespace": false
}
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://dev.office.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,33 @@
{
"name": "builder",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"react": "15.6.2",
"react-dom": "15.6.2",
"@types/react": "15.6.6",
"@types/react-dom": "15.5.6",
"@microsoft/sp-core-library": "~1.4.1",
"@microsoft/sp-webpart-base": "~1.4.1",
"@microsoft/sp-lodash-subset": "~1.4.1",
"@microsoft/sp-office-ui-fabric-core": "~1.4.1",
"@types/webpack-env": ">=1.12.1 <1.14.0"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.4.1",
"@microsoft/sp-module-interfaces": "~1.4.1",
"@microsoft/sp-webpart-workbench": "~1.4.1",
"gulp": "~3.9.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0",
"ajv": "~5.2.2"
}
}

View File

@ -0,0 +1,26 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "2f360afa-5e4c-4697-9262-c10bb0c42b5d",
"alias": "BuilderWebPart",
"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,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "builder" },
"description": { "default": "builder description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "builder"
}
}]
}

View File

@ -0,0 +1,74 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneDropdown
} from "@microsoft/sp-webpart-base";
import * as strings from 'BuilderWebPartStrings';
import Builder from './components/Builder';
import { IBuilderProps } from './components/IBuilderProps';
export interface IBuilderWebPartProps {
selectedMeal: string;
}
export default class BuilderWebPart extends BaseClientSideWebPart<IBuilderWebPartProps> {
public render(): void {
const element: React.ReactElement<IBuilderProps > = React.createElement(
Builder,
{
selectedMeal: this.properties.selectedMeal
}
);
ReactDom.render(element, this.domElement);
}
protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void {
this.properties[this.properties.selectedMeal] = newValue;
this.render();
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
protected get disableReactivePropertyChanges(): boolean {
return true;
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: "Header"
},
groups: [
{
groupName: "Group",
groupFields: [
PropertyPaneDropdown("selectedMeal", {
label: "Select meal",
options: [
{ key: "0", text: "Veg" },
{ key: "1", text: "Nonveg" }
],
selectedKey: 0
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,9 @@
import IPacking from "./IPacking";
class Bottle implements IPacking {
public pack(): string {
return "Bottle";
}
}
export default Bottle;

View File

@ -0,0 +1,74 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/_SPFabricCore.scss';
.builder {
.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 {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// 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: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,76 @@
import * as React from 'react';
import styles from './Builder.module.scss';
import { IBuilderProps } from './IBuilderProps';
import { escape } from '@microsoft/sp-lodash-subset';
import MealBuilder from "./MealBuilder";
import Meal from "./Meal";
import { IPropertyPaneConfiguration } from "@microsoft/sp-webpart-base";
import {
PropertyPaneDropdown
} from "@microsoft/sp-webpart-base";
import {Version} from "@microsoft/sp-core-library";
import { IBuilderState } from './IBuilderState';
export default class Builder extends React.Component<IBuilderProps, IBuilderState> {
private mealBuilder: MealBuilder ;
private items: string;
private meal: Meal;
constructor(props: IBuilderProps, state: IBuilderState) {
super(props);
this.setInitialState();
//this.setMeal = this.setMeal.bind(this);
this.mealBuilder = new MealBuilder();
this.setMeal(props.selectedMeal);
}
public setInitialState(): void {
this.state = {
items: ""
};
}
public componentWillReceiveProps(nextProps: IBuilderProps): void {
if(nextProps.selectedMeal !== this.props.selectedMeal) {
this.setMeal(nextProps.selectedMeal);
}
}
public render(): React.ReactElement<IBuilderProps> {
return (
<div className={styles.builder}>
<div className={styles.container}>
<div className={`ms-Grid-row ms-bgColor-themeDark ms-fontColor-white ${styles.row}`}>
<div className="ms-Grid-col ms-lg10 ms-xl8 ms-xlPush2 ms-lgPush1">
<span className="ms-font-xl ms-fontColor-white">Welcome to Mac Luis!</span>
<p className="ms-font-l ms-fontColor-white">You have selected the following.</p>
<div> {this.state.items}</div>
</div>
</div>
</div>
</div>
);
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
private setMeal(selectedMeal: string): void {
if(selectedMeal===undefined){
this.meal = this.mealBuilder.prepareVegMeal();
}
if(selectedMeal === "0") {
this.meal = this.mealBuilder.prepareVegMeal();
}
if(selectedMeal === "1") {
this.meal = this.mealBuilder.prepareNonVegMeal();
}
this.state = {
items: this.meal.showItems(),
};
}
}

View File

@ -0,0 +1,16 @@
import IItem from "./IItem";
import Wrapper from "./Wrapper";
import IPacking from "./IPacking";
abstract class Burger implements IItem {
public abstract name(): string ;
public packing(): IPacking {
return new Wrapper();
}
public abstract price(): number ;
}
export default Burger;

View File

@ -0,0 +1,13 @@
import Burger from "./Burger";
class ChickenBurger extends Burger {
public price(): number {
return 15;
}
public name(): string {
return "Chicken Burger";
}
}
export default ChickenBurger;

View File

@ -0,0 +1,13 @@
import ColdDrink from "./ColdDrink";
class Coke extends ColdDrink {
public price(): number {
return 2.5;
}
public name(): string {
return "Coca Cola";
}
}
export default Coke;

View File

@ -0,0 +1,16 @@
import IItem from "./IItem";
import IPacking from "./IPacking";
import Bottle from "./Bottle";
abstract class ColdDrink implements IItem {
public abstract name();
public packing(): IPacking {
return new Bottle();
}
public abstract price(): number ;
}
export default ColdDrink;

View File

@ -0,0 +1,3 @@
export interface IBuilderProps {
selectedMeal: string;
}

View File

@ -0,0 +1,4 @@
export interface IBuilderState {
items?: string;
}

View File

@ -0,0 +1,9 @@
import IPacking from "./IPacking";
interface IItem {
name(): string;
packing(): IPacking;
price(): number;
}
export default IItem;

View File

@ -0,0 +1,5 @@
interface IPacking {
pack(): string;
}
export default IPacking;

View File

@ -0,0 +1,32 @@
import IItem from "./IItem";
class Meal {
private items: IItem[] = [];
public addItem(item: IItem): void {
this.items.push(item);
}
public getCost(): number {
let cost: number = 0;
for(let item of this.items) {
cost+= item.price();
}
return cost;
}
public showItems(): string {
let returnStr: string = "";
for(let item of this.items) {
returnStr +="Item:" + item.name();
returnStr +=", Packing:" + item.packing().pack();
returnStr +=", Price: " + item.price();
}
returnStr += ", Total: " + this.getCost();
return returnStr;
}
}
export default Meal;

View File

@ -0,0 +1,22 @@
import Meal from "./Meal";
import VegBurger from "./VegBurger";
import Coke from "./Coke";
import ChickenBurger from "./ChickenBurger";
class MealBuilder {
public prepareVegMeal(): Meal {
let meal: Meal= new Meal();
meal.addItem(new VegBurger());
meal.addItem(new Coke());
return meal;
}
public prepareNonVegMeal(): Meal {
let meal: Meal= new Meal();
meal.addItem(new ChickenBurger());
meal.addItem(new Coke());
return meal;
}
}
export default MealBuilder;

View File

@ -0,0 +1,13 @@
import ColdDrink from "./ColdDrink";
class Pepsi extends ColdDrink {
public price(): number {
return 1.5;
}
public name(): string {
return "Pepsi Cola";
}
}
export default Pepsi;

View File

@ -0,0 +1,13 @@
import Burger from "./Burger";
class VegBurger extends Burger {
public price(): number {
return 11;
}
public name(): string {
return "Veg Burger";
}
}
export default VegBurger;

View File

@ -0,0 +1,9 @@
import IPacking from "./IPacking";
class Wrapper implements IPacking {
public pack(): string {
return "Wrapper";
}
}
export default Wrapper;

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field"
}
});

View File

@ -0,0 +1,10 @@
declare interface IBuilderWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
}
declare module 'BuilderWebPartStrings' {
const strings: IBuilderWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
}
}

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,8 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.4.1",
"libraryName": "factory-method",
"libraryId": "7116d524-988a-4de4-83b0-cd53165f816a",
"environment": "spo"
}
}

View File

@ -0,0 +1,702 @@
# Factory method design Pattern
## Summary
In class-based programming, the factory method pattern is a creational pattern that uses factory methods to deal with the problem of creating objects without having to specify the exact class of the object that will be created. This is done by creating objects by calling a factory method—either specified in an interface and implemented by child classes, or implemented in a base class and optionally overridden by derived classes—rather than by calling a constructor.
Another definition which I liked more and fits perfectly my sample is the following, taken from: https://www.javatpoint.com/factory-method-design-pattern
>A Factory Pattern or Factory Method Pattern says that just define an interface or abstract class for creating an object but let the subclasses decide which class to instantiate. In other words, subclasses are responsible to create the instance of the class.
## 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)
## Prerequisites
> N/A
## Solution
Solution|Author(s)
--------|---------
designpatterns-typescript\factorymethod | [@levalencia](https://www.twitter.com/levalencia)
## Version history
Version|Date|Comments
-------|----|--------
1.0|May 15, 2018|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.**
---
### Factory method pattern
##### Advantage of Factory Design Pattern
>Factory Method Pattern allows the sub-classes to choose the type of objects to create.
It promotes the loose-coupling by eliminating the need to bind application-specific classes into the code. That means the code interacts solely with the resultant interface or abstract class, so that it will work with any classes that implement that interface or that extends that abstract class.
##### When to use Factory Method Design Pattern
1. When a class doesn't know what sub-classes will be required to create
2. When a class wants that its sub-classes specify the objects to be created.
3. When the parent classes choose the creation of objects to its sub-classes.
So lets start this journey explaining how I saw this factory method pattern could be applied.
In a Sharepoint Site we can have multiple lists and all those lists could have different columns or fields, why not create a generic way to build the list item objets depending on the selected list? well in plain english, a webpart where you can select the list, and based on the selected list it will render all the columns, sure you can do this in many ways and probably with lots of switches/if statements, etc, but I wanted a more elegant solution and I believe this sample will do just that.
##### Diagram
![](http://www.luisevalencia.com/content/images/2017/11/DesignPatterns.png)
So instead of classes we have interfaces for the generic list item (IListItem), and then other interfaces that extends the base one to add more fields depending on the list, news, announcements and directory.
On the right side of the diagram we have an IFactory interface that declares the signature of the method getItems and what it should return (any), remember at the end all items will be of type IListItem because they extend that interface but in order to make it work we will have to use an array of any[].
And finally on the FactoryMethod react component we use the ListItem Factory to get the items, from the caller point of view, we dont care what it will return, its the factory method responsibility to actually create the logic inside to know which instance types it should return.
##### Project structure
![](http://www.luisevalencia.com/content/images/2017/11/estructura.png)
###### Models
The models just define the type of objects we want to return from our factory method, better for strong typing our return objects.
```typescript
export interface IListItem {
[key: string]: any;
id: string;
title: string;
modified: Date;
created: Date;
modifiedby: string;
createdby: string;
}
import {IListItem} from "./IListItem";
export interface IAnnouncementListItem extends IListItem {
announcementBody: string;
expiryDate: Date;
}
import {IListItem} from "./IListItem";
export interface IDirectoryListItem extends IListItem {
firstName: string;
lastName: string;
mobileNumber: string;
internalNumber: string;
}
import {IListItem} from "./IListItem";
export interface INewsListItem extends IListItem {
newsheader: string;
newsbody: string;
expiryDate: Date;
}
```
###### Factory classes
So the Factory interface is quite simple, we just have a method to implement in the extended classes.
Its in this method below where we actually have a switch statement to get a different list with a different URL (Select columns), and then on the return we get different concrete types by mapping the json result to a different instance of the needed type.
```typescript
import { SPHttpClient, SPHttpClientResponse } from "@microsoft/sp-http";
import { IWebPartContext } from "@microsoft/sp-webpart-base";
import { IListItem} from "./models/IListItem";
import { IFactory } from "./IFactory";
import { INewsListItem } from "./models/INewsListItem";
import { IDirectoryListItem } from "./models/IDirectoryListItem";
import { IAnnouncementListItem } from "./models/IAnnouncementListItem";
export class ListItemFactory implements IFactory {
// private _listItems: IListItem[];
public getItems(requester: SPHttpClient, siteUrl: string, listName: string): Promise<any[]> {
switch(listName) {
case "GenericList":
let items: IListItem[];
// tslint:disable-next-line:max-line-length
return requester.get(`${siteUrl}/_api/web/lists/getbytitle('${listName}')/items?$select=Title,Id,Modified,Created,Author/Title,Editor/Title&$expand=Author,Editor`,
SPHttpClient.configurations.v1,
{
headers: {
"Accept": "application/json;odata=nometadata",
"odata-version": ""
}
})
.then((response: SPHttpClientResponse): Promise<{ value: IListItem[] }> => {
return response.json();
})
.then((json: { value: IListItem[] }) => {
console.log(JSON.stringify(json.value));
return items=json.value.map((v,i)=>(
{
// key: v.id,
id: v.Id,
title: v.Title,
created: v.Created,
createdby: v.Author.Title,
modified: v.Modified,
modifiedby: v.Editor.Title
}
));
});
case "News":
let newsitems: INewsListItem[];
// tslint:disable-next-line:max-line-length
return requester.get(`${siteUrl}/_api/web/lists/getbytitle('${listName}')/items?$select=Title,Id,Modified,Created,newsheader,newsbody,expiryDate,Author/Title,Editor/Title&$expand=Author,Editor`,
SPHttpClient.configurations.v1,
{
headers: {
"Accept": "application/json;odata=nometadata",
"odata-version": ""
}
})
.then((response: SPHttpClientResponse): Promise<{ value: INewsListItem[] }> => {
return response.json();
})
.then((json: { value: INewsListItem[] }) => {
return newsitems=json.value.map((v,i)=>(
{
id: v.Id,
title: v.Title,
created: v.Created,
createdby: v.Author.Title,
modified: v.Modified,
modifiedby: v.Editor.Title,
newsheader: v.newsheader,
newsbody: v.newsbody,
expiryDate: v.expiryDate
}
));
});
case "Announcements":
let announcementitems: IAnnouncementListItem[];
return requester.get(`${siteUrl}/_api/web/lists/getbytitle('${listName}')/items?$select=Title,Id,Modified,Created,announcementBody,expiryDate,Author/Title,Editor/Title&$expand=Author,Editor`,
SPHttpClient.configurations.v1,
{
headers: {
"Accept": "application/json;odata=nometadata",
"odata-version": ""
}
})
.then((response: SPHttpClientResponse): Promise<{ value: IAnnouncementListItem[] }> => {
return response.json();
})
.then((json: { value: IAnnouncementListItem[] }) => {
return announcementitems=json.value.map((v,i)=>(
{
id: v.Id,
title: v.Title,
created: v.Created,
createdby: v.Author.Title,
modified: v.Modified,
modifiedby: v.Editor.Title,
announcementBody: v.announcementBody,
expiryDate: v.expiryDate
}
));
});
case "Directory":
let directoryitems: IDirectoryListItem[];
return requester.get(`${siteUrl}/_api/web/lists/getbytitle('${listName}')/items?$select=Title,Id,Modified,Created,firstName,lastName,mobileNumber,internalNumber,Author/Title,Editor/Title&$expand=Author,Editor`,
SPHttpClient.configurations.v1,
{
headers: {
"Accept": "application/json;odata=nometadata",
"odata-version": ""
}
})
.then((response: SPHttpClientResponse): Promise<{ value: IDirectoryListItem[] }> => {
return response.json();
})
.then((json: { value: IDirectoryListItem[] }) => {
return directoryitems=json.value.map((v,i)=>(
{
id: v.Id,
title: v.Title,
created: v.Created,
createdby: v.Author.Title,
modified: v.Modified,
modifiedby: v.Editor.Title,
firstName: v.firstName,
lastName: v.lastName,
mobileNumber: v.mobileNumber,
internalNumber: v.internalNumber
}
));
});
default:
break;
}
}
}
```
###### Props and states
Properties we pass from the main webpart to the component are defined in the props interface, things like SPHttpclient are important here, state is where we actually store our returned information from the server, because listitems could be of different types, I created a wrapper Interface and depending on the type of list, then the state would be read from a different state property DetailsListItemState, DetailsNewsListItemState, etc.
We will see later in the series how could achieve this with shorter code, but for this pattern its what I need so far.
```typescript
import { SPHttpClient } from "@microsoft/sp-http";
import IDataProvider from "./dataproviders/IDataProvider";
export interface IFactoryMethodProps {
listName: string;
spHttpClient: SPHttpClient;
siteUrl: string;
dataProvider: IDataProvider;
}
import { IListItem } from "./models/IListItem";
import { INewsListItem } from "./models/INewsListItem";
import { IDirectoryListItem } from "./models/IDirectoryListItem";
import { IAnnouncementListItem } from "./models/IAnnouncementListItem";
import {
IColumn
} from "office-ui-fabric-react/lib/DetailsList";
export interface IFactoryMethodState {
type: string;
status: string;
DetailsListItemState: IDetailsListItemState;
DetailsNewsListItemState: IDetailsNewsListItemState;
DetailsDirectoryListItemState : IDetailsDirectoryListItemState;
DetailsAnnouncementListItemState : IDetailsAnnouncementListItemState;
}
export interface IDetailsListItemState {
columns: IColumn[];
items: IListItem[];
}
export interface IDetailsNewsListItemState {
columns: IColumn[];
items: INewsListItem[];
}
export interface IDetailsDirectoryListItemState {
columns: IColumn[];
items: IDirectoryListItem[];
}
export interface IDetailsAnnouncementListItemState {
columns: IColumn[];
items: IAnnouncementListItem[];
}
```
###### The component
The component has a lot of UI logic, but the real Factory method magic is only in the readItemsAndSetStatus method, where we use the Factory class to get the items and then set the corresponding state, this is pretty nice as we avoid lots of code with the setstateLine and the getitems method here.
Some code I think its really self explanatory so I dont explain it, if needed put a comment below with a question to further explain something.
If you are new to React then I really recommend to read this from the official documentation, which explains the component lifecycle, after reading this, it will clarify concepts for you: https://reactjs.org/docs/react-component.html
```Typescript
//#region Imports
//#region Imports
import * as React from "react";
import styles from "./FactoryMethod.module.scss";
import { IFactoryMethodProps } from "./IFactoryMethodProps";
import {
IDetailsGenericListItemState,
IDetailsNewsListItemState,
IDetailsDirectoryListItemState,
IDetailsAnnouncementListItemState,
IFactoryMethodState
} from "./IFactoryMethodState";
import { IListItem } from "./models/IListItem";
import { IAnnouncementListItem } from "./models/IAnnouncementListItem";
import { INewsListItem } from "./models/INewsListItem";
import { IDirectoryListItem } from "./models/IDirectoryListItem";
import { escape } from "@microsoft/sp-lodash-subset";
import { SPHttpClient, SPHttpClientResponse } from "@microsoft/sp-http";
import { ListItemFactory} from "./ListItemFactory";
import { TextField } from "office-ui-fabric-react/lib/TextField";
import {
DetailsList,
DetailsListLayoutMode,
Selection,
buildColumns,
IColumn
} from "office-ui-fabric-react/lib/DetailsList";
import { MarqueeSelection } from "office-ui-fabric-react/lib/MarqueeSelection";
import { autobind } from "office-ui-fabric-react/lib/Utilities";
import PropTypes from "prop-types";
//#endregion
export default class FactoryMethod extends React.Component<IFactoryMethodProps, IFactoryMethodState> {
constructor(props: IFactoryMethodProps, state: any) {
super(props);
this.setInitialState();
}
// lifecycle help here: https://staminaloops.github.io/undefinedisnotafunction/understanding-react/
//#region Mouting events lifecycle
// the data returned from render is neither a string nor a DOM node.
// it's a lightweight description of what the DOM should look like.
// inspects this.state and this.props and create the markup.
// when your data changes, the render method is called again.
// react diff the return value from the previous call to render with
// the new one, and generate a minimal set of changes to be applied to the DOM.
public render(): React.ReactElement<IFactoryMethodProps> {
switch(this.props.listName) {
case "GenericList":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsGenericListItemState.items} columns={this.state.columns} />;
case "News":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsNewsListItemState.items} columns={this.state.columns}/>;
case "Announcements":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsAnnouncementListItemState.items} columns={this.state.columns}/>;
case "Directory":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsDirectoryListItemState.items} columns={this.state.columns}/>;
default:
return null;
}
}
// invoked once, only on the client (not on the server), immediately AFTER the initial rendering occurs.
public componentDidMount(): void {
// you can access any refs to your children
// (e.g., to access the underlying DOM representation - ReactDOM.findDOMNode).
// the componentDidMount() method of child components is invoked before that of parent components.
// if you want to integrate with other JavaScript frameworks,
// set timers using setTimeout or setInterval,
// or send AJAX requests, perform those operations in this method.
this._configureWebPart = this._configureWebPart.bind(this);
this.readItemsAndSetStatus("GenericList");
}
//#endregion
//#region Props changes lifecycle events (after a property changes from parent component)
public componentWillReceiveProps(nextProps: IFactoryMethodProps): void {
if(nextProps.listName !== this.props.listName) {
this.readItemsAndSetStatus(nextProps.listName);
}
}
//#endregion
//#region private methods
private _configureWebPart(): void {
this.props.configureStartCallback();
}
public setInitialState(): void {
this.state = {
status: this.listNotConfigured(this.props)
? "Please configure list in Web Part properties"
: "Ready",
columns:[],
hasError: false,
DetailsGenericListItemState:{
items:[]
},
DetailsNewsListItemState:{
items:[]
},
DetailsDirectoryListItemState:{
items:[]
},
DetailsAnnouncementListItemState:{
items:[]
},
};
}
// reusable inline component
private ListMarqueeSelection = (itemState: {columns: IColumn[], items: IListItem[] }) => (
<div>
<DetailsList
items={ itemState.items }
columns={ itemState.columns }
setKey="set"
layoutMode={ DetailsListLayoutMode.fixedColumns }
selectionPreservedOnEmptyClick={ true }
compact={ true }>
</DetailsList>
</div>
)
// read items using factory method pattern and sets state accordingly
private readItemsAndSetStatus(listName: string): void {
this.setState({
status: "Loading all items..."
});
const factory: ListItemFactory = new ListItemFactory();
factory.getItems(this.props.spHttpClient, this.props.siteUrl, listName)
.then((items: any[]) => {
const keyPart: string = listName === "GenericList" ? "Generic" : listName;
var myItems = null;
switch(listName) {
case "GenericList":
myItems = items as IListItem[];
break;
case "News":
myItems = items as INewsListItem[];
break;
case "Announcements":
myItems = items as IAnnouncementListItem[];
break;
case "Directory":
myItems = items as IDirectoryListItem[];
break;
}
// the explicit specification of the type argument `keyof {}` is bad and
// it should not be required.
this.setState<keyof {}>({
status: `Successfully loaded ${myItems.length} items`,
["Details" + keyPart + "ListItemState"] : {
items
},
columns: buildColumns(myItems)
});
});
}
private listNotConfigured(props: IFactoryMethodProps): boolean {
return props.listName === undefined ||
props.listName === null ||
props.listName.length === 0;
}
//#endregion
}
```
###### The webpart
And finally the webpart code is below, with self explanatory comments for the reader to understand the events lifecycle.
```Typescript
import * as React from "react";
import * as ReactDom from "react-dom";
import { Version } from "@microsoft/sp-core-library";
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneDropdown,
IPropertyPaneDropdownOption,
IPropertyPaneField,
PropertyPaneLabel
} from "@microsoft/sp-webpart-base";
import * as strings from "FactoryMethodWebPartStrings";
import FactoryMethod from "./components/FactoryMethod";
import { IFactoryMethodProps } from "./components/IFactoryMethodProps";
import { IFactoryMethodWebPartProps } from "./IFactoryMethodWebPartProps";
import * as lodash from "@microsoft/sp-lodash-subset";
import List from "./components/models/List";
import { Environment, EnvironmentType } from "@microsoft/sp-core-library";
import IDataProvider from "./components/dataproviders/IDataProvider";
import MockDataProvider from "./test/MockDataProvider";
import SharePointDataProvider from "./components/dataproviders/SharepointDataProvider";
export default class FactoryMethodWebPart extends BaseClientSideWebPart<IFactoryMethodWebPartProps> {
private _dropdownOptions: IPropertyPaneDropdownOption[];
private _selectedList: List;
private _disableDropdown: boolean;
private _dataProvider: IDataProvider;
private _factorymethodContainerComponent: FactoryMethod;
protected onInit(): Promise<void> {
this.context.statusRenderer.displayLoadingIndicator(this.domElement, "Todo");
/*
Create the appropriate data provider depending on where the web part is running.
The DEBUG flag will ensure the mock data provider is not bundled with the web part when you package the
solution for distribution, that is, using the --ship flag with the package-solution gulp command.
*/
if (DEBUG && Environment.type === EnvironmentType.Local) {
this._dataProvider = new MockDataProvider();
} else {
this._dataProvider = new SharePointDataProvider();
this._dataProvider.webPartContext = this.context;
}
this.openPropertyPane = this.openPropertyPane.bind(this);
/*
Get the list of tasks lists from the current site and populate the property pane dropdown field with the values.
*/
this.loadLists()
.then(() => {
/*
If a list is already selected, then we would have stored the list Id in the associated web part property.
So, check to see if we do have a selected list for the web part. If we do, then we set that as the selected list
in the property pane dropdown field.
*/
if (this.properties.spListIndex) {
this.setSelectedList(this.properties.spListIndex.toString());
this.context.statusRenderer.clearLoadingIndicator(this.domElement);
}
});
return super.onInit();
}
// render method of the webpart, actually calls Component
public render(): void {
const element: React.ReactElement<IFactoryMethodProps > = React.createElement(
FactoryMethod,
{
spHttpClient: this.context.spHttpClient,
siteUrl: this.context.pageContext.web.absoluteUrl,
listName: this._dataProvider.selectedList === undefined ? "GenericList" : this._dataProvider.selectedList.Title,
dataProvider: this._dataProvider,
configureStartCallback: this.openPropertyPane
}
);
// reactDom.render(element, this.domElement);
this._factorymethodContainerComponent = <FactoryMethod>ReactDom.render(element, this.domElement);
}
// loads lists from the site and fill the dropdown.
private loadLists(): Promise<any> {
return this._dataProvider.getLists()
.then((lists: List[]) => {
// disable dropdown field if there are no results from the server.
this._disableDropdown = lists.length === 0;
if (lists.length !== 0) {
this._dropdownOptions = lists.map((list: List) => {
return {
key: list.Id,
text: list.Title
};
});
}
});
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void {
/*
Check the property path to see which property pane feld changed. If the property path matches the dropdown, then we set that list
as the selected list for the web part.
*/
if (propertyPath === "spListIndex") {
this.setSelectedList(newValue);
}
/*
Finally, tell property pane to re-render the web part.
This is valid for reactive property pane.
*/
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
}
// sets the selected list based on the selection from the dropdownlist
private setSelectedList(value: string): void {
const selectedIndex: number = lodash.findIndex(this._dropdownOptions,
(item: IPropertyPaneDropdownOption) => item.key === value
);
const selectedDropDownOption: IPropertyPaneDropdownOption = this._dropdownOptions[selectedIndex];
if (selectedDropDownOption) {
this._selectedList = {
Title: selectedDropDownOption.text,
Id: selectedDropDownOption.key.toString()
};
this._dataProvider.selectedList = this._selectedList;
}
}
// we add fields dynamically to the property pane, in this case its only the list field which we will render
private getGroupFields(): IPropertyPaneField<any>[] {
const fields: IPropertyPaneField<any>[] = [];
// we add the options from the dropdownoptions variable that was populated during init to the dropdown here.
fields.push(PropertyPaneDropdown("spListIndex", {
label: "Select a list",
disabled: this._disableDropdown,
options: this._dropdownOptions
}));
/*
When we do not have any lists returned from the server, we disable the dropdown. If that is the case,
we also add a label field displaying the appropriate message.
*/
if (this._disableDropdown) {
fields.push(PropertyPaneLabel(null, {
text: "Could not find tasks lists in your site. Create one or more tasks list and then try using the web part."
}));
}
return fields;
}
private openPropertyPane(): void {
this.context.propertyPane.open();
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
/*
Instead of creating the fields here, we call a method that will return the set of property fields to render.
*/
groupFields: this.getGroupFields()
}
]
}
]
};
}
}
```
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/designpatterns-typescript/factorymethod" />

View File

@ -0,0 +1,18 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"factory-method-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/factoryMethod/FactoryMethodWebPart.js",
"manifest": "./src/webparts/factoryMethod/FactoryMethodWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"FactoryMethodWebPartStrings": "lib/webparts/factoryMethod/loc/{locale}.js"
}
}

View File

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

View File

@ -0,0 +1,7 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "factory-method",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,12 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "factory-method-client-side-solution",
"id": "7116d524-988a-4de4-83b0-cd53165f816a",
"version": "1.0.0.0",
"includeClientSideAssets": true
},
"paths": {
"zippedPackage": "solution/factory-method.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://dev.office.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,45 @@
{
"$schema": "https://dev.office.com/json-schemas/core-build/tslint.schema.json",
// Display errors as warnings
"displayAsWarning": true,
// The TSLint task may have been configured with several custom lint rules
// before this config file is read (for example lint rules from the tslint-microsoft-contrib
// project). If true, this flag will deactivate any of these rules.
"removeExistingRules": true,
// When true, the TSLint task is configured with some default TSLint "rules.":
"useDefaultConfigAsBase": false,
// Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules
// which are active, other than the list of rules below.
"lintConfig": {
// Opt-in to Lint rules which help to eliminate bugs in JavaScript
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-case": true,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"valid-typeof": true,
"variable-name": false,
"whitespace": false
}
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://dev.office.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,33 @@
{
"name": "factory-method",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"react": "15.6.2",
"react-dom": "15.6.2",
"@types/react": "15.6.6",
"@types/react-dom": "15.5.6",
"@microsoft/sp-core-library": "~1.4.1",
"@microsoft/sp-webpart-base": "~1.4.1",
"@microsoft/sp-lodash-subset": "~1.4.1",
"@microsoft/sp-office-ui-fabric-core": "~1.4.1",
"@types/webpack-env": ">=1.12.1 <1.14.0"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.4.1",
"@microsoft/sp-module-interfaces": "~1.4.1",
"@microsoft/sp-webpart-workbench": "~1.4.1",
"gulp": "~3.9.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0",
"ajv": "~5.2.2"
}
}

View File

@ -0,0 +1,26 @@
{
"$schema": "https://dev.office.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "02e0ac9e-38e7-4d83-b212-8fa38208c1fb",
"alias": "FactoryMethodWebPart",
"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,
"preconfiguredEntries": [{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": { "default": "Other" },
"title": { "default": "FactoryMethod" },
"description": { "default": "FactoryMethod description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "FactoryMethod"
}
}]
}

View File

@ -0,0 +1,191 @@
import * as React from "react";
import * as ReactDom from "react-dom";
import { Version } from "@microsoft/sp-core-library";
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneDropdown,
IPropertyPaneDropdownOption,
IPropertyPaneField,
PropertyPaneLabel
} from "@microsoft/sp-webpart-base";
import * as strings from "FactoryMethodWebPartStrings";
import FactoryMethod from "./components/FactoryMethod";
import { IFactoryMethodProps } from "./components/IFactoryMethodProps";
import { IFactoryMethodWebPartProps } from "./IFactoryMethodWebPartProps";
import * as lodash from "@microsoft/sp-lodash-subset";
import List from "./components/models/List";
import { Environment, EnvironmentType } from "@microsoft/sp-core-library";
import IDataProvider from "./components/dataproviders/IDataProvider";
import MockDataProvider from "./test/MockDataProvider";
import SharePointDataProvider from "./components/dataproviders/SharepointDataProvider";
export default class FactoryMethodWebPart extends BaseClientSideWebPart<IFactoryMethodWebPartProps> {
private _dropdownOptions: IPropertyPaneDropdownOption[];
private _selectedList: List;
private _disableDropdown: boolean;
private _dataProvider: IDataProvider;
private _factorymethodContainerComponent: FactoryMethod;
protected onInit(): Promise<void> {
this.context.statusRenderer.displayLoadingIndicator(this.domElement, "Todo");
/*
Create the appropriate data provider depending on where the web part is running.
The DEBUG flag will ensure the mock data provider is not bundled with the web part when you package the
solution for distribution, that is, using the --ship flag with the package-solution gulp command.
*/
if (DEBUG && Environment.type === EnvironmentType.Local) {
this._dataProvider = new MockDataProvider();
} else {
this._dataProvider = new SharePointDataProvider();
this._dataProvider.webPartContext = this.context;
}
this.openPropertyPane = this.openPropertyPane.bind(this);
/*
Get the list of tasks lists from the current site and populate the property pane dropdown field with the values.
*/
this.loadLists()
.then(() => {
/*
If a list is already selected, then we would have stored the list Id in the associated web part property.
So, check to see if we do have a selected list for the web part. If we do, then we set that as the selected list
in the property pane dropdown field.
*/
if (this.properties.spListIndex) {
this.setSelectedList(this.properties.spListIndex.toString());
this.context.statusRenderer.clearLoadingIndicator(this.domElement);
}
});
return super.onInit();
}
// render method of the webpart, actually calls Component
public render(): void {
const element: React.ReactElement<IFactoryMethodProps > = React.createElement(
FactoryMethod,
{
spHttpClient: this.context.spHttpClient,
siteUrl: this.context.pageContext.web.absoluteUrl,
listName: this._dataProvider.selectedList === undefined ? "GenericList" : this._dataProvider.selectedList.Title,
dataProvider: this._dataProvider,
configureStartCallback: this.openPropertyPane
}
);
//console.log(this._dataProvider.selectedList.Title);
// reactDom.render(element, this.domElement);
this._factorymethodContainerComponent = <FactoryMethod>ReactDom.render(element, this.domElement);
}
// loads lists from the site and fill the dropdown.
private loadLists(): Promise<any> {
return this._dataProvider.getLists()
.then((lists: List[]) => {
// disable dropdown field if there are no results from the server.
this._disableDropdown = lists.length === 0;
if (lists.length !== 0) {
this._dropdownOptions = lists.map((list: List) => {
return {
key: list.Id,
text: list.Title
};
});
}
});
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void {
/*
Check the property path to see which property pane feld changed. If the property path matches the dropdown, then we set that list
as the selected list for the web part.
*/
if (propertyPath === "spListIndex") {
this.setSelectedList(newValue);
}
/*
Finally, tell property pane to re-render the web part.
This is valid for reactive property pane.
*/
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
}
// sets the selected list based on the selection from the dropdownlist
private setSelectedList(value: string): void {
const selectedIndex: number = lodash.findIndex(this._dropdownOptions,
(item: IPropertyPaneDropdownOption) => item.key === value
);
const selectedDropDownOption: IPropertyPaneDropdownOption = this._dropdownOptions[selectedIndex];
if (selectedDropDownOption) {
this._selectedList = {
Title: selectedDropDownOption.text,
Id: selectedDropDownOption.key.toString()
};
this._dataProvider.selectedList = this._selectedList;
}
}
// we add fields dynamically to the property pane, in this case its only the list field which we will render
private getGroupFields(): IPropertyPaneField<any>[] {
const fields: IPropertyPaneField<any>[] = [];
// we add the options from the dropdownoptions variable that was populated during init to the dropdown here.
fields.push(PropertyPaneDropdown("spListIndex", {
label: "Select a list",
disabled: this._disableDropdown,
options: this._dropdownOptions
}));
/*
When we do not have any lists returned from the server, we disable the dropdown. If that is the case,
we also add a label field displaying the appropriate message.
*/
if (this._disableDropdown) {
fields.push(PropertyPaneLabel(null, {
text: "Could not find tasks lists in your site. Create one or more tasks list and then try using the web part."
}));
}
return fields;
}
private openPropertyPane(): void {
this.context.propertyPane.open();
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
/*
Instead of creating the fields here, we call a method that will return the set of property fields to render.
*/
groupFields: this.getGroupFields()
}
]
}
]
};
}
}

View File

@ -0,0 +1,6 @@
export interface IFactoryMethodWebPartProps {
spListIndex: number;
listName: string;
}
export default IFactoryMethodWebPartProps;

View File

@ -0,0 +1,74 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.factoryMethod {
.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 {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// 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: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,199 @@
//#region Imports
import * as React from "react";
import styles from "./FactoryMethod.module.scss";
import { IFactoryMethodProps } from "./IFactoryMethodProps";
import {
IDetailsListItemState,
IDetailsNewsListItemState,
IDetailsDirectoryListItemState,
IDetailsAnnouncementListItemState,
IFactoryMethodState
} from "./IFactoryMethodState";
import { IListItem } from "./models/IListItem";
import { IAnnouncementListItem } from "./models/IAnnouncementListItem";
import { INewsListItem } from "./models/INewsListItem";
import { IDirectoryListItem } from "./models/IDirectoryListItem";
import { escape } from "@microsoft/sp-lodash-subset";
import { SPHttpClient, SPHttpClientResponse } from "@microsoft/sp-http";
import { ListItemFactory} from "./ListItemFactory";
import { TextField } from "office-ui-fabric-react/lib/TextField";
import {
DetailsList,
DetailsListLayoutMode,
Selection,
buildColumns,
IColumn
} from "office-ui-fabric-react/lib/DetailsList";
import { MarqueeSelection } from "office-ui-fabric-react/lib/MarqueeSelection";
import { autobind } from "office-ui-fabric-react/lib/Utilities";
import PropTypes from "prop-types";
//#endregion
export default class FactoryMethod extends React.Component<IFactoryMethodProps, IFactoryMethodState> {
constructor(props: IFactoryMethodProps, state: any) {
super(props);
this.setInitialState();
}
// lifecycle help here: https://staminaloops.github.io/undefinedisnotafunction/understanding-react/
//#region Mouting events lifecycle
// the data returned from render is neither a string nor a DOM node.
// it's a lightweight description of what the DOM should look like.
// inspects this.state and this.props and create the markup.
// when your data changes, the render method is called again.
// react diff the return value from the previous call to render with
// the new one, and generate a minimal set of changes to be applied to the DOM.
public render(): React.ReactElement<IFactoryMethodProps> {
if (this.state.hasError) {
// you can render any custom fallback UI
return <h1>Something went wrong.</h1>;
} else {
switch(this.props.listName) {
case "GenericList":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsListItemState.items} columns={this.state.columns} />;
case "News":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsNewsListItemState.items} columns={this.state.columns}/>;
case "Announcements":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsAnnouncementsListItemState.items} columns={this.state.columns}/>;
case "Directory":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsDirectoryListItemState.items} columns={this.state.columns}/>;
default:
return null;
}
}
}
public componentDidCatch(error: any, info: any): void {
// display fallback UI
this.setState({ hasError: true });
// you can also log the error to an error reporting service
console.log(error);
console.log(info);
}
// componentDidMount() is invoked immediately after a component is mounted. Initialization that requires DOM nodes should go here.
// if you need to load data from a remote endpoint, this is a good place to instantiate the network request.
// this method is a good place to set up any subscriptions. If you do that, dont forget to unsubscribe in componentWillUnmount().
// calling setState() in this method will trigger an extra rendering, but it is guaranteed to flush during the same tick.
// this guarantees that even though the render() will be called twice in this case, the user wont see the intermediate state.
// use this pattern with caution because it often causes performance issues. It can, however, be necessary for cases like modals and
// tooltips when you need to measure a DOM node before rendering something that depends on its size or position.
public componentDidMount(): void {
this._configureWebPart = this._configureWebPart.bind(this);
this.readItemsAndSetStatus(this.props.listName);
}
//#endregion
//#region Props changes lifecycle events (after a property changes from parent component)
// componentWillReceiveProps() is invoked before a mounted component receives new props.
// if you need to update the state in response to prop
// changes (for example, to reset it), you may compare this.props and nextProps and perform state transitions
// using this.setState() in this method.
// note that React may call this method even if the props have not changed, so make sure to compare the current
// and next values if you only want to handle changes.
// this may occur when the parent component causes your component to re-render.
// react doesnt call componentWillReceiveProps() with initial props during mounting. It only calls this
// method if some of components props may update
// calling this.setState() generally doesnt trigger componentWillReceiveProps()
public componentWillReceiveProps(nextProps: IFactoryMethodProps): void {
if(nextProps.listName !== this.props.listName) {
this.readItemsAndSetStatus(nextProps.listName);
}
}
//#endregion
//#region private methods
private _configureWebPart(): void {
this.props.configureStartCallback();
}
public setInitialState(): void {
this.state = {
hasError: false,
status: this.listNotConfigured(this.props)
? "Please configure list in Web Part properties"
: "Ready",
columns:[],
DetailsListItemState:{
items:[]
},
DetailsNewsListItemState:{
items:[]
},
DetailsDirectoryListItemState:{
items:[]
},
DetailsAnnouncementsListItemState:{
items:[]
},
};
}
// reusable inline component
private ListMarqueeSelection = (itemState: {columns: IColumn[], items: IListItem[] }) => (
<div>
<DetailsList
items={ itemState.items }
columns={ itemState.columns }
setKey="set"
layoutMode={ DetailsListLayoutMode.fixedColumns }
selectionPreservedOnEmptyClick={ true }
compact={ true }>
</DetailsList>
</div>
)
// read items using factory method pattern and sets state accordingly
private readItemsAndSetStatus(listName): void {
this.setState({
status: "Loading all items..."
});
const factory: ListItemFactory = new ListItemFactory();
factory.getItems(this.props.spHttpClient, this.props.siteUrl, listName || this.props.listName)
.then((items: any[]) => {
var myItems: any = null;
switch(this.props.listName) {
case "GenericList":
items = items as IListItem[];
break;
case "News":
items = items as INewsListItem[];
break;
case "Announcements":
items = items as IAnnouncementListItem[];
break;
case "Directory":
items = items as IDirectoryListItem[];
break;
}
const keyPart: string = this.props.listName === "GenericList" ? "" : this.props.listName;
// the explicit specification of the type argument `keyof {}` is bad and
// it should not be required.
this.setState<keyof {}>({
status: `Successfully loaded ${items.length} items`,
["Details" + keyPart + "ListItemState"] : {
items
},
columns: buildColumns(items)
});
});
}
private listNotConfigured(props: IFactoryMethodProps): boolean {
return props.listName === undefined ||
props.listName === null ||
props.listName.length === 0;
}
//#endregion
}

View File

@ -0,0 +1,5 @@
import { IListItem } from "./models/IListItem";
import { SPHttpClient, SPHttpClientResponse } from "@microsoft/sp-http";
export interface IFactory {
getItems(requester: SPHttpClient, siteUrl: string, listName: string): Promise<any[]>;
}

View File

@ -0,0 +1,10 @@
import { SPHttpClient } from "@microsoft/sp-http";
import IDataProvider from "./dataproviders/IDataProvider";
export interface IFactoryMethodProps {
listName: string;
spHttpClient: SPHttpClient;
siteUrl: string;
dataProvider: IDataProvider;
configureStartCallback: () => void;
}

View File

@ -0,0 +1,33 @@
import { IListItem } from "./models/IListItem";
import { INewsListItem } from "./models/INewsListItem";
import { IDirectoryListItem } from "./models/IDirectoryListItem";
import { IAnnouncementListItem } from "./models/IAnnouncementListItem";
import {
IColumn
} from "office-ui-fabric-react/lib/DetailsList";
export interface IFactoryMethodState {
hasError: boolean;
status: string;
columns: IColumn[];
DetailsListItemState: IDetailsListItemState;
DetailsNewsListItemState: IDetailsNewsListItemState;
DetailsDirectoryListItemState : IDetailsDirectoryListItemState;
DetailsAnnouncementsListItemState : IDetailsAnnouncementListItemState;
}
export interface IDetailsListItemState {
items: IListItem[];
}
export interface IDetailsNewsListItemState {
items: INewsListItem[];
}
export interface IDetailsDirectoryListItemState {
items: IDirectoryListItem[];
}
export interface IDetailsAnnouncementListItemState {
items: IAnnouncementListItem[];
}

View File

@ -0,0 +1,130 @@
import { SPHttpClient, SPHttpClientResponse } from "@microsoft/sp-http";
import { IWebPartContext } from "@microsoft/sp-webpart-base";
import { IListItem} from "./models/IListItem";
import { IFactory } from "./IFactory";
import { INewsListItem } from "./models/INewsListItem";
import { IDirectoryListItem } from "./models/IDirectoryListItem";
import { IAnnouncementListItem } from "./models/IAnnouncementListItem";
export class ListItemFactory implements IFactory {
// private _listItems: IListItem[];
public getItems(requester: SPHttpClient, siteUrl: string, listName: string): Promise<any[]> {
switch(listName) {
case "GenericList":
let items: IListItem[];
// tslint:disable-next-line:max-line-length
return requester.get(`${siteUrl}/_api/web/lists/getbytitle('${listName}')/items?$select=Title,Id,Modified,Created,Author/Title,Editor/Title&$expand=Author,Editor`,
SPHttpClient.configurations.v1,
{
headers: {
"Accept": "application/json;odata=nometadata",
"odata-version": ""
}
})
.then((response: SPHttpClientResponse): Promise<{ value: IListItem[] }> => {
return response.json();
})
.then((json: { value: IListItem[] }) => {
console.log(JSON.stringify(json.value));
return items=json.value.map((v,i)=>(
{
// key: v.id,
id: v.Id,
title: v.Title,
created: v.Created,
createdby: v.Author.Title,
modified: v.Modified,
modifiedby: v.Editor.Title
}
));
});
case "News":
let newsitems: INewsListItem[];
// tslint:disable-next-line:max-line-length
return requester.get(`${siteUrl}/_api/web/lists/getbytitle('${listName}')/items?$select=Title,Id,Modified,Created,Created By,Modified By,newsheader,newsbody,expiryDate`,
SPHttpClient.configurations.v1,
{
headers: {
"Accept": "application/json;odata=nometadata",
"odata-version": ""
}
})
.then((response: SPHttpClientResponse): Promise<{ value: INewsListItem[] }> => {
return response.json();
})
.then((json: { value: INewsListItem[] }) => {
return newsitems=json.value.map((v,i)=>(
{
id: v.Id,
title: v.Title,
created: v.Created,
createdby: v.Author.Title,
modified: v.Modified,
modifiedby: v.Editor.Title,
newsheader: v.newsheader,
newsbody: v.newsbody,
expiryDate: v.expiryDate
}
));
});
case "Announcements":
let announcementitems: IAnnouncementListItem[];
return requester.get(`${siteUrl}/_api/web/lists/getbytitle('${listName}')/items?$select=Title,Id,Created,Author/Title,Modified,Editor/Title,announcementBody,expiryDate&$expand=Author,Editor`,
SPHttpClient.configurations.v1,
{
headers: {
"Accept": "application/json;odata=nometadata",
"odata-version": ""
}
})
.then((response: SPHttpClientResponse): Promise<{ value: IAnnouncementListItem[] }> => {
return response.json();
})
.then((json: { value: IAnnouncementListItem[] }) => {
return announcementitems=json.value.map((v,i)=>(
{
id: v.Id,
title: v.Title,
created: v.Created,
createdby: v.Author.Title,
modified: v.Modified,
modifiedby: v.Editor.Title,
announcementBody: v.announcementBody,
expiryDate: v.expiryDate
}
));
});
case "Directory":
let directoryitems: IDirectoryListItem[];
return requester.get(`${siteUrl}/_api/web/lists/getbytitle('${listName}')/items?$select=Title,Id`,
SPHttpClient.configurations.v1,
{
headers: {
"Accept": "application/json;odata=nometadata",
"odata-version": ""
}
})
.then((response: SPHttpClientResponse): Promise<{ value: IDirectoryListItem[] }> => {
return response.json();
})
.then((json: { value: IDirectoryListItem[] }) => {
return directoryitems=json.value.map((v,i)=>(
{
id: v.Id,
title: v.Title,
created: v.Created,
createdby: v.Author.Title,
modified: v.Modified,
modifiedby: v.Editor.Title,
firstName: v.firstName,
lastName: v.lastName,
mobileNumber: v.mobileNumber,
internalNumber: v.internalNumber
}
));
});
default:
break;
}
}
}

View File

@ -0,0 +1,11 @@
import { IWebPartContext } from "@microsoft/sp-webpart-base";
import List from "../models/List";
import {IListItem} from "../models/IListItem";
interface IDataProvider {
selectedList: List;
webPartContext: IWebPartContext;
getLists(): Promise<List[]>;
}
export default IDataProvider;

View File

@ -0,0 +1,48 @@
import {
SPHttpClient,
SPHttpClientBatch,
SPHttpClientResponse
} from "@microsoft/sp-http";
import { IWebPartContext } from "@microsoft/sp-webpart-base";
import List from "../models/List";
import IDataProvider from "./IDataProvider";
export default class SharePointDataProvider implements IDataProvider {
private _selectedList: List;
private _lists: List[];
private _listsUrl: string;
private _listItemsUrl: string;
private _webPartContext: IWebPartContext;
public set selectedList(value: List) {
this._selectedList = value;
this._listItemsUrl = `${this._listsUrl}(guid'${value.Id}')/items`;
}
public get selectedList(): List {
return this._selectedList;
}
public set webPartContext(value: IWebPartContext) {
this._webPartContext = value;
this._listsUrl = `${this._webPartContext.pageContext.web.absoluteUrl}/_api/web/lists`;
}
public get webPartContext(): IWebPartContext {
return this._webPartContext;
}
// get all lists, not only tasks lists
public getLists(): Promise<List[]> {
// const listTemplateId: string = '171';
// const queryString: string = `?$filter=BaseTemplate eq ${listTemplateId}`;
// const queryUrl: string = this._listsUrl + queryString;
return this._webPartContext.spHttpClient.get(this._listsUrl, SPHttpClient.configurations.v1)
.then((response: SPHttpClientResponse) => {
return response.json();
})
.then((json: { value: List[] }) => {
return this._lists = json.value;
});
}
}

View File

@ -0,0 +1,6 @@
import {IListItem} from "./IListItem";
export interface IAnnouncementListItem extends IListItem {
announcementBody: string;
expiryDate: Date;
}

View File

@ -0,0 +1,8 @@
import {IListItem} from "./IListItem";
export interface IDirectoryListItem extends IListItem {
firstName: string;
lastName: string;
mobileNumber: string;
internalNumber: string;
}

View File

@ -0,0 +1,9 @@
export interface IListItem {
[key: string]: any;
id: string;
title: string;
modified: Date;
created: Date;
modifiedby: string;
createdby: string;
}

View File

@ -0,0 +1,7 @@
import {IListItem} from "./IListItem";
export interface INewsListItem extends IListItem {
newsheader: string;
newsbody: string;
expiryDate: Date;
}

View File

@ -0,0 +1,7 @@
class List {
public Id?: string;
public ListItemEntityTypeFullName?: string;
public Title: string;
}
export default List;

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field"
}
});

View File

@ -0,0 +1,10 @@
declare interface IFactoryMethodWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
}
declare module 'FactoryMethodWebPartStrings' {
const strings: IFactoryMethodWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,8 @@
/// <reference types="mocha" />
import { assert } from "chai";
describe("FactoryMethodWebPart", () => {
it("should do something", () => {
assert.ok(true);
});
});

Some files were not shown because too many files have changed in this diff Show More