Angular 2 route param changes, but component doesn't reload - javascript

So, basically I need to reload my component after id of url parameter was changed. This is my player.component.ts:
import {Component, OnInit, AfterViewInit} from '#angular/core';
import {ActivatedRoute, Router} from '#angular/router';
declare var jQuery: any;
#Component({
selector: 'video-player',
templateUrl: './player.component.html',
styleUrls: ['./player.component.less']
})
export class VideoPlayerComponent implements OnInit, AfterViewInit {
playerTop: number;
currentVideoId: number;
constructor(
private _route: ActivatedRoute,
private _router: Router
) {
}
ngOnInit(): void {
this._route.params.subscribe(params => {
this.currentVideoId = +params['id'];
console.log( this.currentVideoId );
this._router.navigate(['/video', this.currentVideoId]);
});
}
ngAfterViewInit(): void {
if (this.videoPageParams(this.currentVideoId)) {
console.log( "afterViewInit" );
let params = this.videoPageParams(this.currentVideoId);
let fakeVideoItemsCount = Math.floor(params.containerWidth / params.videoItemWidth);
this.insertFakeVideoItems( this.currentVideoId, fakeVideoItemsCount);
this.changePlayerPosition( params.videoItemTop );
}
}
videoPageParams( id ): any {
let videoItemTop = jQuery(`.videoItem[data-id="${id}"]`).position().top;
let videoItemWidth = jQuery('.videoItem').width();
let containerWidth = jQuery('.listWrapper').width();
return {
videoItemTop,
videoItemWidth,
containerWidth
};
}
changePlayerPosition( videoItemTop ): void {
this.playerTop = videoItemTop;
}
insertFakeVideoItems( id, fakeVideoItemsCount ): void {
let fakeVideoItemHTML = `<div class="videoItem fake"></div>`;
let html5playerHeight = jQuery('#html5player').height();
let videoItemIndex = jQuery(`.videoItem[data-id="${id}"]`).index() + 1;
let videoItemInsertAfterIndex;
let videoItemRow = Math.ceil(videoItemIndex / fakeVideoItemsCount);
let videoItemRowBefore = videoItemRow - 1;
if ( videoItemIndex <= 4 ) {
videoItemInsertAfterIndex = 0;
} else {
videoItemInsertAfterIndex = (videoItemRowBefore * fakeVideoItemsCount);
}
let videoItemInsertAfter = jQuery('.videoItem').eq(videoItemInsertAfterIndex);
for ( let i = 0; i < fakeVideoItemsCount; i++ ) {
$(fakeVideoItemHTML).insertBefore(videoItemInsertAfter);
}
jQuery(`.videoItem.fake`).css('height', html5playerHeight);
}
}
player.component.html:
<video
class="video"
preload="auto"
[attr.data-id]="currentVideoId"
src="">
</video>
<videos-list></videos-list>
videoList.component.html
<div class="videoItem" *ngFor="let video of videos" [attr.data-id]="video.id">
<a [routerLink]="['/video', video.id]">
<img [src]='video.thumbnail' alt="1">
</a>
</div>
So when I click <a [routerLink]="['/video', video.id]"> in videoList.component.html it changes route to /video/10 for example, but the part from player.component.ts which manipulates the DOM doesn't fire again - DOM manipulation doesn't update.
I tried to manually navigate to route via this._router.navigate(['/video', this.currentVideoId]); but somehow it doesn't work.
QUESTION
Is there any way to run functions that manipulate DOM each time route param changes in the same URL?

DOM will not update because ngOnInit is only fired once, so it will not update even if you try to "renavigate" back to the parent from the child, since the parent haven't been removed from the DOM at any point.
One option to solve this, is that you could use a Subject, that when the routing is happening, let's send the chosen video id to parent, which subscribes to the change and does whatever you tell it to do, meaning calling functions that will update the DOM, so probably what you want re-executed is the inside ngOnInit and ngAfterViewInit
You mentioned that you had tried using
this._router.navigate(['/video', this.currentVideoId])
so let's look at that. Probably have some click event that fires a function. Let's say it looks like the following, we'll just add the subject in the play
navigate(id) {
VideoPlayerComponent.doUpdate.next(id)
this._router.navigate(['/video', this.currentVideoId])
}
Let's declare the Subject in your parent, and subscribe to the changes:
public static doUpdate: Subject<any> = new Subject();
and in the constructor let's subscribe to the changes...
constructor(...) {
VideoPlayerComponent.doUpdate.subscribe(res => {
console.log(res) // you have your id here
// re-fire whatever functions you need to update the DOM
});
}

