• Skip to main content
  • Skip to footer

InRhythm

Your partners in accelerated digital transformation

  • Who We Are
  • Our Work
  • Practices & Products
  • Learning & Growth
  • Culture & Careers
  • Blog
  • Contact Us

design patterns

Dec 20 2022

Top 5 Best Practices For Building Forms

Various forms like sign-ups, logins, or checkout pages are seen everywhere in applications and websites, but what attracts a user to fill one out? What exactly makes one click the sign-up or call-to-action button on the page? Forms exist to be filled out, but there are a surprising number of forms which—through poor design, excessive fields, or other factors—push users to abandon them, unfilled. Make your forms simple and easy to navigate, and watch the data come flowing in.

Here are a few best practices when putting together your own forms to keep engagement high:

1. Ask For Less Information (and have an easy login!)

If you have a page with a sign-up flow for a new mobile app, what do you want to know from your user? It depends on what the product is and why you need the information from them. As an example: a date or friend matching platform would want your age, gender, and zip code on one screen. The next page could be preferences about the person you are hoping to match with. All of this information is necessary for the app’s purpose, but connecting to Facebook or Google has also made it much easier to skip certain steps in this process, allowing for less info needing to be manually filled out.

The following demonstration shows just how easy it is for the user to access all of the fields needed—i.e. name, picture, friends—from the Facebook API instead of filling out the same information manually.

No alt text provided for this image

2. Single-Column Ease vs. Multi-Column Stress

When you’re making a form, make sure it’s displayed in a single-column instead of a multi-column. This design element will make it easier for users to scan through and complete instead of trying to tab through different fields and with the potential of resulting frustration.

The following demonstration illustrates, the relative ease for a user to scan through one column, answering questions in a linear format instead of doing a Z-pattern back and forth.

No alt text provided for this image

3. Automate, Automate, Automate

One of the best form practices for improving design is automation. Automation makes it easier for the user to go through fields, without worrying about switching from lower and upper cases. The only fields that should not be capitalized are emails and passwords, since those cause delivery issues.

4. Progress Tracker

A user who is filling out a form—unless it’s a simple sign-up—wants to know how long the process will take them, and maybe even if it should be filled out now or saved for later. For mobile and desktop alike, a progress tracker should be included, like the examples shown below.

No alt text provided for this image

If the user is on a certain step it’s important to note that as well, all within an easily understood display of what they’ve already completed.

5. Testing (even when you don’t want to)

This is the step that many don’t even think of taking, since the design could be flawed and more time could be wasted in redesigning. The form you just created should be tested on a few different devices (at the very least on mobile and desktop) to see if it actually works and makes sense. What’s the point of developing a product that doesn’t work for the user?

The ultimate goal is getting the user to click on the “continue” or “submit” buttons in the end. By following these best practices, you can remove common roadblocks in form submissions and make the experience better for your users. 

Written by Kaela Coppinger · Categorized: Design UX/UI, Learning and Development, Product Development · Tagged: best practices, design, design patterns, Designers, INRHYTHMU, learning and growth, product development, UI, ux, uxui

Apr 24 2017

Preparing your Angular 1 codebase to upgrade to React or Angular 2

Thank you to InRhythm teammates Mike Fisher and Khaled Mohamed for their invaluable contribution to this article.

Managing a gradual, staged transition from Angular 1 to Angular 2 or React is not a pipe dream. Upgrading does not have to mean a full rewrite, nor a significant disruption to the release cycle. Although the risk can be high, you’re probably reading this article because you know the transition is worth it for the superior performance and smaller bundle sizes of the next generation frameworks.

Additional motivation to upgrade might be as a justification to ditch the old Gulp/Bower build process for Webpack 2 + ES6 Modules in all its tree-shaking glory… or even because it has become difficult to hire and retain top talent when engineers don’t want to work on legacy Angular 1 products (you did see the SO developer survey, right?).

Fortunately, there is a simple migration path that can be pursued in three discrete, meaningful and low-risk steps.

  1. Use Webpack (Browserify, Rollup, etc.) to build your application bundle.
  2. Convert stateful services and controllers to ES6 classes.
  3. Abstract the Angular module system out of your code following the ui-router pattern.

