Persistent Data Grids in Angular: A Comprehensive Guide
Tags: angular, storage, softwaredevelopment, tutorial
Published: 2023-12-20 10:15:57

Ever wondered how web applications keep your search data and grid configurations intact as you navigate through pages? This seamless experience is thanks to data grid implementations—a cornerstone of modern web development.

Data grids are essential tools, revolutionizing the presentation and interaction with vast sets of information. Picture online banking platforms allowing effortless transaction filtering or project management tools dynamically updating tasks without a page refresh. These real-world scenarios underscore the power of data grids, offering users intuitive controls for customization, sorting, and real-time updates.

This blog explores the intricacies of achieving persistent storage for data grids in Angular, eliminating the need for manual data synchronization. Join us as we delve into ensuring a seamless user experience where apps effortlessly retain grid configurations and search data.

## Understanding the Challenge

Before delving into the technical details, let's briefly outline the key elements of an application's data grids. Typically, a data grid consists of an external filter that searches a database and a grid that displays the retrieved records. Two critical pieces of information need to be preserved:

1. **Search Data**: Retaining all the searches made in external filters whenever the page is visited.
2. **Grid Configuration**: Storing options like selected columns, column width, and column order, which should persist across visits.

The next important thing is how long this data must be stored. Deciding when to retain or discard the data depends on the context. Search data is often considered temporary, while grid configurations are expected to persist across user sessions.
So, the search data should be stored for a particular session against a data grid, while grid configurations are stored against a data grid and the user ID.

## Choosing the Right Storage Option

Now that we've identified what data to store and when, let's explore the options available to achieve persistent storage. Angular offers an array of options for persistence, each with its own strengths and quirks:
1. **LocalStorage**: Persistent even after closing the tab, ideal for large data and simple settings. Performance would be a concern for large chunks of data since we will be parsing and stringifying the large data.
_Example: User preferences like language or theme persist across logins._

2. **SessionStorage**: Similar to LocalStorage, but data vanishes when the browser window closes.
_Example: Shopping cart contents during a single shopping session._

3. **IndexedDB**: A more powerful local database with richer object storage capabilities. While IndexedDB offers powerful storage capabilities, its complexity and overhead, encompassing factors like error handling challenges, potential performance limitations, and reduced compatibility across browsers and modes, make it a less straightforward choice for simple persistent data grid storage.
_Example: Offline data persistence for complex applications._

4. **Cookies**: small data packages sent between browser and server, useful for tracking user sessions, but their privacy concerns, technical limitations, and potential negative impact on the user experience make them a less ideal choice.
_Example: Remembering login credentials for returning users._

5. **Angular Routes**: Embed data directly in the URL, making it bookmarkable and SEO-friendly. Can be used for simple configurations
_Example: Sharing specific content configurations with a link.
`/analytics/user-report?start_date=2023-10-01&end_date=2023-12-19` (shows specific date range for analysis)_

6. **Angular Services**: Services act as data managers, making your app cleaner, more organized, and easier to work with. Its key advantages are code reusability, abstraction, and synchronization. Data will be lost upon page refresh/ tab closure.

**_Remember_**: The best choice depends on your application's needs. Consider data size, persistence duration, security implications, and potential limitations.

## Storage Options Implementation

In my implementation, I've opted for a hybrid approach, utilizing both local storage and a backend database to store different types of data. Here's a glimpse of the structure used for storing grid configurations.

```
{
    "grid_name": {
        "external_filters": {
            "name": null,
            "customer": [ 1234, 1235],
            "email": null,
            "some_boolean_filter": false,
            "created_at": "2023-12-13T18:30:00.000Z"
        },
        "columns_config": [
            // Individual Column Configurations
            {
                "field": "name",
                "width": 120,
                "title": "Name",
                "orderIndex": 1
            },
            {
                "field": "amount",
                "width": 100,
                "title": "Amount(₹)",
                "cssClass": "text-end",
                "orderIndex": 3
            },
            {
                "field": "email",
                "width": 100,
                "title": "Email",
                "hidden": true,
                "orderIndex": 2
            }

        ], 
		"state": {
            // Grid State Information
			"skip": 0,
			"take": 10,
			"sort": []
		}
    }
}
```

## Saving Grid Configurations

### Functional Breakdown
Let's get to the exact details as to how and when we save the data. First, we need to break down each of the operations into separate functions so that we can reuse them. We will have the following functions:

