This post is in continuation to the previous one (Click here.). We configured a dropdown control in our property pane to let user select a SharePoint list. Now we'll add a checkbox list control to let user select multiple fields of the list selected in the first dropdown control.
Let's discuss the custom react component at the core, AsyncCheckList
first.
AsyncChecklist.tsx
This file is where we render the actual HTML for the component. Check out the render method.
public render()
{
const loading =
this.state.loading ?
<Spinner label={this.props.strings.loading} /> :
<div />;
const error =
this.state.error != null ?
<div
className="ms-TextField-errorMessage ms-u-slideDownIn20">
{
Text.format(
this.props.strings.errorFormat,
this.state.error
)
}
</div> :
<div />;
const checklistItems = this.state.items.map((item, index) => {
return (
<Checkbox
id={ item.id }
label={ item.label }
defaultChecked={ this.isCheckboxChecked(item.id) }
disabled={ this.props.disable }
onChange={ this.onCheckboxChange.bind(this) }
inputProps={ { value: item.id } }
className={ styles.checklistItem }
key={ index } />
);
});
return (
<div className={ styles.checklist }>
<Label>{ this.props.strings.label }</Label>
{ loading }
{!this.state.loading &&
<div className={ styles.checklistItems }>
<div
className={ styles.checklistPadding }>
{ checklistItems }
</div>
</div>
}
{ error }
</div>
);
}
In the method above, every checkbox was equipped with the locally defined method "onCheckboxChange
" as the change event handler and another locally defined method was called for every checkbox to decide whether the checkbox should be rendered as checked or unchecked. Let's examine both methods.
private onCheckboxChange(ev?: React.FormEvent<HTMLInputElement>,
checked?: boolean)
{
let checkboxKey =
ev.currentTarget.attributes.getNamedItem('value').value;
let itemIndex = this.checkedItems.indexOf(checkboxKey);
if(checked)
{
if(itemIndex == -1)
{
this.checkedItems.push(checkboxKey);
}
}
else
{
if(itemIndex >= 0)
{
this.checkedItems.splice(itemIndex, 1);
}
}
if(this.props.onChange)
{
this.props.onChange(this.checkedItems);
}
}
private isCheckboxChecked(checkboxId: string)
{
return
(
this.checkedItems.filter
(
(checkedItem) =>
{
return
checkedItem.toLowerCase().trim() ==
checkboxId.toLowerCase().trim();
}
).length > 0
);
}
Two more methods we should look at in the next class are:
componentDidMount
- This method is a react lifecycle method and it is called once as the react component gets "mounted" to the host DOM HTML element. componentDidUpdate
- This method is also a react lifecycle method and it is called everytime the react component gets redrawn on the browser.
public componentDidMount(): void
{
this.loadItems();
}
public componentDidUpdate(prevProps: IAsyncChecklistProps,
prevState: {}): void
{
if (this.props.disable !== prevProps.disable ||
this.props.stateKey !== prevProps.stateKey)
{
this.loadItems();
}
}
Let's look at the loadItems
method:
private loadItems()
{
let _this_ = this;
_this_.checkedItems = this.getDefaultCheckedItems();
this.setState({
loading: true,
items: new Array<IChecklistItem>(),
error: null
});
this.props.loadItems()
.then
(
(items: IChecklistItem[]) =>
{
_this_.setState
(
(
prevState: IAsyncChecklistState,
props: IAsyncChecklistProps
): IAsyncChecklistState =>
{
prevState.loading = false;
prevState.items = items;
return prevState;
}
);
}
)
.catch((error: any) =>
{
_this_.setState
(
(
prevState: IAsyncChecklistState,
props: IAsyncChecklistProps
): IAsyncChecklistState =>
{
prevState.loading = false;
prevState.error = error;
return prevState;
}
);
}
);
}
Now look at the class signature itself:
export class AsyncChecklist extends
React.Component<IAsyncChecklistProps, IAsyncChecklistState>
Clearly, we should look into interfaces IAsyncChecklistProps
and IAsyncChecklistState
. As evident from the names, the former defines component properties and the latter does the same for component's state. In turn, they make use of two more interfaces, namely IAsyncChecklistStrings
and IChecklistItem
, we shall discuss these too.
IAsyncChecklistProps
export interface IAsyncChecklistProps
{
loadItems: () => Promise<IChecklistItem[]>;
onChange?: (checkedKeys:string[]) => void;
checkedItems: string[];
disable?: boolean;
strings: IAsyncChecklistStrings;
stateKey?: string;
}
IAsyncChecklistState
export interface IAsyncChecklistState
{
loading: boolean;
items: IChecklistItem[];
error: string;
}
IChecklistItem
export interface IChecklistItem
{
id: string;
label: string;
}
IAsyncChecklistStrings
export interface IAsyncChecklistStrings
{
label: string;
loading: string;
errorFormat: string;
}
This was all about the react component. There was nothing SpFX specific in the react component. Next up is the wrapper around the react component. This wrapper extends IPropertyPaneField
which is defined in '@microsoft/sp-webpart-base
', hence it is an SpFx specific class. It is the actual Custom property control. In our case, it also makes use of two interfaces which we have defined. These interfaces are IPropertyPaneAsyncChecklistProps
and IPropertyPaneAsyncChecklistInternalProps
. Let's do away with the two interfaces first.
IPropertyPaneAsyncChecklistProps
export interface IPropertyPaneAsyncChecklistProps
{
loadItems: () => Promise<IChecklistItem[]>;
onPropertyChange: (propertyPath: string, newCheckedKeys: string[]) => void;
checkedItems: string[];
disable?: boolean;
strings: IAsyncChecklistStrings;
}
IPropertyPaneAsyncChecklistInternalProps
export interface IPropertyPaneAsyncChecklistInternalProps extends
IPropertyPaneAsyncChecklistProps,
IPropertyPaneCustomFieldProps
{
}
Next up is the Custom property class, PropertyPaneAsyncChecklist
.
PropertyPaneAsyncChecklist
Class level attributes:
public type: PropertyPaneFieldType = PropertyPaneFieldType.Custom;
public targetProperty: string;
public properties: IPropertyPaneAsyncChecklistInternalProps;
public loadedItems: boolean;
private elem: HTMLElement;
Constructor
of the class:
constructor(targetProperty: string,
properties: IPropertyPaneAsyncChecklistProps)
{
this.targetProperty = targetProperty;
this.properties =
{
loadItems: properties.loadItems,
checkedItems: properties.checkedItems,
onPropertyChange: properties.onPropertyChange,
disable: properties.disable,
strings: properties.strings,
onRender: this.onRender.bind(this),
key: targetProperty
};
}
Render
functions:
public render(): void {
if (!this.elem) {
return;
}
this.onRender(this.elem);
}
private onRender(elem: HTMLElement): void {
if (!this.elem) {
this.elem = elem;
}
const asyncChecklist: React.ReactElement<IAsyncChecklistProps> =
React.createElement(AsyncChecklist,
{
loadItems: this.properties.loadItems,
checkedItems: this.properties.checkedItems,
onChange: this.onChange.bind(this),
disable: this.properties.disable,
strings: this.properties.strings,
stateKey: new Date().toString()
});
ReactDom.render(asyncChecklist, elem);
this.loadedItems = true;
}
onChange
function:
private onChange(checkedKeys: string[]): void
{
this.properties.onPropertyChange(this.targetProperty, checkedKeys);
}
Now the webpart itself.
ListViewWebPart
Let's look at the property pane configuration method first.
private listDD:PropertyPaneAsyncDropdown ;
private viewFieldsChecklist:PropertyPaneAsyncChecklist ;
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration
{
this.listDD = new PropertyPaneAsyncDropdown('listUrl',
{
label: strings.ListFieldLabel,
loadOptions: this.loadLists.bind(this),
onPropertyChange: this.onCustomPropertyPaneChange.bind(this),
selectedKey: this.properties.listUrl
});
this.viewFieldsChecklist = new PropertyPaneAsyncChecklist('viewFields'
,
{
loadItems: this.loadViewFieldsChecklistItems.bind(this),
checkedItems: this.properties.viewFields,
onPropertyChange: this.onCustomPropertyPaneChange.bind(this),
disable: isEmpty(this.properties.listUrl),
strings: strings.viewFieldsChecklistStrings
});
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
}),
this.listDD,
this.viewFieldsChecklist
]
}
]
}
]
};
}
Two callbacks, onCustomPropertyPaneChange
and loadViewFieldsChecklistItems
:
private onCustomPropertyPaneChange(
propertyPath: string, newValue: any):
void
{
const oldValue = get(this.properties, propertyPath);
update(this.properties, propertyPath, (): any => { return newValue; });
this.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
this.resetDependentPropertyPanes(propertyPath);
if (!this.disableReactivePropertyChanges)
this.render();
}
private loadViewFieldsChecklistItems():
Promise<IChecklistItem[]>
{
return this.listService.getViewFieldsChecklistItems(
this.context.pageContext.web.absoluteUrl,
this.properties.listUrl);
}
resetDependentPropertyPanes
method:
private resetDependentPropertyPanes(propertyPath: string):void
{
if (propertyPath == "listUrl")
{
this.resetViewFieldsPropertyPane();
}
}
private resetViewFieldsPropertyPane()
{
this.properties.viewFields = null;
update(
this.properties, "viewFields",
(): any => { return this.properties.viewFields; });
this.viewFieldsChecklist.properties.checkedItems = null;
this.viewFieldsChecklist.properties.disable =
isEmpty(this.properties.listUrl);
this.viewFieldsChecklist.render();
}
This was it for the webpart.
Now look at the list service method to fetch the columns:
public getViewFieldsChecklistItems(
webUrl: string, listUrl: string
):
Promise<IChecklistItem[]>
{
if (isEmpty(webUrl) || isEmpty(listUrl))
{
return Promise.resolve(new Array<IChecklistItem[]>());
}
return new Promise<IChecklistItem[]>(
(resolve, reject) =>
{
this.getListFields(
webUrl, listUrl,
['InternalName', 'Title'],
'Title'
)
.then(
(data:any) =>
{
let fields:any[] = data.value;
let items:IChecklistItem[] =
fields.map
(
(field) =>
{
return
{
id: field.InternalName,
label: Text.format(
"{0} \{\{{1}\}\}",
field.Title, field.InternalName)
};
}
);
resolve(items);
})
.catch((error) => {
reject(error.statusText ? error.statusText : error);
});
});
}
public getListFields(
webUrl: string,
listUrl: string,
selectProperties?: string[],
orderBy?: string):
Promise<any>
{
return new Promise<any>(
(resolve,reject) =>
{
let selectProps = selectProperties ? selectProperties.join(',') : '';
let order = orderBy ? orderBy : 'InternalName';
let endpoint =
Text.format(
"{0}/_api/web/lists/GetByTitle('{1}')/Fields?$select={2}&$orderby={3}",
webUrl,
listUrl.split("/").pop(),
selectProps, order
);
this.spHttpClient
.get(
endpoint,
SPHttpClient.configurations.v1
)
.then(
(response: SPHttpClientResponse)
=>
{
if(response.ok)
{
resolve(response.json());
}
else {
reject(response);
}
})
.catch((error) => { reject(error); });
});
}