Each of above steps will move you tangibly closer to Angular 2 or React and provide real value and flexibility along the way. I hope to prove to you that the process is achievable incrementally, without significantly disrupting the release cycle.

First things first… Prerequisites

This post assumes that you are already familiar with modern build tools like Webpack, Rollup or Browserify and aware of the benefits of upgrading to Angular 2 or React. As such, I’m not going to dive into Webpack configurations nor into the specifics of next-generation frameworks. Instead, I’ll focus on how to sequence the transition in order to minimize disruption to your release schedule.

If you’re working with a legacy Angular 1 codebase, there’s a decent chance that the starting point was John Papa’s famous generator-hottowel. I’m going to start with a freshly generated hottowel project to demonstrate the migration.

Step 1: Integrate Webpack into your build process

Hottowel uses a combination of IIFEs, Gulp, Bower, wiredep, and gulp-useref to read and concatenate the source files into an application bundle. It’s a brilliant pattern that leverages the Bower dependency tree and Angular’s module system and dependency injection to correctly build your application. It works incredibly well, but it in 2017 is showing it’s age. The lack of a proper bundler means that recent, game changing developments like native JavaScript modules, improvements to IDE tooling, tree shaking, live-refresh/hot-module-reloading, and CSS modules are almost impossible to support. Furthermore, Angular 2 and React presuppose the use of a bundler. To move forward, you’ll have to ditch your old gulpfile.

You do not need to modify your existing source code to transition to Webpack.

Amazingly, you do not need to modify your existing source code to transition to Webpack. You simply need to create an application entry point and add an index.js file in each subdirectory of your application to require all of your source files.

For example, this is the directory structure that hottowel generates:

Angular application scaffolded by generator-hottowel demonstrating the default directory structure

First, create a file src/client/app/index.js with the following contents:

// src/client/app/index.js

import './app.module.js';
import './admin';
import './blocks';
import './core';
import './dashboard';
import './layout';
import './widgets';

 

This file is the entry point to your build process. It represents the root of the tree that comprises your application. Inside ./admin and ./blocks  and every other subdirectory there is an index.js file which will import all of the source code in the same directory in addition to any subdirectories.

For example, src/client/app/admin/index.js will look like this:

// src/client/app/admin/index.js

import './admin.controller.js';
import './admin.html';
import './admin.module.js';
import './admin.route.js';

and src/client/app/blocks/index.js which contains three subdirectories and no files should look like this:

// src/client/app/blocks/index.js

import './exception';
import './logger';
import './router';

When complete, a chain of index.js files and import statements will link your entire application. By pointing Webpack at src/client/app/index.js it can generate your entire application bundle. The directory structure should now look like this, with an index.js in src/client/app, src/client/app/admin and in every other subdirectory under src/client/app:

directory structure of the generated application after creating the index.js files

If it’s unclear how this pattern works, I recommend reading up on module loaders and these resources on ES6 import and Node/CommonJS module resolution.

Step 2: Embrace ES6 modules, upgrade Angular services and controllers to ES6 classes

Now that your bundler is in place, you can begin to strip away some of the cruft necessitated by simply concatenating source files. As an example, let’s take a look at the AdminController at src/client/app/admin/admin.controller.js.

// src/client/app/admin/admin.controller.js
(function() {
  'use strict';

  angular
    .module('app.admin')
    .controller('AdminController', AdminController);

  AdminController.$inject = ['logger'];
  /* @ngInject */
  function AdminController(logger) {
    var vm = this;
    vm.title = 'Admin';

    activate();

    function activate() {
      logger.info('Activated Admin View');
    }
  }
})();

Immediately, there are a few things we can do. First, we can remove the wrapping IIFE since Webpack will package each file in its own closure. We can also remove ‘use strict’ because ES6 modules are executed in strict mode by default. Let’s also import angular because we want to be explicit about the dependencies of every file. Importing 3rd party libraries instead of treating them as globals allows Webpack to extract them into a vendor or commons bundle on which you can set appropriate caching headers, potentially saving returning users hundreds of kilobytes of downloaded content and multiple network calls.

After these changes admin.controller.js should now look like this:

