Single Page Web App UI Development Thoughts, Part 1
We take a look at higher-level concerns, such as clean coding standards, design patterns, and more.
Join the DZone community and get the full member experience.
Join For FreeI have been working on some single page web apps’ UIs for several years and want to share some of my experiences.
In this article series, I will divide the content into 3 parts: ”UI Architecture Level Thoughts,” “Large Scale UI Design,” and “UI Testing and Automation.”
1. UI Architecture Level Thoughts
1.1. Localization
Whenever you create a new web application, always implement localization first. Otherwise, there will be tremendous refactoring costs and regression test costs when you want to add this feature to a developed web app. If you don’t want to spend much time implementing a localization system at the beginning, you can define common localization APIs or unimplemented interfaces first and implement the full feature later.
Localization contains the following aspects:
- Language translation.
- Time zone conversion.
- Date, time, currency, number formatting, and parsing.
1.2. How to Make Code Clean and Extensible, With Low Maintenance
This is a common question in computer science. Here I’d like to limit my scope to the front-end. The answers are as follows:
1.2.1. Always Try Your Best to Build Your UI Element as a Broadly Reusable and Configurable Component.
Although in HTML and CSS, it is easier to implement a UI element with fixed size, fixed position, and other fixed configurations, it’s always better to wrap a UI element in a component and make its size, position, and other configurations dynamically depend on a customized configuration or its parent’s size and position, as much as possible. When your web app grows larger and larger, your PM will always ask you to reuse some components in other places and contexts. For example, assume one day you implement a sidebar in a static page. After release, this sidebar turns out to be a popular component among users. Then your PM may ask you to add your sidebar to some dialogs or to some nested elements. If you made all sizes, positions, and other configurations static during your implementation, you would need to refactor the code a lot and spend extra time doing regression testing on your sidebar so that the original feature wouldn’t be broken.
1.2.2. Make Your Component Initializable to as Many States as Possible
Although there are usually many configurations for one component, most components start with the same state as when they were inserted into the DOM. For example, a table component’s initial state could only be in the first page. A tree table’s initial state could only be a tree with all root nodes collapsed. Usually, there would be some extra logic in the conversion process from one state to another. If the developer wants to initialize the state to other states like page 2 in a list/table or some node expanded in a tree table, they have to add extra logic to disable the state conversion logic which makes the code unnecessarily complicated. So it is always better to make your component initializable to as many states as possible to avoid those state conversion logic faux-pas.
1.2.3. Use Object-Oriented Design in JavaScript
Although modern JavaScript versions such as ES6 and Typescript have added better OOD features and many frameworks also have OOD features, my previous experience with JavaScript was mostly non-OOD. OOD has some disadvantage. It is complicated and needs more design thinking which will result in a longer development cycle. If your web application is very small and won’t grow large or your web app is mostly rendered on the backend, then you don’t need to use OOD with JavaScript. You can use simple JavaScript and have fast iterations and short release cycles.
However, if your application will grow very large and the JavaScript code is responsible for all UI logic and rendering, such as single page app, then simple JavaScript without OOD will make your code a mess and fragile.
For example, a table UI library could be used in different components and the UX department or your PM wants to have some level of consistency across the whole project. Without OOD, devs would simply create a common function to initialize the configuration of the table library which may contain both property settings and callback bindings. Since each component has its own requirements, devs would simply put all requirements in the common function and use id attributes to distinguish different components; doing this could cause this function to become a mess. Another problem is when a dev wants to add new features in this function, it is easy to break other features or some special and hidden scenarios, and development or QA would need to spend a lot of time regression testing all related components because of even a simple change in such common functionalities.
Imagine if we do this with OOD. The code would become cleaner, more extensible, and easier to maintain and test. We could model different UI components in a hierarchical order, have a base function in the root class to initialize common configuration across all components, and in each component level implement overridden functions to add special configurations. If we want to add a new feature to a subset of components, we can also achieve this with OOD patterns, such as interface or mixins. We can implement a simple unit test for each class. By doing all of the above, new features or new configurations would usually only affect a small set of components and the stability would be guarded by those class level unit tests and developers could easily find what components their modifications would affect by looking at the class hierarchy. I think that would reduce a lot of manual testing time and regression bug fixing time, which may offset the extra development time for OOD.
Therefore, I think OOD with JavaScript for large and JavaScript-heavy web applications would make the code clean and extensible and reduce a lot of maintenance cost.
1.2.4. Make Your Code Follow Your Business Logic’s Infrastructure and Be as Data Structure Independent as Possible
Even with OOD, there is still space to improve reusability and reduce redundancy. For example, suppose you want to extend a class and override a super class’s function. You have two options:
- Override the function by copying the code and make modifications on the copied code.
- Call the overridden function first and add extra code after it.
The latter option is obviously better but the problem is that not all logic can be added to the end or beginning of the old logic. Some logic may need to be inserted into the middle of the old logic. Some logic may need to swap two lines in the old logic. Sometimes to change the data structure of a function’s input parameters, the developer has to completely change the old code’s structure. For example, the old input parameter could be an array while the new input would be a map. So in the above cases, we have to use the first option. However, it makes code redundant. And redundant code not only looks bad but can also make developers forget about synchronizing the code changes in one place to multiple redundant places. So even with the first option, we should also try to reduce redundancy as much as possible.
To address this problem, I think developers should follow their own business logic’s infrastructure by clearly defining the business logic and having good mapping from this logic to their code.
All code is used to implement business logic. I think bad reusability is always caused by bad mapping from the code function to the business logic. Sometimes we mixed bunches of business logic related code into one big function. And later, to implement a new feature, a different data structure may be needed for the same business logic or only the first third part of the business logic may be needed, in which case we have to either refactor the old code or copy the old code. Sometimes when we define functions, we only considered the immediate needs and tried to find a more concise implementation while ignoring our future needs. And later, similar business logic may also be needed but there is no easy way to reuse the previous, highly optimized code. Sometimes in order to use some tricks in a language or a library, we put part of our business logic in one function and the others in another function. And later the whole business logic may be needed to be implemented in a different way.
To avoid the above mistakes we should go through two steps.
- We should clearly define and componentize business logic. We should create an acyclic graph to represent all business logic. Each vertex is a component representing a piece of business logic which could be described in a few words. Each edge is a relationship. For example, business logic A could be composed of business logic B, C, and D, and A could be used in E and F. We should try our best to divide one piece of business logic into as many subsets of business logic as possible to increase its potential to be reused.
- We should define a function in the code for every piece of business logic. We should make the function as data structure-independent as possible by using an object as input instead of any particular data structure, because in the future we may want to implement the same logic with a different data structure or input. If a particular data structure is really needed to be the input, we can create a wrapper function whose input is a data structure and will call the business logic function inside it.
1.2.5. Use the Same Design Pattern Across the Whole Project
Usually, a framework such as Anguler or Ember provides so many features that developers can achieve the same goal via different means. However, it would add extra complexity and instability when the developer wants to integrate several components which are developed in different patterns by different developers into a new large component and create high dependency among them.
During the infrastructure design stage of a project, it is better to set common design patterns and solutions for some common questions and problems.
1.2.6. All Functions Should Use a Single Object as an Input Parameter
Unlike Java, which looks up functions by both name and parameter, JavaScript looks up functions only by the function name. When calling a function, the developer could put an arbitrary number of input parameters and the remaining would be set as undefined. This feature makes it easy for the developer to change a function’s signature but it also makes the code messy.
Consider the following scenario:
- A function is defined as
func(a, b);
. - A developer adds a few new parameters into this function and it becomes:
func(a, b, c, d, e);
. - Another developer who is working on another feature wants to add one more parameter to that function and change the usage of it in one place to use that parameter. Then the function signature becomes:
func(a , b , c , d , e , f);
. The function call becomes:func( va, vb, undefined, undefined, undefined, vf);
.
As you can see, in order to use one additional parameter, we need to add three undefined
s in the call, which I don't think looks clean, especially when your function parameter set is very large.
To solve this problem, we can use a single object as a function parameter and add input parameters as the object’s value.
For example, the previous function’s definition could be rewritten as func(paramObj)
and in the comment we could specify the paramObj
definition like:
paramObj = {
a: //…..,
b: //…..,
c: //…..,
d: //…..,
e: //…..,
f: //…..
}
Then for the previous modified function call, we can change it to:
func({
a: va,
b: vb,
f: vf
});
As you can see after the change, it looks much cleaner.
1.2.7. The User Action Handler Should Only Be Triggered by User Actions
This statement may seem redundant. But sometimes user action handlers are reused in function calls or user actions could be triggered by a function call. I think it is not good to do that because user action handlers may contain some logic only for that particular user action or it may be new logic added only for that action. This logic should be disabled if the handler is called from the function, which seems to add unnecessary complexity. So when a developer codes an action handler, they should divide and wrap different parts in separate functions to be reused. And other developers shouldn’t directly reuse the handler. Instead, they should reuse the exact function they need in the handler or they should refactor the handler code and wrap what they need in a separate function and then reuse it.
1.2.8. Integrate JavaScript UI Libraries With Javascript Frameworks
Many Javascript UI libraries are written in pure JavaScript and directly use DOM APIs or jQuery to create HTML components. However, most JavaScript frameworks use template languages to generate HTML components. The discrepancy also exists in design patterns. For example, some libraries use observers while others use event handlers. This causes problems and challenges when you want to use a JavaScript UI library with your JavaScript framework because a bad integration could make your code more error-prone, less maintainable, and less extensible.
The solution is to create a wrapper class with your JavaScript framework on the JavaScript UI library which converts all the JavaScript UI library’s patterns to your JavaScript framework’s patterns. All other code in your app should only call the wrapper class’s API and should not access the Javascript UI lib’s API directly. You can put all Javascript UI lib generated HTML in a ShadowRoot to keep the markup structure, style, and behavior hidden and separate from other code on the page so that Javascript UI lib code doesn’t clash with your own code.
To integrate your JavaScript UI library generated HTML with your template language, there are two options:
- Some JavaScript UI library could style your existing HTML code to make it looks as expected. They don’t create new HTML nodes. With such features, you can render your template language into HTML first and then style the HTML.
- Or you can create a placeholder in your template for JavaScript UI libraries first and then render your UI component into the placeholder.
That's all for Part 1! Tune back in tomorrow when we'll discuss single page architectural patterns.
Published at DZone with permission of Swortal w. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments