Skip to content
Back to Blog
1 min read

Microsoft 365 Development: Building Productivity Apps

I wrote “Microsoft 365 Development: Building Productivity Apps” to share practical, production-minded guidance on this topic.

Office Add-ins Overview

Office Add-ins use web technologies and run across platforms:

# Install Yeoman generator
npm install -g yo generator-office

# Create Excel add-in
yo office --projectType taskpane --name "MyExcelAddin" --host excel --ts true

# Create Word add-in
yo office --projectType taskpane --name "MyWordAddin" --host word --ts true

Excel Add-in

Create an Excel add-in for data analysis:

// src/taskpane/taskpane.ts
Office.onReady((info) => {
  if (info.host === Office.HostType.Excel) {
    document.getElementById("analyze-btn")!.onclick = analyzeData;
    document.getElementById("chart-btn")!.onclick = createChart;
  }
});

async function analyzeData(): Promise<void> {
  await Excel.run(async (context) => {
    const sheet = context.workbook.worksheets.getActiveWorksheet();
    const range = sheet.getUsedRange();
    range.load("values");

    await context.sync();

    const values = range.values;

    // Calculate statistics
    const numericValues = values
      .flat()
      .filter((v) => typeof v === "number") as number[];

    const stats = {
      count: numericValues.length,
      sum: numericValues.reduce((a, b) => a + b, 0),
      average: 0,
      min: Math.min(...numericValues),
      max: Math.max(...numericValues),
    };
    stats.average = stats.sum / stats.count;

    // Write results to new sheet
    const resultsSheet = context.workbook.worksheets.add("Analysis Results");

    const resultsRange = resultsSheet.getRange("A1:B5");
    resultsRange.values = [
      ["Metric", "Value"],
      ["Count", stats.count],
      ["Sum", stats.sum],
      ["Average", stats.average],
      ["Min", stats.min],
    ];

    resultsRange.format.autofitColumns();

    const headerRange = resultsSheet.getRange("A1:B1");
    headerRange.format.font.bold = true;
    headerRange.format.fill.color = "#4472C4";
    headerRange.format.font.color = "white";

    await context.sync();
  });
}

async function createChart(): Promise<void> {
  await Excel.run(async (context) => {
    const sheet = context.workbook.worksheets.getActiveWorksheet();
    const dataRange = sheet.getUsedRange();

    const chart = sheet.charts.add(
      Excel.ChartType.columnClustered,
      dataRange,
      Excel.ChartSeriesBy.auto
    );

    chart.title.text = "Data Analysis Chart";
    chart.legend.position = Excel.ChartLegendPosition.right;
    chart.setPosition("E2", "L15");

    await context.sync();
  });
}

async function formatTable(): Promise<void> {
  await Excel.run(async (context) => {
    const sheet = context.workbook.worksheets.getActiveWorksheet();
    const range = sheet.getUsedRange();

    // Create table
    const table = sheet.tables.add(range, true);
    table.name = "DataTable";
    table.style = "TableStyleMedium2";

    // Add conditional formatting
    const conditionalFormat = range.conditionalFormats.add(
      Excel.ConditionalFormatType.colorScale
    );

    conditionalFormat.colorScale.criteria = {
      minimum: {
        type: Excel.ConditionalFormatColorCriterionType.lowestValue,
        color: "#FF6B6B",
      },
      midpoint: {
        type: Excel.ConditionalFormatColorCriterionType.percentile,
        value: 50,
        color: "#FFE66D",
      },
      maximum: {
        type: Excel.ConditionalFormatColorCriterionType.highestValue,
        color: "#4ECDC4",
      },
    };

    await context.sync();
  });
}

Word Add-in

Create a Word add-in for document processing:

// src/taskpane/word-taskpane.ts
Office.onReady((info) => {
  if (info.host === Office.HostType.Word) {
    document.getElementById("insert-btn")!.onclick = insertContent;
    document.getElementById("analyze-btn")!.onclick = analyzeDocument;
  }
});

async function insertContent(): Promise<void> {
  await Word.run(async (context) => {
    const selection = context.document.getSelection();

    // Insert formatted content
    selection.insertHtml(
      `
      <h1>Document Title</h1>
      <p>This is a paragraph with <strong>bold</strong> and <em>italic</em> text.</p>
      <ul>
        <li>Item 1</li>
        <li>Item 2</li>
        <li>Item 3</li>
      </ul>
    `,
      Word.InsertLocation.replace
    );

    await context.sync();
  });
}

async function analyzeDocument(): Promise<void> {
  await Word.run(async (context) => {
    const body = context.document.body;
    body.load("text");

    const paragraphs = body.paragraphs;
    paragraphs.load("items");

    await context.sync();

    const text = body.text;
    const wordCount = text.split(/\s+/).filter((w) => w.length > 0).length;
    const charCount = text.length;
    const paragraphCount = paragraphs.items.length;

    // Calculate reading time (200 words per minute)
    const readingTime = Math.ceil(wordCount / 200);

    // Display results
    const results = document.getElementById("results")!;
    results.innerHTML = `
      <h3>Document Statistics</h3>
      <p>Words: ${wordCount}</p>
      <p>Characters: ${charCount}</p>
      <p>Paragraphs: ${paragraphCount}</p>
      <p>Reading Time: ~${readingTime} minutes</p>
    `;
  });
}

async function searchAndHighlight(searchText: string): Promise<void> {
  await Word.run(async (context) => {
    const body = context.document.body;
    const searchResults = body.search(searchText, {
      matchCase: false,
      matchWholeWord: false,
    });

    searchResults.load("items");
    await context.sync();

    for (const result of searchResults.items) {
      result.font.highlightColor = "yellow";
      result.font.bold = true;
    }

    await context.sync();
  });
}

async function insertTable(): Promise<void> {
  await Word.run(async (context) => {
    const selection = context.document.getSelection();

    const tableData = [
      ["Name", "Department", "Email"],
      ["John Doe", "Engineering", "john@example.com"],
      ["Jane Smith", "Marketing", "jane@example.com"],
      ["Bob Johnson", "Sales", "bob@example.com"],
    ];

    const table = selection.insertTable(
      tableData.length,
      tableData[0].length,
      Word.InsertLocation.after,
      tableData
    );

    table.styleBuiltIn = Word.Style.gridTable5Dark_Accent1;

    await context.sync();
  });
}

Outlook Add-in

Create an Outlook add-in for email processing:

// src/taskpane/outlook-taskpane.ts
Office.onReady((info) => {
  if (info.host === Office.HostType.Outlook) {
    loadEmailData();
    document.getElementById("save-btn")!.onclick = saveToSystem;
  }
});

async function loadEmailData(): Promise<void> {
  const item = Office.context.mailbox.item;

  if (!item) return;

  // Load basic properties
  const subject = item.subject;
  const from = item.from?.emailAddress;

  // Get recipients
  item.to.getAsync((result) => {
    if (result.status === Office.AsyncResultStatus.Succeeded) {
      const recipients = result.value.map((r) => r.emailAddress).join(", ");
      document.getElementById("to-field")!.textContent = recipients;
    }
  });

  // Get body
  item.body.getAsync(Office.CoercionType.Text, (result) => {
    if (result.status === Office.AsyncResultStatus.Succeeded) {
      document.getElementById("body-preview")!.textContent =
        result.value.substring(0, 500) + "...";
    }
  });

  // Get attachments
  if (item.attachments && item.attachments.length > 0) {
    const attachmentList = item.attachments
      .map((a) => `<li>${a.name} (${formatBytes(a.size)})</li>`)
      .join("");
    document.getElementById(
      "attachments"
    )!.innerHTML = `<ul>${attachmentList}</ul>`;
  }

  document.getElementById("subject")!.textContent = subject;
  document.getElementById("from")!.textContent = from || "Unknown";
}

