Here are exemplary implementations of different features that we often run into when developing a complex application.
We focus on:
- Handling async state
- Complex form validation
- Handling application contexts
- Scroll virtualization
Big applications typically involve communicating with servers and sometimes rich UIs need to reflect the pending state of multiple parallel async operations. We can employ a mechanism that reflects if a certain operation or a group of operations are pending. This exempts us from the tedious job of handling the state of each operation. This state can also be seen as a hierarchy: we can disable a button while its associated operation is in progress or we can block an entire area of the screen if it contains one or more UI elements with pending operations associated.
Cancelling async operations is also useful, especially when a certain context of the application is left and the results of the operations are not needed any longer. The capability of grouping operations is especially important, such that they can be cancelled together when needed.
The input data may be temporary data that is created on the spot or the form can reflect existing model data that could also be invalid from the beginning. We may want to validate a certain input when another input is changed, for instance an input that represents an end date could become invalid if the input that represents a start date is updated and the start date is now greater than the end date.
We may need to validate inputs asynchronously and for a good UX we would want to allow interlaid async validations. We described our approach extensively in a previous article.
Gathering data representing big and complex entities is done often through a wizard. The entire flow must represent a single form as a whole. For more details, check the wizards section.
If completing an operation that the user started is very important, or if the user spent a long time working on something in a context of the application, leaving that context by mistake should be prevented until the user confirms that leaving is desired.
There are also cases where entering a context of the application should be prevented, for instance if we have a wizard where each step is a separate route, and we want to enforce a certain order of visiting the wizard steps, in case the user tries to enter a step that shouldn't yet be available, we could redirect them to the right step that needs to be completed before. See the wizards section for details.
Every once in a while, when certain steps are finished we want to bring the application to a state that is similar to the beginning of the flow that the user has just completed. In other words we would want to mimic the user's re-entrance in the current flow of the application. An elegant way of implementing this is by refreshing the current route, meaning the current route or routes (if we have a hierarchy of nested routes) are left one by one from the end to the start and reentered from the start to the end, calling any entrance or exit route guards. Or, even more interesting, we may want to refresh only a part of the hierarchy of routes. This is the best handled by a routing library.
Let's imagine we need to implement a wizard in which each step is a route. In order to get to the next step by using the
next button you have to complete the current step. If a step contains a form, in order to be completed, the form needs to be submitted successfully. If a step doesn't contain a form, the step is completed by simply being visited. You can not jump to a step, by using the step number links, without completing the previous steps, unless the step you're jumping to has already been visited. This allows us to complete a step with a form, visit the next step, go back to the previous step that contains the form, make a change such that the form becomes invalid and jump to the next step without fixing the form. However, on the last page when we hit submit, if there are any steps that have an invalid form we jump to the first one in that category. If we try to access a certain step by transitioning programmatically to that step, if that step hasn't been visited before, we're redirected automatically to the first step that has never been completed. The step number links at the top of the wizard are active only for the steps that have been visited. All the step forms are composed into one single form with which we gather all the data when submitting the wizard data on the last step. As we navigate through the wizard we don't want the state of each step to be lost. However, once we leave the wizard altogether, we want its state to be lost. We also want this wizard mechanism to be reusable for other contexts of the application and be able to focus only on implementing each step of the wizard and on putting the pieces together.
Enterprise applications may contain lots of data and in certain cases you may want to display tables with many entries. Or you may want to display a large grid. This can have a performance impact if we want to render many elements, therefore we may choose to implement scroll virtualization, so that we can display a big list of items without blocking the browser. More precisely, only a part of the items are rendered while for the user it appears that all the items are rendered.
Ideally all the frameworks should make these easy for us to implement so that we can focus instead on the business requirements. Other than what we mentioned, complex applications may also contain capable components such as tables with different features or charts or other capabilities that separate libraries may provide, such as viewing a PDF. What other similar features did you run into that we didn't cover?