Building Custom JavaScript Components

This document outlines how to build custom Universal app components.

Universal is extensible and you can build custom JavaScript components and frameworks. This document will cover how to build custom components that integrate with the Universal app platform.

This is an advanced topic and not required if you simply want to use Universal Apps.

Look at our blog post on how to get started with custom components for full end-to-end example.

Technology Overview

Below is a list of some of the technologies used when building Universal app components. You will not need to be an expert to produce a component but should be aware of what to search when you encounter a problem.

React

Universal App's client-side application is built using the React framework. React makes it easy to build components that update the DOM only when necessary and has a pretty robust ecosystem of users. It's one of the most popular JavaScript frameworks at the time of this writing.

Babel

Babel is a transcompiler for JavaScript. It works well with React and allows you to use modern constructs while compiling for backwards compatibility of browsers. Universal apps uses Babel for its core component frameworks.

Webpack

Webpack is an asset bundler. It's extremely customizable and is responsible for turning your JSX files into a bundle that can then be distributed with Universal App components.

Structure

There are some basic parts to a Universal App component. You will need to understand the structure in order to successfully build your own.

PowerShell Module

Universal App custom components are PowerShell modules. They export functions that can be used to create the component when run within an app. The PowerShell module is also responsible for registering the JavaScript assets with Universal App.

JavaScript Bundle

The JavaScript bundle is produced by the Webpack bundling process. It consists of one or more JS files that you will need to register with Universal.

Example Component Module Structure

The most basic structure for a Universal App component module will include a single JavaScript file, a PSM1 file to export a function and register the JavaScript and a PSD1 module manifest.

- UniversalApp.95
    - index.23adfdasf.js
    - UniversalApp.95.psd1
    - UniversalApp.95.psm1

Step-By-Step

This following section will take you step-by-step through the different aspects of building a Universal App component.

For a full example of a component, click here.

1. Installing Dependencies

You will need to install the following dependencies before creating your component.

2. Initialize the JavaScript Package

After installing Node, you will have access to the npm command. You will need to initialize the node package to start. This will create a package.json file in your directory.

npm init

Here is an example package.json that you can also use as a starting point.

3. Install JavaScript Packages

You will need several JavaScript packages to build your bundle. You will first want to install the dev dependencies. These are used to build your project.

npm install @babel/core --save-dev
npm install @babel/plugin-proposal-class-properties --save-dev
npm install @babel/plugin-syntax-dynamic-import --save-dev
npm install @babel/polyfill --save-dev
npm install @babel/preset-env --save-dev
npm install @babel/preset-react --save-dev
npm install babel-loader --save-dev
npm install webpack --save-dev
npm install webpack-cli --save-dev

Next, you'll want to install the universal-dashboard package along with any other packages you wish to use in your component. We are using React95 in this example. We will build a control based on that library.

npm install universal-dashboard --save
npm install react95 --save
npm install styled-components --save

4. Configure Babel

You will need to create a .babelrc file to configure Babel for React.

{
    "presets": ["@babel/preset-react"]
}

5. Configure Webpack

Webpack is extremely customizable and sometimes very hard to get right. Below is a basic webpack.config.js file you can use to configure Webpack. You can safely change the ud95 entry key name and library value to one that matches your library.

var path = require('path');

var BUILD_DIR = path.resolve(__dirname, 'dist');

module.exports = (env) => {
  const isDev = env == 'development' || env == 'isolated';

  return {
    entry: {
      'ud95' : __dirname + '/index.js'
    },
    output: {
      library: "UD95",
      libraryTarget: "var",
      path: BUILD_DIR,
      filename: isDev ? '[name].bundle.js' : '[name].[hash].bundle.js',
      sourceMapFilename: '[name].[hash].bundle.map',
      publicPath: "/"
    },
    module : {
      rules : [
        { test: /\.(js|jsx)$/, exclude: [/node_modules/, /public/], loader: 'babel-loader'}
      ]
    },
    externals: {
      UniversalDashboard: 'UniversalDashboard',
      'react': 'react',
      'react-dom': 'reactdom'
    },
    resolve: {
      extensions: ['.json', '.js', '.jsx']
    }
  };
}

6. Component.jsx

Now you can build your first component. You will need to export a single function component from your component.jsx file. We suggest the use of functional React components rather than class-based React components. We need to wrap the component in withComponentFeatures to ensure the component has access to the Universal App platform features.

import React from 'react';
import { withComponentFeatures } from 'universal-dashboard';
import { Button } from 'react95';

const UD95Button = props => {

    const p = {
        onClick: () => props.onClick()
    }

    return <Button {...p}>{props.text}</Button>
}

export default withComponentFeatures(UD95Button);

7. Index.js

Once your component is completed, you'll need to add it to an index.js file. The entry point for your library is the first place Webpack will look. It will discover all other components from import statements in your code. The index.js file is where you should register your components. You can use the registerComponent function to do so.

