Developer Programs

Learn

Docs

Stacked Views

Components > User Interaction > Stacked Views
Use this component to...
Display secondary activity in a separate view

Overview

Users occasionally need to initiate a secondary activity while working on a primary activity, without losing the work they’ve done in the primary activity. Once the user completes the secondary activity, they continue where they left off with the primary activity.

Diagram illustrating a primary activity and a secondary activity

This is similar to taking a second call while you’re already talking with someone else on the phone. You can put the first call on hold, take the second call until that’s ended, then return to the original call and pick up where you left off.

One important consideration when switching between primary and secondary activities is preserving unsaved changes. When the user initiates a secondary activity from a primary activity, they may have made changes as part of the primary activity that have not yet been saved to the database. For example, let’s say the user is filling out a form in your application. Each field in the form is initially in an unmodified state, showing some default value. As the user fills out the form, they modify the values in the form fields. These changes are not saved in the system until the user presses the form’s Save button. These unsaved changes cannot be lost if the user jumps from a primary to a secondary activity and back. When the application pushes to a secondary activity and eventually returns to the primary activity, it’s important that we preserve the user’s unsaved form changes as part of this transition.

While it’s relatively common for a user to initiate a secondary activity from a primary activity, it’s also possible for a user to access a third level of activity from a secondary activity, and so on.

Problematic approach: Displaying a secondary activity in a dialog box

Historically, the most popular approach for managing a secondary activity has been to display a dialog box above the primary activity. The main view is the primary activity, while the dialog box is the secondary activity. When the user closes the dialog box, it hides, and the user’s attention returns to the primary activity in the view.

Because the dialog box content is typically part of the view itself (initially hidden), the application doesn’t have to manually save and restore unsaved changes. If the view is a form and the user has made unsaved changes to the form, displaying and hiding a dialog box does not disturb the form’s state. This makes the dialog box approach relatively simple to code.

But the dialog box approach has several limitations:

  • All UI for the secondary activity is constrained to a relatively small window. This works well for trivial interactions, but if the dialog box interaction is complex — especially if it needs to display a scrollbar in order for the user to see all the data or it needs to dynamically change size to accommodate its content — this can become awkward for the user.
  • Because a dialog box has a fixed width and height, it can be confusing and even unusable at smaller screen widths, such as on mobile devices.
  • Responsive UI does not support multiple dialog boxes stacked on top of each other. If your scenario requires the user to access a third level of activity or higher, a dialog box won’t work.
  • Responsive UI also does not support navigation from a dialog box, either to another dialog box or to a separate view; this can be unexpected and disorienting for the user. When the user closes the dialog box, the expectation is that they will return to the primary activity and not navigate somewhere else.

Because of these limitations, Responsive UI only uses dialog boxes for trivial interactions.

Problematic approach: Navigating to a separate view for a secondary activity

Another approach for pushing from a primary to a secondary activity is to navigate from the primary activity’s view to a separate view for the secondary activity. The second view has one or more buttons – typically labeled Save, Cancel, Done, etc – that return the user to the primary activity.

This approach gives you plenty of room in which to display as much content as needed for the secondary activity, but it also has one big drawback. When the user navigates from one view to a completely separate view, the first view (the primary activity) is destroyed in memory. Because of this, in order to preserve the user’s unsaved changes, your application must manually persist all unsaved changes in a memory location other than the view when the user navigates to the second view, then manually restore those changes in the first view when the user returns back to it.

Preferred approach: Stacked views

So how do we handle non-trivial primary/secondary/higher activity interactions in Responsive UI? With stacked views.

A stacked view displays the secondary activity in a separate function view that sits above the first view. Rather than navigating to a separate view, the stacked view fully obscures the first view, appearing like any other regular view to the user.

The stacked view approach combines the best of both the dialog box and separate view approaches:

  • The stacked view looks and behaves like any other view, giving you the full screen width in which to display content, as well as a vertical scrollbar for the page. The user sees no difference between a stacked view and any other view.
  • While the stacked view visually obscures the first view, it does not replace the first view in memory. The first view is kept in place, hidden below the stacked view, which means that you do not have to manually save and restore unsaved changes, making this easier to code.
  • Because a stacked view looks and behaves like any other function view, it’s mobile friendly.
  • The content and code for the stacked view is a completely separate component from the primary view, allowing for a more modular development approach, helping to make your application easier to code, test, and maintain.
