jQuery scrollTo() not working in angular 4 - javascript

I'm using a library called ng2-pdf-viewer for some reason the default way to stick to a page isn't working, so what I've done is I've used jQuerys scrollTo() method to scroll to the the .page class in the PDF so if I wanted the page to be scrolled to page 2 it would be page .page:nth-child(2) now I've got this working.. but only after you refresh the page and not when you first land on the page so If I follow a link to my pdf-view page it doesn't scroll but then when I refresh the page it does.. now I'm not sure if using jQuery scrollTo is the best method but its the only way I've been able to kind of get it to work
HTML
<pdf-viewer [src]="pdfSrc"
[render-text]="true"
[autoresize]="true"
[original-size]="false"
style="display: block;" (after-load-complete)="callBackFn($event)">
</pdf-viewer>
COMPONENT.TS
import { Component, OnInit, Output, EventEmitter, AfterViewInit} from '#angular/core';
import { AppConsts } from '#shared/AppConsts';
import { AppComponentBase } from '#shared/common/app-component-base';
import { ActivatedRoute } from '#angular/router';
import { HeaderTitleService } from '#shared/service/headerTitle.service';
declare var jQuery: any;
const $ = jQuery;
#Component({
selector: 'app-pdfview',
templateUrl: './pdfview.component.html',
styleUrls: ['./pdfview.component.less']
})
export class PdfviewComponent implements OnInit {
pdfSrc: string;
pdf: string;
pageNum: number = 2;
botString: string;
pageNumberVar: string;
#Output() notify: EventEmitter<String> = new EventEmitter<String>();
constructor(
private route: ActivatedRoute,
private headerTitleService: HeaderTitleService
) {
this.pdf = route.snapshot.params['pdfId'];
if (this.pdf === '1') {
this.pdfSrc = '../../../assets/pdf/emeds1.pdf';
this.botString = 'Admission Reconciliation loaded on Page 2 - matching the word ‘reconcile’ for you.';
this.pageNumberVar = '2';
} else if (this.pdf === '2') {
this.pdfSrc = '../../../assets/pdf/medrec.pdf';
this.botService.sendMessage('I have loaded the document on page 21 showing "Medication Reconciliation"');
setTimeout(() => {
this.botService.sendMessage('That saved you hours of reading :)');
}, 2000);
setTimeout(() => {
$('html, body').animate({
scrollTop: $('.page:nth-child(29)').offset().top
}, 300);
}, 1000);
}
}
callBackFn(pdf: PDFDocumentProxy) {
this.botService.sendMessage(this.botString);
}
ngAfterViewInit() {
setTimeout(function(){
$('html, body').animate({
scrollTop: $('.page:nth-child(' + this.pageNumberVar + ')').offset().top
}, 300);
}, 1000);
}
}
as you can see above ive tried putting the scrollTo function in ngAfterViewInit but that didnt fix it either

Generally speaking, I believe that it is not a good idea to use jQuery with Angular, since it accesses the browser's DOM directly, bypassing Angular's (emulated) Shadow DOM.
In this case, I think that you can probably do away with your scrollTo fix, and just get the pdf viewer working properly.
I initially had problems with showing the correct page too, but after trying a few things, came up with this working configuration:
<pdf-viewer [src]="pdfSrc"
[(page)]="page"
[show-all]="false"
[rotation]="0"
[zoom]="0.9"
>
</pdf-viewer>
I would start with this, making sure that you get the proper page rendered, and then add your other properties, one at a time, to see what works and what breaks.

Related

Using custom Angular component inside Contentful rich text renderer

