Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular2: rendering / reloading a component's template

Ideally I would need to reload / rerender my component's template but if there is a better way to do this I will gladly implement it.

Desired behavior:

So, I have a component for a menu element. When (in another component) I click an IBO (some sort of 'client', per say) is clicked I need to add (I'm using *ngIf) a new option in the menu that would be IBO Details and a child list.

IBOsNavigationElement component (menu component):

@Component({
    selector: '[ibos-navigation-element]',
    template: `
        <a href="#"><i class="fa fa-lg fa-fw fa-group"></i> <span
                class="menu-item-parent">{{'IBOs' | i18n}}</span>
        </a>
        <ul>
            <li routerLinkActive="active">
                <a routerLink="/ibos/IBOsMain">{{'IBOs Main' | i18n}} {{id}}</a>
            </li>
            <li *ngIf="navigationList?.length > 0">
                <a href="#">{{'IBO Details' | i18n}}</a>
                <ul>
                    <li routerLinkActive="active" *ngFor="let navigatedIBO of navigationList">
                        <a href="#/ibos/IboDetails/{{navigatedIBO['id']}}">{{navigatedIBO['name']}}</a>
                    </li>
                </ul>
            </li>
        </ul>
    `
})
export class IBOsNavigationElement implements OnInit {
    private navigationList = <any>[];


    constructor(private navigationService: NavigationElementsService) {
        this.navigationService.updateIBOsNavigation$.subscribe((navigationData) => {
                this.navigationList.push(navigationData);
                log.d('I received this Navigation Data:', JSON.stringify(this.navigationList));
            }
        );
    }

    ngOnInit() {
    }
}

Initially, navigationList will be empty [], because the user didn't click any IBO (client), but as soon as an IBO is clicked the list will be populated and, therefore, I need the new option to appear in the menu.

With this code, when I click an IBO, the <li> element and it's children are created but: I only see the <li> element.

Issue:

The menu option is generated but not proccessed by the layout styles. It needs to be initialized with all the elements in order to know how to display the menu options.

I need to reload the template of that component in order to display correctly the menu.

NOTE:

If I use the template without the *ngIf, works well but I would have from the first moment an IBO Details option that has no sense, because no IBO has been clicked when initialized.

template: `
        <a href="#"><i class="fa fa-lg fa-fw fa-group"></i> <span
                class="menu-item-parent">{{'IBOs' | i18n}}</span>
        </a>
        <ul>
            <li routerLinkActive="active">
                <a routerLink="/ibos/IBOsMain">{{'IBOs Main' | i18n}} {{id}}</a>
            </li>
            <li>
                <a href="#">{{'IBO Details' | i18n}}</a>
                <ul>
                    <li routerLinkActive="active" *ngFor="let navigatedIBO of navigationList">
                        <a href="#/ibos/IboDetails/{{navigatedIBO['id']}}">{{navigatedIBO['name']}}</a>
                    </li>
                </ul>
            </li>
        </ul>
    `

Desired Output:

Before clicking anything (on init):

enter image description here

After I clicked an IBO (client):

enter image description here

Update 1:

To clarify what I meant with:

The menu option is generated but not proccessed by the layout styles

If, my menu component is initialized without the *ngIf:

<li>
    <a href="#">{{'IBO Details' | i18n}}</a>
        <ul>
            <li routerLinkActive="active" *ngFor="let navigatedIBO of navigationList">
                <a href="#/ibos/IboDetails/{{navigatedIBO['id']}}">{{navigatedIBO['name']}}</a>
            </li>
        </ul>
</li>

Then the layout styles can generate the menu output (see images) according to this structure:

<li>
   <a>
   <ul>
      <li *ngFor>
         <a>
      </li>
   </ul>
</li>

And therefore add the + symbol and the submenu behavior, etc.

But, if it's initialized without all the elements (when *ngIf is false the <li> and it's children are not rendered so the layout does not take them into account to draw/create the menu) and these elements are added after the rendering, then they will exist in source code, but we won't be able to see them in the menu because:

  • No + created
  • No submenu behavior
like image 601
SrAxi Avatar asked Apr 13 '17 15:04

SrAxi


2 Answers

Angular has two change detection strategies:

  • The default strategy, that automatically detects changes in the model and re-render the components accordingly.

  • OnPush, that only re-renders the component when you explicitly tell it to do so. See also https://angular.io/docs/ts/latest/api/core/index/ChangeDetectionStrategy-enum.html

The OnPush strategy can have a better performance when you have several elements on a page or when you want to have more control over the rendering process.

In order to use this strategy, you have to declare it in your component:

@Component({
    selector: '[ibos-navigation-element]',
    template: `...`,
    changeDetection: ChangeDetectionStrategy.OnPush
})

And inject in your constructor:

constructor(
    private changeDetector: ChangeDetectorRef,
) {}

When you want to fire the change detection so the component can be re-rendered (in your case, right after a new IBO/client is added to the model), call:

this.changeDetector.markForCheck();

Check the live demo from the official tutorial: http://plnkr.co/edit/GC512b?p=preview

If the problem is not about change detection, but related to CSS/SCSS styling, bear in mind that in Angular 2 each component has its own set of CSS classes and they're not "inherited" by the "children" elements. They're completely isolated from each another. One solution could be the creation of global CSS/SCSS styles.

like image 68
Pedro Penna Avatar answered Oct 18 '22 10:10

