added design patterns samples, abstract factory, factory method, builder, singleton (#524)
This commit is contained in:
parent
88cbbf2300
commit
9c7e33b521
|
@ -0,0 +1,25 @@
|
|||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
|
||||
[*]
|
||||
|
||||
# change these settings to your own preference
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# we recommend you to keep these unchanged
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[{package,bower}.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -0,0 +1,32 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
|
||||
# Build generated files
|
||||
dist
|
||||
lib
|
||||
solution
|
||||
temp
|
||||
*.sppkg
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# Visual Studio files
|
||||
.ntvs_analysis.dat
|
||||
.vs
|
||||
bin
|
||||
obj
|
||||
|
||||
# Resx Generated Code
|
||||
*.resx.ts
|
||||
|
||||
# Styles Generated Code
|
||||
*.scss.ts
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"version": "1.4.1",
|
||||
"libraryName": "abstract-factory",
|
||||
"libraryId": "bf29a897-ff82-4d9d-86c2-d13df911c3ef",
|
||||
"environment": "spo"
|
||||
}
|
||||
}
|
|
@ -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" />
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://dev.office.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -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 -->"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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/"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://dev.office.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -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);
|
17069
samples/react-designpatterns-typescript/AbstractFactory/package-lock.json
generated
Normal file
17069
samples/react-designpatterns-typescript/AbstractFactory/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -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",
|
||||
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
export interface IAbstractfactoryWebPartProps {
|
||||
datasource: string;
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
class Customer{
|
||||
public id: string;
|
||||
public firstName: string;
|
||||
public lastName: string;
|
||||
}
|
||||
|
||||
export default Customer;
|
|
@ -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";
|
|
@ -0,0 +1,6 @@
|
|||
enum DataSources {
|
||||
SharepointList = "SharepointList",
|
||||
JsonData = "JsonData"
|
||||
}
|
||||
|
||||
export default DataSources;
|
|
@ -0,0 +1,4 @@
|
|||
export interface IAbstractFactoryProps {
|
||||
datasource: string;
|
||||
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import Customer from "./Customer";
|
||||
|
||||
export interface IAbstractFactoryState {
|
||||
items?: Customer[];
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import Customer from "./Customer";
|
||||
|
||||
interface ICustomerDao {
|
||||
insertCustomer(): number;
|
||||
deleteCustomer(): boolean;
|
||||
findCustomer(): Customer;
|
||||
updateCustomer(): boolean;
|
||||
listCustomers(): Customer[];
|
||||
}
|
||||
|
||||
export default ICustomerDao;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,7 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"PropertyPaneDescription": "Description",
|
||||
"BasicGroupName": "Group Name",
|
||||
"DescriptionFieldLabel": "Description Field"
|
||||
}
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
declare interface IAbstractfactoryWebPartStrings {
|
||||
PropertyPaneDescription: string;
|
||||
BasicGroupName: string;
|
||||
DescriptionFieldLabel: string;
|
||||
}
|
||||
|
||||
declare module 'AbstractfactoryWebPartStrings' {
|
||||
const strings: IAbstractfactoryWebPartStrings;
|
||||
export = strings;
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
|
||||
[*]
|
||||
|
||||
# change these settings to your own preference
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# we recommend you to keep these unchanged
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[{package,bower}.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -0,0 +1,32 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
|
||||
# Build generated files
|
||||
dist
|
||||
lib
|
||||
solution
|
||||
temp
|
||||
*.sppkg
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# Visual Studio files
|
||||
.ntvs_analysis.dat
|
||||
.vs
|
||||
bin
|
||||
obj
|
||||
|
||||
# Resx Generated Code
|
||||
*.resx.ts
|
||||
|
||||
# Styles Generated Code
|
||||
*.scss.ts
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"version": "1.4.1",
|
||||
"libraryName": "builder",
|
||||
"libraryId": "364e2bef-24fe-4408-b37d-6930333fd2b5",
|
||||
"environment": "spo"
|
||||
}
|
||||
}
|
|
@ -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" />
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://dev.office.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -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 -->"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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/"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://dev.office.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -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
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -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
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import IPacking from "./IPacking";
|
||||
|
||||
class Bottle implements IPacking {
|
||||
public pack(): string {
|
||||
return "Bottle";
|
||||
}
|
||||
}
|
||||
|
||||
export default Bottle;
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,3 @@
|
|||
export interface IBuilderProps {
|
||||
selectedMeal: string;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
export interface IBuilderState {
|
||||
items?: string;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import IPacking from "./IPacking";
|
||||
|
||||
interface IItem {
|
||||
name(): string;
|
||||
packing(): IPacking;
|
||||
price(): number;
|
||||
}
|
||||
|
||||
export default IItem;
|
|
@ -0,0 +1,5 @@
|
|||
interface IPacking {
|
||||
pack(): string;
|
||||
}
|
||||
|
||||
export default IPacking;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,9 @@
|
|||
import IPacking from "./IPacking";
|
||||
|
||||
class Wrapper implements IPacking {
|
||||
public pack(): string {
|
||||
return "Wrapper";
|
||||
}
|
||||
}
|
||||
|
||||
export default Wrapper;
|
7
samples/react-designpatterns-typescript/Builder/src/webparts/builder/loc/en-us.js
vendored
Normal file
7
samples/react-designpatterns-typescript/Builder/src/webparts/builder/loc/en-us.js
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"PropertyPaneDescription": "Description",
|
||||
"BasicGroupName": "Group Name",
|
||||
"DescriptionFieldLabel": "Description Field"
|
||||
}
|
||||
});
|
10
samples/react-designpatterns-typescript/Builder/src/webparts/builder/loc/mystrings.d.ts
vendored
Normal file
10
samples/react-designpatterns-typescript/Builder/src/webparts/builder/loc/mystrings.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
declare interface IBuilderWebPartStrings {
|
||||
PropertyPaneDescription: string;
|
||||
BasicGroupName: string;
|
||||
DescriptionFieldLabel: string;
|
||||
}
|
||||
|
||||
declare module 'BuilderWebPartStrings' {
|
||||
const strings: IBuilderWebPartStrings;
|
||||
export = strings;
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
|
||||
[*]
|
||||
|
||||
# change these settings to your own preference
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# we recommend you to keep these unchanged
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[{package,bower}.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -0,0 +1,32 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
|
||||
# Build generated files
|
||||
dist
|
||||
lib
|
||||
solution
|
||||
temp
|
||||
*.sppkg
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# Visual Studio files
|
||||
.ntvs_analysis.dat
|
||||
.vs
|
||||
bin
|
||||
obj
|
||||
|
||||
# Resx Generated Code
|
||||
*.resx.ts
|
||||
|
||||
# Styles Generated Code
|
||||
*.scss.ts
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"version": "1.4.1",
|
||||
"libraryName": "factory-method",
|
||||
"libraryId": "7116d524-988a-4de4-83b0-cd53165f816a",
|
||||
"environment": "spo"
|
||||
}
|
||||
}
|
|
@ -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" />
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://dev.office.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -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 -->"
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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/"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://dev.office.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -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
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export interface IFactoryMethodWebPartProps {
|
||||
spListIndex: number;
|
||||
listName: string;
|
||||
}
|
||||
|
||||
export default IFactoryMethodWebPartProps;
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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, don’t 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 won’t 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 doesn’t call componentWillReceiveProps() with initial props during mounting. It only calls this
|
||||
// method if some of component’s props may update
|
||||
// calling this.setState() generally doesn’t 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
|
||||
}
|
|
@ -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[]>;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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[];
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import {IListItem} from "./IListItem";
|
||||
|
||||
export interface IAnnouncementListItem extends IListItem {
|
||||
announcementBody: string;
|
||||
expiryDate: Date;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import {IListItem} from "./IListItem";
|
||||
|
||||
export interface IDirectoryListItem extends IListItem {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
mobileNumber: string;
|
||||
internalNumber: string;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
export interface IListItem {
|
||||
[key: string]: any;
|
||||
id: string;
|
||||
title: string;
|
||||
modified: Date;
|
||||
created: Date;
|
||||
modifiedby: string;
|
||||
createdby: string;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import {IListItem} from "./IListItem";
|
||||
|
||||
export interface INewsListItem extends IListItem {
|
||||
newsheader: string;
|
||||
newsbody: string;
|
||||
expiryDate: Date;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
class List {
|
||||
public Id?: string;
|
||||
public ListItemEntityTypeFullName?: string;
|
||||
public Title: string;
|
||||
}
|
||||
|
||||
export default List;
|
|
@ -0,0 +1,7 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"PropertyPaneDescription": "Description",
|
||||
"BasicGroupName": "Group Name",
|
||||
"DescriptionFieldLabel": "Description Field"
|
||||
}
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
declare interface IFactoryMethodWebPartStrings {
|
||||
PropertyPaneDescription: string;
|
||||
BasicGroupName: string;
|
||||
DescriptionFieldLabel: string;
|
||||
}
|
||||
|
||||
declare module 'FactoryMethodWebPartStrings' {
|
||||
const strings: IFactoryMethodWebPartStrings;
|
||||
export = strings;
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue