reactjsoffice-jsoffice-addinsexcel-addins

Excel Addin using React for Taskpane with custom functions


I am new to Office Addins and Office.js.

I am developing a Excel Addin surrounding Custom Functions. I would really like to use react for the taskpane but I can't seem to get both working simultaneously.

I was wondering if anyone has gotten a React Taskpane addin with custom functions working? What does you Manifest and webpack files look like and any other traps a beginner might be falling into preventing this from working.

Edit:

Expected outcome: To be able to develop custom functions for excel such as CLOCK (Screenshot below) through a function.js file. enter image description here

I would like to use react as the framework for developing the taskpane (on the right of the screenshot).

I have tried to achieve this a couple times, a mixture of merging yo office templates and following the documentation.

This is the manifest file:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:ov="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="TaskPaneApp">
  <Id>9bf11fda-0020-4224-b93b-28d93053f814</Id>
  <Version>1.0.0.0</Version>
  <ProviderName>Contoso</ProviderName>
  <DefaultLocale>en-US</DefaultLocale>
  <DisplayName DefaultValue="RTS"/>
  <Description DefaultValue="A template to get started."/>
  <IconUrl DefaultValue="https://localhost:3000/assets/icon-32.png"/>
  <HighResolutionIconUrl DefaultValue="https://localhost:3000/assets/icon-64.png"/>
  <SupportUrl DefaultValue="https://www.contoso.com/help"/>
  <AppDomains>
    <AppDomain>https://www.contoso.com</AppDomain>
  </AppDomains>
  <Hosts>
    <Host Name="Workbook"/>
  </Hosts>
  <Requirements>
    <Sets DefaultMinVersion="1.1">
      <Set Name="SharedRuntime" MinVersion="1.1"/>
    </Sets>
  </Requirements>
  <DefaultSettings>
    <SourceLocation DefaultValue="https://localhost:3000/taskpane.html"/>
  </DefaultSettings>
  <Permissions>ReadWriteDocument</Permissions>
  <VersionOverrides xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="VersionOverridesV1_0">
    <Hosts>
      <Host xsi:type="Workbook">
        <Runtimes>
          <Runtime resid="Taskpane.Url" lifetime="long" />
        </Runtimes>
        <DesktopFormFactor>
          <GetStarted>
            <Title resid="GetStarted.Title"/>
            <Description resid="GetStarted.Description"/>
            <LearnMoreUrl resid="GetStarted.LearnMoreUrl"/>
          </GetStarted>
          <FunctionFile resid="Taskpane.Url"/>
          <AllFormFactors>
            <ExtensionPoint xsi:type="CustomFunctions">
              <Script>
                <SourceLocation resid="Functions.Script.Url"/>
              </Script>
              <Page>
                <SourceLocation resid="Taskpane.Url"/>
              </Page>
              <Metadata>
                <SourceLocation resid="Functions.Metadata.Url"/>
              </Metadata>
              <Namespace resid="Functions.Namespace"/>
            </ExtensionPoint>
          </AllFormFactors>
          <ExtensionPoint xsi:type="PrimaryCommandSurface">
            <OfficeTab id="TabHome">
              <Group id="CommandsGroup">
                <Label resid="CommandsGroup.Label"/>
                <Icon>
                  <bt:Image size="16" resid="Icon.16x16"/>
                  <bt:Image size="32" resid="Icon.32x32"/>
                  <bt:Image size="80" resid="Icon.80x80"/>
                </Icon>
                <Control xsi:type="Button" id="TaskpaneButton">
                  <Label resid="TaskpaneButton.Label"/>
                  <Supertip>
                    <Title resid="TaskpaneButton.Label"/>
                    <Description resid="TaskpaneButton.Tooltip"/>
                  </Supertip>
                  <Icon>
                    <bt:Image size="16" resid="Icon.16x16"/>
                    <bt:Image size="32" resid="Icon.32x32"/>
                    <bt:Image size="80" resid="Icon.80x80"/>
                  </Icon>
                  <Action xsi:type="ShowTaskpane">
                    <TaskpaneId>ButtonId1</TaskpaneId>
                    <SourceLocation resid="Taskpane.Url"/>
                  </Action>
                </Control>
              </Group>
            </OfficeTab>
          </ExtensionPoint>
        </DesktopFormFactor>
      </Host>
    </Hosts>
    <Resources>
      <bt:Images>
        <bt:Image id="Icon.16x16" DefaultValue="https://localhost:3000/assets/icon-16.png"/>
        <bt:Image id="Icon.32x32" DefaultValue="https://localhost:3000/assets/icon-32.png"/>
        <bt:Image id="Icon.80x80" DefaultValue="https://localhost:3000/assets/icon-80.png"/>
      </bt:Images>
      <bt:Urls>
        <bt:Url id="Functions.Script.Url" DefaultValue="https://localhost:3000/functions.js"/>
        <bt:Url id="Functions.Metadata.Url" DefaultValue="https://localhost:3000/functions.json"/>      
        <bt:Url id="GetStarted.LearnMoreUrl" DefaultValue="https://go.microsoft.com/fwlink/?LinkId=276812"/>
        <bt:Url id="Commands.Url" DefaultValue="https://localhost:3000/commands.html"/>
        <bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3000/taskpane.html"/>
      </bt:Urls>
      <bt:ShortStrings>
        <bt:String id="Functions.Namespace" DefaultValue="CONTOSO"/>
        <bt:String id="GetStarted.Title" DefaultValue="Get started with your sample add-in!"/>
        <bt:String id="CommandsGroup.Label" DefaultValue="Commands Group"/>
        <bt:String id="TaskpaneButton.Label" DefaultValue="Show Taskpane"/>
      </bt:ShortStrings>
      <bt:LongStrings>
        <bt:String id="GetStarted.Description" DefaultValue="Your sample add-in loaded succesfully. Go to the HOME tab and click the 'Show Taskpane' button to get started."/>
        <bt:String id="TaskpaneButton.Tooltip" DefaultValue="Click to Show a Taskpane"/>
      </bt:LongStrings>
    </Resources>
  </VersionOverrides>