Related

How to prevent page refresh when using ng5-slider for Angular

I have an ng5-slider which has a range from 0 - 1000 and is adjustable by the user.
When I adjust the slider at the bottom of the page, the page refreshes and pulls me to the top of the page.
I would like to prevent that from occurring. How do I do this ?
Here is my code:
page.component.html
<div>
<ng5-slider
[(value)]="minValue"
[(highValue)]="maxValue"
(userChangeStart)="onUserChangeStart($event)"
(userChangeEnd)="onUserChangeEnd($event)"
[options]="options"
(userChange)="onUserChange($event)">
</ng5-slider>
</div>
page.component.ts
#Component({
selector: 'app-detail',
templateUrl: './detail.component.html',
styleUrls: ['./detail.component.scss']
})
export class DetailComponent implements OnInit {
logText = '';
arr_price = [];
minValue = 0;
maxValue = 1000;
options: Options = {
floor: 0,
ceil: 1000
};
getChangeContextString(changeContext: ChangeContext): string {
return `${changeContext.value} ${changeContext.highValue}`;
}
onUserChangeStart(changeContext: ChangeContext): void {
this.logText = this.getChangeContextString(changeContext);
}
onUserChangeEnd(changeContext: ChangeContext): void {
this.logText = this.getChangeContextString(changeContext);
this.apply();
}
onUserChange(changeContext: ChangeContext): void {
this.logText = this.getChangeContextString(changeContext);
const value = this.logText;
const value_arr = value.split(' ');
this.arr_price = value_arr;
}
}
I've tried to remove (userChangeStart) and (userChangeEnd) and the page did not reload which is what I want.
However, when I manually click the refresh button, the slider values reverted back to its original 0 --------------- 1000.
I am thinking that I have to implement userChangeStart and userChangeEnd for data to be persisted but if only I can somehow prevent page reload from within these handler functions...
I fixed the bug by setting scrollPositionRestoration to disabled in the app routing module.
Initially it was set to enabled and thus any click event at the bottom of the page will bring the page to the top.
imports: [RouterModule.forRoot(routes, { scrollPositionRestoration: 'disabled' })]

nativeElement select wait for binding data

Let's say that I have a child component called inputComponent that has a single input element as follow
#Component({ template: `<input #count [(ngModel)]="item.count" />`})
export class inputComponent implements OnInit {
#Input item;
#ViewChild("count") count : ElementRef ;
focus(){
this.count.nativeElement.focus();
this.count.nativeElement.select();
}
}
and I'm including it in a parent container as follow
<app-input-component [item]="item" ></app-input-component>
What I'm trying to achieve is to select the text input on a certain event.
for example
#ViewChild("input") count : inputComponent ;
foo(){
this.item = item ;
this.count.focus();
}
The problem is when I call focus change right after changing the binding data (item) it doesn't select anything hover if I called focus() after a short timeout it works perfectly .
I know it's not the proper way to use setTimeOut to solve it.
Stackblitz url
https://stackblitz.com/edit/angular-svgmtg
Apparently, ngModel updates the view's value asynchronously when the model is changed. I.e. the <input> value is not changed until the next change detection cycle!
From the ngModel source code:
/**
* `ngModel` forces an additional change detection run when its inputs change:
* E.g.:
* ```
* <div>{{myModel.valid}}</div>
* <input [(ngModel)]="myValue" #myModel="ngModel">
* ```
* I.e. `ngModel` can export itself on the element and then be used in the template.
* Normally, this would result in expressions before the `input` that use the exported directive
* to have and old value as they have been
* dirty checked before. As this is a very common case for `ngModel`, we added this second change
* detection run.
*
* Notes:
* - this is just one extra run no matter how many `ngModel` have been changed.
* - this is a general problem when using `exportAs` for directives!
*/
const resolvedPromise = Promise.resolve(null);
Then when the model is updated, the view is updated asynchronously:
private _updateValue(value: any): void {
resolvedPromise.then(
() => { this.control.setValue(value, {emitViewToModelChange: false}); });
}
So the setTimeout ensured that the input was selected after its view was updated.
If you want to avoid this asynchronous behavior, you can use FormControl instead of ngModel (Demo StackBlitz):
import { Component, Input, ViewChild, ElementRef } from '#angular/core';
import { FormControl } from '#angular/forms';
#Component({
selector: 'hello',
template: `<input #input [formControl]="count" />`,
styles: [`h1 { font-family: Lato; }`]
})
export class HelloComponent {
private _item;
#Input() set item(value) {
this._item = value;
this.count.setValue(value.count);
this.focus();
}
get item() {
return this._item;
}
#ViewChild('input') input: ElementRef;
count = new FormControl();
focus() {
this.input.nativeElement.focus();
this.input.nativeElement.select();
}
}
With this approach, you don't need to call focus() explicitly from the parent component; the child component will call its own focus method whenever the input changes.
As I understood, you trying to get an element before it has been rendered. That is impossible.
I advice you to read about Lifecycle hooks in Angular. https://angular.io/guide/lifecycle-hooks
You can solve this problem, calling your foo() function in lifecycle hook - ngAfterViewInit.
ngAfterViewInit() {
this.foo();
}

