Sharepoint - How does one pass parameters to SharePoint Framework Extensions in practice?

Using Properties in Your Extension

The this.properties property of your extension are defined by the Interface you setup. By default, the interface gets defined in your main extension class file.

For instance, in the jQuery-Field-ItemOrder FieldCustomizer sample the properties interface is defined in the ISpfxItemOrderFieldCustomizerProperties interface. In this case, it's just a simple string called OrderField.

This property is then referenced in the main class during the onInit method using this.properties.OrderField. It sounds like you're not having any trouble with this aspect of the properties but rather how to set them up on a per "instance" basis.

Specifying Properties During Development

During development, you're absolutely correct that you specify them as a JSON string in the debug query string parameters like this:

?loadSPFX=true&debugManifestsFile=https://localhost:4321/temp/manifests.js&fieldCustomizers={"Reorder":{"id":"e6bdc269-2080-47b8-b096-e7bf2a9263a9","properties":{"OrderField":"CustomOrder"}}}

Specifying Properties During Deployment

But what about when deploying to production? You are correct that there is a single interface for your extension. However, you can easily provide specific property values for your extension for each instance (whether per site, list, or even field).

If you have allowed tenant wide deployment for your extension then the extension is available everywhere (but it isn't actually configured anywhere). You configure your extension through the use of CustomActions. This is where you will specify the location, ClientSideComponentId and ClientSideComponentProperties. You can see examples of these properties in the PnP PowerShell Cmdlet Add-PnPCustomAction.

The ClientSideComponentProperties is where you would specify that same JSON string. So for the debug URL above, the equivalent value would be:

{"OrderField":"CustomOrder"}

Your custom actions can have different property values when deployed to various sites. You can deploy these through the feature framework (although this will eliminate the tenant wide deployment and have the effect of always setting the same properties for your extension), through the PnP PowerShell Cmdlet (or PnP Core) shown above, or through PnP Remote Provisioning. There is even an open source command line tool to do it: spfx-extensions-cli


theChrisKent's answer set me on the right track, but as I noted in my comment on his answer, I was really looking for something that could be used from a web interface (such as a client side webpart), without having to use PowerShell.

And this is indeed possible using SharePoint's REST APIs:

Field Customizer

  • To associate a field with a particular field customizer that has been installed in the site or tenant-wide, set the field's ClientSideComponentId property to the ID (guid) of your field customizer.
  • To set the properties that are passed into a field customizer for a particular list, set the field's ClientSideComponentProperties to the JSON value for the properties you want to pass in.

ListView Command Set

  • To add a command set (that has been installed in a given site or tenant-wide) to your list, add an item to the list's UserCustomActions, with a Location property value of ClientSideExtension.ListViewCommandSet.CommandBar, indicating the new custom action's ClientSideComponentId property as the ID (guid) of your extension.
  • To set the properties that are passed in, include the properties' JSON value as the ClientSideComponentProperties property on the new UserCustomAction that you are adding.
  • To modify the properties for an item after it has been added, locate the UserCustomAction that has already been added and use a MERGE operation to modify its ClientSideComponentProperties property.

I have created some example code that shows how to carry out all of these operations. It's not very beautifully organized, but hopefully it is enough to go on. setAndCheckClientSideComponent is the function for configuring a field customizer, and setCustomAction is the function for configuring a listview command set. Everything else is helper functions that are used by these two functions.

var fetchJson = async (url, options) => (await fetch(url, options)).json();

async function getDigest(site) {
    const resObj = await fetchJson(site + '/_api/contextinfo', {
        method: 'POST',
        headers: {
            Accept: 'application/json'
        },
        credentials: 'include'
    });

    return resObj.FormDigestValue;
}

function fieldUrl(site, listName, fieldName) {
    return site + "/_api/web/lists/getbytitle('" + listName + "')/fields/getbyinternalnameortitle('" + fieldName + "')";
}

async function setClientSideComponent(site, listName, fieldName, digest, customizerId, customizerProperties) {
    var data = { '__metadata': { 'type': 'SP.Field' } };
    if (customizerId) {
        data.ClientSideComponentId = customizerId;
    }
    if (customizerProperties) {
        data.ClientSideComponentProperties = JSON.stringify(customizerProperties);
    }

    const resp = await fetch(fieldUrl(site, listName, fieldName), {
        method: "POST",
        headers: {
            "X-RequestDigest": digest,
            "content-type": "application/json;odata=verbose",
            "X-HTTP-Method": "MERGE"
        },
        body: JSON.stringify(data),
        credentials: 'include'
    });

    return resp.status;
}

async function getField(site, listName, fieldName) {
    const resObj = await fetchJson(fieldUrl(site, listName, fieldName), {
        credentials: 'include',
        headers: {
            Accept: 'application/json;odata=verbose'
        }
    })

    return resObj.d;
}

async function setAndCheckClientSideComponent(site, listName, fieldName, customizerId, customizerProperties) {
    try {
        const digest = await getDigest(site);

        const status = await setClientSideComponent(site, listName, fieldName, digest, customizerId, customizerProperties);

        if (!/2\d\d/.test(status)) {
            throw new Error('Error code ' + status + ' received attempting to change field settings.');
        }

        console.log('Finished setting field settings with status code:', status);
        const field = await getField(site, listName, fieldName);

        console.log('Field settings retrieved:');
        console.log('  ClientSideComponentId:', field.ClientSideComponentId);
        console.log('  ClientSideComponentProperties:', field.ClientSideComponentProperties);
    } catch (error) {
        console.error(error);
    }
}

async function setCustomAction(site, listName, componentId, componentProperties, customActionId, title = '', description = '') {
    try {
        const digest = await getDigest(site);

        const urlBase = `${site}/_api/web/lists/getbytitle('${listName}')/UserCustomActions`;
        const url = customActionId ? `${urlBase}('${customActionId}')` : urlBase;

        const body = JSON.stringify({
            '__metadata': { 'type': 'SP.UserCustomAction' },
            Location: 'ClientSideExtension.ListViewCommandSet.CommandBar',
            Title: title,
            Description: description,
            ClientSideComponentId: componentId,
            ClientSideComponentProperties: JSON.stringify(componentProperties),
        });
        const headers = {
            'X-RequestDigest': digest,
            "content-type": "application/json;odata=verbose",
        };

        const fullHeaders = {
            ...headers,
            ...(customActionId ? { "X-HTTP-Method": "MERGE" } : {}),
        };

        await fetch(url, { method: 'POST', body, headers, credentials: 'include' });
    } catch (error) {
        console.error(error);
    }
}

Tags:

Spfx