Implementing Valo Connect custom widgets

Valo Connect is a product I was working on in Valo. It is a Microsoft Teams application that, among many features, delivers a user-friendly dashboard that can be customised with OOTB widgets and extended with custom ones. In this article, I’m going to describe the process of developing new widgets. The working example is available in my GitHub repository.

Valo Connect and extensibility model

Valo Connect allows users to create their own dashboard, called Connect Me, built on widgets that integrate Office 365 features and more.

connect me dashboard

The solution allows the creation of custom widgets with an extensibility model – a set of methods and interfaces to communicate with Connect Me. This way anyone can integrate their own services and applications into Teams.

Starting the project

Create a new project using Yeoman Generator, select the “extension” project type, and then, an application customizer. The next step is to install dependencies.

Mandatory:

@valo/extensibility – Access to Connect Me API

Recommended:

@fluentui/react-northstar – Connect Me is built with northstar controls. They can be used for consistency 

@fluentui/react-northstar-emotion-renderer – This package is needed to ensure correct styling of the northstar components.

npm i @fluentui/react-northstar @fluentui/react-northstar-emotion-renderer @valo/extensibility

Registering widgets

In the <extension_name>ApplicationCustomizer.ts file, I started with creating an object of a widget registering class:

export default class CustomWidgetsApplicationCustomizer extends BaseApplicationCustomizer<{}> {    
    @override
    public onInit(): Promise<void> {
        const loader = new WidgetLoader(this.context);
        IconUtils.registerIcons();
        loader.registerWidgets();
        return Promise.resolve();
    }
}

I will get back to the icons later.

export class WidgetLoader {
    private connectWidgetService: ConnectWidgetService;
 
    constructor(private context: ApplicationCustomizerContext) {
        this.connectWidgetService = ConnectWidgetService.getInstance();
    }
 
    public async registerWidgets() {
        for(let w of this.widgets) {
            this.connectWidgetService.registerWidget(w);
        }
    }
 
    private widgets: ConnectWidget<any>[] = [
        new WordPressWidget(),
        new CryptoWidget()
    ];
}

WidgetLoader class uses ConnectWidgetService from @valo/extensibility npm package to register custom widgets in the Connect Me dashboard. Widget definitions are implemented in separate files/classes. Context is not used, yet, but it may be useful for some widgets.

export class CryptoWidget implements ConnectWidget<CryptoProps> {
    public title: string = "Cryptocurrency";
    public id: string = "kr-crypto-widget";
    public size: ConnectWidgetSize = ConnectWidgetSize.Single;
    public description?: string = "Shows the cryptocurrency rates";
    public iconProps?: IIconProps = { iconName: "kr-crypto" };
    public category?: string = "Elnathsoft";
   
    public widgetComponentsFactory = (config: CryptoProps) => {
        return [
            {
                id: "kr-crypto-widget-1",
                title: "Rates",
                content: <CryptoRates {...config} />,
            },
        ];
    }
 
    public widgetConfigComponentFactory = (currentConfig: CryptoProps, onConfigUpdated: (config: CryptoProps) => void) => {
        return <CryptoConfig onConfigurationUpdated={onConfigUpdated} widgetConfiguration={currentConfig} />;
    }
}

In the widget registration class, one has to specify an id and size, widgetComponentsFactory, and widgetConfigComponentFactory. A few optional properties are available and explained in Valo Connect documentation. In this article, I’m describing the most important ones.

widgetComponentsFactory returns an array of tabs for a widget. Each tab has an id, a title, and content which is a react component. If only one tab is implemented, then the widget displays the content without tabs.

widgetConfigComponentFactory returns just one component for the widget configuration panel. The onConfigUpdate method is used to save the configuration.iconProps allows setting a widget icon. It can be a Fluent UI icon, image, or a custom icon.

Registering custom icons

Custom icons can be created in a vector graphics tool, saved in SVG format, and converted to JSX objects. They can be registered in Fluent UI with a custom and unique name, and then, accessed by this name, as I did in the code snippet above.

import * as React from "react";
import { registerIcons } from "office-ui-fabric-react/lib/Styling";
 
