Updating props in a react-based SPFX webpart after initial Render

The react-based spfx webparts that are generated by the yeoman templates do not allow us to update the properties of the react component after it is initially rendered. It can sometimes be useful to do so.

To be able to update the properties after initial rendering make the following changes to the webpart class:

  1. Make the local variable element in the Render method a class variable.
  2. Declare another class variable called formComponent whose type is the type of your react class
  3. Save the result of reactDOM.render in the webparts render method to the formComponent class variable

So the class variables look like this for a webpart called ‘Test’:


private element: React.ReactElement;
private formComponent:Test;

and your render method looks like this:

public render(): void {
this.element= React.createElement(
Test,
{
description: this.properties.description
}
);
this.formComponent=ReactDom.render(this.element, this.domElement) as Test;
}

Now, in any function in your webpart class you can update the props of the component as shown here:

private someFunction():void {
let newProps:ITestProps = this.element.props;
newProps.description="New Description";
this.element.props = newProps;
this.formComponent.forceUpdate();

}

 

This can be useful in a number of cases. For instance, say you have a react based spfx component that has a bunch of dropdowns  with data that comes from different sharepoint lists. You would typically use either of two options. First, you could fetch all the information the onInit method of your webpart and pass it to your component as props, so that your component could render the dropdowns using data from your props (this can cause the initial render to be slow).  Second option would be to have the react component call sharepoint directly when it needs to render the list (this can cause a lag when the user clicks the dropdown).

 

The other option, discussed here, is to render your component initially with just the first few of the dropdown lists populated in the props. The onInit method gets the values for the first few dropdowns and then calls render. The render method does the render as shown above, and then continues to fetch the information for the additional dropdowns. When it’s done, it adds them to the props and re-renders the component as shown in SomeFunction above.

Advertisements
Posted in react, spfx | Tagged | Leave a comment

Creating and Outlook Add-in using an SPFX Webpart

This post demonstrates how you can create an outlook add-in using an SPFX webpart.

The first step is to create a page that will host your webpart and be displayed in outlook.  On your site create a new page (I called mine test.aspx).  Add the following code to the Additional Page head:

addin5
The first line will allow the page to be opened in an Iframe (which is how add-ins open). The second line loads the Office.js script needed to talk to office.

The second step is  to create the webpart that will make up the office add-in.

Add the following member variables to the webpart class:

  private from: string;

private attachments: Array;

private body: string;

private office: office;

private subject: string;

In the onOnit method of the spfx webpart  add the following code.

public onInit(): Promise {

return new Promise((resolve: (args: T) => void, reject: (error: Error) => void) => {

window[“Office”][“initialize”] = () => {

debugger;

this.office = window[“Office”];

this.attachments = window[“Office”].context.mailbox.item.attachments;

this.from = window[“Office”].context.mailbox.item.from.emailAddress;

this.subject = window[“Office”].context.mailbox.item.subject;

window[“Office”].context.mailbox.item.body.getAsync(

“html”,

{ asyncContext: “This is passed to the callback” },

(result) => {

debugger;

this.body = result.value;

resolve(window[“Office”]);// or undefined

}

);

};

});

}

This code will load up the office runtime and set the attachments, from ,and subject member variables from the currently opened email. The full code for the webpart can be found at https://github.com/russgove/fpaoutlookaddin.

Note that at this point the webpart has a context in the sharepoint site as well as the users email so we can exchange data between the two.

Now that the webpart is built, go ahead and  deploy it and add it to the page created in step one.

Last thing we need is a manifest file used to load our add-in into outlook. A sample  manifest is  can be found in the SampleOfficeManifest.xml file on the github repo.  Change   YOURTENANT in the sample to the name of your tenant, and change ‘sites/fpa/siteassets/test.aspx’ to the page where you added your webpart. Save the file to your local disk.

 

Now open outlook on O365 and select Manage Addins:addin1

Select My add-ins –> add a custom add-in –> from file

addin2

And select the manifest file you just created.

Now that the add-in was added, open an email in your web outlook and notice the new icon (my icon is set to local host in my manifest so I am getting that default icon in the example below):

addin3

Click on the  icon and , voila:

addin4

We have an spfx webpart running as an outlook add-in!

Posted in Add-in, office-ui-fabric-react, sharepoint, spfx, Uncategorized | Tagged , , | Leave a comment

Cancel Running workflows prior to saving an item.

This is a problem for workflows that delay until a certain date before sending out reminders. The user creates an item with 1/27/2018 as the due date and the workflow starts waiting until 1/27/2018 to send out a reminder. If the user then edits the item and changes the date to 1/27/2018, no new workflow is

Started, and the running workflow is unaware the date has changed.

This is typically resolved by  setting up a workflow to run as part of a retention policy. This can sometimes be troublesome if you want to send out multiple reminders –say  7 days before due and 3 days before due—because you need to set up calculated fields with the reminder dates.

But there is another way.  With a bit of JSOM code, a sharepoint edit form can be made to cancel

the running workflow prior to saving the item to the list. This way a new workflow gets started when you save the item (provided you have the workflow configured to run when and item is added AND when an item is changed.

If you are using an SPFX webpart as your edit form (which is unfortunately not currently supported on modern lists), the following code can be used to cancel the running workflow just prior to saving the new version.

 private async getWorkFlowDefinitionByName(workflowDeploymentService: SP.WorkflowServices.WorkflowDeploymentService, workFlowName: string): Promise {
    let context = workflowDeploymentService.get_context();
    let wfDefinitions = workflowDeploymentService.enumerateDefinitions(true);
    context.load(wfDefinitions);
    await new Promise((resolve, reject) => {
      context.executeQueryAsync((x) => {
        resolve();
      }, (error) => {
        console.error("an error occured getting workflow definitions");
        console.log(error);
        reject();
      });
    });
    let foundDefinition: SP.WorkflowServices.WorkflowDefinition;
    let defEnum = wfDefinitions.getEnumerator();
    while (defEnum.moveNext()) {
      const wfDefinition = defEnum.get_current();
      if (wfDefinition.get_displayName() === workFlowName) {
        foundDefinition = wfDefinition;
        break;
      }
    }
    return Promise.resolve(foundDefinition);
  }
  private async getWorkFlowSubscriptionByDefinitionIdListId(workflowSubscriptionService: SP.WorkflowServices.WorkflowSubscriptionService, workFlowDefinitionId: string, listId): Promise {
    let context: SP.ClientRuntimeContext = workflowSubscriptionService.get_context();
    let wfSubscriptions: SP.WorkflowServices.WorkflowSubscriptionCollection =
      workflowSubscriptionService.enumerateSubscriptionsByList(listId);
    context.load(wfSubscriptions);
    await new Promise((resolve, reject) => {
      context.executeQueryAsync((x) => {
        resolve();
      }, (error) => {
        console.error("an error occured gettin workflow subscriptions");
        console.log(error);
        reject();
      });
    });
    if (!wfSubscriptions) {
      alert("Failed to load workflow subscriptsion. Running workflows were not cancelled. This can happen if the Office 365 workflow service is unavailable.");
      console.error("Failed to load Workflow instances.");
      return Promise.reject("Failed to load Workflow instances.");
    }
    let foundSubscription: SP.WorkflowServices.WorkflowSubscription;
    let subscriptionEnum = wfSubscriptions.getEnumerator();
    while (subscriptionEnum.moveNext()) {
      const wfSubscription: SP.WorkflowServices.WorkflowSubscription = subscriptionEnum.get_current();
      if (wfSubscription.get_definitionId().toString().toUpperCase() === workFlowDefinitionId.toString().toUpperCase()) {
        foundSubscription = wfSubscription;
        break;
      }
    }
    return Promise.resolve(foundSubscription);
  }
  private async cancelRunningWorkflows(ItemId: number, listId: string, workflowName: string): Promise {
    if (!workflowName) {
      return Promise.resolve();
    }
    var context = SP.ClientContext.get_current();
    // get all the workflow service managers
    var workflowServicesManager: SP.WorkflowServices.WorkflowServicesManager = SP.WorkflowServices.WorkflowServicesManager.newObject(context, context.get_web());
    var workflowInstanceService: SP.WorkflowServices.WorkflowInstanceService = workflowServicesManager.getWorkflowInstanceService();
    var workflowSubscriptionService: SP.WorkflowServices.WorkflowSubscriptionService = workflowServicesManager.getWorkflowSubscriptionService();
    var workflowDeploymentService: SP.WorkflowServices.WorkflowDeploymentService = workflowServicesManager.getWorkflowDeploymentService();
    //Get all the definitions from the Deployment Service, or get a specific definition using the GetDefinition method.
    let wfDefinition: SP.WorkflowServices.WorkflowDefinition = (await this.getWorkFlowDefinitionByName(workflowDeploymentService, workflowName));
    if (!wfDefinition) {
      console.error("Coold not find workflow Definition for workflow named : " + workflowName);
      alert("Coold not find workflow Definition for workflow named : " + workflowName);
      return Promise.resolve();
    }
    let wfDefinitionId: string = wfDefinition.get_id();
    // get the subscription for the list
    let wfSubscription: SP.WorkflowServices.WorkflowSubscription =
      await this.getWorkFlowSubscriptionByDefinitionIdListId(workflowSubscriptionService, wfDefinitionId, listId);
    if (!wfSubscription) {
      console.error("Could not find a subscription for  workflow named : " + workflowName + " ib the TR List");
      alert("Could not find a subscription for  workflow named : " + workflowName + " ib the TR List");
      return Promise.resolve();
    }
    let wfSubscriptionId: string = wfSubscription.get_id().toString().toUpperCase();
    let wfInstances: SP.WorkflowServices.WorkflowInstanceCollection = workflowInstanceService.enumerateInstancesForListItem(listId, ItemId);
    context.load(wfInstances);
    await new Promise((resolve, reject) => {
      context.executeQueryAsync((x) => {
        resolve();
      }, (error) => {
        console.log(error);
        reject();
      });
    });
    if (!wfInstances) {
      debugger;
      alert("Failed to load workflow instances. Running workflows were not cancelled. This can happen if the Office 365 workflow service is unavailable.");
      console.error("Failed to load Workflow instances.");
      return Promise.resolve();
    }
    var instancesEnum = wfInstances.getEnumerator();
    let runningInstance;
    while (instancesEnum.moveNext()) {
      var instance = instancesEnum.get_current();
      let instanceSubscriptionId = instance.get_workflowSubscriptionId().toString();
      let instanceStatus = instance.get_status();
      if (instanceSubscriptionId.toUpperCase() === wfSubscriptionId && instanceStatus === 1) {
        runningInstance = instance;
      }
    }
    if (runningInstance) {
      workflowInstanceService.terminateWorkflow(runningInstance);
      await new Promise((resolve, reject) => {
        context.executeQueryAsync((x) => {
          console.log("Workflow Termination Successful");
          resolve();
        }, (error) => {
          console.log(error);
          debugger;
          console.error("Failed to terminate workflow.");
          resolve();
        });
      });
    }
  }

With the above methods in place , you just need to call


if (originalReuiredDate != tr.RequiredDate) {
await this.cancelRunningWorkflows(itemId, listId, workflowName).then((x) => {
console.log("Workflow has been terminated");
});
}

prior to saving your list item. If there is an instance of the workflow already running , it will be canceled and a new workflow will start once your item is saved.

Posted in react, spfx, Uncategorized | Tagged , | Leave a comment

Code Editor Property Pane Control for SPFX WebParts

I submitted a PR to the spfx-property-controls repository today for a new SPFX property pane control – the PropertyFieldCodeEditor control.
The new control uses the Ace editor under the hood (see https://ace.c9.io/).
The Ace editor supports editing many types of content with auto-complete, error checking, syntax highlighting , etc.

The PropertyFieldCodeEditor property pane control allows you to edit Json, Javascript, Sass, Typescript, Plain text, HTML, Handlebars and XML code within a language-aware editor, right from the property pane.

I can be use to :
• edit the XML needed to add an SPFX webpart to a page (that’s the reason it was created originally)
• edit HTML snippets to be shown in an SPFX webpart
• edit plain text to be shown in an spfx webpart

• edit a CAML query to be passed to renderListDataAsStream
• edit JSON values to pass complex data structures to a webpart
• edit Handlebars templates to be used in an spfx webpart
• edit javascript snippets -?
• Edit typescript code _? (we could have an azure job compile!)
• and more…

Merry Christmas!

Capture

Capture

Posted in react, sharepoint, spfx, Uncategorized | Tagged , | Leave a comment

Using Async/Await with JSOM

Async/Await can make JSOM coding much easier and is simple to set up. All you need to do is wrap your executeQuery calls in a Promise, then you can await them!


await new Promise((resolve, reject) => {

clientContext.executeQueryAsync((x) => {

resolve();

}, (error) => {

console.log(error);

reject();

});

});

Here’s a full example of a method that hides the firs webpart on the page:


public async AddWebPartToEditForm(webRelativeUrl: string, editformUrl) {

const clientContext: SP.ClientContext = new SP.ClientContext(webRelativeUrl);

var oFile = clientContext.get_web().getFileByServerRelativeUrl(editformUrl);

var limitedWebPartManager = oFile.getLimitedWebPartManager(SP.WebParts.PersonalizationScope.shared);

let webparts = limitedWebPartManager.get_webParts();

clientContext.load(webparts, 'Include(WebPart)');

clientContext.load(limitedWebPartManager);

await new Promise((resolve, reject) => {

clientContext.executeQueryAsync((x) => {

resolve();

}, (error) => {

console.log(error);

reject();

});

});

let originalWebPartDef = webparts.get_item(0);

let originalWebPart = originalWebPartDef.get_webPart();

originalWebPart.set_hidden(true);

originalWebPartDef.saveWebPartChanges();

await new Promise((resolve, reject) => {

clientContext.executeQueryAsync((x) => {

console.log("the webpart was hidden");

resolve();

}, (error) => {

console.log(error);

reject();

});

});

}

 

Posted in Uncategorized | Leave a comment

Displaying Rotated Column Headers on an Office UI Fabric Details List within an SPFX Webpart

Sometimes you need to display a grid where the data displayed within the grid’s columns is much narrower than the column headers. For instance you may need to show a user’s name in the column header and just a checkbox in the details indicating that the user has some attribute. In such cases it is often useful to rotate the grids column headers at a 45-degree angle so that more columns can fit on the screen as described here.

 

The link above shows how this can be accomplished in HTML Tables. The Office UI Fabric DetailsList is, however, rendered using <div> tags (at least for now). The <div> tags used to display the DetailsList each have a css class representing their use—for instance ms-DetailsHeader or ms-DetailsHeader-cell. We cannot set these css classes in our application’s app.module.scss because the css clasnames would be renamed to be specific top our application, and fabric would not recognize those class names as noted here.

 

Instead we can add a .scss file to our solution in addition to the .module.scss file. The .scss file won’t have its classes renamed as noted in the link above. Then in the .scss file we can enter the following code:

.ms-DetailsHeader { //* when adjusting line height also need to adjust second parameter to transform:translate below

height: 140px;

white-space: nowrap;

}

//*Set the wrappers around the headers to display at 45degree angles

.ms-DetailsHeader-cell.rotatedColumnHeader{

transform: translate(25px,40px ) //*Change second parameter when adjusting line height

rotate(315deg);

padding-bottom: 90px;

width: 36px !important;

 

 

}

.ms-DetailsHeader-cell{

vertical-align: bottom;

}

.ms-DetailsHeader-cell span {

overflow: visible;

}

This scss sets up the classes to rotate the column headers of any column defined with

headerClassName: “rotatedColumnHeader”,

minWidth: 20,

maxWidth: 20,

With this configuration the columns only take up 20 pixels, bit the headers are displayed at a 45 degree angle so that they are visible.

 

In addition to setting the headerClassName of each column, we also need to import our .scss file into our react component with this code, placed in the react component:

      require(‘./spSecurity.css’);

 

Once this has been set up properly the column headers will display as noted in the article above:

rotadeheaders

 

 

A working example can be found here

 

 

Posted in office-ui-fabric-react, react, spfx | Tagged , , , , , , , | Leave a comment

Using the Office UI Fabric Grouped DetailsList in React-based SPFX Webparts.

Say you have an array of objects in your application that looks like this

export class Widget {

public constructor(

public id: number,

public title: string,

public isActive:string,

public manufacturer:string

) { }

}

And you want to display the items in a react DetailsList  showing the ‘title’ of each item grouped by manufacturer.

 

The JSX to render such a display could be set up like this:

<DetailsList

layoutMode={DetailsListLayoutMode.fixedColumns}

selectionMode={SelectionMode.none}

groups={this.getAvailableWidgetGroups()}

items={this.getAvailableWidgets()}

setKey=”id”

columns={[

{ key: “title”, name: “Widget Name”, fieldName: “title”, minWidth: 20, maxWidth: 100 },

]}

/>

 

 

The getAvailableWidgets is defined as

public getAvailableWidgets(): Array<Widget> {

var tempWidgets = _.filter(this.props.widgets, (widget: Widget) => {

return !this.trContainsWidget(this.state.tr, widget.id);

});

var widgets = _.map(tempWidgets, (widget) => {

return {

title: widget.title,

manufacturer: (widget.manufacturer)?widget.manufacturer:”(none)”,

id: widget.id,

isActive:widget.isActive

};

}).filter((p)=>{return p.isActive===”Yes”});

return _.orderBy(this.state,widgets, [“manufacturer”], [“asc”]);

}

 

The key point is that the list returned must be sorted by whatever fields you want to group on.

 

 

The getAvailableWidgetGroups is defined like this:

public getAvailableWidgetGroups(): Array<IGroup> {

var pigs: Array<Widget> = this.getAvailableWidgets();

var widgetManufactureres = _.countBy(pigs, (p1: Widget) => { return p1.manufacturer; });

var groups: Array<IGroup> = [];

for (const pm in widgetManufactureres) {

groups.push({

name: pm,

key: pm,

startIndex: _.findIndex(pigs, (pig) => { return pig.manufacturer === pm; }),

count: widgetManufactureres[pm],

isCollapsed: true

});

}

return groups;

}

 

The method returns an array of IGroup which is required to group a Detailslist. The IGroup needs the starting row number of each group and the number of element in the group. The method uses lodash to get these values.

 

The lodash countBy function returns an object whose keys are the distinct names of the manufacturers and whose values are the number of rows with that manufacturer.

So to create the array of IGroup, it just loops through each manufacturer and creates an IGroup. It uses the findIndex  function to find the first row with the manufacturer and gets the count from the widgetManufactureres object.

 

Posted in fabric, react, spfx, Uncategorized | Tagged , , | Leave a comment