I have a rich text element in Contentful that has an embedded entry within it. I am attempting to have that embedded entry render inside a custom Angular component. However, when I pass in options to the Contentful documentToHtmlString function, it displays the tag, but does not render anything or even trigger the console.log() function I have inside the custom component typescript file.
TYPESCRIPT for the rendering
convertToHTML(document:any[]) {
const options = {
renderNode: {
[BLOCKS.EMBEDDED_ENTRY]: (node:any, next:any) => `<embed-element [node]="${node}" [content]="${next(node.content)}"></embed-element>`
}
}
let unsafe = documentToHtmlString(document, options);
return this.sanitizer.bypassSecurityTrustHtml(unsafe);
}
HTML for the rendering
<span [innerHTML]="convertToHTML(article.fields.content)"></span>
I have loaded the custom element <embed-element></embed-element> in the providers section of the app.module.ts file as well.
import { EmbedElementComponent } from './shared/components/embed-element/embed-element.component';
#NgModule({
declarations: [
...
EmbedElementComponent
],
providers: [
...
EmbedElementComponent
]
})
Then inside typescript file for the custom element, I just simply have a console.log in the onInit function for testing purposes. I am not seeing this occur in the console.
import { Component, OnInit, Input } from '#angular/core';
#Component({
selector: 'embed-element',
templateUrl: './embed-element.component.html',
styleUrls: ['./embed-element.component.css']
})
export class EmbedElementComponent implements OnInit {
#Input() node: any;
#Input() content: any;
constructor() { }
ngOnInit(): void {
console.log(this.node);
}
}
And in the HTML for the custom element, I removed the <p> tag just in case it was stripping it out as "unsafe" and replaced it with the following:
EMBEDDED ELEMENT WORKS!!!!!
Finally, I see nothing appear on screen for this custom element once everything is rendered. Inside the inspect element however, this is what I get.
<embed-element [node]="[object Object]" [content]=""></embed-element>
How do I manage to make the custom element actually get called in this aspect? And at least receive the console log message I am requesting inside the custom element?
Not sure if you still needed another solution, but I came across ngx-dynamic-hooks https://github.com/MTobisch/ngx-dynamic-hooks - and was able to reuse my custom components within the inner html.
const cmpt_data = {
title: 'This is a test'
};
const headerFooterRichTextOption: Partial<Options> = {
renderNode: {
["embedded-entry-inline"]: (node, next) => `${this.embeddedContentfulEntry(node.nodeType, node.data)}`,
["paragraph"]: (node, next) => `<span>${next(node.content)}</span>`,
}
};
const d = documentToHtmlString(tt.value, headerFooterRichTextOption);
const hSanit = this.sanitizer.bypassSecurityTrustHtml(d);
embeddedContentfulEntry(nodeType: any, data: any) {
return "<custom-component [title]='context.title'></custom-component>";
}
<ngx-dynamic-hooks [content]="hSanit" [context]="cpmt_data"></ngx-dynamic-hooks>
I believe that using a custom component for this type of situation was not working because it was rendering outside of Angulars scope or after initial components initiation.
I resolved this issue by essentially just removing the custom element all together and creating a function that renders the embedded element as I wanted.
// Notice the new function call, renderCustomElement()
convertToHTML(document:any[]) {
const options = {
renderNode: {
[BLOCKS.EMBEDDED_ENTRY]: (node:any, next:any) => this.renderCustomElement(node)
}
}
let unsafe = documentToHtmlString(document, options);
return this.sanitizer.bypassSecurityTrustHtml(unsafe);
}
And finally that function that simply creates some basic html with Bootstrap styling and returns that back to be rendered.
renderCustomElement(node:any) {
let link = `/support-center/article/${node.data.target.fields.category[0].sys.id}/${node.data.target.sys.id}`;
return `<style>.card:hover{box-shadow: 0 0.5rem 1rem rgb(0 0 0 / 15%) !important;}</style><a class="card clickable mb-2 text-decoration-none text-dark" href="${link}" target="_blank"><div class="card-header">Article</div><div class="card-body"><h4 class="fw-light">${node.data.target.fields.title}</h4><p class="fw-light pb-0 mb-0">${node.data.target.fields.preview}</p></div></a>`
}