- **saveGridSettings**: This function builds a data structure containing column configurations, grid state, and external filter data. It then saves this data to LocalStorage.
```
    saveGridSettings() {
        const external_filters = this.searchForm.value;
        const grid_config = {
            contractor_list: {
                state: this.grid_state,
                external_filters: external_filters,
                columns_config: this.columnsConfig
            }
        };
        this.persistentStorageService.setGridSettings('grid_name', grid_config);
    }
```
```
setGridSettings(grid_key: string, gridConfig: any): void {
    localStorage.setItem(grid_key, JSON.stringify(gridConfig));
}
```

- **saveColumnConfigSettings**: This function saves the user-specific column configuration for the current grid to the backend via an API call. It's triggered when the "Apply" button is clicked.
```
	saveColumnConfigSettings(event: any) {
        const input_data = {
            grid: 'grid_name',
            columns_config: event
        };
        this.saveGridSettings();

        this.persistentStorageService.saveColumnConfigSettings(input_data).subscribe({
            next: (response: any) => {
                this.toast.showSuccess(response.message);
                this.getColumnConfigSettings();
                this.contractor_list_grid_loading = false;
            },
            error: (error: any) => {
                this.toast.showError(error.message);
            }
        });
    }
```

- **getColumnConfigSettings**: This function retrieves the user-specific column configuration for the current grid from the backend. It's used during component initialization.
```
    getColumnConfigSettings() {
        const input_data = {
            grid: 'grid_name'
        };

        this.persistentStorageService.getColumnConfigSettings(input_data).subscribe({
            next: (response: any) => {
                if (response && response['columns_config'].length !== 0) {
                    this.mapGridSettings(response['columns_config']);
                } else {
                    this.initialGridSettings();
                }
            },
            error: (error: any) => {
                this.toast.showError(error.message);
                this.initialGridSettings();
            }
        });
    }
```

- **initializeGridRememberSettings**: This function checks for existing grid settings in LocalStorage. If unavailable, it fetches the user's column configuration via API or uses the default configuration if none exists.
```
	ngOnInit(): void {
		// Other initializations
        this.initializeGridRememberSettings();
    }
	
	initializeGridRememberSettings() {
        if (!!this.savedState) {
            this.mapGridSettings();
        } else {
            this.getColumnConfigSettings();
        }
    }
```

### Application Flow

![Persistent data grids in angular workflow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jkk20kcat00bfuzl2hdq.png)

- **User interaction**: When the user applies filters or modifies the grid layout, the updated state (including external filters, column configurations, and grid state) is saved to LocalStorage with the help of state change events.

- **Local Storage cache**: This cached data ensures quick restoration of configurations when the user revisits the page.

- **API calls**: Saving of user-specific column configurations to the backend happens only when the user explicitly confirms with the "Apply" button, triggering the saveColumnConfigSettings function.

- **Component initialization**: During initial page load, the initializeGridRememberSettings function checks for LocalStorage data. If unavailable, it retrieves the user's configuration via API or uses the default configuration.

Overall, this approach leverages LocalStorage for efficient caching and utilizes the backend for persistent, user-specific configuration storage.

## Conclusion

Choosing the right approach for persistent storage in Angular data grids involves careful consideration of your application's requirements. In my case, LocalStorage with a backend database for grid configurations provided a flexible and scalable solution.

However, LocalStorage is not without drawbacks. Performance concerns arise due to the parsing and stringification processes involved. Instead, leveraging an Angular service to store grid data can optimize performance by avoiding these processes. This comes at the cost of losing data upon tab closure. Ultimately, the choice depends on your specific needs.

To further enhance this solution, consider maintaining a queue in LocalStorage with a maximum capacity of 10 data grids. This minimizes storage size by evicting older entries, assuming users navigating beyond 10 pages are unlikely to revisit those specific searches.

Remember, thorough analysis and planning before diving into code is crucial for ensuring a robust and efficient solution for persistent data grids in your Angular application. Happy coding!

## Further Resources

- What are data grids? [Introducing In-Memory Data Grid — Hazelcast IMDG | by Dina Bogdan | The Startup | Medium](https://medium.com/swlh/introducing-in-memory-data-grid-hazelcast-imdg-1f2af2be5344)

- For a deeper dive into Angular Routes, see [Angular Tutorial](https://www.mygreatlearning.com/blog/angular-routing-tutorial/).

- Learn more about LocalStorage and SessionStorage [Beginner's Guide to Web Storage](https://levelup.gitconnected.com/beginners-guide-to-web-storage-a88b6e5b522e)

- A Developer's Guide to HTTP Cookies: [cookies - Mozilla | MDN](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/cookies)

- Introducing IndexedDB: [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)