Custom Dojo Middleware
Rene Rubalcava | September 30, 2019
Dojo provides a middleware system that you can use in developing widgets for your applications. There is a comprehensive list of available middleware that you can use to manage local widget state, styling, or DOM related information.
Middleware is really interesting because they can be used to help you interact with the DOM or with the properties of your widget.
You can create middleware the same way you would a widget, except instead of returning a vnode, you could return an object or function that can be used to do some extra work for your widget.
The Dojo documentation touches on creating your own middleware. How could you implement your own custom middleware for your own widgets?
Validation Middleware
Maybe I'm building some form based widgets and I want to provide my own validation. For example, I might want to validate that a phone number is entered correctly.
In this case, I'm interested in wrapping an input in some form of validation. So I'm going to create a PhoneValidator
widget to wrap DOM input
. The result would look something like this.
// src/widgets/PhoneNumber.tsx
import { create, tsx } from "@dojo/framework/core/vdom";
import icache from "@dojo/framework/core/middleware/icache";
import PhoneValidator from "./PhoneValidator";
import * as css from "./styles/PhoneNumber.m.css";
const factory = create({ icache });
export const PhoneNumber = factory(function PhoneNumber({
middleware: { icache }
}) {
// use local phone value to pass to validator
const phone = icache.getOrSet("phone", "");
return (
<PhoneValidator phone={phone}>
<input
placeholder="Enter phone number"
pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}"
required
classes={[css.root]}
type="tel"
onkeyup={e => {
icache.set("phone", (e.target as HTMLInputElement).value);
}}
/>
</PhoneValidator>
);
});
export default PhoneNumber;
The idea here is that I just want the PhoneValidator
to place a red or green outline to my input to let me know if it's a valid phone number or not. It's pretty simple, but is something I could reuse across multiple applications.
// src/widgets/PhoneValidator.tsx
import { create, tsx } from "@dojo/framework/core/vdom";
import phoneNumberMiddleware from "../middleware/phoneNumberMiddleware";
import * as css from "./styles/PhoneValidator.m.css";
interface Properties {
phone: string;
}
const factory = create({ phoneNumberMiddleware }).properties<Properties>();
export const PhoneValidator = factory(function PhoneValidator({
children,
middleware: { phoneNumberMiddleware }
}) {
const { valid, value } = phoneNumberMiddleware();
let validCss = "";
if (value.length) {
validCss = valid ? css.valid : css.invalid;
}
return <div classes={[css.root, validCss]}>{children()}</div>;
});
export default PhoneValidator;
The PhoneValidator
uses some middleware that returns the a valid
property that is either true
or false
. It will also return the value
of the phone number that was tested. Based on whether the phone number is valid or not, it will use some CSS for a red or green border.
Notice that I never pass the phone
property to the middleware. By provide the phoneNumberMiddleware
as a middleware to the PhoneValidator
widget, the middleware will have access to the properties of the widget. Let's see what that looks like.
// src/middleware/phoneNumberMiddleware.tsx
import { create } from "@dojo/framework/core/vdom";
const factory = create().properties<{ phone?: string }>();
export const phoneNumberMiddleware = factory(({ properties }) => {
return () => {
// extract the `phone` value from the properties of
// the parent widget
const { phone } = properties();
// test the phone number
const valid = /^\(?(\d{3})\)?[- ]?(\d{3})[- ]?(\d{4})$/.test(phone || "");
return {
valid,
value: phone
};
};
});
export default phoneNumberMiddleware;
The middleware returns a function that will test the phone number and return whether it is valid or not.
Here is what this looks like in a sample application.
Geolocation Middleware
You could also do some fun middleware that interacts with the DOM of you widgets. For example, there is the intersection
and resize
middleware.
You could use a similar pattern to grab the browsers geolocation.
// src/middleware/geolocation.ts
import { create } from "@dojo/framework/core/vdom";
import icache from "@dojo/framework/core/middleware/icache";
const factory = create({ icache });
type Coords = Pick<Coordinates, "latitude" | "longitude">;
// utility to get current geolocation
const getGeolocation = async (): Promise<Coords> => {
return new Promise(resolve => {
if (!("geolocation" in navigator)) {
resolve({ latitude: 0, longitude: 0 });
} else {
navigator.geolocation.getCurrentPosition(({ coords }) => {
const { latitude, longitude } = coords;
resolve({ latitude, longitude });
});
}
});
};
// default coordinates
const defaultCoordinates = { latitude: 0, longitude: 0 };
export const geolocation = factory(({ middleware: { icache } }) => {
return (): Coords => {
// get current value or default
const coords = icache.getOrSet("coords", defaultCoordinates);
if (coords.latitude === 0 && coords.longitude === 0) {
// only get location if it is not the default
getGeolocation().then(results => {
if (
coords.latitude !== results.latitude &&
coords.longitude !== results.longitude
) {
// only update cache if different from current value
// this will invalidate the widget
icache.set("coords", results);
}
});
}
return coords;
};
});
export default geolocation;
This middleware uses the icache
middleware so that when the geolocation properties are updated, it will invalidate the middleware and this will in turn invalidate the widget so it can rerender with new data.
// src/main.tsx
import { renderer, create, tsx } from "@dojo/framework/core/vdom";
import "@dojo/themes/dojo/index.css";
import Hello from "./widgets/Hello";
import geolocation from "./middleware/geolocation";
const factory = create({ geolocation });
const App = factory(function App({ middleware: { geolocation } }) {
// get my geolocation middleware values
const { latitude, longitude } = geolocation();
return (
<div key="container">
<Hello name="Dojo CodeSandbox" />
<h2>{"Start editing to see some magic happen \u2728"}</h2>
<section>
<ul>
<li>Latitude: {latitude.toFixed(3)}</li>
<li>Longitude: {longitude.toFixed(3)}</li>
</ul>
</section>
</div>
);
});
Here is a demo of what this looks like. You may need to open it in a new window to get your location.
Summary
There are numerous ways you could build middleware for your applications. Device orientation, mouse interactivity, media queries, hardware devices, drag and drop, full screen, authentication, and so much more. I'm looking forward to all the different ways middleware can be implemented into Dojo widgets!