export class IconUtils {
    public static registerIcons(): void {
        registerIcons({
            icons: {
                "kr-crypto": (
                    <svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122 122" >
                        <path
                            d="M61 0a60.69 60.69 0 1 0 61 60.69A60.84 60.84 0 0 0 61 0ZM49.41 84.36a30.47 30.47 0 0 0 8.2 2.36 41.76 41.76 0 0 0 6.22.44h1.83a35.16 35.16 0 0 0 8.2-1.21A21 21 0 0 0 87.28 75h12c-3.26 9-11.45 16.18-25.47 18.53v8h-8.2v-7.19h-2.33c-2 0-3.87-.09-5.72-.24v7.43h-8.2v-8.67c-17.76-4.22-26.72-16.8-26.72-31.77 0-14.33 8.88-27.54 26.77-32.11V19.8h8.2v7.77c2-.18 4-.28 6.17-.28h1.88V19.8h8.2V28c15 2.32 23.29 9.91 25.5 19h-12c-2.29-5.07-6.26-9.24-13.5-11.23a36.51 36.51 0 0 0-8.2-1.21c-.76 0-1.55-.05-2.36-.05a39.43 39.43 0 0 0-5.69.39 29.72 29.72 0 0 0-8.2 2.32c-9.94 4.52-14.21 14-14.21 23.61 0 9.28 4.06 18.92 14.21 23.53Z"
                            style={{
                                fill: "#34C7D1",
                            }}
                        />
                    </svg>
                ),
            },
        });
    }
}

Ensuring correct styling for different themes

To ensure correct styling, rendered components have to be wrapped with RendererContext provider, using emotionRenderer and with FluentUIThemeProvider to ensure theming. I’ve created a wrapping component to achieve this. It can be implemented as a HOC component as well.

import * as React from "react";
import { Provider as FluentUIThemeProvider } from "@fluentui/react-northstar/dist/es/components/Provider/Provider";
import { RendererContext } from "@fluentui/react-bindings/dist/es/renderer/RendererContext";
import { createEmotionRenderer } from "@fluentui/react-northstar-emotion-renderer";
import { ThemeManager } from "../../managers/ThemeManager";
 
export interface ThemeProviderWrapperProps {
    className?: string;
}
 
export const ThemeProviderWrapper = (props: React.PropsWithChildren<ThemeProviderWrapperProps>) => {
    return (
        <div className={props.className}>
            <RendererContext.Provider value={createEmotionRenderer()}>
                <FluentUIThemeProvider theme={ThemeManager.getTheme()}>{props.children}</FluentUIThemeProvider>
            </RendererContext.Provider>
        </div>
    );
};

ThemeManager class determines whether Connect Me uses a dark, light, or high contrast theme.

public static getTheme(): ThemePrepared {
    if (document.querySelector('body').classList.contains('valo-theme-dark')) {
        return teamsDarkTheme;
    }
    if (document.querySelector('body').classList.contains('valo-theme-contrast')) {
        return teamsHighContrastTheme;
    }
    return teamsTheme;
}

Deployment

The solution should be bundled, like every other SharePoint Framework project, and deployed to the tenant’s app catalogue.

Build a package:

gulp bundle --ship; gulp package-solution --ship;

Deploy it to the app catalogue and make it available to all sites.

The widgets are available in Connect Me:

all widgets

Debugging

The easiest way to debug widgets is to serve a project with gulp serve, open Teams in a browser, and execute the following command in the browser’s dev tools console.

document.querySelector("iframe[name='embedded-page-container']").src += '%26debug%3Dtrue%26noredir%3Dtrue%26debugManifestsFile%3Dhttps%3A%2F%2Flocalhost%3A4321%2Ftemp%2Fmanifests.js'

If you forget this command you can always follow these steps as an alternative:

  1. Serve solution and copy debug URI from a console
?debug=true&noredir=true&debugManifestsFile=https://localhost:4321/temp/manifests.js
  1. Replace the question mark with an ampersand and pass it as an argument of the encodeURIComponent command. Copy the result to the clipboard.
encodeURIComponent("&debug=true&noredir=true&debugManifestsFile=https://localhost:4321/temp/manifests.js")
  1. Inspect website DOM and find the Connect Me iframe.
iframe dom element
  1. Edit the iframe src attribute and paste the URI component from your clipboard.
iframe src attribute

My Widgets

I’ve implemented two simple widgets that one may find useful.

Cryptocurrencies

Displays the current rate of a few cryptocurrencies: Bitcoin, Ethereum, Polkadot, Solana, FTXToken, Terra. Every currency can be hidden.

cryptocurrencies widget

WordPress

Downloads latest posts from blogs and websites based on WordPress. Works with WordPress-hosted blogs and blogs operating on private servers, if CORS is configured accordingly.

wordpress widget

I hope this article will help you create your own Valo Connect widgets. Feel free to use examples shared on GitHub. If you like my widgets, you can use them on your tenant, too.