How to reload a page once using angular or javascript

I need to reload a page after routing to a new component
I have tried location.reload() option but it keeps on reloading without stoping
ngOninit(){
location.reload()
}
Expected result :
I need to reload a page only oncee
Actual result :
It is reloading many times
I don't understand your use case but there is lot of way to make what you want.
I propose you a way with localStorage.
Check if a localStorage exist. By default the key don't exist, so you create it and reload the page too.
After the refresh you check the same key (that exist now). So you don't reload the page and remove the key for the next time
ngOnInit() {
if (!localStorage.getItem('foo')) {
localStorage.setItem('foo', 'no reload')
location.reload()
} else {
localStorage.removeItem('foo')
}
}
you should save (for example) "IsLoadedBefore" boolean in a variable and check it before reloading your page. but a variable that keeps data after page reloaded! (not local ones)
localStorage is a good choice:
function reloadIfNecessary() {
var isLoadedBefore = localstorage.getItem("IsLoadedBefore");
if(isLoadedBefore=="true")
return;
}
else {
localstorage.setItem("IsLoadedBefore",true);
/*use your reload method*/
})
If you want to reload child component use this.ngOnInit(); (on child component which may be refreshed) to reload just this child component.
parent.component.ts
#ViewChild(ChildComponent) child: ChildComponent;
refreshed = false;
ngAfterViewInit() {
if (!refreshed)
this.child.refresh()
refreshed = true;
}
child.component.ts
refresh() {
this.ngOnInit()
}
you can import location
constructor(location: Location)
then call reload method to reload the page
this.location.reload()
Create a service to store the previous route. Get the previous route in your component if it has route or the route you define in condition, then reload.
import { Injectable } from '#angular/core';
import { Router, NavigationEnd } from '#angular/router';
#Injectable()
export class PreviousRouteService {
private prev: string;
private curr: string;
constructor(private router: Router) {
this.currentUrl = this.router.url;
router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
this.prev = this.curr;
this.curr = event.url;
};
});
}
public getPreviousUrl() {
return this.prev;
}
}
Use it in your component:
ngOninit(){
if (this.previousRouteService.getPreviousUrl() {
location.reload()
}
}
Use the service as provider and instantiate it in the constructor.
constructor(private previousRouteService: PreviousRouteService){}
Here you can make use of local-storage like below
ngOnInit(){
const firstTime = localstorage.getItem('key')
if(!firstTime){
localstorage.setItem('key','loaded')
location.reload()
}else {
localStorage.removeItem('key')
}
}
In javascript, you can use
location.reload()
for page reload

Accessing Style of Ionic 2 DOM Elements

I'm trying to access the DOM elements of one of my pages with the following:
ionViewDidEnter() {
this.platform.ready().then(() => {
let elm = <HTMLElement>document.querySelector("ion-navbar.toolbar.toolbar-ios.statusbar-padding");
console.log(elm.style);
})
}
However, it appears this element has no style - I have tried various combinations to access it but no luck.
Specifically, I'm looking for the height of the ion-navbar. Is this possible?
You can get the actual height of an element with element.offsetHeight.
As for the style property, it will give you only the attributes defined in the element's inline style attribute (e.g. <div style="height: 20px;">...</div>), not the ones applied by the CSS using selector rules. See this MDN article for more details.
This is my workaround for that.
let tabs = document.querySelectorAll('.show-tabbar');
if (tabs !== null) {
Object.keys(tabs).map((key) => {
tabs[key].style.transform = 'translateY(56px)';
});
}
I always have found it terribly useful to simply use
ionic serve
inspect the element in chrome and easily see the style for the given device.
To access the DOM element in angular I have used the #id route. The following snippet was used to verify the appropriate class was applied by ionic.
html- home.html
<ion-spinner #testspinner name="crescent" paused="{{isPaused}}"></ion-spinner>
ts- home.ts
import {Component} from '#angular/core';
import {ViewChild} from '#angular/core';
#Component({
selector: 'page-home',
templateUrl: 'home.html'
})
export class HomePage {
isPaused: boolean;
#ViewChild('testspinner') testspinner;
constructor() {
this.isPaused = false; // set to true will cause to never have animation state running.
}
containsPausedClass(list: string[]) {
return (list.indexOf('spinner-paused') != -1);
}
ngAfterViewInit() {
// if the spinner is allows to load without 'spinner-paused' then safari will work.
setTimeout(() => {
this.isPaused = true;
}, 0);
console.log('test spinner is paused ',
this.containsPausedClass(this.testspinner._elementRef.nativeElement.classList.value));
}
togglePause() {
this.isPaused = !this.isPaused;
setTimeout(() => {
console.log('test spinner is paused ',
this.containsPausedClass(this.testspinner._elementRef.nativeElement.classList.value));
}, 100);
}
}

