Question

I am currently in the process of getting up to speed with SPFx and have gone through the Extensions Getting Started guide of the official documentation and skimmed a fair portion of the other documentation pages, but one point that eludes me is how to make effective use of the .properties member that belongs to each extension type.

I'm aware of two ways to pass in a value for these properties:

  • Pass them in via the URL during debugging
  • Specify them in the elements.xml file (for Application Customizers)

The first seems to be only applicable during development.
The second is very limiting because (a) It only applies to Application Customizers and (b) it means having a fixed set of configuration values for a given .sppkg, which at the very least means a fixed set of values for the entire tenancy.

What's further puzzling is that I have looked at some official and unofficial samples for field customizers, and all of them have a documentation line showing how to pass in config values via the debug URL, but no indication of how these values would be specified in an actual deployment:

Conditional Formatting
Weather
Multilingual Fields

I can see that client-side webparts have a rich feature set for defining and specifying configuration values, but I am at a complete loss as to how to configure the three extension types (Application Customizers, Field Customizers, ListView Commands) on a per-site or per-list basis. Can someone point me in the right direction?

Was it helpful?

Solution 2

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);
    }
}

OTHER TIPS

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

Apart from the two points mention you, you have the complete power of JS and HTML5. You can take advantages of those concepts as well. I will suggest you take advantage of Client Side Cookies, Local Storage and Session Storage. Most of the modern browsers have these two features. Also you wont be limited to SPFx to get or set these properties. Also your different client side web parts and SPFx extensions can communicate with each other using these.

I personally love to store relevant data in Local Storage but choice is yours. You can prefer anyone out of these.

Another advantage of using client side storage is that, may be you can pass data via query string from your custom navigation but what if use navigates to modern pages for list/lib or view all content page directly? In that case the properties wont be available. But if you use above methods to store data, they will be available for the entire site.

Licensed under: CC-BY-SA with attribution
Not affiliated with sharepoint.stackexchange
scroll top