async function saveToSystem(): Promise<void> {
  const item = Office.context.mailbox.item;

  if (!item) return;

  item.body.getAsync(Office.CoercionType.Html, async (result) => {
    if (result.status === Office.AsyncResultStatus.Succeeded) {
      const emailData = {
        subject: item.subject,
        from: item.from?.emailAddress,
        body: result.value,
        receivedDate: item.dateTimeCreated,
      };

      // Send to your API
      try {
        const response = await fetch("https://api.example.com/emails", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(emailData),
        });

        if (response.ok) {
          showNotification("Success", "Email saved to system");
        }
      } catch (error) {
        showNotification("Error", "Failed to save email");
      }
    }
  });
}

function showNotification(title: string, message: string): void {
  Office.context.mailbox.item?.notificationMessages.addAsync("notification", {
    type: Office.MailboxEnums.ItemNotificationMessageType.InformationalMessage,
    message: message,
    icon: "icon-16",
    persistent: false,
  });
}

function formatBytes(bytes: number): string {
  if (bytes === 0) return "0 Bytes";
  const k = 1024;
  const sizes = ["Bytes", "KB", "MB", "GB"];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}

SharePoint Framework (SPFx)

Build SharePoint web parts:

// src/webparts/dataViewer/DataViewerWebPart.ts
import { Version } from "@microsoft/sp-core-library";
import {
  IPropertyPaneConfiguration,
  PropertyPaneTextField,
} from "@microsoft/sp-property-pane";
import { BaseClientSideWebPart } from "@microsoft/sp-webpart-base";
import { SPHttpClient, SPHttpClientResponse } from "@microsoft/sp-http";
import styles from "./DataViewerWebPart.module.scss";

export interface IDataViewerWebPartProps {
  listName: string;
}

export default class DataViewerWebPart extends BaseClientSideWebPart<IDataViewerWebPartProps> {
  public render(): void {
    this.domElement.innerHTML = `
      <div class="${styles.dataViewer}">
        <h2>List Data Viewer</h2>
        <div id="listData">Loading...</div>
      </div>
    `;

    this.loadListData();
  }

  private async loadListData(): Promise<void> {
    const listName = this.properties.listName || "Documents";

    try {
      const response: SPHttpClientResponse = await this.context.spHttpClient.get(
        `${this.context.pageContext.web.absoluteUrl}/_api/web/lists/getbytitle('${listName}')/items?$top=10`,
        SPHttpClient.configurations.v1
      );

      const data = await response.json();
      this.renderList(data.value);
    } catch (error) {
      this.domElement.querySelector("#listData")!.innerHTML =
        "Error loading data";
    }
  }

  private renderList(items: any[]): void {
    const html = items
      .map(
        (item) => `
      <div class="${styles.listItem}">
        <span class="${styles.title}">${item.Title || item.FileLeafRef}</span>
        <span class="${styles.date}">${new Date(item.Modified).toLocaleDateString()}</span>
      </div>
    `
      )
      .join("");

    this.domElement.querySelector("#listData")!.innerHTML =
      html || "No items found";
  }

  protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: { description: "Configuration" },
          groups: [
            {
              groupName: "Settings",
              groupFields: [
                PropertyPaneTextField("listName", {
                  label: "List Name",
                  description: "Enter the name of the SharePoint list",
                }),
              ],
            },
          ],
        },
      ],
    };
  }

  protected get dataVersion(): Version {
    return Version.parse("1.0");
  }
}

Summary

Microsoft 365 development offers:

  • Cross-platform Office Add-ins
  • SharePoint Framework for portals
  • Rich APIs for document manipulation
  • Integration with Microsoft Graph
  • Marketplace distribution

Build productivity-enhancing solutions that integrate seamlessly with Microsoft 365.

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.