How to change this body manipulation with section manipulation in Angular?

We are trying to change CSS id's based on time. The point is that currently, it manipulates the body. How can we change it into section manipulation?
Angular part
ngOnInit() {
this.run(1000, 10)
}
run(interval, frames) {
var int = 1;
function func() {
document.body.id = "b"+int;
int++;
if(int === frames) { int = 1; }
}
var swap = window.setInterval(func, interval);
}
HTML
<section class='full-screen'>
...
...
</section>
there are different css snippets for #b1, #b2, #b3... since above code changes these ids during each time period. I assume something should be changed here:
document.body.id = "b"+int;
How move that function usage from body into above HTML section?
Add a Template reference variable in your template for the section tag:
<section #section class='full-screen'>
...
...
</section>
Add a #ViewChild decoratored variable in your component's ts file to get this element:
#ViewChild('section', { read: ElementRef }) mySection: ElementRef;
Now you can use it like this in your component's ts file:
ngOnInit() {
this.run(1000, 10)
}
run(interval, frames) {
var int = 1;
function func() {
this.mySection.nativeElement.id = "b"+int;
int++;
if(int === frames) { int = 1; }
}
var swap = window.setInterval(func.bind(this), interval);
}
See this simple DEMO
UPDATE:
Note that you're using function func(), this will cause you a scoping problem with using this as your component object. One way to fix this is by using bind function:
var swap = window.setInterval(func.bind(this), interval);
Updated the demo to show this in action.
document.getElementById("div_top1").setAttribute("id", "div_top2");
You can use this to change section id.
You can do it thanks to angular viewChild feature
in your html:
<div #foo class="myClass"></div>
in your component ts file
import { Component, ViewChild, ElementRef, AfterViewInit } from '#angular/core';
// ....
export MyComponernt implement AfterViewInit {
// ....
#ViewChild('foo') foo: ElementRef;
// ....
ngAfterViewInit(): void {
// this is how you manipulate element id properly thanks to angular viewChild feature
this.foo.nativeElement.id = 'something';
}
// ....
}

ngx-bootstrap accordion open panel dynamically

I use ngx-bootstrap accordion to show a list of blog posts.
Here is the template:
<accordion id="blog-list">
<accordion-group *ngFor="let post of posts; let first = first;" [isOpen]="first" id="post-{{post.id}}">
<!-- Here goes content irrelevant to the question -->
</accordion-group>
</accordion>
I also use some global config, to have only one open accordion panel at a time.
export function getAccordionConfig(): AccordionConfig {
return Object.assign(new AccordionConfig(), { closeOthers: true });
}
Now, when a post gets updated, I update it in the list, like so:
constructor(private elementRef: ElementRef, private postService: PostService) {
this.postService.updatedPost.subscribe(val => {
let i = this.posts.findIndex(post => post.id === val.id);
this.posts[i] = val;
let element = elementRef.nativeElement.querySelector('#post-' + val.id);
element.setAttribute('isOpen', true); // <- this does not work
element.scrollIntoView(true);
});
}
Updating and scrolling works fine, but I can't figure out how to get the panel to open. After the view gets updated and scrolled all panels are closed. I want the panel with the updated post to be open.
So issue is in [isOpen]="first", first post will be opened by default
direct manipulation with DOM will no trigger bindings updates
what you could do is:
[isOpen]="activPostIndex === index"
activPostIndex = 0;
constructor(private elementRef: ElementRef, private postService: PostService) {
this.postService.updatedPost.subscribe(val => {
this.activPostIndex = this.posts.findIndex(post => post.id === val.id);
this.posts[i] = val;
});
}

Create custom script for DOM Manipulation

