Im currently struggling with something where i want to add a data attribute to a component and then based on when NUXT is loaded have a click event bind to all nodes that have this data attribute. Im not using v-on because i want to have this separated from current Vue logic. So example:
component a.vue is tagged with data-element
<a href="/somelink" data-element>link</a>
component b.vue also has HTM elements tagged with data-element
<button data-element>link</button>
When the app loads i need to then loop through all data-element and bind an eventlistner to them.
I tried the above method and that works to some degree but fails when reactivity sets in and the DOM is updated. I checked mixins (not recommended using VueJS3), used composition API, used a mutation observer that checked the DOM status for changes and based on new elements loaded it ads click events, looked at hooks etc but now im getting confused at what is the best way to proceed. Some solutions work to some extend but feels hacky. Or is there a completely different approach i am missing.
To be Sure that the DOM has been initialized you need to set your event listeners on "mounted" lifecycle
mounted() {
const elements = [...document.querySelectorAll('[data-element]')];
elements.forEach(element => {
element.addEventListener('click', () => { /* your code here. */ })
})
}
Related
With React I'm inside of one repository, and the HTML elements are loading from another repo, which I watch for using pageLoaded. Inside updateHeader there is just more HTML element selecting and attribute/class manipulation.
useEffect(() => {
if (pageLoaded.status) {
if (someCondition) {
updateHeader(ubeName);
} else {
document.querySelector('.headerbar-menu').style.display = 'block';
if (document.querySelector('.headerbar-menu.affiliates-wrapper')) {
document.querySelector('.headerbar-menu.affiliates-wrapper').style.display = 'none';
}
}
}
}, [pageLoaded.status])
The problem here is obviously we shouldn't be using querySelector, and i think it may be causing some unexpected functionality. The elements dont properly evaluate and render until some piece of state changes i.e. a scroll state handler, so on initial page load the elements dont show with their new attributes until a scroll.
I'd like to attempt to resolve using useRef, but don't have direct access to the html. Is there a way to dynamically connect a ref to an element without access to the HTML code?
Thanks for your time and attention!
The best way to do this, in my opinion, is in this sequence:
Attempt to select the element
If non-existant, set up a DOMSubtreeModified event handler or a MutationObserver
Clean up DOMSubtreeModified event handler or a MutationObserver (or keep it around to watch for updates)
This allows for use of query selectors in a safe and modern way which doesn't stray too far from React's recommendations
We are using single file vue components and in a mousemove event handler we'd like to be able to detect if the target element is clickable.
In our Vue templates we are using v-on directives: v-on:click="someCallback".
Unfortunately there doesn't seem to be an easy way to tell for a given element if an event listener was registered for it (i.e. via v-on directive).
For this we'd like to add a custom attribute to those elements with a v-on:click directve - i.e. "clickable". But this should happen automatically.
So we'd have to either wrap Vue's own "on"-directive into a custom one or somehow hook into Vue's rendering cycle - but this seems not very straight-forward: Couldn't find the directive on the Vue instance or the Vue component object.
What we have tried:
retrieving the information about registered listeners from the target element provided by the event object passed to the event handler. But apparently browsers don't provide this information.
searching the Vue component object for some object that stores information about which event listener has been registered for which element with which handler. We were not able to find this information - but it should be somewhere, right?
Hope anyone has a nice idea on how to accomplish adding a custom attribute to elements with v-on:click directive automatically.
Thanks!
EDIT:
So we have i.e.
<div id="x" #click="someMethod" />
in our template.
But we want to add a custom attribute automatically (we dont want to add it manually for all the trillion cases):
<div id="x" clickable #click="someMethod" />
Then in the event handler for addEventListener('mousemove', handler) we would check for this attribute: if (e.target.hasAttribute('clickable'))
But any other way of accomplishing this (so being able to tell inside the handler for mousemove if the element is clickable) would be fine too.
You could create a container component and import it into all your other vue components, ensuring it's the first component in your template, like:
<template>
<v-container>
// your template here
</v-container>
</template>
<script>
// Obviously replace the path and point to your location of the component
import ComponentContainer from './common/ComponentContainer.vue'
export default {
name: 'MyClientComponent',
components: {
'v-container': ComponentContainer
}
}
</script>
And this is the container component that looks for click events and adds the clickable class:
<template>
<div class="component-container">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'ComponentContainer',
mounted() {
this.$slots.default.forEach(vn => {
this.addClickableClassNames(vn);
});
},
methods: {
addClickableClassNames(vnode) {
if (vnode) {
let data = vnode.data;
if (data && data.on) {
// Check for click events and add a
// clickable class if one exists
if (data.on.click && vnode.elm && vnode.elm.classList) {
vnode.elm.classList.add('clickable');
}
}
// Now recursively check children
if (vnode.children) {
vnode.children.forEach(vn => {
this.addClickableClassNames(vn);
});
}
}
}
}
}
</script>
This works, but I wouldn't like to comment on the performance of a large dom. And you, and other devs, need to remember to import into all components, which isn't ideal. But, it's a solution that might give you other ideas on improving and making more scalable.
I can't think of a way to "automatically" add this clickable attribute. I think unfortunately you will still need to "tag" your clickable elements one by one.
I would have a directive which you can add to any element in your templates.
Directive
Vue.directive('myDirective', {
inserted(el, bindings) {
el.addEventListener('mouseover', () => {
alert(bindings.value);
})
}
});
Usage
<span v-my-directive="true">Element 1</span>
<span v-my-directive="false">Element 2</span>
You will notice that in the template when using the directive a value is being passed to it. This is then read via bindings.value. Of course based on this value you can do whatever functionality you need.
JSFiddle: https://jsfiddle.net/tc45xf82/2/
Vue Docs: https://v2.vuejs.org/v2/guide/custom-directive.html
Is there a way to access the HTML element that rendered by JS plugin in React JS Ecosystem. I can't use ref on those elements.
for example:- If I use React Slick plugin, It adds many HTML elements that can't access by React JS. I need to trigger a mouse hover event on those elements.
Does anyone have a solution for this kind of situation?
Thanks.
Suppose the id of the container div that wraps the <Slider/> component is called container then the following code should work in attaching the mouseover event to all the nodes (divs in the slider):
componentDidMount(){
let nodes = document.getElementsById("container").childNodes[0].childNodes
nodes.forEach(node => {
node.addEventListener('mouseover', () => {
//action to be performed here
}
})
}
maybe it will help
componentdidmount{
const elemnts = [...document.getElemetntByClassName('ClassName')]
elements.forEach(e => e.addEventListener('mouseover', () => {
// your code here
}))
}
Plugins has their own class and id. You can always inspect and customize the class they are using. But this may cause global changes. What you can do is to use custom class and selectors for separate components. Then you can make any changes in the css.
I'm trying to implement this (https://codepen.io/iprodev/pen/azpWBr) in Vue. How can I do that?
I have tried to make implement this like this
<template>
<div>
<canvas height="100" id="confetti" width="100"></canvas>
</div>
</template>
<script>
export default {
created () {
// JS code from codepen
}
}
</script>
Certainly, it is doable. There are a few things you need to do. All the code in the Codepen snippet is wrapped inside DOMContentLoaded event handler since you need access to actual DOM tree. With Vue, you cannot use this event as Vue applications are SPA and the loaded event will be fired much before your actual view is getting rendered.
In this case, you should use mounted event instead of created lifecycle event of the component. mounted ensures that component's DOM is actually attached to the main document.
Further, you will need access to the actual HTMLCanvasElement. Instead of locating it by ID, your Vue.js $refs construct like this:
<canvas ref="confetti" height="100" width="100"></canvas>
Inside your component, you can then access the DOM element as:
mounted() {
this.$refs.confetti // Reference to HTMLCanvasElement
}
This is how you avoid using global id attributes with Vue.js. Now rest of the code is just how you want to organize it inside your component. You can abstract canvas rendering logic into a separate module and pass Canvas element or the methods can become part of Vue instance.
Third and final. There are a couple of event handlers assigned on window object like resize. You have to ensure that you are cleaning them up when the component is getting destroyed. If you don't do this, then even if the component is destroyed, these events will continue to trigger handlers. For this use beforeDestroy lifecycle event provided by Vue. Clean up all your globally registered event handlers.
Right now, I'm binding events to the parent element of my custom tag's rendered content, then using classes to target the event onto the element which my custom tag actually renders. I feel this is likely to cause strange bugs. For instance, if anyone on my team places two custom tags using the same targeting-classes under the same immediate parent element, it would cause multiple events to fire, associated with the wrong elements.
Here's a sample of the code I'm using now:
$.views.tags({
toggleProp: {
template: '<span class="toggle">{{include tmpl=#content/}}</span>',
onAfterLink: function () {
var prop = this.tagCtx.view.data;
$(this.parentElem).on('click', '.toggle', function () {
prop.value(!prop.value());
});
},
onDispose: function () {
$(this.parentElem).off('click', '.toggle');
}
}
// ... other custom tags simply follow the same pattern ...
});
By the time we hit onAfterLink, is there any reliable way to access the rendered DOM Element (or DOM Elements) corresponding to the custom tag itself? With no risk of hitting the wrong element by mistake? I understand that the custom tag may be text without an HTML Element, but it would still be a text node, right? (Could I even bind events to text nodes?)
In other places, and using (far) older versions of JsViews, I've bound events after the render using (sometimes a lot of) targeting logic built into the rendered elements as data- attributes. Not only is this a far more fragile method than I like for accessing the rendered data, it would be incredibly risky and convoluted to try to apply this approach to some of our deeply-nested-and-collection-ridden templates.
I also don't like needing to insert a span with my custom tag, just so I can apply classes to it, but if it's still necessary for the event, I'll cope.
I ask, then, what is a safe, modular way to bind events to the DOM so that I also have access to the data rendered directly against those elements?
Edit: As an additional concern, using onAfterLink won't let me bind events to non-data-linked rendered content. This may be part of the design intent of JsViews vs pure JsRender, but I don't yet understand why that would be the case.
Rather than using this.parentElem, you can use
this.contents()
which is a jQuery object containing all immediate content elements within the tag.
You can also provide a selector argument,
this.contents("someselector")
to "filter" , and include an optional boolean "deep" flag to both "filter" and "find" - i.e.
this.contents("someselector", true).
Using the above APIs ensures you are only taking elements that are actually within the tag content.
You may not need to remove the handlers in onDispose, if the tag is only deleted along with its content, you can rely on the fact that jQuery will dispose handlers when the elements are removed from the DOM.
You can only attach events to elements, not to text nodes. So if your content does not include elements, you would need to add your wrapper element, but not otherwise.
$.views.tags({
toggleProp: {
template: '{{include tmpl=#content/}}',
onAfterLink: function () {
var prop = this.tagCtx.view.data;
this.contents().on('click', function () {
prop.value(!prop.value());
});
},
onDispose: function () {
this.contents().off('click');
}
}
});
Also take a look at samples such as http://www.jsviews.com/#samples/tagcontrols/tabs which use the above approach.