Supporting multiple simultaneous "screens" in dynamic Tabs inside one Ember application

We are new to Ember and have an ambitious requirement for an Ember 2.0 app: We need to support the concept of multiple open screens inside the application where each screen is opened from a navbar menu into a dynamic tab control on the home page.

An example of a screen might be to create a new Order where the user can create multiple new Orders simultaneously where each order will appear inside of a new tab. Those Orders can be edited separately and separately saved and processed or even closed (closing that particular Order screen/tab). A Client might also be opened inside of another tab for editing while editing an Order in another tab. Each screen will have it’s own Model instance.

We are trying to work out architecturally how to structure such an application:

  • have a home controller and template that displays the navbar menu and dynamic screens tab control
  • represent each “screen” as a component
  • clicking a navbar menu item would dynamically add a tab to the tab control and display the relevant “screen” component inside of the tab-pane for the new tab
  • load the model for each “screen” inside the didInsertElement() function of each screen component

The above sort of works but as we continue to build it out we are running into routing problems as each tab needs to be able to be bookmarked (e.g. link emailed to Order #1234 - clicking on such a link will open the app with a single tab displaying the required Order). Switching between the tabs should allow the browser back/forward button to be used to retrace the navigation.

Closing a tab by clicking the (x) button on the right corner of each tab should check whether the contained screen and model are dirty and display a warning message if so.

Looking for some architectural ideas / patterns on how best to do this in Ember 2.0 world. I realize our requirement is a bit advanced for new Ember enthusiasts, but that is a business requirement.

Thanks in advance

+1 I am also looking for same dynamic tabbed interface

Haven’t really implemented anything like this before, but to throw out a thought: what if rather than thinking as if all of the tabs were “open” at the same time (with only one being visible), the tabs were instead just links to different routes within the application?

Then, your tabs could just be an array of links/transitions/urls kept within an application service. Because you’d want to know wether the underlying model is dirty if you try to close one of those tabs, you’d also want to keep your service up to date with which of the tabs are dirty or not.

So something along the lines of this:

// services/application.js
import Ember from 'ember';
export default Ember.Service.extend({
  openTabs: Ember.A(),
  addTab(url, isDirty) {
    this.get('openTabs').pushObject({
      url: url,
      isDirty: isDirty
    });
  },
  closeTab(url) {
    let tab = this.get('openTabs').findBy('url', url);
    if(tab.isDirty) {
      alert('The tab you are closing is unsaved!');
    }
    this.get('openTabs').removeObject(tab);
  }
});

Then, you could create a helper like {{link-to-new-tab}} which would save the current route in a tab by calling addTab and then transition to the new route after that.

Thoughts?

@Spencer_Price - thanks for the input. The concept of using a Service to keep track of the “open screens/tabs” is a good one and we managed to make this work.

However, there is one downside that we will have to overcome in order for this to be viable and that is that we would like to maintain the state between screens/tabs as you work on them. With the current implementation the screen would always be reloaded when when you click on a tab to activate a route.

We are thinking about perhaps saving the state of a screen in local storage before switching routes, but that is a bit cumbersome and does not elegantly take care of data displayed in data grids that a reloaded from the server for example.

is there anything built into Ember that we may have missed that will allow us to cache a screen/tab when switching to another screen/tab and to then restore it when switching back?

Thanks in advance

Are you using Ember data? Once of the newer features that it has is some control, in your adapter, to manage wether records should reload in the background. So, all of the models you have are stored locally in the Ember Data Store — when you return to a tab, those records would not need to be reloaded (unless you’ve determined, through your Adapter, that they should be). I think that would probably be the cleanest solution.

Yes we are using Ember Data so that might work for the models as you suggested so thanks for that tip.

However, there are a number of transient data properties on a screen, for example, the currently selected search criteria, currently selected search text, currently selected search filter, etc and then we use the Kendo Grid for displaying data grids that are populated directly from an OData backend (to make sorting/filtering easier). So there is going to be no easy way to keep the state for all those transient properties or data.

Perhaps there is a way to simply show and hide a screen where we keep the screen hidden whilst not selected…

Actually, just thinking about this some more, Ember Data automatically keeping the state of the models might work for existing models loaded for editing, but will not work for newly created models that have not been saved as yet, as the screen will simply not know which new model to load as all new models (not saved as yet) will have a PK of 0.

Unless we somehow allocate a PK or some other unique key for each of the newly created models and update the route with that so we can find it next time, but then it starts getting messy.

As stated above, showing and hiding might after all be the best way to treat the screens…

The approach you’re describing would have me concerned for one critical reason: what happens if your user refreshes and expects all of their tabs to still be there?

Using Query Parameters might answer a good number of your questions about some of the transient properties (search text, criteria, etc) as those are designed for precisely that purpose.

To answer your latter point about unsaved models — I agree that there’s not necessarily an ‘out of the box’ solution for that. But, you can have an “unsaved models” service, and a query parameter that looks up that model so it can be loaded cleanly in your model hook. Something like this:

// ??/route.js
export default Ember.Route.extend({
  unsavedModels: Ember.inject.service(),
  model(params) {
    let id = params.queryParams.unsavedId;
    let model;

    if(id && model = this.get('unsavedModels').fetch(id)) {
      return model;
    } else {
      return this.store.createRecord('model-type');
    }
  },
  actions: {
    saveAsTab() {
      let model = this.currentModel;
      let id;

      if(model.get('isDirty')) {
        id = this.get('unsavedModels'). addModel(this.currentModel);
      }
      // Stuff to save this data/tab to the tabs service
    }
  }
});
// services/unsaved-model.js
export default Ember.Service.extend({
  fetch(id) {
    // Find unsaved model from Map or something
    return model;
  },
  addModel(model) {
    // Load this model into the Map & return it's ID
    return id;
  }
});

Thanks for all your input, it has greatly helped us.

I understand your concern with the user pressing F5 when multiple tabs are open and this project is a bit of an experiment in a new ember framework for us, but the business requirements demand the multiple tab approach to allow real time communication with the server in each of those tabs without using up all the available connections in a browser in the case where you open multiple browser tabs instead.

In the end we went with a combination of:

  1. a single route
  2. service to keep track of all the open/active tabs
  3. the removeScreen() method on the service will check whether the screen.model is dirty and warn the user if required
  4. each tab contents (aka screen) is a component and takes care of it’s own model loading and saving
  5. a set of query parameters: screen, p0, p1, p2, etc to allow bookmarking a specific screen/tab

This seems so far to work well with the caveat that F5 will lose the tabs of course. I will post back again once we get some user feedback.

Thanks again

Awesome! You’re going down a path I haven’t…so, any learnings and/or feedback you have, please share! (This is something I imagine our product may need in the future).

You can easily persist the tabs to localStorage, storing the dirty models is probably a whole other issue though.

The first thing that comes to mind to easily tackle this would be using the localStorage adapter:

https://github.com/kurko/ember-localstorage-adapter

The biggest issue is the storage limit though. It sounds like with using kendo and a lot of grid based data it would be possible to hit that limit rather quickly

+1 again, if some one have any solution for this