Angular 2 Focus on first invalid input after Click/Event

I have an odd requirement and was hoping for some help.
I need to focus on the first found invalid input of a form after clicking a button (not submit). The form is rather large, and so the screen needs to scroll to the first invalid input.
This AngularJS answer would be what I would need, but didn't know if a directive like this would be the way to go in Angular 2:
Set focus on first invalid input in AngularJs form
What would be the Angular 2 way to do this? Thanks for all the help!
This works for me. Not the most elegant solution, but given the constraints in Angular we are all experiencing for this particular task, it does the job.
scrollTo(el: Element): void {
if(el) {
el.scrollIntoView({ behavior: 'smooth' });
}
}
scrollToError(): void {
const firstElementWithError = document.querySelector('.ng-invalid');
this.scrollTo(firstElementWithError);
}
async scrollIfFormHasErrors(form: FormGroup): Promise <any> {
await form.invalid;
this.scrollToError();
}
This works, allowing you to evade manipulating the DOM. It simply goes to the first element with .ng-invalid on the page through the document.querySelector() which returns the first element in the returned list.
To use it:
this.scrollIfFormHasErrors(this.form).then(() => {
// Run any additional functionality if you need to.
});
I also posted this on Angular's Github page: https://github.com/angular/angular/issues/13158#issuecomment-432275834
Unfortunately I can't test this at the moment, so might be a few bugs, but should be mostly there.
Just add it to your form.
import {Directive, Input, HostListener} from '#angular/core';
import {NgForm} from '#angular/forms';
#Directive({ selector: '[scrollToFirstInvalid]' })
export class ScrollToFirstInvalidDirective {
#Input('scrollToFirstInvalid') form: NgForm;
constructor() {
}
#HostListener('submit', ['$event'])
onSubmit(event) {
if(!this.form.valid) {
let target;
for (var i in this.form.controls) {
if(!this.form.controls[i].valid) {
target = this.form.controls[i];
break;
}
}
if(target) {
$('html,body').animate({scrollTop: $(target.nativeElement).offset().top}, 'slow');
}
}
}
}
If you are using AngularMaterial, the MdInputDirective has a focus() method which allow you to directly focus on the input field.
In your component, just get a reference to all the inputs with the #ViewChildren annotation, like this:
#ViewChildren(MdInputDirective) inputs: QueryList<MdInputDirective>;
Then, setting focus on the first invalid input is as simple as this:
this.inputs.find(input => !input._ngControl.valid).focus()
I don't know if this is valid approach or not but this is working great for me.
import { Directive, Input, HostListener, ElementRef } from '#angular/core';
import { NgForm } from '#angular/forms';
import * as $ from 'jquery';
#Directive({ selector: '[accessible-form]' })
export class AccessibleForm {
#Input('form') form: NgForm;
constructor(private el: ElementRef) {
}
#HostListener('submit', ['$event'])
onSubmit(event) {
event.preventDefault();
if (!this.form.valid) {
let target;
target = this.el.nativeElement.querySelector('.ng-invalid')
if (target) {
$('html,body').animate({ scrollTop: $(target).offset().top }, 'slow');
target.focus();
}
}
}
}
In HTML
<form [formGroup]="addUserForm" class="form mt-30" (ngSubmit)="updateUser(addUserForm)" accessible-form [form]="addUserForm"></form>
I have mixed the approach of angularjs accessible form directive in this.
Improvements are welcomed!!!
I've created an Angular directive to solve this problem. You can check it here ngx-scroll-to-first-invalid.
Steps:
1.Install the module:
npm i #ismaestro/ngx-scroll-to-first-invalid --save
2.Import the NgxScrollToFirstInvalidModule:
import {BrowserModule} from '#angular/platform-browser';
import {NgModule} from '#angular/core';
import {NgxScrollToFirstInvalidModule} from '#ismaestro/ngx-scroll-to-first-invalid';
#NgModule({
imports: [
BrowserModule,
NgxScrollToFirstInvalidModule
],
bootstrap: [AppComponent]
})
export class AppModule { }
3.Use the directive inside a form:
<form [formGroup]="testForm" ngxScrollToFirstInvalid>
<input id="test-input1" type="text" formControlName="someText1">
<button (click)="saveForm()"></button>
</form>
Hope it helps!
:)
Plain HTML solution.
If you don't need to be scrolling , just focus on first valid input, I use :
public submitForm() {
if(this.form.valid){
// submit form
} else {
let invalidFields = [].slice.call(document.getElementsByClassName('ng-invalid'));
invalidFields[1].focus();
}
}
This is for template driven form here. We focus on second element of invalidFields cuz first is the whole form which is invalid too.
For Angular Material ,
The below worked for me
#ViewChildren(MatInput) inputs: QueryList <MatInput>;
this.inputs.find(input => !input.ngControl.valid).focus();
I recommend putting this in a service, for me it worked like this:
if (this.form.valid) {
//submit
} else {
let control;
Object.keys(this.form.controls).reverse().forEach( (field) => {
if (this.form.get(field).invalid) {
control = this.form.get(field);
control.markAsDirty();
}
});
if(control) {
let el = $('.ng-invalid:not(form):first');
$('html,body').animate({scrollTop: (el.offset().top - 20)}, 'slow', () => {
el.focus();
});
}
}
#HostListener('submit', ['$event'])
onSubmit(event) {
event.preventDefault();
if (!this.checkoutForm.valid) {
let target;
target = $('input[type=text].ng-invalid').first();
if (target) {
$('html,body').animate({ scrollTop: $(target).offset().top }, 'slow', ()=> {
target.focus();
});
}
}
}

