As front end development embraces component-centered design, so should the approach towards constructing user interfaces.
Two years ago Thinking in React changed the way I design user interfaces. To put it lamely, it made designing UI more programmer-friendly: divide a chunk of UI into pieces, approach each piece individually as stateless markup, and add logic only after the whole structure is finished. The workflow of component-based design is so intuitive I often forget I am even using it.
Unfortunately, mapping out applications in the real world is never as simple as described in the article. Project manager needs, wants, and goals are forever in flux. Designing a user interface structure-first, logic-later, has a major pain-point: until the end of development, the code has no minimum viable product for demonstration. That means changes in scope or project direction can greatly impact work put towards development. Spending the time to structure an application is meaningless if the functionality of that application changes dramatically due to a different direction.
How can development teams provide quick, meaningful product iterations while still maintaining the advantages of component-based design? The solution is to approach development in the opposite direction of that described in Thinking in React:
This workflow solves the problem of the demonstrable product. With each component finished, the development team is able to show off a fully-functional application, even if it is wired up with fake data. The problem with this method lies in the development process: it is often not easy to develop an individual component in isolation without defining the rest of the application’s structure. However, there are some great tools that help us get around this issue and allow components to be developed in isolation easily and efficiently.
When developing a full application, components should be staged and iterated upon individually. This technique is called sandboxing, or providing a complete and isolated environment for a piece of data. For modern front end frameworks, two open source sandboxing tools are Storybook and Angular Playground.
Disclaimer: I am a contributor on Angular Playground. Therefore, I will focus on Playground since it is the tool I am most familiar with.
Playground takes the components that are already written in your application code and mounts them in “scenarios”. Each scenario is a lightweight wrapper built around your component that contains only the markup that you define in that scenario. All other dependencies of the application are left out. This means you can iterate and test components without the weight of an entire application. This also means you can stage components with whatever data and dependencies you so choose, with full control over the data they provide.
Since components are created separately from scenarios in the application itself, Playground simply grabs the existing component and bootstraps it in its own environment. That means the only code you need to work on a component is its list of dependencies and its selector, e.g. <my-component>
. Changes within the code base are instantly reflected in all scenarios that use that component.
These features provide developers a lot of power when constructing components.
Angular Playground works off of an application’s existing Angular environment, so there isn’t much boilerplate to set up. Since most Angular development uses Angular CLI, I am going to begin under the assumption that your Angular application (CLI 1.2.0+
) is already scaffolded. There are just a few extra metadata files that need to be added in order for Playground to hook up with your app.
If you want to skip configuration and just dive into my example project, here’s the code.
Since Playground uses dynamic import expressions to ensure sandboxed components are truly loaded in isolated chunks, make sure that the TypeScript version in your project is 2.4.0+
. We also need to change a tsconfig.app.json
setting to set module output to esnext
.
npm install --save-dev angular-playground [email protected]
The next step is to add the code that Playground uses to bootstrap your app’s components. Within the src/
directory, add the following file: main.playground.ts
Note that my Angular apps’s name in this case is ng-app
.
Next are the metadata files that Angular CLI will use to compile the Playground project. Add an extra app
entry to angular-cli.json
:
Now for Playground-specific configuration at the root of your project, my-app-dir/angular-playground.json
. The Playground CLI tool will use this file when building your sandboxes.
Finally, add an npm script to your project’s package.json
to use the Playground CLI.
"scripts": {"playground": "angular-playground"}
Run Angular Playground with npm run playground
and navigate to localhost:4201.
Playground running in localhost:4201/
With the Playground up and running we are ready to create sandboxes for our components. Here is a simple component that we will build scenarios for:
This component allows a user to edit particular fields associated with a profile, if that user is editable. Note that this component takes in an input (@Input() user
) and includes a separate component in its template, <app-details-form>
(source).
We create sandboxes with the Playground API function, sandboxOf()
. This function works much like Angular’s NgModule
decorator in that we use it to describe the dependencies required for a particular component. Since the InfoSummaryComponent
includes controls from the ReactiveFormsModule
, we need to bring that module in as an import. Additionally, since the DetailsFormComponent
is used within the InfoSummaryComponent
’s template, Playground needs to know about it so it can properly render the entire component.
Say we want to set up two different states of this component: one with a default, editable user and one with a non-editable user. We can do this by setting up two different “scenarios” for the component, each with a different user provided to the @Input() user
variable.
Sandboxing the info-summary component
Here is the respective sandbox file (info-summary.component.sandbox.ts
) that stages the component with two scenarios, “Default” and “Non-editable User”.
First we instantiate the component and its dependencies with sandboxOf(InfoSummaryComponent, { /** … **/})
. Then we use the .add()
method to add different scenarios to the sandbox, setting up our environment. Each scenario is assigned a name and a set of options.
The only required property for the options object is template
, which takes in the template that will be rendered on the page. In this simple case, we are only staging the component we care about with different types of user data. Finally, context
is an optional property that provides data to the context of the component. Here we use it to provide our two different users, one editable and one not.
With this sandbox structure, we can stage many different scenarios for the components we develop, modelling all of their data requirements and uses. As components increase in complexity, the sandbox provides a great environment to help focus on the important details.
Although this is a simple example, I hope that it demonstrates the power of sandboxing components. Angular Playground works with all different types of component setups, allowing you to provide injected services, set up scenario-specific styles, and easily model both @Input()
and @Output()
. There are also a ton of benefits that are outside the scope of this post that can help your development team. Here are just a few: