SPFx SharePoint Development: Quickly Generate List-Based Models Using a Single Command

SPFx SharePoint Development: Quickly Generate List-Based Models Using a Single Command

In a recent post, I shared my CLI @spfxappdev/cli which has some awesome features. Many of these features come from my other posts, like "My personal tips how to configure a SPFx project after creation"

Today I published a new version of the CLI, v1.1.2. This version comes with a new feature. It will help you to save a lot of time.

I guess almost everyone who works with SharePoint and SharePoint API has the same challenge. You need to aggregate a list and the items in it. So far so good, but sometimes the (internal) field names are, shall we say, "unfriendly". For example, if you create a new field via the UI and choose a display name like My Fancy Column, SharePoint generates the internal/static name and sets it to My_x0020_Fancy_x0020_Column

To get the value from the list item (via SharePoint REST API), you need to do something like this:

const myModel = new MyModel();
const listItem = listItemCollection[i];
myModel.myFancyColumn = listItem['My_x0020_Fancy_x0020_Column'];

And that's exactly what can be done faster and easier with the CLI in just a few seconds. Let's demonstrate it using the standard SitePages library.

#Long
spfxappdev generate model Page --weburl https://{tenant}.sharepoint.com --username "your@email.com" --password "yourPW" --list SitePages
#Short (with alias)
spfx g m Page --u https://{tenant}.sharepoint.com --user "your@email.com" --p "yourPW" --l SitePages

The generated interface is:

export interface IPage {
    name: string;
    complianceAssetId: string;
    wikiContent: string;
    title: string;
    authoringCanvasContent: any;
    bannerImageURL: UrlFieldValue;
    description: string;
    promotedState: number;
    firstPublishedDate: Date;
    pageLayoutContent: any;
    authorBylineId: number[];
    topicHeader: string;
    sitePageFlags: string[];
    callToAction: string;
    originalSourceUrl: string;
    originalSourceSiteID: string;
    originalSourceWebID: string;
    originalSourceListID: string;
    originalSourceItemID: string;
    id: number;
    contentType: string;
    created: Date;
    createdById: number;
    modified: Date;
    modifiedById: number;
    copySource: string;
    checkedOutToId: number;
    checkInCommentId: number;
    type: string;
    fileSize: string;
    itemChildCountId: number;
    folderChildCountId: number;
    commentCountId: number;
    likeCountId: number;
    sensitivityId: number;
    edit: string;
    sourceVersionConvertedDocumentId: number;
    sourceNameConvertedDocumentId: number;
}

The generated class looks like this:

import { IPage } from './';
import { mapper } from '@spfxappdev/mapper';
import { UrlFieldValue } from './';
export class Page implements IPage {
    @mapper({ nameOrPath: 'FileLeafRef',  })
    public name: string;

    @mapper({ nameOrPath: 'ComplianceAssetId', toClassOnly: true })
    public complianceAssetId: string;

    @mapper({ nameOrPath: 'WikiField',  })
    public wikiContent: string;

    @mapper({ nameOrPath: 'Title',  })
    public title: string;

    @mapper({ nameOrPath: 'CanvasContent1',  })
    public authoringCanvasContent: any;

    @mapper({ nameOrPath: 'BannerImageUrl', type: UrlFieldValue,  })
    public bannerImageURL: UrlFieldValue;

    @mapper({ nameOrPath: 'Description', toClassOnly: true })
    public description: string;

    @mapper({ nameOrPath: 'PromotedState', toClassOnly: true })
    public promotedState: number;

    @mapper({ nameOrPath: 'FirstPublishedDate', type: Date, toClassOnly: true })
    public firstPublishedDate: Date;

    @mapper({ nameOrPath: 'LayoutWebpartsContent',  })
    public pageLayoutContent: any;

    @mapper({ nameOrPath: 'OData__AuthorBylineId.results',  })
    public authorBylineId: number[];

    @mapper({ nameOrPath: 'OData__TopicHeader',  })
    public topicHeader: string;

    @mapper({ nameOrPath: 'OData__SPSitePageFlags.results', toClassOnly: true })
    public sitePageFlags: string[];

    @mapper({ nameOrPath: 'OData__SPCallToAction',  })
    public callToAction: string;