Pedro Penna


I finally got it working!

So, my problem was:

When the new HTML elements are generated (with *ngIf) they don't get displayed because they don't get processed the same way as the other menu elements do.

So I asked how to reload or re-render the template with all the 'new' elements... But I did not find where to reload a component or a component's template. As instead, I applied the logic that process the menu to my updated template.

(If you want the short story version, go at the bottom and read the Summary)

So, I dived into my template's deepest logic and created a directive to render the menu:

MenuDirective (directive)

@Directive({
    selector: '[menuDirective]'
})
export class MenuDirective implements OnInit, AfterContentInit {

  constructor(private menu: ElementRef,
            private router: Router,
            public layoutService: LayoutService) {
    this.$menu = $(this.menu.nativeElement);
  }

  // A lot of boring rendering of layout

  ngAfterContentInit() {
        this.renderSubMenus(this.$menu);
    }

  renderSubMenus(menuElement) {
            menuElement.find('li:has(> ul)').each((i, li) => {
                let $menuItem = $(li);
                let $a = $menuItem.find('>a');
                let sign = $('<b class="collapse-sign"><em class="fa fa-plus-square-o"/></b>');
                $a.on('click', (e) => {
                    this.toggle($menuItem);
                    e.stopPropagation();
                    return false;
                }).append(sign);
            });
    }
}

So here I create the menu directive that renders the layout of the menu according to the existing html elements. And, as you can see, I isolated the behavior that processes the menu elements adding the + icon, creating the submenu feature, etc...: renderSubMenus().

How does renderSubMenus() behave:

It loops through the DOM elements of the nativeElement passed as parameter and applies the logic to display the menu in the correct way.

menu.html

<ul menuDirective>    
    <li ibos-navigation-element></li>
    <li>
        <a href="#"><i class="fa fa-lg fa-fw fa-shopping-cart"></i> <span
                            class="menu-item-parent">{{'Orders' | i18n}}</span></a>
        <ul>
            <li routerLinkActive="active">
                <a routerLink="/orders/ordersMain">{{'Orders' | i18n}}</a>
            </li>
        </ul>
    </li>        
</ul>

And that would be how I build the menu.

Now let's see the IBOsNavigationElement component, that is included in the menu with the attribute [ibos-navigation-element].

IBOsNavigationElement (component)

@Component({
    selector: '[ibos-navigation-element]',
    template: `
        <a href="#"><i class="fa fa-lg fa-fw fa-group"></i> <span
                class="menu-item-parent">{{'IBOs' | i18n}}</span>
        </a>
        <ul class="renderMe">
            <li routerLinkActive="active">
                <a routerLink="/ibos/IBOsMain">{{'IBOs Main' | i18n}} {{id}}</a>
            </li>
            <li *ngIf="navigationList?.length > 0">
                <a href="#">{{'IBO Details' | i18n}}</a>
                <ul>
                    <li routerLinkActive="active" *ngFor="let navigatedIBO of navigationList">
                        <a href="#/ibos/IboDetails/{{navigatedIBO['id']}}">{{navigatedIBO['name']}}</a>
                    </li>
                </ul>
            </li>
        </ul>
    `
})
export class IBOsNavigationElement implements OnInit, DoCheck {
    private $menuElement: any;
    private navigationList = <any>[];
    private menuRendered: boolean = false;

    constructor(private navigationService: NavigationElementsService, private menuDirective: MenuDirective, private menuElement: ElementRef) {
        this.$menuElement = $(this.menuElement.nativeElement);

        this.navigationService.updateIBOsNavigation$.subscribe((navigationData) => {
                this.navigationList.push(navigationData);
                log.d('I received this Navigation Data:', JSON.stringify(this.navigationList));
            }
        );
    }

    ngOnInit() {
    }

    ngDoCheck() {
        if (this.navigationList.length > 0 && !this.menuRendered) {
            log.er('calling renderSubMenus()');
            this.menuDirective.renderSubMenus(this.$menuElement.find('ul.renderMe'));
            this.menuRendered = true;
        }
    }
}

Ok, so what have I done different here? Several things...

  1. I import the directive MenuDirective so I can call its renderSubMenus() method.
  2. I use ElementRef and find() to select the block of code that I want to send to this.menuDirective.renderSubMenus(). I find it through its class, see: this.$menuElement.find('ul.renderMe').
  3. I implement Angular's DoCheck hook to detect the changes that I want and apply logic to that change event. See ngDoCheck() method where I check if the list navigationList is populated and if I have already rendered this block of code (I had issues because was rendering too many times and I had like 6 + buttons: disaster).

Summary:

To 'reload' the template:

  1. I created a directive with a method that applies the logic that usually occurs on init.
  2. I instance that directive in the component that I want to reload.
  3. With ElementRef I get the portion of template that I want to 'reload'.
  4. I choose when I want to apply the 'reload' method, in my case I did it with ngDoCheck(). You can call that method whenever you want.
  5. I call the directive's 'reload' method passing as parameter the portion of code within my template that I want to reload (I could have passed the entire template if I wanted).
  6. The method will apply to the portion of template that I sent the same logic that would have applied if I instanced the component with the hidden elements by *ngIf.

So, technically, I did not reload the component. I applied to the component's template the same logic that would have been applied if I reloaded it.

like image 29
SrAxi Avatar answered Oct 18 '22 09:10

SrAxi