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):
After I clicked an IBO (client):
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:
+
createdAngular 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.
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...
MenuDirective
so I can call its renderSubMenus()
method.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')
.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:
ElementRef
I get the portion of template that I want to 'reload'.ngDoCheck()
. You can call that method whenever you want.*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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With