</OfficeApp>

Here is the webpack file:

 /* eslint-disable no-undef */
const CustomFunctionsMetadataPlugin = require("custom-functions-metadata-plugin");
const devCerts = require("office-addin-dev-certs");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const webpack = require("webpack");

const urlDev = "https://localhost:3000/";
const urlProd = "https://www.contoso.com/"; // CHANGE THIS TO YOUR PRODUCTION DEPLOYMENT LOCATION

async function getHttpsOptions() {
  const httpsOptions = await devCerts.getHttpsServerOptions();
  return { ca: httpsOptions.ca, key: httpsOptions.key, cert: httpsOptions.cert };
}

module.exports = async (env, options) => {
  const dev = options.mode === "development";
  const config = {
    devtool: "source-map",
    entry: {
      polyfill: ["core-js/stable", "regenerator-runtime/runtime"],
      vendor: ["react", "react-dom", "core-js", "@fluentui/react"],
      taskpane: ["react-hot-loader/patch", "./src/taskpane/index.tsx", "./src/taskpane/taskpane.html"],
      commands: "./src/commands/commands.ts",
      functions: "./src/functions/functions.js"
    },
    output: {
      clean: true,
    },
    resolve: {
      extensions: [".ts", ".tsx", ".html", ".js"],
    },
    module: {
      rules: [
        {
          test: /\.ts$/,
          exclude: /node_modules/,
          use: {
            loader: "babel-loader",
            options: {
              presets: ["@babel/preset-typescript"],
            },
          },
        },
        {
          test: /\.tsx?$/,
          exclude: /node_modules/,
          use: ["react-hot-loader/webpack", "ts-loader"],
        },
        {
          test: /\.html$/,
          exclude: /node_modules/,
          use: "html-loader",
        },
        {
          test: /\.(png|jpg|jpeg|gif|ico)$/,
          type: "asset/resource",
          generator: {
            filename: "assets/[name][ext][query]",
          },
        },
      ],
    },
    plugins: [
      new CustomFunctionsMetadataPlugin({
        output: "functions.json",
        input: "./src/functions/functions.js", 
     }),
      new CopyWebpackPlugin({
        patterns: [
          {
            from: "assets/*",
            to: "assets/[name][ext][query]",
          },
          {
            from: "manifest*.xml",
            to: "[name]" + "[ext]",
            transform(content) {
              if (dev) {
                return content;
              } else {
                return content.toString().replace(new RegExp(urlDev, "g"), urlProd);
              }
            },
          },
        ],
      }),
      new HtmlWebpackPlugin({
        filename: "taskpane.html",
        template: "./src/taskpane/taskpane.html",
        chunks: ["taskpane", "vendor", "polyfills", "functions"],
      }),
      new webpack.ProvidePlugin({
        Promise: ["es6-promise", "Promise"],
      }),
    ],
    devServer: {
      hot: true,
      headers: {
        "Access-Control-Allow-Origin": "*",
      },
      server: {
        type: "https",
        options: env.WEBPACK_BUILD || options.https !== undefined ? options.https : await getHttpsOptions(),
      },
      port: process.env.npm_package_config_dev_server_port || 3000,
    },
  };

  return config;
};

I create a function such as this in src/functions/functions.js

/**
* Returns the provided message
 * @customfunction
 * @param {string} message Message
 * @returns {string} Returns the message.
 */
export function testMessage(message) {
    return message;
}

I can see it exports the function metadata when npm run build is done. enter image description here

With this current setup, I can continue using react and editing the taskpane like a normal React Addin. However none of the functions appear in Excel at all. enter image description here