// src/client/app/admin/admin.controller.js
import angular from 'angular';

angular
  .module('app.admin')
  .controller('AdminController', AdminController);

AdminController.$inject = ['logger'];
/* @ngInject */
function AdminController(logger) {
  var vm = this;
  vm.title = 'Admin';

  activate();

  function activate() {
    logger.info('Activated Admin View');
  }
}

Next, we want to convert the services and controller to ES6 classes. Both React and Angular 2 encourage implementing application components with ES6 classes. If you’re unfamiliar with ES6 classes, I highly recommend reading up on them and on prototypal inheritance in JavaScript in general. As an ES6 class, AdminController looks like this:

// src/client/app/admin/admin.controller.js
import angular from 'angular';

angular
  .module('app.admin')
  .controller('AdminController', AdminController);

class AdminController {
  static $inject = ['logger'];

  constructor(logger) {
    this.logger = logger;
  }

  $onInit() {
    this.title = 'Admin';
    this.logger.info('Activated Admin View');
  }
}

A few things to consider

There are a few subtle patterns to recognize here. First, we ditched the /* ngInject */ annotation in favor of a static class property.

Second, there is no private scope. With ES6 classes, all members are public (for now). The body of an ES6 class can only contain static and prototype property declarations. This means that the initialization logic in the body of the AdminController function needs to be moved into the constructor or into the new $onInit lifecycle method introduced in Angular 1.5.

* Technically, to take advantage of lifecycle methods we need to convert this controller into a component. Take a look at Todd Mottos upgrade guides to familiarize yourself with the new patterns and for tips on easing the transition to Angular 2.

Finally,  vm = this;, also known as “controller as” syntax has been eliminated. Instance and prototype properties and methods are accessible on this so we don’t need that workaround anymore. This is a mixed blessing because vm helped us avoid JavaScript’s notoriously confusing rules about resolving this. When you refactor to a class you’ll need to decide on a pattern for how to keep this proper this in scope. Concepts to investigate include arrow functions (lexical scoping of this), binding prototype methods to the instance in the constructor, class property initializers, and the proposed bind operator ::.

Take your time–you don’t have to do this all at once

Take your time as you refactor your services and controllers into classes and your directives into components–you don’t have to do this all at once. In fact, you can write new code with the new patterns and leave your old code alone if it’s working just fine. The point of Step 1 was to give you the flexibility to refactor at your own pace.

Step 3: Abstract away the Angular module system and decouple your source code from the framework

If you’ve made it this far, your formerly legacy codebase is starting to look a little more fresh. You now have a proper build process and you can tell prospective hires that yes, your company uses the latest ES6 features ????.

The final step is to decouple your source code as much as possible from the framework. This can also be done incrementally, file-by-file as time permits.

Utilizing the following patterns will breathe new life into your legacy application while setting the stage for an eventual migration to the framework of your choosing. By the end, you might find that you don’t need to switch frameworks at all!

  1. Use the ui-router pattern to decouple your codebase form the ng-module system.
  2. Replace Angular helper functions with Lodash. Tree shaking dramatically reduces the cost of utilizing small portions of 3rd party libraries.
  3. Write pure functions and turn any Angular service that does not manage private state into a plain old JavaScript library. If a service isn’t stateful then it’s just business logic. Treat your business logic like an external dependency and import functionality as needed.
  4. Use Angular 1.5 components for everything. Break up large templates into components using the presentational/container component pattern. This pattern was popularized by the React community but works just as well with Angular 1.5 components.
  5. Ditch “scope soup”, $broadcast,  and 2-way binding in favor of 1-way component bindings and framework-independent state management libraries like Redux and RxJS that support unidirectional data flow. Redux and RxJS are used extensively in React, Vue and Angular 2 development and helper libraries for Angular 1 exist (ng-redux, rx.angular.js).

If you’re serious about making the transition to Angular 2, then thoroughly read the official upgrade guide and consider developing a hybrid application if you really can’t afford the risk of a hard cutover.

UI-Router pattern

Of all the recommendations above, the ui-router pattern for eliminating the Angular module system might be the least familiar, though I’ve found it essential for easing the migration away from Angular 1.