import { registerComponent } from 'universal-dashboard'
import UD95Button from './component';

registerComponent("ud95-button", UD95Button);

8. Bundle JavaScript

To bundle the JavaScript, run the following command to start webpack. This will output a file into the dist folder.

npm run build

9. PowerShell Script

Now you will need to create a PowerShell script that registers and creates your component.

First, register the JavaScript with Universal.

$JsFile = Get-ChildItem "$PSScriptRoot\ud95.*.js"
$AssetId = [UniversalDashboard.Services.AssetService]::Instance.RegisterAsset($JsFile.FullName)

Next, create a function that returns a hashtable that defines which component we are creating and which props to set.

The type property of your hashtable needs to match with the first parameter of registerComponent that you called in your JavaScript.

function New-UD95Button {
    param(
        [Parameter()]
        [string]$Id = [Guid]::NewGuid(),
        [Parameter()]
        [string]$Text,
        [Parameter()]
        [Endpoint]$OnClick
    )

    if ($OnClick)
    {
        $OnClick.Register($Id, $PSCmdlet)
    }

    @{
        type = "ud95-button"
        isPlugin = $true 
        assetId = $AssetId

        id = $Id 
        text = $Text 
        onClick = $OnClick
    }
}

10. InvokeBuild (optional)

We suggest the use of InvokeBuild to create a build script to run all the steps of packaging and staging your module. The below build script deletes the dist folder, runs an NPM install to install packages, runs an NPM build to bundle the JavaScript and then copies the PS module to the dist folder.

task Clean {
    Remove-Item "$PSScriptRoot\dist" -Recurse -Force
}

task NpmInstall {
    & {
        $ErrorActionPreference = 'SilentlyContinue'

        Push-Location $PSScriptRoot
        npm install
        Pop-Location
    }
}

task NpmBuild {
    & {
        $ErrorActionPreference = 'SilentlyContinue'

        Push-Location $PSScriptRoot
        npm run build
        Pop-Location
    }
}

task Stage {
    Copy-Item "$PSScriptRoot\UniversalDashboard.95.*" "$PSScriptRoot\dist"
}

task . Clean, NpmInstall, NpmBuild, Stage

Props

Props are values that are either passed from the PowerShell hashtable provided by the user or by the Universal App withComponentsFeature high-order function.

Standard

The properties that you set in your hashtable in PowerShell will automatically be sent in as props to React component.

For example, if you set the text property of the hashtable like this.

function New-UDText {
    param(
        [Parameter()]
        [string]$Text
    )

    @{
        type = "text"
        isPlugin = $true
        assetId = $AssetId 

        text = $Text
    }
}

Then you will have access to that prop in React.

import React from 'react';
import { withComponentFeatures } from 'universal-dashboard';

const UDText = props => {
    return <div>{props.text}</div>
}

export default withComponentFeatures(UDText);

Endpoints

Endpoints are special in the way they are registered and the way that they are passed as props to your component. You will need to call Register on the endpoint in PowerShell and pass in the Id and PSCmdlet variables.

function New-UD95Button {
    param(
        [Parameter()]
        [string]$Id = [Guid]::NewGuid(),
        [Parameter()]
        [string]$Text,
        [Parameter()]
        [Endpoint]$OnClick
    )

    if ($OnClick)
    {
        $OnClick.Register($Id, $PSCmdlet)
    }

    @{
        type = "ud95-button"
        isPlugin = $true 
        assetId = $AssetId

        id = $Id 
        text = $Text 
        onClick = $OnClick
    }
}

Endpoints are created from ScriptBlocks and are executed when that event happens.

New-UD95Button -Text 'Hello' -OnClick {
    Show-UDToast -Message 'Test' 
}

Universal will automatically wire up the endpoint to a function within JavaScript. This means that you can use the props to call that endpoint.

Notice the props.onClick function call. This will automatically call the PowerShell script block on the server.

import React from 'react';
import { withComponentFeatures } from 'universal-dashboard';
import { Button } from 'react95';

const UD95Button = props => {

    const p = {
        onClick: () => props.onClick()
    }

    return <Button {...p}>{props.text}</Button>
}

export default withComponentFeatures(UD95Button);

setState

The setState prop is used to set the state of the component. This ensures that the state is tracked and your component will work with Get-UDElement.

For example, with a text field, you'll want to call props.setState and pass in the new text value for the state.

const UDTextField = (props) => {
    const onChange = (e) => {
        props.setState({value: e.target.value})
    }

    return <TextField  {...props} onChange={onChange} />
}

export default withComponentFeatures(UDTextField);

children

The children prop is a standard React prop. If your component supports child items, such as a list or select box, you should use the standard props.children prop to ensure that the cmdlets Add-UDElement , Remove-UDElement and Clear-UDElement function correctly.

Last updated

Copyright 2022 Ironman Software