SplitView migration guide

Since the inception of the new angular backend, the splitView has been the go-to solution when we needed a way to facilitate navigation between a number of related views. Over time, this way of navigation handling has become bloated and increasingly difficult to implement and maintain. With the new angular based routing we created a lightweight and easy to implement solution to replace it. This guide will provide you with step-by-step instructions on replacing old splitView routing with our new angular routing solution.

Step by step to angular routing

1. As a first step, we take a look at the routing config. Since there are no routes available to lead us past the overview (filter + table) yet, we need to define additional routes for those views previously reachable through the splitView. Create a new file *.routing.ts (next to your module and/or entry component) which exports a constant defining all the routes and register those routes by assigning it to the children property of the base route of your UI.

{
        path:        'settings/properties/configuration',
        component:   PropertiesComponent,
        data:        {
           permissions: {
               key:       'settings_properties',
               namespace: 'GuiConfiguration'
           },
           label:       'properties.configuration',
           isHidden:    true
        },
        children: propertyConfigurationRoutes,
        canActivate: GUARDS
},
  
export const propertyConfigurationRoutes:TerraRoutes = [
   {
       path:       '',
       pathMatch:  'full',
       redirectTo: 'overview',
       data:       {
           label:       'properties.modules.filters',
           permissions: {
               isStatic:  true,
               key:       'settings_properties',
               namespace: 'GuiConfiguration'
           },
           isHidden:    true
       }
   },
   {
       path:      'overview',
       component: PropertiesOverviewComponent,
       data:      {
           label:       'properties.modules.table',
           permissions: {
               key:       'settings_properties',
               namespace: 'GuiConfiguration'
           },
           isHidden:    true
       }
   },
   {
       path:      'overview/new',
       component: PropertiesAddViewComponent,
       data:      {
           label:       'properties.modules.createProperty',
           permissions: {
               key:       'settings_properties',
               namespace: 'GuiConfiguration'
           },
           isHidden:    true
       },
       resolve: {
           destinations: PropertyDestinationsResolver
       }
   },
   {
       path:       'overview/:propertyId',
       pathMatch:  'full',
       redirectTo: 'overview/:propertyId/general',
       data:       {
           label:       (translation:TranslationService, params:Params, data:Data):string =>
                        {
                            let lang:string = localStorage.getItem('plentymarkets_lang_');
                            return SystemPropertyHelper.getPropertyNameWithId(data.property as PropertyInterface, lang);
                        },
           permissions: {
               key:       'settings_properties',
               namespace: 'GuiConfiguration'
           },
           isHidden:    true
       },
       resolve:    {
           property: PropertyResolver
       }
   }
  
  • permissions, gwt, is Hidden etc.
  • flat routing

2. For our ‘entry-point’ or base route, we adjust the entry component so that it holds our breadcrumbs and router-outlet. E.g. properties.component.html

    <terra-breadcrumbs></terra-breadcrumbs>
    <div class="outlet-container">
       <router-outlet></router-outlet>
    </div>
  

See https://angular.io/api/router/RouterOutlet for more information about the router-outlet.

3. Instead of directly routing to the old components, we create some new view-components which take care of things like asynchronous data loading, breadcrumbs handling and routing.

What is async?

https://angular.io/api/common/AsyncPipe

Why View-Components?

View-Components are used to allow usual components to work in a specific context. Components are supposed to only display data and contain a small amount of logic that is directly related to the data displayed, such as saving the shown data. View-Components on the contrary can handle specific tasks that depend on the context in which the component is used. For example, you should be able to use an address-component in various places in plentymarkets to display or edit address data. But when it comes to routing, you may want to navigate to different URLs when the address is saved or deleted. This is what the View-Component can take care of, what the component itself is not supposed to do.

4. Now we need to remove all the references to the old splitView handling from our components and replace them by code that uses our new routing approach.

a. SplitView-Params become Inputs.

b. Instead of adding views to our splitConfig, we navigate to the Views routes directly.

c. removeView becomes closeBreadcrumb/reset component data.

    @Component({
      selector: 'terra-properties-add-view',
      template: `
                    <ng-container *ngIf="data$ | async as data">
                        <terra-settings-properties-add
                            [destinations]="data.destinations"
                            (propertyCreated)="onPropertyCreation($event)">
                        </terra-settings-properties-add>
                    </ng-container>`
   })
   export class PropertiesAddViewComponent
   {
      protected data$:Observable<Data>;

      constructor(private route:ActivatedRoute,
                  private router:Router,
                  private propertiesComponent:PropertiesComponent)
      {
          this.data$ = this.route.data;
      }

      protected onPropertyCreation(property:PropertyInterface):void
      {
   this.propertiesComponent.breadcrumbsService.closeBreadcrumbByUrl('/' + this.route.snapshot.url.join('/'));
          this.router.navigate(['../', property.id], {relativeTo: this.route});
      }
   }
  

d. Make sure to implement ngOnChanges where Inputs can change (especially for components that are loaded on parameterised routes) and remember that the view needs to be updated.


public ngOnChanges(changes:SimpleChanges):void
{
   if(changes.hasOwnProperty('property'))
   {
       this.updateFormFields();
   }

   if(changes.hasOwnProperty('destination'))
   {
       this.propertyOptionsConfig = this.propertyDynamicViewService.processOptionsConfig(this.destination);
   }

   if(changes.hasOwnProperty('options'))
   {
       this.propertyOptionsData = this.propertyDynamicViewService.updateSelectedOptions(this.propertyOptionsConfig, this.options);
       this.propertyOptionsConfig = this.propertyDynamicViewService.setupOptionsId(this.propertyOptionsConfig, this.options);
   }
}
  

5. Since we are directly routing to views now, we don’t need any submodules. All the components, services (including resolvers) and modules we declared/imported into our submodules can be moved into our main ‘feature’ module and the submodules can be removed.

6. The filter and table component which we previously displayed by adding them to our splitView-Config when loading the properties-configuration route now need their own view component to be displayed. Inside this component, we use the TerraTwoColumn component and the attributes ‘left’ and ‘right’ to designate the two components that we want to be shown next to each other and to position them.

    @Component({
       selector: 'terra-properties-overview',
       template: `
                     <terra-2-col>
                         <terra-settings-properties-filter left></terra-settings-properties-filter>
                         <terra-settings-properties-table right></terra-settings-properties-table>
                     </terra-2-col>`
    })
    export class PropertiesOverviewComponent
    {
    }
  

7. Now we can set up our resolvers to load preloadable data like countries, user roles and similar static data that we will require in our views and to load the view specific data when we route to one of our views.

    @Injectable()
    export class PropertyResolver implements Resolve<PropertyInterface>
    {
       constructor(private propertiesService:PropertiesService)
       {
       }

       public resolve(route:ActivatedRouteSnapshot):Observable<PropertyInterface>
       {
           let propertyId:number = +route.params['propertyId'];

           if(isNullOrUndefined(propertyId) || isNaN(propertyId))
           {
               return;
           }

           return this.propertiesService.getProperty(propertyId);
       }
    }
  

8. Although our views are now working as they should, there are still some artifacts of the old SplitView-routing that we need to get rid of. Components that still extend the MultiSplitViewBaseComponent need to be changed and logic that is based on methods inherited from it (translation, error messages) has to be refactored to work without it. You can either change the extension to TerraAlertBase or remove the extension if you are not using any of the alert handling methods like handleMessage. Moreover, configs need to be replaced (see Episode 2.3).

    export class SomeComponent extends TerraAlertBase
    {
       constructor(translation:TranslationService)
       {
           super(translation);
       }

       private someFunction():void
       {
           this.handleMessage(this.translation.translate('test'));
       }
    }
  

Episode 2: Attack of the code quality

Now that the last remnants of splitView resistance have been wiped out, there are still a couple of improvements we can make.

  1. Replacing for and for-of loops with foreach loops for better readability and less risk of off-by-one errors.
  2. Removing unused properties, methods and imports.
  3. Replacing unneeded *.config.ts files. If you have config files to handle the synchronisation of data between two components in the splitView, please consider replacing the config files by direct component communication using Inputs and Outputs.