Angular 2 runOutsideAngular still change the UI

From my understanding of runOutsideAngular(), if I need to run something that won't trigger the Angular change detection, I need to use this function. My code is not working, however; when I click the button, the UI is changing and the number is 2.
#Component({selector: 'my-cmp',
template: `<h1>{{num}}</h1>
<button (click)="onClick()">Change number</button>`})
class MyComponent implements OnChanges {
num = 1;
constructor(private _ngZone: NgZone ) {
}
onClick() {
this._ngZone.runOutsideAngular(() => {
this.num = 2;
}}));
}
}
If anything is causing change detection, and a bound event like (click)="onClick()" does cause change detection, then Angular will detect the change.
runOutsideAngular doesn't mean Angular won't see the change, it only means that the code run this way doesn't cause change detection, but because the click event already does, it's meaningless in your example.
[In short] you need to change one line in your current code
onClick() {
this._ngZone.runOutsideAngular(() => {
setTimeout(()=>this.num = 2,0); // instead of this.num = 2;
}}));
}
now if you click the on the <button>, this.num will become 2, but you won't see any change in the UI (a temporary inconsistency between view and model)
[Explanation] without runOutsideAngular(), async functions like addEventListener() or setTimeout() behaves differently (monkey patched). their callbacks will try to update UI with Angular after running user's code.
For example, you can treat (click)="onClick()" as:
addEventListener("click",function modifiedCallback(){
onClick();
updateUIifModelChanges(); //call to Angular
})
In order to not triggered UI update we need to satisfy the following two conditions:
not modify model in function onClick (so, modify inside setTimeout())
when the model is indeed modified, do not invoke updateUIifModelChanges (call setTimeout() inside runOutsideAngular)
[More] of cause, the explanation I gave is a very very...simplified version of what happens. setTimeout() has the same function signature whether it's running inside runOutsideAngular() or not. The reason that it behaves differently is because it's running in a different Zone
If you want to prevent change detection then you can
1) subscribe on ngZone.onMicrotaskEmpty like this:
import { NgZone, ChangeDetectorRef } from '#angular/core';
import 'rxjs/add/operator/first';
...
export class MyComponent {
constructor(private ngZone: NgZone, private cdRef: ChangeDetectorRef) {}
onClick() {
// to do something
this.cdRef.detach();
this.ngZone.onMicrotaskEmpty.first().subscribe(() => {
// reattach changeDetector after application.tick()
this.cdRef.reattach();
});
}
}
This handler will run after Application.tick
See also Plunker Example
2) use custom directive like this:
#Directive({
selector: '[outSideEventHandler]'
})
class OutSideEventHandlerDirective {
private handler: Function;
#Input() event: string = 'click'; // pass desired event
#Output('outSideEventHandler') emitter = new EventEmitter();
constructor(private ngZone: NgZone, private elRef: ElementRef) {}
ngOnInit() {
this.ngZone.runOutsideAngular(() => {
this.handler = $event => this.emitter.emit($event);
this.elRef.nativeElement.addEventListener(this.event, this.handler);
});
}
ngOnDestory() {
this.elRef.nativeElement.removeEventListener(this.event, this.handler);
}
}
and then in template you can write:
<button (outSideEventHandler)="onClick()">Click outside zone</button>
or
<button event="mousedown" (outSideEventHandler)="onClick()">Click outside zone</button>
Plunker
3) write custom DOM event handler as described in this article.
https://medium.com/#TheLarkInn/creating-custom-dom-events-in-angular2-f326d348dc8b#.bx4uggfdy
Other solutions see here:
Angular 2 how to keep event from triggering digest loop/detection cycle?
Using ngZone.run is a bit better than the setTimeout solutions since it uses angular specific functionality. Run is meant to be used within ngZone.runOutsideAngular functions.
From the docs:
Running functions via run allows you to reenter Angular zone from a
task that was executed outside of the Angular zone (typically started
via {#link #runOutsideAngular}).
This is actually a very practical example of say a button that increments a number by one but only triggers change detection when the number is even.
#Component({selector: 'my-cmp',
template: `<h1>{{num}}</h1>
<button (click)="onClick()">Change number</button>`})
class MyComponent implements OnChanges {
num = 1;
constructor(private _ngZone: NgZone ) {
}
onClick() {
this._ngZone.runOutsideAngular(() => {
if(this.num % 2 === 0){
// modifying the state here wont trigger change.
this.num++;
} else{
this._ngZone.run(() => {
this.num++;
})
}
}}));
}
}
...
constructor(
private ngZone: NgZone
){
ngZone.runOutsideAngular(() => {
setInterval(()=>{
this.num= new Date().Format('yyyy-MM-dd HH:mm:ss');
},1000);
});
}
...
This is how I have tried to check the difference insideAngular and OutsideAngular
constructor(private zone: NgZone) { }
setProgressOutsideAngular() {
this.zone.runOutsideAngular(() => {
setInterval(() => { ++this.progress, console.log(this.progress) }, 500)
})
}
setProgressInsideAngular() {
this.zone.run(() => setInterval(() => { ++this.progress, console.log(this.progress) }, 500))
}

Categories

Resources