It’s important to note that stacked views do not replace your application’s primary navigation. Your users should continue to use your application’s primary navigation to move from view to view. Stacked views are only used when you need to stack a secondary (or higher) level of activity above a primary activity.

Buttons for Returning to the Previous Level

Each stacked view must include one or more buttons that return the user to the previous level:

  • If the stacked view can update the application’s state, then it should include at least one primary button to save the changes and one secondary button labeled Cancel to abandon the changes. Both buttons close the stacked view and return the user to the previous level.
  • If the stacked view does not update the application’s state, then it should include one primary button labeled Done that closes the stacked view and returns the user to the previous level.

Example

Let’s look at an example of pushing from a primary to a secondary activity, then pushing from a secondary to a third-level activity, and back.

In the view below, the user is ordering a product. This view is a primary activity. The form requires the user to enter a shipping address. The form also includes a “Select Shipping Address” button that allows the user to select one of their predefined addresses instead of manually having to type the address.

Example: primary activity

The user presses the “Select Shipping Address” button and the application displays the “Select Shipping Address” view as a stacked view. This is a secondary activity. The stacked view looks and acts like any other function view.

Example: secondary activity

This stacked view includes the following functionality:

  • The user can press the Select this Address button for any predefined address to use that address as the shipping address and return to the original order form. This is the primary action in the view. The application removes this stacked view when this happens.
  • The user can press the Cancel button to cancel the selection and return to the original order form. The application removes this stacked view when this happens.
  • The user can also click the Stacked Views breadcrumb at the top to cancel the selection and return to the original order form. The application removes this stacked view when this happens. Using breadcrumbs helps the user see where they are in the stacked interaction and is strongly recommended, but this is optional for you to use.
  • The user can press the Edit button to edit an address. When the user presses this button, the application pushes the new view as another stacked view. We show this next.
  • The user can press the Delete button to delete an address (not shown here).

Let’s say the user presses the “Edit” button in the secondary activity. The application then displays the “Edit Address” view as another stacked view. This is a third level activity.

Example: tertiary activity

This stacked view includes the following functionality:

  • The user can press the Save button to save the address update and return to the shipping address selection. The application removes this stacked view when this happens.
  • The user can press the Cancel button to cancel this activity and return to the shipping address selection. The application removes this stacked view when this happens.
  • The user can click the Select Shipping Address breadcrumb at the top to cancel this activity and return to the shipping address selection. The application removes this stacked view when this happens (confirming loss of changes if appropriate).
  • The user can click the Stacked Views breadcrumb at the top to cancel both this activity and the shipping address selection, returning all the way back to the original order form. The application removes both stacked views when this happens (confirming loss of changes if appropriate).
  • Using breadcrumbs helps the user see where they are in the stacked interaction and is strongly recommended, but this is optional for you to use.

After the user edits an address in the previous step, the application removes the “Add Shipping Address” stacked view and the user returns to the “Select Shipping Address” view. The application pops the user from the third level of activity back down to the secondary activity. We see the updated address in the list.

Example, after editing address

Let’s say the user selects the updated address by pressing the Select this Address button for it. The application removes the “Select Shipping Address” stacked view and returns the user to the primary activity (the form) with the selected shipping address filled in. This completes this 3-level interaction.

Example, after selecting an address

At each level of the interaction, we saw that the user had use of the full screen width in each view, with a vertical scrollbar displayed as needed (not shown here). This keeps the interaction simple and intuitive, making it appropriate for all screen sizes, including mobile.

One thing that the user doesn’t see here but is just as important, is that the application was able to retain all 3 levels of the interaction at once, passing data between each level, without having to save and restore data at any point.

Confirming Unsaved Changes

If a stacked view contains unsaved changes, your application must confirm loss of those changes in any of the following cases:

  • The user presses a Cancel button in the current stacked view.
  • The user clicks a breadcrumb to return to any level below the current stacked view.
  • The user attempts to navigate to a new view, using either the application navigation or the back button.

Depending on the circumstances, the user may have unsaved changes at the primary, secondary, or higher level of changes. In our previous example, if the user was in the middle of adding a new address, they could have unsaved changes at both the primary level (the main form) and at the 3rd level (Add Shipping Address). Your application should prompt the user if they want to lose unsaved changes before (a) removing a stacked view or (b) navigating away from the main view.


Development

Web component development

Implementation

Stacked views allow you to open additional view components over a base view in order to complete a complex secondary task while not leaving a primary task. For example, if the primary action is to create a new order, but the user needs to update a saved address during the process, they can do so without losing their place in the order process.

Setting up stacked views will allow the application to stack a maximum of two views on top of the current view.

To allow your current (or base) view to stack additional views, start by importing the JhaDynamicComponentLoaderService and initiating it in the constructor. You will also need to import the component that you want to stack on top of your base view. Initiate this inside the class so it can be referenced later.

Import the service
import { JhaDynamicComponentLoaderService } from '@jkhy/responsive-ui-angular/jha-responsive-core';
import { SelectAddressComponent } from './components/select-address/select-address.component';

@Component({
    ...
})
export class StackedViewsComponent {

    SelectAddressComponentClass = SelectAddressComponent;

    constructor(private dynamicComponentLoaderService: JhaDynamicComponentLoaderService) { }

}

When opening a stacked view, you can send along a data object to be shared between the base view, and any stacked views above it. In this example, addressData is our shared data object.

Create a function that will be used to stack a view on your base view. Call the addStackedView method in the JhaDynamicComponentLoaderService service. The addStackedView method takes 3 arguments: the component class itself, a unique name assigned to the component view and the data object. This addStackedView method will be called by an action in the view.

Code to add stacked views
export class StackedViewsComponent implements OnInit {

    SelectAddressComponentClass = SelectAddressComponent;
    public addressData: AddressData = new AddressData(1, 1, []);

    constructor(private dynamicComponentLoaderService: JhaDynamicComponentLoaderService) { }

    ngOnInit() {
        // Build out data object to share across views
        this.addressData.SelectedAddressId = 1;
        this.addressData.EditingAddressId = 1;
        this.addressData.AddressList = this.addressList;
    }

    // Add new stacked view
    public addStackedView(stackedComponent: any, stackedComponentName: string) {
        if (stackedComponent) {
            this.dynamicComponentLoaderService.addStackedView(stackedComponent, stackedComponentName, this.addressData);
        }
    }

}

To ensure any stacked views are removed and cleaned up if the user leaves the base view, create a function that calls the removeAllStackedViews method, and call it on ngOnDestroy.

Code to remove stacked views
export class StackedViewsComponent implements OnInit, OnDestroy {

    ...

    ngOnDestroy() {
        this.removeAllStackedViews();
    }

    ...

    // Close all stacked views
    public removeAllStackedViews() {
        this.dynamicComponentLoaderService.removeAllStackedViews();
    }

}

Within the markup, call the addStackedView method sending in the component class and a unique name for the component.

Trigger button HTML
<rui-button (rui-click)="addStackedView(SelectAddressComponentClass, 'SelectAddressComponent')" text="Select Existing Shipping Address"></rui-button>

The SelectAddressComponent is now loaded in a container above the base view. Inside of the stacked SelectAddressComponent view component, import the JhaDynamicComponentLoaderService service and add it to the constructor.

On ngOnInit of the component, subscribe to stackedViewData and populate your local data object with the object returned from the base view.

In this example, once the user has selected an address, remove the SelectAddressComponent stacked view. To do this, create a function to call removeStackedView, sending along the unique name of the view (’SelectAddressComponent’) and the updated address data object.

Stacked views example
import { JhaDynamicComponentLoaderService } from '@jkhy/responsive-ui-angular/jha-responsive-core';

@Component({
    ...
})
export class SelectAddressComponent implements OnInit {

    constructor(private dynamicComponentLoaderService: JhaDynamicComponentLoaderService) { }

    ngOnInit() {

        // Subscribe to stacked view data service
        this.dynamicComponentLoaderService.stackedViewData.subscribe(data => {
            if (data) {
                this.addressList = data.AddressList;
                this.addressData = new AddressData(data.SelectedAddressId, data.EditingAddressId, data.AddressList);
            }
        });

    }

    // Select a new address, update address data and remove view
    public selectAddress(addressId: any) {
        this.addressData.SelectedAddressId = addressId;
        this.removeStackedView(this.addressData);
    }

    // Once an address has been selected, call the service to remove the view and send the updated address
    private removeStackedView(address?: any) {
        this.dynamicComponentLoaderService.removeStackedView('SelectAddressComponent', address);
    }

}

In the view, when the user selects an address, the click event on the rui-button will call the selectAddress function and send in an address id, removing the stacked view and sending the updated data to the stackedViewData observable.

Trigger button click handling
<rui-button text="Select this Address" buttonStyle="primary" class="button-with-separator" (rui-click)="selectAddress(address.Id)"></rui-button>

Back in the base page, within the ngAfterViewInit lifecycle hook, subscribe to the stackedViewData observable and update the local data object with the data returned.

Subscribing to service
export class StackedViewsComponent implements OnInit, AfterViewInit, OnDestroy {

    SelectAddressComponentClass = SelectAddressComponent;
    public addressData: AddressData = new AddressData(1, 1, []);

    constructor(private dynamicComponentLoaderService: JhaDynamicComponentLoaderService) { }

    ngOnInit() {
        // Build out data object to share across views
        this.addressData.SelectedAddressId = 1;
        this.addressData.EditingAddressId = 1;
        this.addressData.AddressList = this.addressList;
    }

    ngAfterViewInit(): void {
        // Subscribe to stacked view data service
        this.dynamicComponentLoaderService.stackedViewData.subscribe(data => {
            if (data) {
                this.addressData = new AddressData(data.SelectedAddressId, data.EditingAddressId, data.AddressList);

                const filteredAddressList = data.AddressList.filter((address) => address.Id === data.SelectedAddressId);
                if (filteredAddressList.length > 0) {
                    this.editForm.controls['shippingName'].setValue(filteredAddressList[0].Name);
                    this.editForm.controls['shippingStreet'].setValue(filteredAddressList[0].Street);
                    this.editForm.controls['shippingCity'].setValue(filteredAddressList[0].City);
                    this.editForm.controls['shippingState'].setValue(filteredAddressList[0].State);
                    this.editForm.controls['shippingZip'].setValue(filteredAddressList[0].ZipCode);
                }
            }
        });
    }

    ngOnDestroy() {
        this.removeAllStackedViews();
    }

    // Add new stacked view
    public addStackedView(stackedComponent: any, stackedComponentName: string) {
        if (stackedComponent) {
            this.dynamicComponentLoaderService.addStackedView(stackedComponent, stackedComponentName, this.addressData);
        }
    }

    // Close all stacked views
    public removeAllStackedViews() {
        this.dynamicComponentLoaderService.removeAllStackedViews();
    }

}

Adding Unsaved Changes

Start setting up unsaved changes support by adding the guard to your base page route, adding the JhaUnsavedChangesService service and adding canDeactivate() to your component class.

In the canDeactivate function, call the stackedViewLevel method in the JhaDynamicComponentLoaderService to see if any views are currently stacked on the base page. If there are stacked views (stackLevel is greater than zero), call the checkForUnsavedChanges method to see if any of the stacked views currently have unsaved changes.

If either of the stacked views, or the base page have unsaved changes, return promptUnsavedChangesDialog sending along the stack level. We’ll construct the promptUnsavedChangesDialog function in the next step.

Support for unsaved changes
public async canDeactivate(): Promise<boolean> {
    // Test if stacked views are opened
    return this.dynamicComponentLoaderService.stackedViewLevel().then((stackedLevel) => {
        if (stackedLevel > 0) {
            // If stacked views exists, check if any have unsaved changes
            return this.dynamicComponentLoaderService.checkForUnsavedChanges().then((unsavedChanges) => {
                if (!unsavedChanges && !this.editForm.dirty) {
                    return true;
                } else {
                    return this.promptUnsavedChangesDialog(stackedLevel);
                }
            });
        // Otherwise, test if base page's form is dirty
        } else {
            // If base page form is dirty, prompt user for action
            if (this.editForm.dirty) {
                return this.promptUnsavedChangesDialog(stackedLevel);
            // Otherwise return true and continue with route changes
            } else {
                return true;
            }
        }
    });
}

