Wait for querySelectorAll - javascript

I am developing an app via Ionic Framework. I upgraded my app from Ionic 3 to Ionic 4. Now hyperlinks do not work anymore. The HTML content is loading dynamically based on the chosen page.
I've read I have to set new eventListeners for my clicks on my a elements.
I am trying:
ngOnInit()
{
this.objAnswerService.getAntworten(objFrage).then(arrAnswer =>
{
this.arrAnswers = arrAnswer;
}
}
ngAfterViewInit()
{
console.log('_enableDynamicHyperlinks');
this._enableDynamicHyperlinks();
}
private _enableDynamicHyperlinks()
{
let arrUrls = this._element.nativeElement.querySelectorAll('a');
console.log(JSON.stringify(arrUrls)); // No elements
console.log(arrUrls); // Correct elements
arrUrls.forEach((objUrl) =>{
console.log('do something'); // Never reached because 0 elements
});
}
answer.page.html
<div *ngIf="arrAnswers">
<div *ngFor="let objAnswer of arrAnswers"
class="antworten">
<div *ngIf="objAnswer"
class="antwort">
<div [innerHTML]="safeHtml(objAnswer.strText)"></div>
</div>
</div>
</div>
How can I wait for querySelectorAll() to find all existing elements?

since this.arrAnswers is initialized in a Promise it is undefined when the component fiirst loads. As a result of this <div *ngIf="arrAnswers"> evaluates to false and there are no elements for querySelectorAll to return on ngOnInit or ngAfterViewInit because they both gets called once during component lifecycle.
what you need is ngAfterViewChecked to be called when this.arrAnswers is initialized and dom is updated.
ngAfterViewChecked() {
console.log('_enableDynamicHyperlinks');
if (!this._element) return;
let arrUrls = this._element.nativeElement.querySelectorAll('p');
console.log("arrUrls.length:", arrUrls.length);
console.log("arrUrls:", arrUrls);
}
also do not forget to use { static: false } on #ViewChild as explained here.
here is a simple demo

The better way to handle this is using angular lifecycle hook.
If it doesn't work with ngOnInit() you can take a look at ngAfterViewInit()which respond after Angular initializes the component's views and child views / the view that a directive is in.
ngAfterViewInit() {
let arrUrls = this._element.nativeElement.querySelectorAll('a');
console.log(JSON.stringify(arrUrls)); // No elements
console.log(arrUrls); // Correct elements
arrUrls.forEach((objUrl) =>{
console.log('do smthng'); //Never reached because 0 elements
});
}

Related

How to bind a react hook to an element created by jquery function

I'm working with a project written in jquery and I want to integrate this code in my react project. I'm trying to send a query to my graphql server by clicking a button.
There is a JQ function that creates multiple elements like this:
canvas.append($(`<div class="d-flex flex-wrap my-2"><img class="else_picture" src="${elem.artworkUrl600}" height="150px"><div class="mx-2"><div class="elseTitle" id="${elem.trackId}">${elem.trackName}</div><h6>by:${elem.artistName}</h6><h6>keywords:</h6><p>${elem.genres}</p><div class="d-flex justify-content-start"><button value="${elem.feedUrl}" class="get_description"><a target="_blank" href="${elem.trackViewUrl}">Info link</a></i></button><div class="like_box"><i id="like" class="fa fa-thumbs-up"></div></div></div><hr>`)
);
My goal is to connect a function "onClick()" to all buttons inside these created elements. For that I'm trying to define a function that would query all elements by id and connect it to a function with a hook to my graphql server:
function connectFunctionalityToLike(){
$("#like").on("click", function(){ Like(this); });
}
function Like(x){
console.log("clicked");
const { loading, error, data } = useQuery(ME_QUERY);
if (loading) return 'Loading...';
if (error) return `Error! ${error.message}`;
console.log(data);
}
My function Like() does not really work because I'm missing in understanding how elements in react actually work all together with jquery. I cannot rewrite code in JQuery. I just want to integrate this existing code. Is there any way around connecting react hooks to created elements by id?
Is there
You should create a React component wrapper for the JQuery function.
You will have to manage the React lifecycle manually - by calling the JQuery function in a useEffect() callback and taking care to remove the DOM elements in the cleanup:
function ReactWrapper() {
const { loading, error, data } = useQuery(ME_QUERY);
const [ data, setData ] = React.useState();
React.useEffect(() => {
jqueryFn();
$('#like').on('click', function(){ setData('from click') });
return () => {
/* code that removes the DOM elements created by jqueryFn() */
};
}, []);
if (loading) return 'Loading...';
if (error) return `Error! ${error.message}`;
return <div>{data}</div>;
}
Then, in this React component, will be allowed to use hooks.
Also, if your JQuery function is so simple, you are probably much better off with rewriting it in React.
As Andy says in his comment: "You shouldn't be mixing jQuery and React." - But, we've all be in spots where we are given no choice. If you have no choice but to use jQuery along side React, the code in the following example may be helpful.
Example is slightly different (no canvas, etc), but you should be able to see what you need.
This example goes beyond your question and anticipates the next - "How can I access and use the data from that [card, div, section, etc] that was added by jQuery?"
Here is a working StackBlitz EDIT - fixed link
Please see the comments in code:
// jQuery Addition - happens outside the React App
$('body').append(
$(`<div class="d-flex flex-wrap my-2"><img class="else_picture" src="${elem.artworkUrl600}" height="10px"><div class="mx-2"><div class="elseTitle" id="${elem.trackId}">${elem.trackName}</div><h6>by:${elem.artistName}</h6><h6>keywords:</h6><p>${elem.genres}</p><div class="d-flex justify-content-start"><button value="${elem.feedUrl}" class="get_description"><a target="_blank" href="${elem.trackViewUrl}">Info link</a></i></button><div class="like_box"><i id="like" class="fa fa-thumbs-up"></div></div></div><hr>`)
);
export default function App() {
// state to hold the like data
const [likes, setLikes] = useState([]);
// create reference to the jquery element of importance
// This ref will reference a jQuery object in .current
const ref = useRef($('.elseTitle'));
// Like function
function Like(x) {
// ref.current is a jQuery object so use .text() method to retrieve textContent
console.log(ref.current.text(), 'hit Like function');
setLikes([...likes, ' :: ' + ref.current.text()]);
}
// This is where you put
useEffect(() => {
// Add event listener and react
$('#like').on('click', function () {
Like(this);
});
// Remove event listener
return () => $('#like').off('click');
});
return (
<div>
<h1>Likes!</h1>
<p>{likes}</p>
</div>
);
}

Angular how to get height of dynamically created div and send it to sibling component?

I have one parent and 2 child components like below
Parent-component.html
<child-component-1 [id]="id"></child-component-1>
<child-component-2></child-component-2>
child-component-1 has ngb-carousel component to show warnings and alerts to user.And this component is being created due to id and its result comes from API like below
child-component-1.ts
alertFound;
this.service.checkAlert(id).subscribe((result:any)=>{
if(result.alertFound){
this.alertFound=true;
}
})
child-component-1.html
<ngb-carousel #carousel *ngIf="alertFound" class="carousel-alert" [interval]="false" [wrap]="false"
[showNavigationIndicators]="false">
........................(Things go on here after result comes)
</ngb-carousel>
In child-component-2,I need to get the height of this ngb-alert to make dynamic height calculation on the screen.If it exists,I need to subtract it from window.innerheight.
child-component-2.html
<div [style.height.px]="actualHeight"></div>
child-component-2.ts
this.actualHeight = window.innerHeight - 269;
Interestingly on child-component-1 when I tried to track it like below
#ViewChild('carousel', {read: ElementRef, static:false}) elementView: ElementRef;
ngAfterViewInit(){
console.log(this.elementView.nativeElement.offSetHeight);
//throws height of created carousel first,then if next id doesnt have alert still shows me the height which previously created
}
it shows previously created carousel height even though result is false and carousel couldn't be created at that moment.I guess this happening because result sometimes arrives very late,sometimes fast.To listen detections on viewchild I found something like below which works exactly as I wanted and solved emitting non-created alert height problem
#ViewChildren('carousel', {read: ViewContainerRef}) viewContainerRefs;
ngAfterViewInit() {
this.viewContainerRefs.changes.subscribe(item => {
if (item) {
console.log(item._results[0]._lContainer[0][0].offsetHeight);
}
})
}
Now problem is sending this height to child-component-2 where I should calculate height dynamically.
I considered 2 options.First one is creating subject on a service,emitting height to it from child-component-1 and listening it on child-component-2 like below
service.ts
alertActive = new BehaviorSubject(0);
setAlertHeight(value:number){
this.alertActive.next(value)
}
child-component-1
ngAfterViewInit() {
this.viewContainerRefs.changes.subscribe(item => {
if (item) {
this.service.setAlertHeight(item._results[0]._lContainer[0][0].offsetHeight);
}
})
}
child-component-2
this.alertActive.subscribe(value=>{
if(value){
this.actualHeight-=value;
}
})
On the example above,it causes the problem to bring height,even though alert not created.I logged subscribes on console from child-2 and child-1 and noticed that child-2 prints to console even though child1 didnt emit anything to it.
So I considered another option to send height from child-1 to parent via #Output and then via Input to
child2 like below
child-1
#Output() transferHeight: EventEmitter<any> = new EventEmitter();
this.viewContainerRefs.changes.subscribe(item => {
if (item) {
this.transferHeight.emit(item._results[0]._lContainer[0][0].offsetHeight);
}
})
parent
<child-component-1 (transferHeight)="transferedHeight($event)" [id]="id"></child-component-1>
<child-component-2 [transferredHeight]="transferredHeight"></child-component-2>
alertHeight;
transferedHeight(comingHeight){
this.alertHeight=comingHeight;
}
child-2
#Input() transferredHeight;
ngOnInit(){
this.actualHeight-=this.transferredHeight;
}
this one really handles the problem that I mentioned previously.But if carousel created after child-2 created due to late network resut ,it returns undefined.Therefore I tried to use ngOnChanges to listen changes on the Input variable like below
ngOnChanges(changes: SimpleChanges) {
console.log(changes.transferredHeight.currentValue)
}
this console.log doesnt print anything although I implemented onChanges.So Im waiting for your help.If there's a way to listen viewContainerRefs.changes of child-1 from child-2 would be the best case
The reason this attempt:
alertActive = new BehaviorSubject(0);
setAlertHeight(value:number){
this.alertActive.next(value)
}
triggered the height to be set even when the alert was not active was because you used a BehaviorSubject rather than just a Subject. A BehaviorSubject provides an initial value (0 in your case), so immediately upon subscription the subscriber will receive that initial value (or the latest emitted value).
So you were on the right track, you just needed to use a regular Subject instead because then subscribers won't receive any values until the Subject emits.
So your service could look like this:
private _alertActive = new Subject<number>();
get alertActive(): Observable<number> {
return this._alertActive.asObservable();
}
setAlertHeight(height: number) {
this._alertActive.next(height);
}
(Note that the Subject is a private member but is exposed via a getter as an Observable. In general, subscribers shouldn't have access to the raw Subject unless they'll also be emitting values from it.)
And your child-2 component would subscribe to it like so:
this.service.alertActive.subscribe((height: number) => {
this.actualHeight -= height;
});
You were also on the right track using ViewChildren and subscribing to its changes, but it might be simpler if you reference the ElementRefs rather than the ViewContainerRefs. That would look like this:
import { NgbCarousel } from "#ng-bootstrap/ng-bootstrap";
#ViewChildren(NgbCarousel, { read: ElementRef }) carousels: QueryList<ElementRef>;
ngAfterViewInit() {
this.carousels.changes.subscribe((els: QueryList<ElementRef>) => {
const height = els.first ? els.first.nativeElement.offsetHeight : 0;
this.service.setAlertHeight(height);
});
}
Here's a StackBlitz showing it working with this approach.

Vue and Prismic rich text: add event listener to a span node

The content of my Vue app is fetched from Prismic (an API CMS). I have a rich text block, some parts of which are wrapped inside span tags with a specific class. I want to get those span nodes with Vue and add to them an event listener.
With JS, this code would work:
var selectedSpanElements = document.querySelectorAll('.className');
selectedSpanElements[0].style.color = "red"
But when I use this code in Vue, I can see that it works just a fraction of a second before Vue updates the DOM. I've tried using this code on mounted, beforeupdate, updated, ready hooks... Nothing has worked.
Update: Some hours later, I found that with the HTMLSerializer I can add HTML code to the span tag. But this is regular HTML, I cannot access to Vue methods.
#Bruja
I was able to find a solution using a closure. The folks at Prismic reminded/showed me.
Of note, per Phil Snow's comment above: If you are using Nuxt you won't have access to Vue's functionality and will have to go old-school JS.
Here is an example where you can pass in component-level props, data, methods, etc... to the prismic htmlSerializer:
<template>
<div>
<prismic-rich-text
:field="data"
:htmlSerializer="anotherHtmlSerializer((startNumber = list.start_number))"
/>
</div>
</template>
import prismicDOM from 'prismic-dom';
export default {
methods: {
anotherHtmlSerializer(startNumber = 1) {
const Elements = prismicDOM.RichText.Elements;
const that = this;
return function(type, element, content, children) {
// To add more elements and customizations use this as a reference:
// https://prismic.io/docs/vuejs/beyond-the-api/html-serializer
that.testMethod(startNumber);
switch (type) {
case Elements.oList:
return `<ol start=${startNumber}>${children.join('')}</ol>`;
}
// Return null to stick with the default behavior for everything else
return null;
};
},
testMethod(startNumber) {
console.log('test method here');
console.log(startNumber);
}
}
};
I believe you are on the right track looking into the HTML Serializer. If you want all your .specialClass <span> elements to trigger a click event that calls specialmethod() this should work for you:
import prismicDOM from 'prismic-dom';
const Elements = prismicDOM.RichText.Elements;
export default function (type, element, content, children) {
// I'm not 100% sure if element.className is correct, investigate with your devTools if it doesn't work
if (type === Elements.span && element.className === "specialClass") {
return `<span #click="specialMethod">${content}</span>`;
}
// Return null to stick with the default behavior for everything else
return null;
};

Can you pass an element to a function within the template in Vue?

I'm trying to calculate and set an element's max-height style programmatically based on the number of children it has. I have to do this on four separate elements, each with a different number of children, so I can't just create a single computed property. I already have the logic to calculate the max-height in the function, but I'm unable to pass an element from the template into a function.
I've tried the following solutions with no luck:
<div ref="div1" :style="{ maxHeight: getMaxHeight($refs.div1) }"></div>
This didn't work because $refs is not yet defined at the time I'm passing it into the function.
Trying to pass this or $event.target to getMaxHeight(). This didn't work either because this doesn't refer to the current element, and there was no event since I'm not in a v-on event handler.
The only other solution I can think of is creating four computed properties that each call getMaxHeight() with the $ref, but if I can handle it from a single function called with different params, it would be easier to maintain. If possible, I would like to pass the element itself from the template. Does anyone know of a way to do this, or a more elegant approach to solving this problem?
A cheap trick I learned with Vue is that if you require anything in the template that isnt loaded when the template is mounted is to just put a template with a v-if on it:
<template v-if="$refs">
<div ref="div1" :style="{ maxHeight: getMaxHeight($refs.div1) }"></div>
</template>
around it. This might look dirty at first, but the thing is, it does the job without loads of extra code and time spend and prevents the errors.
Also, a small improvement in code length on your expandable-function:
const expandable = el => el.style.maxHeight =
( el.classList.contains('expanded') ?
el.children.map(c=>c.scrollHeight).reduce((h1,h2)=>h1+h2)
: 0 ) + 'px';
I ended up creating a directive like was suggested. It tries to expand/compress when:
It's clicked
Its classes change
The element or its children update
Vue component:
<button #click="toggleAccordion($event.currentTarget.nextElementSibling)"></button>
<div #click="toggleAccordion($event.currentTarget)" v-accordion-toggle>
<myComponent v-for="data in dataList" :data="data"></myComponent>
</div>
.....
private toggleAccordion(elem: HTMLElement): void {
elem.classList.toggle("expanded");
}
Directive: Accordion.ts
const expandable = (el: HTMLElement) => el.style.maxHeight = (el.classList.contains("expanded") ?
[...el.children].map(c => c.scrollHeight).reduce((h1, h2) => h1 + h2) : "0") + "px";
Vue.directive("accordion-toggle", {
bind: (el: HTMLElement, binding: any, vnode: any) => {
el.onclick = ($event: any) => {
expandable($event.currentTarget) ; // When the element is clicked
};
// If the classes on the elem change, like another button adding .expanded class
const observer = new MutationObserver(() => expandable(el));
observer.observe(el, {
attributes: true,
attributeFilter: ["class"],
});
},
componentUpdated: (el: HTMLElement) => {
expandable(el); // When the component (or its children) update
}
});
Making a custom directive that operates directly on the div element would probably be your best shot. You could create a directive component like:
export default {
name: 'maxheight',
bind(el) {
const numberOfChildren = el.children.length;
// rest of your max height logic here
el.style.maxHeight = '100px';
}
}
Then just make sure to import the directive in the file you plan on using it, and add it to your div element:
<div ref="div1" maxheight></div>

VueJs manipulate inline template and reinitialize it

this question is similar to VueJS re-compile HTML in an inline-template component and also to How to make Vue js directive working in an appended html element
Unfortunately the solution in that question can't be used anymore for the current VueJS implementation as $compile was removed.
My use case is the following:
I have to use third party code which manipulates the page and fires an event afterwards. Now after that event was fired I would like to let VueJS know that it should reinitialize the current DOM.
(The third party which is written in pure javascript allows an user to add new widgets to a page)
https://jsfiddle.net/5y8c0u2k/
HTML
<div id="app">
<my-input inline-template>
<div class="wrapper">
My inline template<br>
<input v-model="value">
<my-element inline-template :value="value">
<button v-text="value" #click="click"></button>
</my-element>
</div>
</my-input>
</div>
Javascript - VueJS 2.2
Vue.component('my-input', {
data() {
return {
value: 1000
};
}
});
Vue.component('my-element', {
props: {
value: String
},
methods: {
click() {
console.log('Clicked the button');
}
}
});
new Vue({
el: '#app',
});
// Pseudo code
setInterval(() => {
// Third party library adds html:
var newContent = document.createElement('div');
newContent.innerHTML = `<my-element inline-template :value="value">
<button v-text="value" #click="click"></button>
</my-element>`; document.querySelector('.wrapper').appendChild(newContent)
//
// How would I now reinialize the app or
// the wrapping component to use the click handler and value?
//
}, 5000)
After further investigation I reached out to the VueJs team and got the feedback that the following approach could be a valid solution:
/**
* Content change handler
*/
function handleContentChange() {
const inlineTemplates = document.querySelector('[inline-template]');
for (var inlineTemplate of inlineTemplates) {
processNewElement(inlineTemplate);
}
}
/**
* Tell vue to initialize a new element
*/
function processNewElement(element) {
const vue = getClosestVueInstance(element);
new Vue({
el: element,
data: vue.$data
});
}
/**
* Returns the __vue__ instance of the next element up the dom tree
*/
function getClosestVueInstance(element) {
if (element) {
return element.__vue__ || getClosestVueInstance(element.parentElement);
}
}
You can try it in the following fiddle
Generally when I hear questions like this, they seem to always be resolved by using some of Vue's more intimate and obscured inner beauty :)
I have used quite a few third party libs that 'insist on owning the data', which they use to modify the DOM - but if you can use these events, you can proxy the changes to a Vue owned object - or, if you can't have a vue-owned object, you can observe an independent data structure through computed properties.
window.someObjectINeedtoObserve = {...}
yourLib.on('someEvent', (data) => {
// affect someObjectINeedtoObserve...
})
new Vue ({
// ...
computed: {
myObject () {
// object now observed and bound and the dom will react to changes
return window.someObjectINeedtoObserve
}
}
})
If you could clarify the use case and libraries, we might be able to help more.

Categories

Resources