- Published on
Office UI Fabric Callout in SharePoint Framework (SPFx)
This article explains how to implement the Office UI Fabric Callout in a SharePoint Framework (SPFx) ListView Command Set extension.
The Callout control is useful for displaying inline property panels or edit dialogs directly in the list view without redirecting users to a separate form.
⚙️ Create a new SPFx extension project
Run the following command to create a new ListView Command Set extension:
yo @microsoft/sharepoint
When prompted:
- Solution Name:
FabricCalloutExtension
- Target Environment: SharePoint Online only (latest)
- Component Type: Extension
- Extension Type: ListView Command Set
- Framework: React
📦 Install dependencies
Install the required Office UI Fabric and SPFx libraries:
npm install office-ui-fabric-react --save
npm install @microsoft/sp-dialog --save
npm install @microsoft/sp-http --save
💻 Implementation
The following code demonstrates how to create a callout dialog using Office UI Fabric's Callout
control inside an SPFx extension.
CalloutComponent.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { ICalloutProps, ICalloutState } from './ICalloutProps';
import { Callout } from 'office-ui-fabric-react/lib/Callout';
import styles01 from './Callout.module.scss';
import { BaseDialog, IDialogConfiguration } from '@microsoft/sp-dialog';
import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http'
export default class CalloutComponent extends BaseDialog {
public itemTitle: string;
public itemID: number;
public spcontext?: any | null;
public render(): void {
ReactDOM.render(<Cillout itemID={this.itemID} spcontext={this.spcontext} Title={this.itemTitle} domElement={document.activeElement.parentElement} onDismiss={this.onDismiss.bind(this)} />,
this.domElement);
}
public getConfig(): IDialogConfiguration {
return {
isBlocking: false
};
}
private onDismiss() {
ReactDOM.unmountComponentAtNode(this.domElement);
}
}
class Cillout extends React.Component<ICalloutProps, ICalloutState> {
constructor(props: ICalloutProps) {
super(props);
this.state = {
Title: this.props.Title
};
this.setState({ Title: this.props.Title });
this._saveClicked = this._saveClicked.bind(this);
this._onChangedTitle = this._onChangedTitle.bind(this);
}
public render(): JSX.Element {
return (
<div>
<Callout
className={styles01["ms-CalloutExample-callout"]}
role="alertdialog"
gapSpace={0}
target={this.props.domElement}
onDismiss={this.onDismiss.bind(this)}
setInitialFocus={true}
hidden={false}
>
<div className={styles01["ms-CalloutExample-header"]}>
<p className={styles01["ms-CalloutExample-title"]}>
Property panel
</p>
</div>
<div className={styles01["ms-CalloutExample-inner"]}>
<div className={styles01["ms-CalloutExample-content"]}>
<p className={styles01["ms-CalloutExample-subText"]}>
<TextField label="Title" value={this.state.Title} underlined onChanged={this._onChangedTitle} />
</p>
</div>
<div className={styles01["ms-CalloutExample-actions"]}>
<PrimaryButton text="Save" onClick={this._saveClicked} />
</div>
</div>
</Callout>
</div>
);
}
private onDismiss(ev: any) {
this.props.onDismiss();
}
private _onChangedTitle(newValue: string): void {
this.setState({ Title: newValue });
}
private _saveClicked() {
const body: string = JSON.stringify({
'__metadata': {
'type': 'SP.Data.' + this.props.spcontext.pageContext.list.title + 'ListItem'
},
'Title': this.state.Title
});
this.props.spcontext.spHttpClient.get(this.props.spcontext.pageContext.web.absoluteUrl + `/_api/web/lists/getbytitle('${this.props.spcontext.pageContext.list.title}')/items(` + this.props.itemID + ')', SPHttpClient.configurations.v1).then
((Response: SPHttpClientResponse) => {
this.props.spcontext.spHttpClient.post(this.props.spcontext.pageContext.web.absoluteUrl + `/_api/web/lists/getbytitle('${this.props.spcontext.pageContext.list.title}')/items(` + this.props.itemID + ')', SPHttpClient.configurations.v1,
{
headers: {
'Accept': 'application/json;odata=nometadata',
'Content-type': 'application/json;odata=verbose',
'odata-version': '',
'IF-MATCH': Response.headers.get('ETag'),
'X-HTTP-Method': 'MERGE'
},
body: body
}).then((response: SPHttpClientResponse) => {
console.log(`Status code: ${response.status}`);
console.log(`Status text: ${response.statusText}`);
this.props.onDismiss();
});
});
}
}
FabricCalloutCommandSet.ts
import { override } from '@microsoft/decorators';
import {
BaseListViewCommandSet,
Command,
IListViewCommandSetListViewUpdatedParameters,
IListViewCommandSetExecuteEventParameters
} from '@microsoft/sp-listview-extensibility';
import Callout from '../components/Callout';
export default class FabricCalloutCommandSet extends BaseListViewCommandSet<{}> {
@override
public onInit(): Promise<void> {
return Promise.resolve();
}
@override
public onListViewUpdated(event: IListViewCommandSetListViewUpdatedParameters): void {
const compareOneCommand: Command = this.tryGetCommand('COMMAND_1');
if (compareOneCommand) {
compareOneCommand.visible = event.selectedRows.length === 1;
}
}
@override
public onExecute(event: IListViewCommandSetExecuteEventParameters): void {
switch (event.itemId) {
case 'COMMAND_1':
const callout: Callout = new Callout();
callout.itemTitle=event.selectedRows[0].getValueByName('Title');
callout.itemID=event.selectedRows[0].getValueByName('ID');
callout.spcontext= this.context;
callout.show();
break;
default:
throw new Error('Unknown command');
}
}
}
🧠 How it works
- A custom Callout dialog is displayed when the user selects a list item and clicks the custom command button.
- The Callout allows inline editing of fields (like Title) without navigating away from the list.
- The SPHttpClient API is used to update the list item directly.
🚀 Deploy the solution
Once ready, build and deploy the SPFx extension:
gulp build
gulp bundle --ship
gulp package-solution --ship
Upload the generated .sppkg
file to your App Catalog and deploy the extension to your site.
Author
- Ravichandran@Hi_Ravichandran