Create a promptUnsavedChangesDialog function that accepts a stackLevel number. Return the promptUnsavedChangesDialog method from the unsavedChangesService service.

If the user selects Save, call the saveAndCloseStackedViews method which will trigger a save in any open stacked view starting with the top view. Once that promise is returned, save any changes on the base page and store all changes permanently.

If the user selects Lose Changes (returned as DontSave), return true so the route change can continue. If the user selects Continue Editing, return false to cancel the route change and remove the unsaved changes dialog.

Responding to unsaved changes dialog result
public async promptUnsavedChangesDialog(stackLevel: number): Promise<boolean> {
    if (stackLevel > 0) {
        this.viewsStacked = true;
    } else {
        this.viewsStacked = false;
    }
    
    const promptResult =  this.unsavedChangesService.promptUnsavedChangesDialog();
    
    let promptUserResult = await lastValueFrom(promptResult);
    
    if (promptUserResult === 'Save') {
        //  If the views are stacked, run service method to save and close all stacked views
        if (this.viewsStacked) {
            const saveAndClose =  this.dynamicComponentLoaderService.saveAndCloseStackedViews();
    
            let saveAndCloseResult = await lastValueFrom(saveAndClose);
    
            if (saveAndCloseResult) {
                // Stacked views changes saved
                // Save all changes in database, then return true
                return true;
            }
        // Otherwise just save changes to base page and continue with route change
        } else {
            // Save changes to base page in database, then return true
            return true;
        }
    } else if (promptUserResult === 'DontSave') {
        // Losing changes
        return true;
    } else {
        // Not leaving page
        return false;
    }
}

In ngOnInit of the stacked view page, subscribe to the stackedViewAction method of the JhaDynamicComponentLoaderService service. Take the result of that subscription and send it to a processMessage function, sending the result.

Responding to stacked view action service
ngOnInit() {
    // Subscribe to stacked view action service
    this.stackedViewActionSubscription = this.dynamicComponentLoaderService.stackedViewAction.subscribe(action => {
        if (action) {
            this.processMessage(action);
        }
    });
}

Create a processMessage function to handle the actions coming from the stackedViewAction service. Test the stackedViewName to see if it matches the current stacked view unique name. If it does, test the stackedViewAction for either JHA_STACKEDVIEWS_UNSAVEDCHANGES or JHA_STACKEDVIEWS_SAVEANDCLOSEVIEW.

  • If the action is JHA_STACKEDVIEWS_UNSAVEDCHANGES, run the stackedViewCallback with either true or false, depending on whether this stacked view has unsaved changes.
  • If the action is JHA_STACKEDVIEWS_SAVEANDCLOSEVIEW, save any unsaved changes and remove this stacked view before running the stackedViewCallback with true.
Responding to service
private processMessage(stackedViewAction: any) {
    if (stackedViewAction.stackedViewPayload.stackedViewName === 'EditAddressComponent') {
        if (stackedViewAction.stackedViewPayload.stackedViewAction === 'JHA_STACKEDVIEWS_UNSAVEDCHANGES') {
            if (this.editForm.dirty) {
                this.formIsDirty = true;
            } else {
                this.formIsDirty = false;
            }
            stackedViewAction.stackedViewPayload.stackedViewCallback(this.formIsDirty);
        } else if (stackedViewAction.stackedViewPayload.stackedViewAction === 'JHA_STACKEDVIEWS_SAVEANDCLOSEVIEW') {
            if (stackedViewAction.stackedViewPayload.stackedViewName === 'EditAddressComponent') {
                // Save Address information and update this.addressData
                this.removeStackedView(this.addressData);
                stackedViewAction.stackedViewPayload.stackedViewCallback(true);
            }
        }
    }
}

// Remove this view in the stack
private removeStackedView(data?: any): void {
    this.dynamicComponentLoaderService.removeStackedView('EditAddressComponent', data);
}

Make sure to unsubscribe from the stackedViewData and stackedViewAction subscriptions on destroy of the class.

Unsubscribing from services
ngOnDestroy() {
    this.stackedViewDataSubscription.unsubscribe();
    this.stackedViewActionSubscription.unsubscribe();
}

Angular wrapper development

Implementation