The ui-router team put together an amazing sample application that shows off the power of their library along with demonstrating beautifully how to build a modern, modular application. One thing it illustrates admirably is how to abstract the Angular wiring into a single file in the application.

Currently, every file in our example application is still tightly coupled to Angular because they contain this little preamble or something like it:

import angular from 'angular';

angular
  .module('app.admin')
  .controller('AdminController', AdminController);

This repetition just feels wrong when Don’t Repeat Yourself is a programmer’s mantra. To DRY up this anti-pattern, the ui-router team followed a three-pronged strategy:

  1. Maintain a single Angular module for the entire application. You may have heard that it’s helpful to create many modules for your application because it aids code reuse. However, now that JavaScript has real modules, we don’t need Angular’s module system anymore.
  2. Convert ngModules into mini-libraries of plain JavaScript classes, functions and objects to bundle related functionality.
  3. Expose a helper method to wire the single module and the mini-libraries into an Angular 1 application.

Applying this to our sample hottowel application, we’ll modify src/client/app/app.module.js to create the single Angular module and expose the helper function that handles registering application components with the injector.

Creating the single module and helper method

After completing step 2, app.module.js should look like this:

// src/client/app/app.module.js
import angular from 'angular';

angular.module('app', [
  'app.core',
  'app.widgets',
  'app.admin',
  'app.dashboard',
  'app.layout'
]);

After applying the ui-router refactor, app.module.js now looks like this:

// src/client/app/app.module.js

/* Adapted from http://github.com/ui-router/sample-app-ng1/blob/master/app/bootstrap/ngmodule.js */
import angular from 'angular';

/**
 * Creates the module 'app' and exports it for other modules to import and 
 * register providers and controllers
 */
export const ngModule = angular.module('app', [
  /* List 3rd party Angular dependencies */
]);

const BLANK_MODULE = {
  states: [],
  components: {},
  directives: {},
  services: {},
  filters: {},
  configBlocks: [],
  runBlocks: [],
  constants: {},
  values: {}
};

/**
 * Register each app module's states, directives, components, filters, services,
 * constants, values, and config/run blocks with the `ngModule.
 *
 * @param ngModule the `angular.module()` object
 * @param appModule the feature module consisting of components, states, services, etc
 */
export function loadModule(ngModule, appModule) {
  const module = Object.assign({}, BLANK_MODULE, appModule);

  ngModule.config(['$stateProvider', $stateProvider => module.states.forEach(state => $stateProvider.state(state))]);

  Object.keys(module.components).forEach(name => ngModule.component(name, module.components[name]));

  Object.keys(module.directives).forEach(name => ngModule.directive(name, module.directives[name]));

  Object.keys(module.services).forEach(name => ngModule.service(name, module.services[name]));

  Object.keys(module.filters).forEach(name => ngModule.filter(name, module.filters[name]));

  Object.keys(module.constants).forEach(name => ngModule.constant(name, module.constants[name]));
  
  Object.keys(module.values).forEach(name => ngModule.value(name, module.values[name]));

  module.configBlocks.forEach(configBlock => ngModule.config(configBlock));
  
  module.runBlocks.forEach(runBlock => ngModule.run(runBlock));

  return ngModule;
}

The export ngModule is our single application module. 'app.core', 'app.admin', etc. are gone. In their place we will attach every provider to ngModule via the loadModule function.

loadModule is how we wire up the Angular application. It accepts two arguments, an Angular module and an object describing a mini-library composed of ui-router state definitions, services, factories, controllers, etc. When invoked, loadModule will iterate through each collection and dictionary, registering the providers within on the module. Any class, object or function that we want to expose as an injectable in our Angular app we can wire up with via loadModule. With this framework in place, we can remove the Angular preamble from the beginning of every file.

Refactoring the Admin module into a mini-library

Let’s see how this can be applied to src/client/app/admin, converting it from an Angular module to a mini-library.

Before, index.js and admin.controller.js look like this:

// src/client/app/admin/index.js

import './admin.controller.js';
import './admin.html';
import './admin.module.js';
import './admin.route.js';


// src/client/app/admin/admin.controller.js

import angular from 'angular';

