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