Stacked views allow you to open additional view components over a base view in order to complete a complex secondary task while not leaving a primary task. For example, if the primary action is to create a new order, but the user needs to update a saved address during the process, they can do so without losing their place in the order process.

Setting up stacked views will allow the application to stack a maximum of two views on top of the current view.

To allow your current (or base) view to stack additional views, start by importing the JhaDynamicComponentLoaderService and initiating it in the constructor. You will also need to import the component that you want to stack on top of your base view. Initiate this inside the class so it can be referenced later.

Import the service
import { JhaDynamicComponentLoaderService } from '@jkhy/responsive-ui-angular/jha-responsive-core';
import { SelectAddressComponent } from './components/select-address/select-address.component';

@Component({
    ...
})
export class StackedViewsComponent {

    SelectAddressComponentClass = SelectAddressComponent;

    constructor(private dynamicComponentLoaderService: JhaDynamicComponentLoaderService) { }

}

When opening a stacked view, you can send along a data object to be shared between the base view, and any stacked views above it. In this example, addressData is our shared data object.

Create a function that will be used to stack a view on your base view. Call the addStackedView method in the JhaDynamicComponentLoaderService service. The addStackedView method takes 3 arguments: the component class itself, a unique name assigned to the component view and the data object. This addStackedView method will be called by an action in the view.

Code to add stacked views
export class StackedViewsComponent implements OnInit {

    SelectAddressComponentClass = SelectAddressComponent;
    public addressData: AddressData = new AddressData(1, 1, []);

    constructor(private dynamicComponentLoaderService: JhaDynamicComponentLoaderService) { }

    ngOnInit() {
        // Build out data object to share across views
        this.addressData.SelectedAddressId = 1;
        this.addressData.EditingAddressId = 1;
        this.addressData.AddressList = this.addressList;
    }

    // Add new stacked view
    public addStackedView(stackedComponent: any, stackedComponentName: string) {
        if (stackedComponent) {
            this.dynamicComponentLoaderService.addStackedView(stackedComponent, stackedComponentName, this.addressData);
        }
    }

}

To ensure any stacked views are removed and cleaned up if the user leaves the base view, create a function that calls the removeAllStackedViews method, and call it on ngOnDestroy.

Code to remove stacked views
export class StackedViewsComponent implements OnInit, OnDestroy {

    ...

    ngOnDestroy() {
        this.removeAllStackedViews();
    }

    ...

    // Close all stacked views
    public removeAllStackedViews() {
        this.dynamicComponentLoaderService.removeAllStackedViews();
    }

}

Within the markup, call the addStackedView method sending in the component class and a unique name for the component.

Trigger button HTML
<jha-button (click)="addStackedView(SelectAddressComponentClass, 'SelectAddressComponent')" jhaText="Select Existing Shipping Address"></jha-button>

The SelectAddressComponent is now loaded in a container above the base view. Inside of the stacked SelectAddressComponent view component, import the JhaDynamicComponentLoaderService service and add it to the constructor.

On ngOnInit of the component, subscribe to stackedViewData and populate your local data object with the object returned from the base view.

In this example, once the user has selected an address, remove the SelectAddressComponent stacked view. To do this, create a function to call removeStackedView, sending along the unique name of the view (’SelectAddressComponent’) and the updated address data object.

Stacked views example
import { JhaDynamicComponentLoaderService } from '@jkhy/responsive-ui-angular/jha-responsive-core';

@Component({
    ...
})
export class SelectAddressComponent implements OnInit {

    constructor(private dynamicComponentLoaderService: JhaDynamicComponentLoaderService) { }

    ngOnInit() {

        // Subscribe to stacked view data service
        this.dynamicComponentLoaderService.stackedViewData.subscribe(data => {
            if (data) {
                this.addressList = data.AddressList;
                this.addressData = new AddressData(data.SelectedAddressId, data.EditingAddressId, data.AddressList);
            }
        });

    }

    // Select a new address, update address data and remove view
    public selectAddress(addressId: any) {
        this.addressData.SelectedAddressId = addressId;
        this.removeStackedView(this.addressData);
    }

    // Once an address has been selected, call the service to remove the view and send the updated address
    private removeStackedView(address?: any) {
        this.dynamicComponentLoaderService.removeStackedView('SelectAddressComponent', address);
    }

}

