Search code examples
angulartypescriptsingle-page-applicationangular-module

Several pages with the same structure in Angular


I'm relatively new to Angular and I'm struggling to create the same behavior for several pages using a common module.

Here is the situation: I'm creating a CRUD SPA with several "pages" (using routing). Turns out that several of those pages shares the same structure and behaviors, and only differs (visually) from the way the items are rendered in a list and its details when selected. Think like several pages of Asana, where the only difference between the pages are the way the list and the details are renderer.

I was thinking to create a Module for each of these pages, but it seemed lame to copy and paste all the code just to change a small portion of the files (updating would be a nightmare too).

The solution I'm trying to achieve is to create a master CRUD Module, with one Component/html/css where I can in some way parameterize which renderer to use for each page. Or any other solution that has as minimum copy-paste as possible.

Thanks for the help.


Solution

  • I read about the tool and some other things like dynamically created components, but none was exactly what I wanted (specially considering the learning curves). So, it took me some time, but I found a (pretty crazy) solution on my own.

    First, I created a Base Module. Within it, I created a PageComponent that calls the two parts of the pages that I needed: one for the list and other for the details (like Asana). The page.component.html template looks like this:

    <div class="mdl-grid">
        <div class="mdl-cell mdl-cell--8-col">
            <app-list></app-list>
        </div>
        <div class="mdl-cell mdl-cell--4-col">
            <app-details></app-details>
        </div>
    </div>`
    

    In this base module, I created an abstract base class for each of the components that I needed (an AbstractListComponent and AbstractDetailsComponent). I had some issues with dependency injection and automatic variable creation using the access word in the constructor, so my solution was the following:

    export abstract class AbstractDetailsComponent {
        protected service: BaseService;
    
        protected constructor(service: BaseService) {
            this.service = service;
        }
    }
    

    Note that I didn't use the @Component decorator since it will be ignored (in PageComponent too). Another thing was that some Base* classes and interfaces were created to ensure the consistency (like BaseService).

    As you can see, nobody will answer when the PageComponent requests for <app-list> and <app-details>. Here starts the crazy part.

    For each page I created a module and within it, components that filled the gaps. Also, the main component for the page extends PageComponent. For example, in my app, I have a Skills page, which leads to the following SkillsComponent:

    @Component({
        selector: 'skills-page', // doesn't really matter, this will be called by the router
        templateUrl: '../base-module/page.component.html',
        styleUrls: [ '../base-module/page.component.css', './skills.component.css' ]
    })
    export class SkillsComponent extends PageComponent { }
    

    Note that the templateUrl and one of the styles points to files inside the base module. This was the way I found to share the same page structure between all pages.

    Now, the gaps. Both components that I needed are similar. Basically, I extend the base class for each one and inject the right service. Here is the SkillDetailsComponent:

    @Component({
        selector: 'app-details', // <-- the name required by the PageComponent template
        templateUrl: './skill-details.component.html',
        styleUrls: [ '../base-module/details.component.css', './skills-details.component.css' ]
    })
    export class SkillsDetailsComponent extends DetailsComponent {
        public constructor(service: SkillsService) {
            super(service);
        }
    }
    

    The selector is exactly the one required by page.component.html. All list and details components for all pages share the same approach. This makes the same template useful in all pages, since the selector is the same.

    Another thing is the constructor. I had to make it like this to inject the right service.

    The final file structure looks like this:

    app/
        ├ base-module/
        │    ├ base.component.css
        │    ├ base.component.html
        │    ├ base.component.ts
        │    ├ base.module.ts
        │    ├ details.component.css
        │    ├ details.component.ts
        │    ├ list.component.css
        │    ├ list.component.html
        │    └ list.component.ts
        └ skills-module/
            ├ skills-details.component.css
            ├ skills-details.component.ts
            ├ skills-list.component.css
            ├ skills-list.component.ts
            ├ skills.component.css
            ├ skills.component.html
            ├ skills.component.ts
            └ skills.module.ts
    

    I don't know if it's the best solution, but it's woking like I needed. It was kinda hard to explain all the steps I've made (usually this is a sign of a too-much-complicated solution) but I hope it's understandable.