Archive for the 'Work' Category

Using Puppeteer 19 on Google Cloud Functions

I have a Google Cloud Function which uses Puppeteer to convert HTML documents to PDF. I recently tried to upgrade Puppeteer to version 19. It worked fine locally, but not when deployed to Google Cloud:

Error: Could not find Chromium (rev. 1069273). This can occur if either
1. you did not perform an installation before running the script (e.g. `npm install`) or
2. your cache path is incorrectly configured (which is: /root/.cache/puppeteer).
For (2), check out our guide on configuring puppeteer at https://pptr.dev/guides/configuration.

The fix was relatively straightforward:

First, add a new .puppeteerrc.cjs file at the root of the project (documented here):

const { join } = require("path");

/**
 * @type {import("puppeteer").Configuration}
 */
module.exports = {
  cacheDirectory: join(__dirname, ".cache", "puppeteer"),
};

Second, add .cache to your .gitignore file:

# Puppeteer cache
.cache

This fixed everything for my first deployment, but it broke again on subsequent deployments. This lead to Step 3: Add a new gcp-build item to the scripts object in your project’s package.json (documented here). This re-installs puppeteer after every deployment:

"scripts": {
  "gcp-build": "node node_modules/puppeteer/install.js",
  /* etc */
}

(Thanks to Justus Blümer for writing up the last part).

HTML forms with multiple buttons

Lets say you have an HTML form with multiple buttons. One button submits the form, but the others do something else. Here’s a simple example:

<form>
  <label>
    Email address
    <input type="email" name="email" />
  </label>
  <button onClick="handleCancel()">Cancel</button>
  <button>Save</button>
</form>

I might type my email address into the form, press Enter, and… oops. For some reason, pressing Enter chooses the Cancel button.

The fix is to add type="button" to any button which should not submit the form, and type="submit" to any buttons which should submit the form, like so:

<form>
  <label>
    Email address
    <input type="email" name="email" />
  </label>
  <button type="button" onClick="handleCancel()">Cancel</button>
  <button type="submit">Save</button>
</form>

Pressing Enter will now submit the form, as expected.

Using Flow types with Reach Router route components

I have a project which uses Flow for static typing, and Reach Router for routing. Reach Router uses Route components to assign components to URLs:

<Router>
  <Component path="/somewhere" />
  <AnotherComponent path="/somewhere-else" />
  <YetAnotherComponent default />
</Router>

So if you browse to /somewhere-else, <AnotherComponent /> will be rendered. So far, so good. However, if one of these components doesn’t accept any props, Flow will complain:

export const AnotherComponent = () => {
  return <>I am another component.</>;
};

Error:(116, 10) Cannot create AnotherComponent element because property path is missing in function type [1] but exists in props [2].

To work around that, I created a Route component and a DefaultRoute component:

// @flow

import type { DefaultRouteProps, RouteProps } from "@reach/router";
import * as React from "react";

type RouteComponentProps = RouteProps & {
  component: React$ComponentType<*>,
};

type DefaultRouteComponentProps = DefaultRouteProps & {
  component: React$ComponentType<*>,
};

export const Route = (props: RouteComponentProps) => (
  <props.component {...props} />
);

export const DefaultRoute = (props: DefaultRouteComponentProps) => (
  <props.component {...props} />
);

(Note the imported types are pulled from flow-typed‘s Reach Router definition).

They’re used like so:

<Router>
  <Route component={Component} path="/somewhere" />
  <Route component={AnotherComponent} path="/somewhere-else" />
  <DefaultRoute component={YetAnotherComponent} default />
</Router>

(And yes, eventually we’ll likely migrate this project to React Router.)

React’s useEffect and arrays in its’ dependency array

Could I get any more arrays into that title? I’m working on a React app at the moment. Adopting hooks has made my life a bit easier. But useEffect‘s dependency array took a while to get my head around – specifically what to do if your only dependency is itself an array.

If useEffect‘s dependency array itself contains an array, like this, you get an infinite loop and effectively perform a DOS attack on your own API.

const Thing = () => {
  const [arrayOfThings, setArrayOfThings] = useState([]);

  useEffect(() => {
    // Imagine some code here to 
    // fetch things from an API
    setArrayOfThings(loadedArrayOfThings);
  }, [arrayOfThings])

  return (
    <ul>
      {arrayOfThings.map(el => (<li>{el.title}</li>))}
    </ul>
  )
}

I imagine it’s because useEffect isn’t doing a deep compare on the dependencies. So, perhaps changing it so useEffect is dependent on the length of the array will help?

useEffect(() => {
  // Imagine some code here to
  // fetch things from an API
  setArrayOfThings(loadedArrayOfThings);
}, [arrayOfThings.length])

Well, it does, but it still requests the data from the API twice – once on first render, and again after the first fetch because the length of the array changes. The fix I found is to use a second useState variable:

const Thing = () => {
  const [arrayOfThings, setArrayOfThings] = useState([]);
  const [doFetchData, setDoFetchData] = useState(true);

  useEffect(() => {
    if (doFetchData) {
      // Imagine some code here to 
      // fetch things from an API
      setArrayOfThings(loadedArrayOfThings);
      setDoFetchData(false)
    }
  }, [doFetchData])

  return (
    <ul>
      {arrayOfThings.map(el => (<li>{el.title}</li>))}
    </ul>
  )
}

The ensures the data is only loaded once. Additionally, it gives us a way to precisely control when to go and re-fetch the data (using setDoFetchData(true)). Better.

Use React.memo() wisely

Use React.memo() wisely by Dmitri Pavlutin is the article which finally made React.memo() click for me.

(Thanks for sharing, Remy.)

Using DNN OpenForm

Sacha Trauwaen’s OpenForm is a module for DNN which is used for adding forms to a page.

It requires that the OpenContent module is installed first. The configuration is based on AlpacaJS.

Official documentation

Creating a new form

  1. Browse to the page which will hold the form and switch to edit mode
  2. Add a module
  3. Choose OpenForm
  4. Click Template Exchange
    1. Under Action, choose Copy Template
    2. Under From Template, choose an existing form (e.g. “Site:Contact”)
    3. Under To New Name, type the name of the new form (e.g. “Further Information Request”)
    4. Press Copy
  5. When you see Copy Successful, close the modal box and wait for the page to refresh
  6. Under Choose a template, choose Site:Whatever-you-just-called-it and wait for the page to refresh
  7. Click Template Settings
  8. Here you can:
    • Set the Message after Submit that appears after the user successfully sends the form
    • Add Email Notifications (see section below)
    • Add other things (Tracking script / reCaptcha keys)
    • Click Save
  9. Hover the module, open the Pencil menu and choose Form Builder
  10. Add your fields (see section below) and click Save

Form Builder

  1. Edit the page
  2. Hover the module, open the Pencil menu and choose Form Builder
  3. Give it a moment to load the left column, it can be a bit slow

From here you can add and remove items to the form. The left column shows the form builder, while the right shows a preview (albeit using plain bootstrap styles, so it’s not a true WYSIWYG).

  • To add a control, click the Plus icon
  • To remove a control, click the Minus icon
  • To move a control up, click the up arrow
  • To move a control down, click the down arrow
  • To edit a control, click its name.
    • From here, set the field name (no spaces or special characters)
    • The type
    • The Label, which will appear on the screen
    • Required
    • Advanced (default value, helper text, placeholder text, position if this is a multi-column form)
    • Values (if this is a drop-down, radio buttons, or multi-select checkboxes)
    • Dependencies (this control can be dependent on the value of other controls)
  • Save or Cancel your changes

Note: It’s a good idea to press Save often, as it can occasionally get confused when moving new un-named controls up and down.

Form Settings

You can reach the form settings (for email notifications, etc.) by:

  1. Edit the page
  2. Hover the module, open the Pencil menu and choose Form Settings

Message after submit

You have a WYSIWYG editor to control the content shown after the form is submitted. To include form data in the message, you can use special tokens.

