Feathers UI for Haxe and OpenFL: Now live on Kickstarter

UPDATE: Feathers UI for Haxe and OpenFL is fully funded on Kickstarter!

UPDATE: For the latest news and announcements about alpha/beta preview builds, follow the Feathers UI blog.

About eight years ago, I open sourced the code for what would become Feathers UI. I was spending my days trying to be an indie game developer at the time, and I had recently tried out Daniel Sperl’s excellent Starling library. I could see that if my games switched to Starling, I’d get smoother animations and other performance improvements. For my games, I had already created a basic set of user interface components (buttons, lists, toggle switches, sliders and things like that) that worked on both iPhone and Android. After recreating those UI components in Starling, I decided that others might find them useful for building apps too. Publishing them on Github would change my life for nearly a decade.

Today, the technology landscape has changed a lot compared to 2011, with all sorts of new mobile devices — and web browsers that no longer allow plugins. While Feathers UI was generously sponsored by a company for a number of years, they’ve now moved in different strategic directions that, sadly, won’t include Feathers. Yet, over all this time, Feathers UI has grown and matured well beyond anything that I had ever imagined. Software developers have created so many amazing apps and games, and I am constantly inspired by how they use Feathers in incredible new ways. Certainly, much has changed on the web and in the wider realm of technology, but I believe that Feathers UI still has great value for software developers building creative apps and games.

That’s why I’m launching a Kickstarter for Feathers UI, and I hope that you’ll support this project by becoming a backer. I’m looking to rebuild Feathers UI on a new foundation that will continue to make it easy to build cross-platform user interfaces on mobile, desktop, smart TVs, and even game consoles. This new version of Feathers UI will use the Haxe programming language and the OpenFL library. Developers who are already experienced with Feathers UI will feel right at home with a similar language and familiar APIs, but also new features and access to new platforms.

Support Feathers UI on Kickstarter

Additionally, I’m going to make Feathers UI into a more sustainable business that can fund itself going forward. I’m planning to introduce new premium add-ons that developers can purchase to support Feathers directly. These add-ons will include new advanced components (like charts and graphs), pre-made themes in a variety of styles, and in-depth technical educational content that teaches you the inner workings of Feathers UI. Of course, the core Feathers UI library will remain open source, and it will continue to expand with new UI components and features! These add-ons will be for developers with more specialized needs.

I may have written the code for Feathers UI, but the community gave it life. I hope that you will back the project on Kickstarter and help me re-imagine a modern Feathers UI that takes the best of the original Starling version, combines it with the advantages of Haxe and OpenFL, and makes it better than ever. Keep this excellent UI component and layout library in your toolbox by showing your support on Kickstarter.

Upgrading a React and TypeScript app from react-scripts-ts to Create React App v2.1

Recently, the React team updated Create React App with official support for TypeScript. Create React App makes it easy to create web apps with React by keeping the details of the build process out of your way. No need to configure Webpack, Babel, and other tools because Create React App offers very high quality defaults. It’s not a boilerplate, though. You don’t even need to see the build dependencies and scripts, unless you want to, and upgrading to new versions is super easy. TypeScript is a popular extension to JavaScript that adds compile-time type checking. The two combined, in my opinion, provides one of the best developer experiences you can get for modern frontend web development.

Previously, to use TypeScript with Create React App, you had to create your project with the custom react-scripts-ts, created by Will Monk. Starting in version 2.1 of Create React App, TypeScript works out of the box. I have several projects that were started with react-scripts-ts, and I recently decided to start upgrading them to Create React App v2.1. I thought I’d share the steps that I followed and my experiences with this process.

Create a new baseline app

To start, I decided to create a completely new app with Create React App. I figured that it would be a good idea to have a baseline app, where I could compare its files — like package.json and tsconfig.json — with the files from my existing projects. This new app would give me clues about what exactly I need to change to make the existing apps work properly with the new version of Create React App.

To create a new app, I ran the following command in my terminal:

npx create-react-app my-new-app --typescript

Update library dependencies

In the existing project that I was upgrading, some of my dependencies hadn’t been updated in a little while, so the next step I decided on was to update the react, react-dom, typescript dependencies, along with the type definitions, like @types/react, @types/react-dom, @types/node, and @types/jest. After updating the versions in package.json, I ran npm install to download them all.

I left react-scripts-ts untouched for now. The idea was to make sure that the existing project was using the exact same versions of the dependencies as the new baseline project, and it should still build properly with react-scripts-ts. It’s best to ensure that I wouldn’t run into any issues with new versions of React or TypeScript that might confuse me as a worked through any issues caused by the differences between react-scripts-ts and the official react-scripts.

I needed to make a couple of minor tweaks to so that the new version of the TypeScript compiler was happy, so it was a good thing that I took this extra step.

Switch from react-scripts-ts to react-scripts

Once all of my library dependencies matched and the project would still build with react-scripts-ts, it was time to switch to the official react-scripts. Inside package.json, I replaced the react-scripts-ts dependency with react-scripts. I copied the version number of react-scripts from my new baseline project. Then, I needed to update the npm scripts inside package.json to use react-scripts:

"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject"
}

I replaced react-scripts-ts with react-scripts in each of the npm scripts above. Then, inside the build script, I saw that my existing project included a --env=jsdom option, but the new baseline project did not. I removed this option so that they matched.

Update configuration files

Next, I compared tsconfig.json in both projects. I could see some major differences, like outputting ES Modules instead of CommonJS, and preserving JSX so that Babel could handle it instead of letting the TypeScript compiler do everything. I decided that I would simply copy the contents of tsconfig.json from the new baseline project to the existing project and then make a couple of small tweaks. I ended up setting the lib compiler option to ["es2015", "dom"] so that I could access some newer JS APIs, and I set the strict compiler option to false because I haven’t updated my project to use strict mode yet. Otherwise, I used Create React App’s defaults, which all seem to be working well.

Similarly, I compared package.json in the two projects. I had already updated the dependencies and npm scripts, so the differences at this point were very minor. I saw that the new baseline project included two new fields that were not in the existing project, eslintConfig and browserslist. These defaults (which configure eslint and Babel) provided by Create React App seemed fine, so I copied them over.

In theory, the migration process should be done at this point. If all goes well, you should be able to run npm start to launch the development server, or you can run npm run build to create a production build. For my project, I ran into a couple of issues that I needed to resolve, but nothing major.

Fix compiler errors

I use the react-loadable library to load parts of my app at runtime using code splitting. My imports in TypeScript originally looked like this:

import * as ReactLoadable from "react-loadable"

However, there seemed to be a problem with the following code:

let AdminHomeScreen = ReactLoadable({
  loader: () => import("./AdminHomeScreen"),
  loading: LoadingScreen
});

The TypeScript compiler gave me the following error:

Cannot invoke an expression whose type lacks a call signature. Type ‘{ default: Loadable; Map<Props, Exports extends { [key: string]: any; }>(options: OptionsWithMap<Props, Exports>): (ComponentClass<Props, any> & LoadableComponent) | (FunctionComponent<Props> & LoadableComponent); preloadAll(): Promise<…>; preloadReady(): Promise<…>; Capture: ComponentType<…>; }’ has no compatible call signatures.

For some reason, I could not call the ReactLoadable module as a function. It turned out that I needed to change the import to look like this instead:

import ReactLoadable from "react-loadable"

In order to call a module like a function, you need to import the module’s default export. I think this may have been related to switching from CommonJS modules to ES Modules in tsconfig.json.

After fixing this issue, I could successfully run npm start to launch the development server, and I could open my app in a web browser and use it! However, I discovered one last issue that I needed to fix.

Fix production build

While npm start worked for development, I got a strange error when I ran npm run build to create a production build of my React app.

module.js:550
throw err;
^

Error: Cannot find module ‘webpack/lib/RequestShortener’
at Function.Module._resolveFilename (module.js:548:15)
at Function.Module._load (module.js:475:25)
at Module.require (module.js:597:17)
at require (internal/module.js:11:18)
at Object. (C:\Users\josht\Development\karaoke-butler\karaoke\client\node_modules\terser-webpack-plugin\dist\index.js:19:25)
at Module._compile (module.js:653:30)
at Object.Module._extensions..js (module.js:664:10)
at Module.load (module.js:566:32)
at tryModuleLoad (module.js:506:12)
at Function.Module._load (module.js:498:3)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! react-client@1.0.0 build: `react-scripts build`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the react-client@1.0.0 build script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR! C:\Users\josht\AppData\Roaming\npm-cache\_logs\2018-11-25T01_57_14_654Z-debug.log

Unfortunately, a quick Google search did not find anyone having the same issue with Create React App. However, I saw some folks using other frameworks, like Vue or Angular, were getting similar errors. Eventually, I found someone who discovered that it was some kind of conflict inside the notorious package-lock.json. Ever since npm started using this file, it’s been a terrible source of problems, in my experience.

Anyway, I deleted package-lock.json and the node_modules folder. Then, I ran npm install to install all of my dependencies from scratch. That did the trick because npm run build started working correctly.

I don’t really know why package-lock.json always seems to cause problems like this, but I find that deleting this file and the entire node_modules folder frequently fixes issues that I run into. I don’t know if I’m doing something wrong, or if it’s a bug in npm.

My project is upgraded

Updating dependencies in some of my web projects can be daunting sometimes. For some of them, I usually need to set a full day aside to fix everything. With that in mind, I was expecting to hit a few more snags in the process of upgrading from react-scripts-ts to the official version of Create React App. However, the changes required in my app were relatively minor. The process felt very smooth, and I’m happy that I’ll be able to easily update to new versions of Create React App in the future as the React team continues to innovate.

Enabling the back button to cancel confirmation dialogs in React apps

Users expect that dialogs, action sheets, alerts, drawers, and other pop-ups will close when the back button is pressed. This is especially true on Android (where the back button is globally available in all apps), but this behavior makes sense for any apps running in a web browser too — on both mobile and desktop. Usually, when a dialog is open, the back button should act the same as if the user had chosen the “Cancel” option. The worst thing that could happen (but it’s surprisingly common, unfortunately) is that the dialog will close and the browser will navigate away from the page that opened the dialog!

In a React app, we can use the browser’s history (exposed by our routing library of choice) to ensure that pressing the back button when a dialog is open closes the dialog and keeps the app on the same screen that opened the dialog. As a bonus, if the user navigates forward again, we can restore the dialog to its previous state very easily. Let’s take a look.

For convenience, I created an example on CodeSandbox to demonstrate the final result. In this example, I’ve added Reach Router and Material UI as dependencies to provide the routing/navigation and some UI controls. The concept that I’m demonstrating here should work just as well with React Router and other UI control libraries without too many changes.


Edit React dialogs with back button

In our example app, we have a list of users. When you click/tap a user’s name, a dialog will pop up with some actions that can be performed. Or you can press cancel to close the dialog. Our goal is to ensure that the browser’s back button will close the dialog without navigating away from the list of all users — the same as if the cancel option were chosen with a click/tap.

We can pass the user to dialog using the browser history’s state object. We’ll push a new state onto the stack (without changing the URL), and the dialog will check the current state to know when it should open or close.

Let’s start by rendering the list of users using some components from Material UI:

// inside render()
<List>
  {users.map(user => {
    return (
      <ListItem
        key={user.id}
        button
        onClick={() => this.selectUser(user)}
      >
        <ListItemIcon>
          <Person />
        </ListItemIcon>
        <ListItemText primary={user.name} />
      </ListItem>
    );
  })}
</List>

We have an array of users that we map to ListItem components. When a list item is clicked/tapped, we call a function called selectUser() that will manipulate the browser’s history.

For reference, a user in the array looks like this:

{ id: 1, name: "Franklin Nelson" }

In selectUser(), we use the navigate() function provided to our component by Reach Router to add the user to the history stack.

selectUser(user) {
  let newState = { selectedUser: user };
  this.props.navigate(this.props.location.pathname, { state: newState });
};

Notice that we use this.props.location.pathname (also provided by Reach Router) to ensure that the URL does not change. We’re staying at the same location, but a new history entry is added that has a different state. This means that when we go back, we’ll stay on the same page.

Next, let’s see how our UserActionsDialog component is rendered.

As a first step, we should check if the current history state has a selected user. If no user is selected, we’ll simply return null because we don’t want to render the dialog:

// inside UserActionsDialog's render()
let user = undefined;
let state = this.props.location.state;
if (state && "selectedUser" in state) {
  user = state.selectedUser;
}
if(!user)
{
  return null;
}

If a user is selected, we should render our dialog. It might look something like this:

// continued from above
return (
  <Dialog open>
    <DialogTitle>{user.name}</DialogTitle>
    <List>
      <ListItem
        button
        onClick={() => alert("Option 1 selected for " + user.name)}>
        <ListItemText primary="Option 1" />
      </ListItem>
      <ListItem
        button
        onClick={() => alert("Option 2 selected for " + user.name)}>
        <ListItemText primary="Option 2" />
      </ListItem>
      <ListItem
        button
        onClick={this.cancelItem_onClick}>
        <ListItemText primary="Cancel" />
      </ListItem>
    </List>
  </Dialog>
);

Notice that we have access to the full user object selected in the parent component. We’re accessing its name field here.

For simplicity, the first two actions simply call alert(). The third option is named “Cancel” and it should close the dialog. Clicking this item calls cancelItem_onClick(), which appears below:

cancelItem_onClick = () => {
  window.history.back();
};

When we call window.history.back(), the selected user is removed from the history stack and our UserActionsDialog component will re-render and return null. That will cause the dialog to be hidden.

Notice that we don’t do anything to cancel except to navigate back. With that in mind, we don’t need to do to do anything more to enable the back button. It just works! In fact, after we navigate back, the browser’s forward button will work too, and the dialog will re-open… including the same selected user.

Back in the parent component that contains our List of users, let’s see how to add our UserActionsDialog:

// this goes after the list of all users
<Router>
  <UserActionsDialog path={this.props.location.pathname} />
</Router>

Notice that we place UserActionsDialog inside a Router component. This ensures that the location prop is passed into the dialog so that we can find the selected user. You can nest routers, so it’s fine if there’s another router further up the component tree.

As I mentioned before, the route’s path is the same as its parent. We shouldn’t need to change the URL when selecting a user because that wouldn’t make much sense if the user wanted to share the page that contains the dialog. Since the current state object stores the user, that’s what we use to determine if the dialog should be open or closed.

Even when using some kind of router, many web apps don’t account for all cases where the back button should behave a certain way. Opening dialogs to select an action is a common place where the user might press the back button to cancel, and you don’t want your app to navigate to a different page unexpectedly. On Android, this is especially important because the global back button is such a core part of the user experience. However, it’s just as useful in a web browser on any platform, including on desktop.