Luis Valencia
Luis Valencia

Reputation: 33998

How to use factory method in typescript properly

Well, this is a hard question to write, lets see if I can explain myself correctly.

In Sharepoint a ListItem has by default the properties, id, title, createdby, createddate, modifiedby, modifieddate.

But you can create custom lists with more columns, but they inherit from the base list, so any new columns will be added to the previous ones.

My idea is to create a generic solution in Sharepoint Framework with typescript and react to be able to read from any list and render using Office UI Fabric DetailsList component: https://developer.microsoft.com/en-us/fabric#/components/detailslist

So I started with the models:

ListIitem.ts

export  class ListItem {
    constructor(
        public id: string,
        public title: string,
        public modified: Date,
        public created: Date,
        public modifiedby: string,
        public createdby: string,
    ) { }
}

DirectoryListItem.ts

import {ListItem} from './ListItem';

export class DirectoryListItem extends ListItem {
    constructor(
        public id: string,
        public title: string,
        public modified: Date,
        public created: Date,
        public modifiedby: string,
        public createdby: string,
        public firstName: string,
        public lastName: string,
        public mobileNumber: string,
        public internalNumber: string,
    ) {
        super(id, title, modified, created, modifiedby, createdby);
     } 
}

AnnoucementLIstItem.ts

import {ListItem} from './ListItem';

export class  AnnouncementListItem extends ListItem {
    constructor(
        public id: string,
        public title: string,
        public modified: Date,
        public created: Date,
        public modifiedby: string,
        public createdby: string,
        public announcementBody: string,
        public expiryDate: Date
    ) {
        super(id, title, modified, created, modifiedby, createdby);
     }   
}

and so on.

Then I created a ListItemFactory with only one method, which you can see returns an array of ListItem

import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
import { IWebPartContext } from '@microsoft/sp-webpart-base';
import {ListItem} from './models/ListItem';

export class ListItemFactory{    
    public _getItems(requester: SPHttpClient, siteUrl: string, listName: string): ListItem[] {
        let items: ListItem[];
        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: ListItem[] }> => {
            return response.json();
        })
        .then((response: { value: ListItem[] }): void => {
            items= response.value;
        });
        return items;
    }
}

And the other factories look similar as well:

import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
import {ListItemFactory} from './ListItemFactory';
import {ListItem} from './models/ListItem';
import {DirectoryListItem} from './models/DirectoryListItem';

export class DirectoryListItemFactory extends ListItemFactory {
    public _getItems(requester: SPHttpClient, siteUrl: string, listName: string): DirectoryListItem[] {
        let items: DirectoryListItem[];
        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: DirectoryListItem[] }> => {
            return response.json();
        })
        .then((response: { value: DirectoryListItem[] }): void => {
            items= response.value;
        });
        return items;
    }
}

the only difference is that instead of returning ListItem, it returns DirectoryListItem array.

Until there, everything is clear to me, then I have my component which will receive as one of the parameters, the list name.

Pay close attention to the readItems method, and the render method where my question is focused.

  1. In the render method, the component receives an item array but also a column array.

  2. On the readItems I have a switch statement and depending on the selected list name, I use a different factory and return the items on the appropiate array type.

However I am not sure, how to properly pass both items and columns parameters to the DetailList Component to make this solution as generic as possible.

import * as React from 'react';
import styles from './FactoryMethod.module.scss';
import { IFactoryMethodProps } from './IFactoryMethodProps';
import { IFactoryMethodCrudState } from './IFactoryMethodCrudState';
import { ListItem } from './models/ListItem';
import { escape } from '@microsoft/sp-lodash-subset';
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
import { ListItemFactory} from './ListItemFactory';
import { AnnouncementListItemFactory} from './AnnouncementListItemFactory';
import { DirectoryListItemFactory} from './DirectoryListItemFactory';
import { NewsListItemFactory} from './NewsListItemFactory';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import {
  DetailsList,
  DetailsListLayoutMode,
  Selection
} 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';

let _items: any[];

let _columns = [
  {
    key: 'column1',
    name: 'Name',
    fieldName: 'name',
    minWidth: 100,
    maxWidth: 200,
    isResizable: true
  },
  {
    key: 'column2',
    name: 'Value',
    fieldName: 'value',
    minWidth: 100,
    maxWidth: 200,
    isResizable: true
  },
];

export default class FactoryMethod extends React.Component<any, any> {
  private listItemEntityTypeName: string = undefined;
  private _selection: Selection;

