Example of navigation from "item list page" to "item details page"

Hi all!

We have been struggling a little bit until we found a way to implement an "item list page" -> "item details page" pattern that we feel confortable with, so hope it helps out other people or it is useful for the Retool team as feedback.

Please, take into account that I am sharing a lot of assumptions made reading the docs and forum posts, but also just "playing the piano" with Retool, so maybe I am wrong in some of them :grimacing:

Feel free to reply with improvement suggestions and feedback to keep iterating :blush:

Thanks!

:open_file_folder: Item list page

We have an application to manage certain type of items (companies) structured this way in terms of navigation:

We have a main appContainer component with different views, being the "Manage companies" the default one. This view contains the main table with the list of companies.

The appContainer navigation component is hidden as you can see in the screenshot because it does not make sense to navigate to an empty "Company details" view without selecting a row from the table, and we prefer to add a clear primary call to action for creating new resources as shown with the "Create company" button.

Therefore, that main table has a Click row Event handler to navigate to the company details view:

That Event handler triggers the following navigateToCompanyDetails.js JS query (we call "section" to what Retool calls "view" in order to propagate it in the URL):

const appId = retoolContext.appUuid;
const sectionTo = "company_details";

utils.openApp(
  appId,
  {
    queryParams: {
      section: sectionTo,
      company_id: companyId
    },
    newTab: false
  }
);

// The previous openApp function only changes the URL but does not modify the `urlParams` in order to load the specific Company details in the destination URL, so we have to do it manually:
localStorage.setValue("company_id", companyId);
appContainer.setCurrentView(sectionTo);

:sparkles: Learning #1: Decouple from the UI

Note that the companyId is an Additional Scope variable as shown in the previous screenshot.

We have done so to allow calling this JS query from different parts of the application, and to decouple from the UI component ID to make it easier if they are upgraded (as recently done with the table component).

This way if we have to manually create a new table based on the new component as recently happened, we do not have to go over all our JS queries modifying the component ID.

That pattern also maintains consistency with other JS queries that could be more complex due to start acquiring business logic. In this other scenario it turns out to be even more critical for us to decouple the JS query from the Retool UI in order to be able to reuse them, and also make it easy for a possible future scenario where we can just grab these JS snippets and paste them as pure functions in our own HTTP API for instance.

:sparkles: Learning #2: queryParams better than hashParams

I have to admit that we initially went for the alternative of storing these kind of things such as the section and company_id in the hashParams. I think that having the Retool Application Settings specifying the hashParams as something that you can map to components and so on, made us think that "this is the Retool way of doing these things, so lets embrace it".

Later we learned that for sure we can use urlParams as with any other web application, so we changed it due to feeling it more HTTP standard compliant :blush:.

:sparkles: Learning #3: utils.openApp does not modify the urlParams variable

As you can see in the JS snippet, we are having to store the companyId in the Local Storage in order to be able to recover it from the Company Details view.

This is already explained in the snippet code comment. It would be definitely awesome to save having to do so updating the urlParams underneath once calling the openApp function, but I did not find a better approach rather than this or Retool variables.

:page_facing_up: Item details page

The main appContainer component has a Change Event handler configured in order to trigger a loadCompanyDetailsPage.js JS query if we load that view/section, and a Default view that depends on the urlparams as we will see later. It looks as:

This Event handler finally triggers the loadCompanyDetailsPage.js JS query in order to populate the details of that company such as its licenses.

Remember that calling to utils.openApp does not store the specified queryParams in the urlparams, so here we need to do so by prioritizing the company_id from the Local Storage for in-app navigation, and defaulting to the one in the URL in case of directly linking to a specific company instead of to the companies list with a URL like https://xxx.retool.com/app/companies?section=company_details&company_id=yyyy:

const companyId = localStorage.values.company_id || urlparams.company_id;

const company = await companies__select_id.trigger({
  additionalScope: { companyId }
});

appTitle.setValue(`# 🧑‍🏭 ${company.legal_name[0]}`);

const licenses = await licenses__search_by_company_id.trigger({
  additionalScope: { companyId }
});

return licenses;

:thinking: Why not multiple apps

This could be an obvious question and we thought about it for some time. Even nowadays, having to for instance update the appTitle from the JS query feels a little hacky or not embracing "the Retool way of structuring app and pages".

At least in our case, we understand Retool apps as different containers for different purposes or entities. That is, one app for managing customers, other for invoices, other for emails, and so on (for example).

That is because having to switch between different apps just because of wanting to take a quick look to a very specific item (a company for instance) from the current list of these kind of items (companies list), feels a little overwhelming because as far as we understand, it implies:

  • Reloading the whole page
  • Loading new assets
  • Not being able to share JS queries that have a very high potential of being reused. We would like to perform the 100% of actions available as Row actions in the item list table also from the item details page.
    • We can do so with shared modules or exporting the JS query, however, it feels exposing too much in our global shared elements just because 2 very related pages need them. That is, this would open the door of reusing them or even being tempted of abstracting them for being used from other apps making them unnecessarily too complex.

Which kind of approaches do you have for this common UI/UX pattern? What do you think about this one?

Thanks!