angular
  .module('app.admin')
  .controller('AdminController', AdminController);

class AdminController {
  static $inject = ['logger'];

  constructor(logger) {
    this.logger = logger;
  }

  $onInit() {
    this.title = 'Admin';
    this.logger.info('Activated Admin View');
  }
}

After the refactor, they look like this:

/ src/client/app/admin/index.js

import { ngModule, loadModule } from '../app.module.js';
import AdminController from './admin.controller.js';
import adminRoute from './admin.route.js';  Modified to return a ui-router state definition object
// import './admin.html';                   The template will be imported in the route as a string
// import './admin.module.js';              Deleted. This file is no longer needed

const adminModule = {
  controllers: { AdminController },
  states: { adminRoute }
}

loadModule(ngModule, adminModule);


// src/client/app/admin/admin.controller.js

export default class AdminController {
  static $inject = ['logger'];

  constructor(logger) {
    this.logger = logger;
  }

  $onInit() {
    this.title = 'Admin';
    this.logger.info('Activated Admin View');
  }
}

When we invoke loadModule(ngModule, adminModule) it will register the admin module’s components on the main 'app' module. Applying this pattern to the rest of the project, we have now removed the Angular wiring from nearly every file in the application! Our source code is nearly clear of coupling to the framework and it’s in much better shape that when we started out.

What’s next?

It would be easy to mistake the refactored admin.module.js for something that’s not Angular 1. With most of the framework ceremony stripped away, it’s clear that we could quickly factor AdminController into a React component that expects a logger prop and whose render method returns the template from admin.html.

If you decided opted to replace $scope inheritance and $rootscope.$broadcast with ngRedux then moving to React becomes a task that’s more tedious than complex. You can also look into ngUpgrade which supports loading hybrid ng1/ng2 applications, giving you even more granular control over how you fully leave Angular 1.

Extra credit: refactor your source code to TypeScript

TypeScript can be a game changer, eliminating whole classes of errors that generally aren’t diagnosable until runtime with plain JavaScript. If you’re unfamiliar with statically typed languages and the benefits thereof, I recommend starting with the TypeScript documentation to see how it can make your projects more resilient.

An incremental transition to TypeScript is simple with a Webpack build process. First, change the extension of any file that you want to write in TypeScript to .ts and then add ts-loader to your Webpack config, configured to resolve and transpile files with a .ts extension. Since TypeScript is a superset of JavaScript you can mix and match as you please.

Additional Reading

Below are links to articles and repositories which you might find helpful when planning your upgrade. Please post any helpful articles or migration advice in the comments.

  • The wonderful ui-router example application
  • ngReact to register React components as ng1 directives
  • Managing state with Redux and Angular and an example application
  • Comparison of major frameworks including Vue, React, Angular 1/2, and Ember
  • Using TypeScript with React

Written by InRhythm · Categorized: InRhythm News, Software Engineering · Tagged: Angular, angularjs, architecture, design patterns, project management, React, refactoring, upgrade, Webpack

Footer

Interested in learning more?
Connect with Us
InRhythm

110 William St
Suite 2601
New York, NY 10038

1 800 683 7813
get@inrhythm.com

Copyright © 2023 · InRhythm on Genesis Framework · WordPress · Log in

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Cookie settingsACCEPT
Privacy & Cookies Policy

Privacy Overview

This website uses cookies to improve your experience while you navigate through the website. Out of these cookies, the cookies that are categorized as necessary are stored on your browser as they are essential for the working of basic functionalities of the website. We also use third-party cookies that help us analyze and understand how you use this website. These cookies will be stored in your browser only with your consent. You also have the option to opt-out of these cookies. But opting out of some of these cookies may have an effect on your browsing experience.
Necessary
Always Enabled
Necessary cookies are absolutely essential for the website to function properly. This category only includes cookies that ensures basic functionalities and security features of the website. These cookies do not store any personal information.
Non-necessary
Any cookies that may not be particularly necessary for the website to function and is used specifically to collect user personal data via analytics, ads, other embedded contents are termed as non-necessary cookies. It is mandatory to procure user consent prior to running these cookies on your website.
SAVE & ACCEPT