I'm currently working on an Angular 2 Project where I have a menu that should be closable by a click on a button. Since this is not heavy at all, I would like to put it outside of Angular (without using a component for the menu).
But I'm not sure of how to do it, actually I've just put a simple javascript in my html header, but shouldn't I put it somewhere else?
Also, what the code should be? Using class, export something? Currently this is my code:
var toggleMenuButton = document.getElementById('open-close-sidebar');
var contentHolder = document.getElementById('main-content');
var menuHolder = document.getElementById('sidebar');
var menuIsVisible = true;
var updateVisibility = function() {
contentHolder.className = menuIsVisible ? "minimised" : "extended";
menuHolder.className = menuIsVisible ? "open" : "closed";
}
toggleMenuButton.addEventListener('click', function() {
menuIsVisible = !menuIsVisible;
updateVisibility();
});
Finally moved to something with MenuComponent and a service, but I'm still encountering an issue.
MenuService.ts
#Injectable()
export class MenuService {
isAvailable: boolean = true;
isOpen: boolean = true;
mainClass: string = "minimised";
sidebarClass: string = "open";
updateClassName() {
this.mainClass = this.isOpen ? "minimised" : "extended";
this.sidebarClass = this.isOpen ? "open" : "closed";
}
toggleMenu(newState: boolean = !this.isOpen) {
this.isOpen = newState;
this.updateClassName();
}
}
MenuComponent.ts
export class MenuComponent {
constructor(private _menuService: MenuService) { }
public isAvailable: boolean = this._menuService.isAvailable;
public sidebarClass: string = this._menuService.sidebarClass;
toggleMenu() {
this._menuService.toggleMenu();
}
}
MenuComponent.html
<div id="sidebar" [class]="sidebarClass" *ngIf="isAvailable">
...
<div id="open-close-sidebar"><a (click)="toggleMenu()"></a></div>
The action are rightly triggered, if I debug the value with console.log, the class name are right but it didn't change the value of the class. I thought the binding was automatic. And I still do not really understand how to change it. Do I have to use Emmit like AMagyar suggested?
The advantage of using angular2 above your own implementation, greatly outweigh the marginal benefit in performance you will get from using plane JavaSccript. I suggest not going on this path.
If you however do want to continue with this, you should export a function and import and call this function inside the ngAfterViewInit of your AppComponent. The exported function should add the click EventListener and (important) set the document.getElementById variables. Because your script possibly won't be able to find those elements yet when it's loaded.
But let me emphasise once more, that angular2 is optimised for exactly these tasks, and once you get more familiar with it, it will also be a lot easier to code it.
update
For inter component communication you should immediately think about a service. Just create a service which stores the menu state and add this to your global ngModule providers array. For instance:
export class MenuService {
public get menuOpen(): boolean {
return this._menuOpen;
}
private _menuOpen: boolean;
public openMenu() : void {
this._menuOpen = true;
}
public closeMenu() : void {
this._menuOpen = false;
}
public toggleMenu() : void {
this._menuOpen = !this._menuOpen;
}
}
You can then inject this service into your menu component and bind the classes open/closed and minimized/extended to the MenuService.menuOpen.
#Component({
selector : 'menu'
template : `
<button (click)="menuService.toggleMenu()">click</button>
<div id="open-close-sidebar" [class.open]="menuService.menuOpen"></div>
`
})
export class MenuComponent {
constructor(public menuService: MenuService){}
}
For other component you can use the same logic to see if the menu is open or closed
update #2
You have to use a getter to get the value from menuService. There is only one way binding:
export class MenuComponent {
constructor(private _menuService: MenuService) { }
public get isAvailable(): boolean {
return this._menuService.isAvailable;
}
public get sidebarClass(): string {
return this._menuService.sidebarClass;
}
toggleMenu() {
this._menuService.toggleMenu();
}
}
FYI, it's better practice to use [class.open] instead of a string class name. If you want to do it like that, it will only require minimal change in your current css.
The main reason of why I want to avoid using Angular component is the
fact that my manipulation should be done over all the website and not
just the "menu" component.
You can create many components in Angular 2, it's easy and very practical.
The action will change the class on my menu (located in my menu
component) and on my main content (located outside of the component).
I don't know how to do it, and I'm not sure that this is the best
way... Maybe by binding the service value directly... –
The main content can have a child that is the Menu itself.
Take a look in this link. There are many solutions, one of them is to "emit" the child changes to the parent.
If you need an example I can provide one quickly.

Categories

Resources