In the view, when the user selects an address, the click event on the jha-button will call the selectAddress function and send in an address id, removing the stacked view and sending the updated data to the stackedViewData observable.

Trigger button click handling
<jha-button jhaText="Select this Address" jhaButtonStyle="Primary" class="button-with-separator" (jhaClick)="selectAddress(address.Id)"></jha-button>

Back in the base page, within the ngAfterViewInit lifecycle hook, subscribe to the stackedViewData observable and update the local data object with the data returned.

Subscribing to service
export class StackedViewsComponent implements OnInit, AfterViewInit, OnDestroy {

    SelectAddressComponentClass = SelectAddressComponent;
    public addressData: AddressData = new AddressData(1, 1, []);

    constructor(private dynamicComponentLoaderService: JhaDynamicComponentLoaderService) { }

    ngOnInit() {
        // Build out data object to share across views
        this.addressData.SelectedAddressId = 1;
        this.addressData.EditingAddressId = 1;
        this.addressData.AddressList = this.addressList;
    }

    ngAfterViewInit(): void {
        // Subscribe to stacked view data service
        this.dynamicComponentLoaderService.stackedViewData.subscribe(data => {
            if (data) {
                this.addressData = new AddressData(data.SelectedAddressId, data.EditingAddressId, data.AddressList);

                const filteredAddressList = data.AddressList.filter((address) => address.Id === data.SelectedAddressId);
                if (filteredAddressList.length > 0) {
                    this.editForm.controls['shippingName'].setValue(filteredAddressList[0].Name);
                    this.editForm.controls['shippingStreet'].setValue(filteredAddressList[0].Street);
                    this.editForm.controls['shippingCity'].setValue(filteredAddressList[0].City);
                    this.editForm.controls['shippingState'].setValue(filteredAddressList[0].State);
                    this.editForm.controls['shippingZip'].setValue(filteredAddressList[0].ZipCode);
                }
            }
        });
    }

    ngOnDestroy() {
        this.removeAllStackedViews();
    }

    // Add new stacked view
    public addStackedView(stackedComponent: any, stackedComponentName: string) {
        if (stackedComponent) {
            this.dynamicComponentLoaderService.addStackedView(stackedComponent, stackedComponentName, this.addressData);
        }
    }

    // Close all stacked views
    public removeAllStackedViews() {
        this.dynamicComponentLoaderService.removeAllStackedViews();
    }

}

Adding Unsaved Changes

Start setting up unsaved changes support by adding the guard to your base page route, adding the JhaUnsavedChangesService service and adding canDeactivate() to your component class.

In the canDeactivate function, call the stackedViewLevel method in the JhaDynamicComponentLoaderService to see if any views are currently stacked on the base page. If there are stacked views (stackLevel is greater than zero), call the checkForUnsavedChanges method to see if any of the stacked views currently have unsaved changes.

If either of the stacked views, or the base page have unsaved changes, return promptUnsavedChangesDialog sending along the stack level. We’ll construct the promptUnsavedChangesDialog function in the next step.

Support for unsaved changes
public async canDeactivate(): Promise<boolean> {
    // Test if stacked views are opened
    return this.dynamicComponentLoaderService.stackedViewLevel().then((stackedLevel) => {
        if (stackedLevel > 0) {
            // If stacked views exists, check if any have unsaved changes
            return this.dynamicComponentLoaderService.checkForUnsavedChanges().then((unsavedChanges) => {
                if (!unsavedChanges && !this.editForm.dirty) {
                    return true;
                } else {
                    return this.promptUnsavedChangesDialog(stackedLevel);
                }
            });
        // Otherwise, test if base page's form is dirty
        } else {
            // If base page form is dirty, prompt user for action
            if (this.editForm.dirty) {
                return this.promptUnsavedChangesDialog(stackedLevel);
            // Otherwise return true and continue with route changes
            } else {
                return true;
            }
        }
    });
}

Create a promptUnsavedChangesDialog function that accepts a stackLevel number. Return the promptUnsavedChangesDialog method from the unsavedChangesService service.

If the user selects Save, call the saveAndCloseStackedViews method which will trigger a save in any open stacked view starting with the top view. Once that promise is returned, save any changes on the base page and store all changes permanently.