e.g. If your form contains fields named name and email, put this into the WYSIWYG:

Thanks {{ name }}, your email address is {{ email }}.

Email notifications

To make it send an email notification, you need to:

  1. Edit the page
  2. Pencil > Form Settings
  3. Add an email notification

Note: If the emails start failing, you can look at the Admin Logs in the PersonaBar.

The message uses a WYSIWYG to allow you to customise the HTML email you send back. You can use the same tokens as the Message After Submit to customise it, e.g.

Thanks {{ name }}, your email address is {{ email }}.

There is also a special {{{ FormData }}} token, which outputs all of the data submitted. See https://openform.readme.io/docs/getting-started#section-email-messages-and-user-feedback

You can add multiple email notifications. This means you can send a notification back to the person who filled in the form another to the staff members who need to receive the information, and yet another to a CRM system.

Pre-filling the form with querystring data

You can use Javascript to pre-fill the form with data from the querystring. See https://openform.readme.io/docs/prefill-form-with-query-parameters for an example.

Pre-filling the form with user data

If the current user is logged in, you can pre-fill the form with their profile information. Make sure you use the following field names in your form:

  • Username
  • FirstName
  • LastName
  • Email
  • DisplayName
  • Telephone
  • Street
  • City
  • Country

See https://openform.readme.io/docs/getting-started#section-auto-initialization-of-fields-from-dnn-user-when-logged-in

Viewing form submissions

As well as being emailed out, all form submissions are stored in the database. If you have admin or super user access, you can view them by:

  1. Edit the page
  2. Hover the module, open the Pencil menu and choose Submissions

From there you can download the data in Excel format.

If you have superuser permissions, you can get to the raw JSON data:

  1. Edit the page
  2. Hover the module, open the Pencil menu and choose Edit Raw Data

At the time of writing, you cannot delete form submission data. (Jan 2020: I believe a subsequent release has now added this feature.)

Advanced features

Certain features can only be reached by directly editing the config files. For instance, some features (such as multi-page forms) cannot be added using the Form Builder. There is also the ability to use custom CSS and Javascript on the module, and a C#/Razor post-submission message which can do more than the regular submission message (with full access to the DNN API).

If you have Super User access, you can get to these by:

  1. Edit the page
  2. Hover the module, open the Pencil menu and choose Edit Template Files

If you’re developing a new form on your development machine, you may find it easier to find the files on the file system. They’re stored in:

[path to site]\Portals\[portal id]\OpenForm\Templates

So on my dev machine, where I’ve used nvQuickSite to install DNN, they’re here:

C:\Websites\sesame\Website\Portals\0\OpenForm\Templates

And on an Azure App Service, they’re likely to be under:

site\wwwroot\Portals\0\OpenForm\Templates

DNN Liquid Content – Getting the first item in a visualizer

Sometimes, for example when working with a Bootstrap carousel, you need to know when you’re on the first item in the visualizer. Liquid Content apparently can’t do this natively (yet) so you’ll need to do this with Javascript.

To add the active class to the first item in a Bootstrap carousel, I put the following code into the script section of the visualizer. You should be able to adapt it to suit your needs:

var items = document.querySelectorAll(".carousel");
if (items.length) {
items.querySelector(".carousel-item:first-child").classList.add("active");
}

I raised this as an enhancement request with DNN Software, as DNN-26912.

DNN Liquid Content – Getting the content item id in a visualizer

To get a unique id for a content item, you simply use {{id}}, in the template part of the visualizer editor.

For example:

<article class="news-headline" id="{{id}}">

It’ll come out as something like:

<article class="news-headline" id="aa1a93b2-cca4-4a32-bc0a-2c52a1b28019">

If you need to link to it, e.g. for a Bootstrap modal, use something like this:

<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#{{id}}">

It comes out like:

<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#aa1a93b2-cca4-4a32-bc0a-2c52a1b28019">

The ID is tied to the specific content item, so you can use it across multiple visualizers (e.g. put all the buttons in one visualizer, and all the modals into another one).

This might seem obvious, but I didn’t find it in the documentation.