    @mapper({ nameOrPath: 'OData__OriginalSourceUrl', toClassOnly: true })
    public originalSourceUrl: string;

    @mapper({ nameOrPath: 'OData__OriginalSourceSiteId', toClassOnly: true })
    public originalSourceSiteID: string;

    @mapper({ nameOrPath: 'OData__OriginalSourceWebId', toClassOnly: true })
    public originalSourceWebID: string;

    @mapper({ nameOrPath: 'OData__OriginalSourceListId', toClassOnly: true })
    public originalSourceListID: string;

    @mapper({ nameOrPath: 'OData__OriginalSourceItemId', toClassOnly: true })
    public originalSourceItemID: string;

    @mapper({ nameOrPath: 'ID', toClassOnly: true })
    public id: number;

    @mapper({ nameOrPath: 'ContentType',  })
    public contentType: string;

    @mapper({ nameOrPath: 'Created', type: Date, toClassOnly: true })
    public created: Date;

    @mapper({ nameOrPath: 'AuthorId', toClassOnly: true })
    public createdById: number;

    @mapper({ nameOrPath: 'Modified', type: Date, toClassOnly: true })
    public modified: Date;

    @mapper({ nameOrPath: 'EditorId', toClassOnly: true })
    public modifiedById: number;

    @mapper({ nameOrPath: 'OData__CopySource', toClassOnly: true })
    public copySource: string;

    @mapper({ nameOrPath: 'CheckoutUserId', toClassOnly: true })
    public checkedOutToId: number;

    @mapper({ nameOrPath: 'OData__CheckinCommentId', toClassOnly: true })
    public checkInCommentId: number;

    @mapper({ nameOrPath: 'DocIcon', toClassOnly: true })
    public type: string;

    @mapper({ nameOrPath: 'FileSizeDisplay', toClassOnly: true })
    public fileSize: string;

    @mapper({ nameOrPath: 'ItemChildCountId', toClassOnly: true })
    public itemChildCountId: number;

    @mapper({ nameOrPath: 'FolderChildCountId', toClassOnly: true })
    public folderChildCountId: number;

    @mapper({ nameOrPath: 'OData__CommentCountId', toClassOnly: true })
    public commentCountId: number;

    @mapper({ nameOrPath: 'OData__LikeCountId', toClassOnly: true })
    public likeCountId: number;

    @mapper({ nameOrPath: 'OData__DisplayNameId', toClassOnly: true })
    public sensitivityId: number;

    @mapper({ nameOrPath: 'Edit', toClassOnly: true })
    public edit: string;

    @mapper({ nameOrPath: 'ParentVersionStringId', toClassOnly: true })
    public sourceVersionConvertedDocumentId: number;

    @mapper({ nameOrPath: 'ParentLeafNameId', toClassOnly: true })
    public sourceNameConvertedDocumentId: number;    
}

As you can see, the CLI generates the class using the @mapper decorator. This decorator is a part of my npm package. It is developed to map a model to a SharePoint API result and vice versa. Another point you may have noticed is that the internal names are stored in the nameOrPath property, but the model property is more the general name specification in camelCase.

But how you can "convert" the API Result to your model and back to the SP List Model? It is very easy:

import { toClass, toPlain  } from '@spfxappdev/mapper';
import { Page } from '@src/models';

class MyService {

    public async getPages(): Promise<IPage[]> {
        //const pageCollection = await ...Your API call
        const pages: Page[] = toClass(Page, pageCollection);
        return pages;
    }

    public updatePage(page: IPage): Promise<void> {
        const spData = toPlain(page);

        //YOUR REST API CALL TO UPDATE THE PAGE
    }
}

The toPlain method ignores properties labeled as toClassOnly: true because they are read-only fields, like Editor/Author, and can't be updated.

That is great, isn't it?

By the way, you don't have to pass all the values like --weburl, --username and --password every time. You can set the values once via the config set command and then these values will be used (the password is encrypted).

Also new in this version: You can now create local configuration files. So you can have different configurations per project.

What do you think? How do you like it? I would be happy about feedback.

Happy coding ;)

Did you find this article valuable?

Support $€®¥09@ by becoming a sponsor. Any amount is appreciated!