If the user selects Lose Changes (returned as DontSave), return true so the route change can continue. If the user selects Continue Editing, return false to cancel the route change and remove the unsaved changes dialog.

Responding to unsaved changes dialog result
public async promptUnsavedChangesDialog(stackLevel: number): Promise<boolean> {
    if (stackLevel > 0) {
        this.viewsStacked = true;
    } else {
        this.viewsStacked = false;
    }
    
    const promptResult =  this.unsavedChangesService.promptUnsavedChangesDialog();
    
    let promptUserResult = await lastValueFrom(promptResult);
    
    if (promptUserResult === 'Save') {
        //  If the views are stacked, run service method to save and close all stacked views
        if (this.viewsStacked) {
            const saveAndClose =  this.dynamicComponentLoaderService.saveAndCloseStackedViews();
    
            let saveAndCloseResult = await lastValueFrom(saveAndClose);
    
            if (saveAndCloseResult) {
                // Stacked views changes saved
                // Save all changes in database, then return true
                return true;
            }
        // Otherwise just save changes to base page and continue with route change
        } else {
            // Save changes to base page in database, then return true
            return true;
        }
    } else if (promptUserResult === 'DontSave') {
        // Losing changes
        return true;
    } else {
        // Not leaving page
        return false;
    }
}

In ngOnInit of the stacked view page, subscribe to the stackedViewAction method of the JhaDynamicComponentLoaderService service. Take the result of that subscription and send it to a processMessage function, sending the result.

Responding to stacked view action service
ngOnInit() {
    // Subscribe to stacked view action service
    this.stackedViewActionSubscription = this.dynamicComponentLoaderService.stackedViewAction.subscribe(action => {
        if (action) {
            this.processMessage(action);
        }
    });
}

Create a processMessage function to handle the actions coming from the stackedViewAction service. Test the stackedViewName to see if it matches the current stacked view unique name. If it does, test the stackedViewAction for either JHA_STACKEDVIEWS_UNSAVEDCHANGES or JHA_STACKEDVIEWS_SAVEANDCLOSEVIEW.

  • If the action is JHA_STACKEDVIEWS_UNSAVEDCHANGES, run the stackedViewCallback with either true or false, depending on whether this stacked view has unsaved changes.
  • If the action is JHA_STACKEDVIEWS_SAVEANDCLOSEVIEW, save any unsaved changes and remove this stacked view before running the stackedViewCallback with true.
Responding to service
private processMessage(stackedViewAction: any) {
    if (stackedViewAction.stackedViewPayload.stackedViewName === 'EditAddressComponent') {
        if (stackedViewAction.stackedViewPayload.stackedViewAction === 'JHA_STACKEDVIEWS_UNSAVEDCHANGES') {
            if (this.editForm.dirty) {
                this.formIsDirty = true;
            } else {
                this.formIsDirty = false;
            }
            stackedViewAction.stackedViewPayload.stackedViewCallback(this.formIsDirty);
        } else if (stackedViewAction.stackedViewPayload.stackedViewAction === 'JHA_STACKEDVIEWS_SAVEANDCLOSEVIEW') {
            if (stackedViewAction.stackedViewPayload.stackedViewName === 'EditAddressComponent') {
                // Save Address information and update this.addressData
                this.removeStackedView(this.addressData);
                stackedViewAction.stackedViewPayload.stackedViewCallback(true);
            }
        }
    }
}

// Remove this view in the stack
private removeStackedView(data?: any): void {
    this.dynamicComponentLoaderService.removeStackedView('EditAddressComponent', data);
}

Make sure to unsubscribe from the stackedViewData and stackedViewAction subscriptions on destroy of the class.

Unsubscribing from services
ngOnDestroy() {
    this.stackedViewDataSubscription.unsubscribe();
    this.stackedViewActionSubscription.unsubscribe();
}

Design

Stacked views look exactly like any other function view, so there is are no specific design elements for them.

Each stacked view must include a primary button that closes it, which returns the user to the view below the stacked view being closed.


Support options
Have questions on this topic?
Join the Responsive UI team in Microsoft Teams to connect with the community.
See something in this page that needs to change?
Send us feedback on this page.
Last updated Mon May 1 2023