Mathias Osterkamp

Specialist – focus development Microsoft technology stack

Javascript - Publish classic list form webparts

Problem

For classic SharePoint 2019 libraries you still need your EditForm.aspx and DispForm.aspx to edit your properties. If you like to make any changes, create another form page or repair it by code, it is a little bit more complicated. Most time you try to edit the “DefaultEditFormUrl” property of your list. If you try to change the “DefaultEditFormUrl” property it leads some time to the following error:

Unable to find an SPForm matching URL...

The reason is, sharepoint evaluates your target url and try to find the form on the page. So you have also to upload the list form webpart.

Solution

Here i show some helpers to realise this with javascript code. Most samples on internet are powershell, but doing this in a SPFX solution it is also very smart. We use here some help from pnp js (https://pnp.github.io/pnpjs/) framework. You can download the content of the aspx file from a existing library by open your folder in internet explorer and copy the file.

Upload your editform.aspx


const folderUrl = '/sites/sitecollection/library/';
const dispFormUrl = `${folderUrl}/Forms/EditForm.aspx`;
const content = `..........`; //replace your content
await this.uploadFile(folderUrl, dispFormUrl, content);

/**
  * Upload a file from a library
  *
  * @param folderUrl relative path of a library or the folder in a library
  * @param fileUrl new relative url of the file
  * @param content content of the file
  */
public async uploadFile(folderUrl: string, fileUrl: string, content: Blob | ArrayBuffer | string): Promise<void> {
  const props: IAddUsingPathProps = { Overwrite: true };
  await sp.web.getFolderByServerRelativeUrl(folderUrl).files.addUsingPath(fileUrl, content, props);
} 

Import Webparts

You need to import the list webpart, don’t forget to set the listid. Your class should also have a property for spHttpClient and absoluteWebUrl from pnp js.


const listInfoId:string = 'xxxxx-xx-xxxx-..';

const editwebpartxml = `<?xml version="1.0" encoding="utf-8"?>
  <WebPart xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.microsoft.com/WebPart/v2">
    <Title />
    <FrameType>Default</FrameType>
    <Description />
    <IsIncluded>true</IsIncluded>
    <ZoneID>Main</ZoneID>
    <PartOrder>1</PartOrder>
    <FrameState>Normal</FrameState>
    <Height />
    <Width />
    <AllowRemove>true</AllowRemove>
    <AllowZoneChange>true</AllowZoneChange>
    <AllowMinimize>true</AllowMinimize>
    <AllowConnect>true</AllowConnect>
    <AllowEdit>true</AllowEdit>
    <AllowHide>true</AllowHide>
    <IsVisible>true</IsVisible>
    <DetailLink />
    <HelpLink />
    <HelpMode>Modeless</HelpMode>
    <Dir>Default</Dir>
    <PartImageSmall />
    <MissingAssembly>Dieses Webpart kann nicht importiert werden.</MissingAssembly>
    <PartImageLarge />
    <IsIncludedFilter />
    <Assembly>Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</Assembly>
    <TypeName>Microsoft.SharePoint.WebPartPages.ListFormWebPart</TypeName>
    <ListName xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">{${listInfoId}}</ListName>
    <ListId xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">${listInfoId}</ListId>
    <PageType xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">PAGE_EDITFORM</PageType>
    <FormType xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">6</FormType>
    <ControlMode xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">Edit</ControlMode>
    <ViewFlag xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">1048576</ViewFlag>
    <ViewFlags xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">Default</ViewFlags>
    <ListItemId xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">0</ListItemId>
  </WebPart>`;

const displaywebpartxml = `<?xml version="1.0" encoding="utf-8"?>
  <WebPart xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.microsoft.com/WebPart/v2">
    <Title />
    <FrameType>Default</FrameType>
    <Description />
    <IsIncluded>true</IsIncluded>
    <ZoneID>Main</ZoneID>
    <PartOrder>1</PartOrder>
    <FrameState>Normal</FrameState>
    <Height />
    <Width />
    <AllowRemove>true</AllowRemove>
    <AllowZoneChange>true</AllowZoneChange>
    <AllowMinimize>true</AllowMinimize>
    <AllowConnect>true</AllowConnect>
    <AllowEdit>true</AllowEdit>
    <AllowHide>true</AllowHide>
    <IsVisible>true</IsVisible>
    <DetailLink />
    <HelpLink />
    <HelpMode>Modeless</HelpMode>
    <Dir>Default</Dir>
    <PartImageSmall />
    <MissingAssembly>Dieses Webpart kann nicht importiert werden.</MissingAssembly>
    <PartImageLarge />
    <IsIncludedFilter />
    <Assembly>Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</Assembly>
    <TypeName>Microsoft.SharePoint.WebPartPages.ListFormWebPart</TypeName>
    <ListName xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">{${listInfoId}}</ListName>
    <ListId xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">${listInfoId}</ListId>
    <PageType xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">PAGE_DISPLAYFORM</PageType>
    <FormType xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">4</FormType>
    <ControlMode xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">Display</ControlMode>
    <ViewFlag xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">1048576</ViewFlag>
    <ViewFlags xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">Default</ViewFlags>
    <ListItemId xmlns="http://schemas.microsoft.com/WebPart/v2/ListForm">0</ListItemId>
  </WebPart>`;


await this.importClassicPageWebpart(dispFormUrl, 'Main', 1, displaywebpartxml);
await this.importClassicPageWebpart(editFormUrl, 'Main', 1, editwebpartxml);


/**
  * Imports webpart on classic page
  * @param pageUrl
  * @param zone Main
  * @param order 1
  * @param webpartxml
  * @returns
  */
public async importClassicPageWebpart(
  pageUrl: string,
  zone: string,
  order: number,
  webpartxml: string
): Promise<void> {
  const webid = (await sp.web.get()).Id;
  const siteid = (await sp.site.get()).Id;
  webpartxml = this.encodeXml(webpartxml);
  const requestBody = `<?xml version="1.0" encoding="UTF-8"?>
  <Request
  xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="Javascript Library">
  <Actions>
    <ObjectPath Id="41" ObjectPathId="40" />
    <ObjectPath Id="43" ObjectPathId="42" />
    <ObjectIdentityQuery Id="44" ObjectPathId="42" />
    <ObjectPath Id="46" ObjectPathId="45" />
    <ObjectPath Id="48" ObjectPathId="47" />
    <ObjectIdentityQuery Id="49" ObjectPathId="47" />
    <Query Id="50" ObjectPathId="45">
      <Query SelectAllProperties="true">
        <Properties />
      </Query>
    </Query>
  </Actions>
  <ObjectPaths>
    <Method Id="40" ParentId="28" Name="GetLimitedWebPartManager">
      <Parameters>
        <Parameter Type="Number">1</Parameter>
      </Parameters>
    </Method>
    <Method Id="42" ParentId="40" Name="ImportWebPart">
      <Parameters>
        <Parameter Type="String">${webpartxml}</Parameter>
      </Parameters>
    </Method>
    <Property Id="45" ParentId="42" Name="WebPart" />
    <Method Id="47" ParentId="40" Name="AddWebPart">
      <Parameters>
        <Parameter ObjectPathId="45" />
        <Parameter Type="String">${zone}</Parameter>
        <Parameter Type="Number">${order}</Parameter>
      </Parameters>
    </Method>
    <Identity Id="28" Name="82d7c99e-f015-0000-6402-fdbb9f2aa54d|740c6a0b-85e2-48a0-a494-e0f1759d4aa7:site:${siteid}:web:${webid}:file:${pageUrl}" />
  </ObjectPaths>
</Request>`;
  return await this.postcsom(requestBody).then((result) => {
    if (result[0].ErrorInfo !== null) {
      throw new Error(JSON.stringify(result[0].ErrorInfo));
    }
    return;
  });
}


/**
  * xml encodes a string
  * @param str
  * @returns
  */
private encodeXml(str: string): string {
  const xml_special_to_escaped_one_map = {
    '&': '&amp;',
    '"': '&quot;',
    '<': '&lt;',
    '>': '&gt;'
  };

  return str.replace(/([\&"<>])/g, function (str, item) {
    return xml_special_to_escaped_one_map[item];
  });
}




/**
  * sends a request to client svc
  * @param data request data
  */
private async postcsom(data: string): Promise<any> {
  const clientServiceUrl = this.absoluteWebUrl + '/_vti_bin/client.svc/ProcessQuery';

  const formDigest = await this.getFormDigest();

  const requestHeaders: Headers = new Headers();
  requestHeaders.append('Accept', 'application/json');
  requestHeaders.append('Content-Type', 'text/xml');
  requestHeaders.append('X-RequestDigest', formDigest);

  const httpPostOptions: IHttpClientOptions = {
    headers: requestHeaders,
    body: data
  };

  const serviceResponse: IResponse = await this.spHttpClient.post(clientServiceUrl,SPHttpClient.configurations.v1, httpPostOptions);
  const serviceJSONResponse = await serviceResponse.json();
  if (serviceResponse.ok) {
    return serviceJSONResponse;
  }
  return null;
}

/**
  * digest is needed for post requests
  */
private async getFormDigest(): Promise<string> {
  const contextInfoUrl = this.absoluteWebUrl + '/_api/contextinfo';

  const requestHeaders: Headers = new Headers();
  requestHeaders.append('Accept', 'application/json');
  requestHeaders.append('Content-Type', 'text/xml');

  const httpPostOptions: IHttpClientOptions = {
    headers: requestHeaders
  };
  const contextInfoResponse: IResponse = await this.spHttpClient.post(contextInfoUrl, httpPostOptions);
  const contextInfoJsonResponse = await contextInfoResponse.json();
  const formDigest: string = contextInfoJsonResponse.FormDigestValue;

  return formDigest;
}


Update DefaultDisplayFormUrl

Finally you can update your list form without errors.


await this.updateList(listTitle, {
    DefaultDisplayFormUrl: dispFormUrl,
    DefaultEditFormUrl: editFormUrl,
    ContentTypesEnabled: false
  });

/**
  * Updates List Properties
  * @param listTitle
  * @param properties of type IListInfo possible
  */
public async updateList(listTitle: string, properties: Record<string, unknown>): Promise<void> {
  await sp.web.lists.getByTitle(listTitle).update(properties);
} 

By the way, you see also a easy method how to interact with jsom api, without having the complete api included.