Solution

  • So it seems it was probably a mix of things as per the the comments above.

    The final fix that got it working was an error in my manifest file. My <AllFormFactors> tag was within the <DesktopFormFactor> tag.

    Working Manifest File:

    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:ov="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="TaskPaneApp">
      <Id>9bf11fda-0020-4224-b93b-28d93053f814</Id>
      <Version>1.0.0.0</Version>
      <ProviderName>Contoso</ProviderName>
      <DefaultLocale>en-US</DefaultLocale>
      <DisplayName DefaultValue="RTS"/>
      <Description DefaultValue="A template to get started."/>
      <IconUrl DefaultValue="https://localhost:3000/assets/icon-32.png"/>
      <HighResolutionIconUrl DefaultValue="https://localhost:3000/assets/icon-64.png"/>
      <SupportUrl DefaultValue="https://www.contoso.com/help"/>
      <AppDomains>
        <AppDomain>https://www.contoso.com</AppDomain>
      </AppDomains>
      <Hosts>
        <Host Name="Workbook"/>
      </Hosts>
      <Requirements>
        <Sets DefaultMinVersion="1.1">
          <Set Name="SharedRuntime" MinVersion="1.1"/>
        </Sets>
      </Requirements>
      <DefaultSettings>
        <SourceLocation DefaultValue="https://localhost:3000/taskpane.html"/>
      </DefaultSettings>
      <Permissions>ReadWriteDocument</Permissions>
      <VersionOverrides xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="VersionOverridesV1_0">
        <Hosts>
          <Host xsi:type="Workbook">
            <Runtimes>
              <Runtime resid="Taskpane.Url" lifetime="long"/>
            </Runtimes>
            <AllFormFactors>
                <ExtensionPoint xsi:type="CustomFunctions">
                  <Script>
                    <SourceLocation resid="Functions.Script.Url"/>
                  </Script>
                  <Page>
                    <SourceLocation resid="Taskpane.Url"/>
                  </Page>
                  <Metadata>
                    <SourceLocation resid="Functions.Metadata.Url"/>
                  </Metadata>
                  <Namespace resid="Functions.Namespace"/>
                </ExtensionPoint>
              </AllFormFactors>
            <DesktopFormFactor>
              <GetStarted>
                <Title resid="GetStarted.Title"/>
                <Description resid="GetStarted.Description"/>
                <LearnMoreUrl resid="GetStarted.LearnMoreUrl"/>
              </GetStarted>
              <FunctionFile resid="Taskpane.Url"/>
              
              <ExtensionPoint xsi:type="PrimaryCommandSurface">
                <OfficeTab id="TabHome">
                  <Group id="CommandsGroup">
                    <Label resid="CommandsGroup.Label"/>
                    <Icon>
                      <bt:Image size="16" resid="Icon.16x16"/>
                      <bt:Image size="32" resid="Icon.32x32"/>
                      <bt:Image size="80" resid="Icon.80x80"/>
                    </Icon>
                    <Control xsi:type="Button" id="TaskpaneButton">
                      <Label resid="TaskpaneButton.Label"/>
                      <Supertip>
                        <Title resid="TaskpaneButton.Label"/>
                        <Description resid="TaskpaneButton.Tooltip"/>
                      </Supertip>
                      <Icon>
                        <bt:Image size="16" resid="Icon.16x16"/>
                        <bt:Image size="32" resid="Icon.32x32"/>
                        <bt:Image size="80" resid="Icon.80x80"/>
                      </Icon>
                      <Action xsi:type="ShowTaskpane">
                        <TaskpaneId>ButtonId1</TaskpaneId>
                        <SourceLocation resid="Taskpane.Url"/>
                      </Action>
                    </Control>
                  </Group>
                </OfficeTab>
              </ExtensionPoint>
            </DesktopFormFactor>
          </Host>
        </Hosts>
        <Resources>
          <bt:Images>
            <bt:Image id="Icon.16x16" DefaultValue="https://localhost:3000/assets/icon-16.png"/>
            <bt:Image id="Icon.32x32" DefaultValue="https://localhost:3000/assets/icon-32.png"/>
            <bt:Image id="Icon.80x80" DefaultValue="https://localhost:3000/assets/icon-80.png"/>
          </bt:Images>
          <bt:Urls>
            <bt:Url id="Functions.Script.Url" DefaultValue="https://localhost:3000/functions.js"/>
            <bt:Url id="Functions.Metadata.Url" DefaultValue="https://localhost:3000/functions.json"/>      
            <bt:Url id="GetStarted.LearnMoreUrl" DefaultValue="https://go.microsoft.com/fwlink/?LinkId=276812"/>
            <bt:Url id="Commands.Url" DefaultValue="https://localhost:3000/commands.html"/>
            <bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3000/taskpane.html"/>
          </bt:Urls>
          <bt:ShortStrings>
            <bt:String id="Functions.Namespace" DefaultValue="CONTOSO"/>
            <bt:String id="GetStarted.Title" DefaultValue="Get started with your sample add-in!"/>
            <bt:String id="CommandsGroup.Label" DefaultValue="Commands Group"/>
            <bt:String id="TaskpaneButton.Label" DefaultValue="Show Taskpane"/>
          </bt:ShortStrings>
          <bt:LongStrings>
            <bt:String id="GetStarted.Description" DefaultValue="Your sample add-in loaded succesfully. Go to the HOME tab and click the 'Show Taskpane' button to get started."/>
            <bt:String id="TaskpaneButton.Tooltip" DefaultValue="Click to Show a Taskpane"/>
          </bt:LongStrings>
        </Resources>
      </VersionOverrides>
    </OfficeApp>