12

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?

7

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

  • 2
    Thank you for your answer! I was clear on the first two parts, but hadn't seen anything about configuring properties by using custom actions. I have a few clarifications to ask: (1) Do you know of a way to add these custom actions using client-side code (e.g. JSOM or REST)? My end users would not be too keen on running PowerShell or a cli, but I could imagine providing them a web interface to configure the extensions that I provide to them. (2) Could you provide examples of how the usage of Add-PnPCustomAction would look for configuring properties for a site vs. a list vs. a field? – JLRishe Oct 3 '17 at 19:37
  • @theChrisKent I'm having the same question as @JLRishe, I can't seem to find any up-to-date documentation on the location param. Is ClientSideExtension.ApplicationCustomizer correct for a field customizer? – Jordan Jan 8 at 17:37
  • @Jordan Took me a long time to post it, but I've finally added an answer to my own question. Please see my answer below if you're still looking for this. – JLRishe Mar 6 at 9:03
1

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 you 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);
    }
}
  • Thanks for the code. Anyway I can achieve this using PS Set-PNPField method? – Garima Jun 24 at 10:01
  • @Garima I haven't used Set-PNPField, but it looks like you could use that to set the ClientSideComponentId and ClientSideComponentProperties on a field to enable a field customizer. – JLRishe Jun 25 at 2:24
  • Yes I tried but it is not working. I am actually trying to use Feature framework to associate field customizer with existing field but it is not working – Garima Jun 25 at 2:31
-1

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.

  • 2
    While you could use cookies or storage for persisting some values, they wouldn't be the right mechanism to use for storing the extension's configuration. After all, that configuration should be present there from the start and be consistent for all users. – Waldek Mastykarz - MVP Oct 4 '17 at 6:10
  • Yes, agreed to your point. The configurations must be there from the beginning and we must set default properties for the SPFx Extension. But I found that as more static options (also not able to find relative documentation where SPFx Ext can take dynamic properties. Although I saw while in debug mode you can pass properties from URL but when solution deployed it take only static config from manifest files.) and was looking for more dynamic approach, thats why suggested client side approach. – ThinkB4Code Oct 4 '17 at 8:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.