  constructor(props: IFactoryMethodProps, state: IFactoryMethodCrudState) {
    super(props);

    /* this.state = {
      status: this.listNotConfigured(this.props) ? 'Please configure list in Web Part properties' : 'Ready',
      items: []
    }; */

    this._selection = new Selection({
      onSelectionChanged: () => this.setState({ selectionDetails: this._getSelectionDetails() })
    });

    this.state = {
      status: this.listNotConfigured(this.props) ? 'Please configure list in Web Part properties' : 'Ready',
      items: _items,
      selectionDetails: this._getSelectionDetails()
    };
  }



  public componentWillReceiveProps(nextProps: IFactoryMethodProps): void {
    this.listItemEntityTypeName = undefined;
    this.setState({
      status: this.listNotConfigured(nextProps) ? 'Please configure list in Web Part properties' : 'Ready',
      items: []
    });
  }

  public render(): React.ReactElement<IFactoryMethodProps> {
    let { items, selectionDetails } = this.state;

        return (
          <div>
            <div>{ selectionDetails }</div>
            <TextField
              label='Filter by name:'
              onChanged={ this._onChanged }
            />
            <MarqueeSelection selection={ this._selection }>
              <DetailsList
                items={ items }
                columns={ _columns }
                setKey='set'
                layoutMode={ DetailsListLayoutMode.fixedColumns }
                selection={ this._selection }
                selectionPreservedOnEmptyClick={ true }
                onItemInvoked={ this._onItemInvoked }
                compact={ true }
              />
            </MarqueeSelection>
          </div>
        );


  }

  private readItems(): void {
    this.setState({
      status: 'Loading all items...',
      items: []
    });

    //Here its where we actually use the pattern to make our coding easier.
    switch(this.props.listName)
    {
      case "List":
        let factory = new  ListItemFactory();
        let listItems  = factory._getItems(this.props.spHttpClient, this.props.siteUrl, this.props.listName);  
        this.setState({
          status: `Successfully loaded ${listItems.length} items`,
          items: listItems
        });      
        break;
      case "Announcements":
        let announcementFactory = new  AnnouncementListItemFactory();
        let announcementlistItems  = announcementFactory._getItems(this.props.spHttpClient, this.props.siteUrl, this.props.listName);  
        this.setState({
          status: `Successfully loaded ${listItems.length} items`,
          items: announcementlistItems
        });         
        break;
      case "News":
        let newsFactory = new  NewsListItemFactory();
        let newsListItems  = newsFactory._getItems(this.props.spHttpClient, this.props.siteUrl, this.props.listName);  
        this.setState({
          status: `Successfully loaded ${listItems.length} items`,
          items: newsListItems
        });  
        break;
      case "Directory":
        let directoryFactory = new  DirectoryListItemFactory();
        let directoryListItems  = directoryFactory._getItems(this.props.spHttpClient, this.props.siteUrl, this.props.listName);  
        this.setState({
          status: `Successfully loaded ${listItems.length} items`,
          items: directoryListItems
        });  
        break;
      default : 
        break;
    }    
  }

  private _getSelectionDetails(): string {
    let selectionCount = this._selection.getSelectedCount();

    switch (selectionCount) {
      case 0:
        return 'No items selected';
      case 1:
        return '1 item selected: ' + (this._selection.getSelection()[0] as any).name;
      default:
        return `${selectionCount} items selected`;
    }
  }

  private listNotConfigured(props: IFactoryMethodProps): boolean {
    return props.listName === undefined ||
      props.listName === null ||
      props.listName.length === 0;
  }

  @autobind
  private _onChanged(text: any): void {
    this.setState({ items: text ? _items.filter(i => i.name.toLowerCase().indexOf(text) > -1) : _items });
  }

  private _onItemInvoked(item: any): void {
    alert(`Item invoked: ${item.name}`);
  }
}

Upvotes: 1

Views: 1503

Answers (1)

Luca Taccagni
Luca Taccagni

Reputation: 1059

Sorry but i've probably misunderstood your question; However if you want to inject an obj in another, why are you extending the list? I'm going to explain with code:

import {ListItem} from './ListItem';

export class DirectoryListItem {
    constructor(
        public listIt: ListItem; 
        // instead of repeating code, you inject your base-obj listItem
        public firstName: string,
        public lastName: string,
        public mobileNumber: string,
        public internalNumber: string,
    ) { } 
}

Probably this is not what you're asking for, but if i've understood your class problem could be useful. Hope this helped.

Upvotes: 1

Related Questions