Since there's no IntersectionObserver library for Vue 3, I'd like to implement my own little solution.
I read about directives, and from what I understand it's the right direction. (?)
My local directive:
directives: {
'lazyload': {
mounted(el) {
if ('IntersectionObserver' in window) {
let intersectionObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const lazyImage = entry.target;
// set data-srcset as image srcset
lazyImage.srcset = lazyImage.getAttribute('data-srcset');
// add class after image has loaded
lazyImage.addEventListener('load', () => {
lazyImage.classList.add('is-lazyloaded');
};
// unobserve after
lazyLoadItemObserver.unobserve(lazyImage);
}
});
});
// observe every image
const lazyLoadItems = document.querySelectorAll('[lazyload]')
lazyLoadItems.forEach((lazyImage) => {
lazyLoadItemObserver.observe(lazyImage);
});
}
}
}
}
The vanilla way would be to make an array out of every <IMG> element that has the attribute lazyload for instance. The thing I don't get is how to make an array out of every <IMG> that has the v-lazyload binding.
Something like "if this image has the v-lazyload binding, put it into the IntersectionObserver's array." I might understand it wrong though.
So I'd like to come up with a directive which sets one single IntersectionObserver which observes an array of all the images that have the v-lazyload binding.
The v-lazyload directive will be on the target elements already, so no need to query the document.
In the directive's mounted hook, you can attach an IntersectionObserver instance to the parent node if it doesn't already exist. Then, use that instance to observe the target element (also unobserve the element in unmounted):
directives: {
mounted(el) {
el.parentNode.lazyLoadItemObserver = el.parentNode.lazyLoadItemObserver || new IntersectionObserver(/*...*/)
el.parentNode.lazyLoadItemObserver.observe(el)
},
unmounted(el) {
el.parentNode.lazyLoadItemObserver.unobserve(el)
},
}
demo
Related
I am using a custom v-directive "v-clip" and it requires a value. In my case i want to use 'data binding'. Which means the value for v-clip can change on the fly from other interactions on the website.
In this case I've implemented a simple example where every time a user clicks a button called 'Counter' it increments the value by 1. How can I retrieve the most up to date value when the user clicks on the custom directive which prints to the console.
Is there a way i can use vnode or something to retrieve the value. I would imagine the directive would be able to someone get the updated value.
clip directive
Vue.directive('clip', {
bind: (el, binding, vnode) => {
const clickEventHandler = (event) => {
console.log(binding.value)
}
el.addEventListener('click', clickEventHandler)
},
})
It's used like this where the variable counter is dynamically changed when a button is clicked in the ui.
<div v-clip="counter">Clip A {{ counter }}</div>
From Vue.js 2 guide:
If you need to share information across hooks, it is recommended to do so through element’s dataset.
So, if I understand correctly your question, you can try in the following way:
Vue.directive('clip', {
bind: (el, binding) => {
const clickEventHandler = () => {
console.log(el.getAttribute('data-clipvalue'))
}
el.addEventListener('click', clickEventHandler)
el.setAttribute('data-clipvalue',binding.value)
},
update: (el,binding) => {
el.setAttribute('data-clipvalue',binding.value)
}
})
I am running into an issue trying to integrate a third party product tour (Intercom) with a react application. There is no way to programmatically end a tour that I have found.
Basically, I need a prop that can change inside the react app whenever a certain non-react DOM element exists or not. I need to be able to tell in a hook or in componentDidUpdate whether or not a certain non-React element exists in the DOM.
I am not sure what to do because obviously when this tour opens and closes there is no change to state or props as far as react is concerned.
Is there a way I can wrap a component with the result of something like document.getElementById("Id-of-the-product-tour-overlay") as a prop? Is there a way I can watch for it with a hook?
Ideally something like
componentDidUpdate(){
if(elementExists){
//Do stuff that needs to happen while tour is on
}
if(!elementExists){
//do app stuff to end the tour
}
}
//OR
useEffect(()=>{
//do stuff conditional on element's existence
},[elementExists])
The easy way of doing so is to prepare a funcion that receives an HTML element and returns a function that receives a callback as an argument (function that returns other function - currying for purity). The result of the returned function is a new MutationObserver with the callback set.
const observeTarget = target => callback => {
const mutationObserver = new MutationObserver(callback);
mutationObserver.observe(target, { childList: true });
}
In non-react file you can feed this function with an HTML element that is a container of 3rd party element which you want to investigate.
Then export the function and you can use it in a react component.
export const observeProductTourOverlay = observeTarget(containerOfProductTourOverlay);
Then in a React component, you can use useEffect hook and use the function
const checkIFMyCompExists = () => !!document.querySelector("#my-component");
export const FromOutside = () => {
const [elementExists, setElementExist] = useState(checkIFMyCompExists());
const [indicator, setIndicator] = useState(3);
useEffect(() => {
observeProductTourOverlay((mutationRecord, observer) => {
const doesExist = checkIFMyCompExists();
setElementExist(doesExist);
// this will fire every time something inside container changes
// (i.e. a child is added or removed)
});
// garbage collector should take care of mutationObserver in a way there are no memory leaks, so no need to disconnect it on compoment unmouting.
}, []);
useEffect(() => {
setIndicator(elementExists);
//do stuff when elementExistance changes
}, [elementExists]);
return (
<div>
<div>{"my component has been added: " + indicator}</div>
</div>
);
};
Find the working demo here: https://codesandbox.io/s/intelligent-morning-v1ndx
Could you use a while loop?
useEffect(()=>{
while (document.getElementById('theTour') !== null) {
// do stuff
}
// do cleanup
})
I need to add class name to some Vue components using their ref names. The ref names are defined in a config file. I would like to do it dynamically, to avoid adding class manually on each Vue component.
I have tried to find each component using $refs and if found, add the class name to the element's class list. The class is added, but it is removed as soon as user interaction begins (e.g. the component is clicked, receives new value etc.)
Here is some sample code I've tried:
beforeCreate() {
let requiredFields = config.requiredFields
this.$nextTick(() => {
requiredFields.forEach(field => {
if(this.$refs[field]) {
this.$refs[field].$el.classList.add('my-class')
}
})
})
}
You can use this:
this.$refs[field].$el.classList.value = this.$refs[field].$el.classList.value + 'my-class'
the only thing that you need to make sure of is that your config.requiredFields must include the ref name as a string and nothing more or less ... you can achieve that with :
//for each ref you have
for (let ref in this.$refs) {
config.requiredFields.push(ref)
}
// so config.requiredFields will look like this : ['one','two]
here is an example of a working sample :
Vue.config.devtools = false;
Vue.config.productionTip = false;
Vue.component('one', {
template: '<p>component number one</p>'
})
Vue.component('two', {
template: '<p>component number two</p>'
})
new Vue({
el: "#app",
beforeCreate() {
let requiredFields = ['one','two'] // config.requiredFields should be like this
this.$nextTick(() => {
requiredFields.forEach(field => {
if(this.$refs[field]) {
this.$refs[field].$el.classList.add('my-class')
}
})
})
}
})
.my-class {
color : red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<one ref="one" ></one>
<two ref="two" ></two>
</div>
I know this question was posted ages ago, but I was playing around with something similar and came across a much easier way to add a class to $refs.
When we reference this.$refs['some-ref'].$el.classList it becomes a DOMTokenList which has a bunch of methods and properties you can access.
In this instance, to add a class it is as simple as
this.$refs['some-ref'].$el.classList.add('some-class')
You've to make sure classList.value is an array. By default its a string.
methods: {
onClick(ref) {
const activeClass = 'active-submenu'
if (!this.$refs[ref].classList.length) {
this.$refs[ref].classList.value = [activeClass]
} else {
this.$refs[ref].classList.value = ''
}
},
},
this post helped me tremendously. I needed to target an element within a v-for loop and I ended up writing a little method for it (i'm using Quasar/Vue).
hopefully this will save someone else some time.
addStyleToRef: function(referEl, indexp, classToAdd) {
//will add a class to a $ref element (even within a v-for loop)
//supply $ref name (referEl - txt), index within list (indexp - int) & css class name (classToAdd txt)
if ( this.$refs[referEl][indexp].$el.classList.value.includes(classToAdd) ){
console.log('class already added')
} else {
this.$refs[referEl][indexp].$el.classList.value = this.$refs[referEl][indexp].$el.classList.value + ' ' + classToAdd
}
}
let tag = this.$refs[ref-key][0];
$(tag).addClass('d-none');
Simply get the tag with ref let tag = this.$refs[ref-key][0]; then put this tag into jquery object $(tag).addClass('d-none'); class will be added to required tag.
In Vue.js, is there a way to register an event if any component updates its data?
My usecase: I am modeling a RPG character via a set of Javascript classes. The TCharacter class has several attributes that can be modified: name, level, HP, magic. While "name" is a simple string, "HP" and "magic" is a custom class TResource which has its own consumption and refill rules.
Instance of the TCharacter class is a source of truth, and I created some Vue components that are views of it.
I created a character component and a resource component in Vue, vaguely like this:
<div class=template id=character>
<input v-model="ch.name">
<resource :attr="ch.magic"></resource>
<resource :attr="ch.hp"></resource>
</div>
<div class="template" id="resource">
you have {{ attr.available }} points
<button #click="attr.consume">X</button>
</div>
<div id="main">
<character :ch="lancelot"></character>
</div>
and the javascript:
class TCharacter {
constructor() {
this.name = "Lancelot"
this.hp = new Resource(20)
this.magic = new Resource(10)
}
}
class TResource {
constructor(limit) {
this.available = limit
this.limit = limit
}
consume() {
if (this.available > 0) this.available--;
}
}
let lancelot = new TCharacter()
Vue.component('character', {
template: '#character',
props: ['ch'],
})
Vue.component('resource', {
template: '#resource',
props: ['attr'],
})
new Vue({
el: "#main",
data() { return { lancelot } }
})
(I'm not sure the code works exactly as written, but hopefully the intent is clear. Something very similar to this is already working for me.)
Now, I'd like to save the character object to localstorage every time the user makes a modification: changes its name, clicks on a button that consumes a point of magic, etc.
So for instance, I want to be notified that the value of ch.name changed because the user typed something into the input box. Or that a magic point was lost because the user clicked a button for that.
I could detect changes to the character component by installing an updated() handler, which notifies me whenever a DOM is modified (viz). However, this won't trigger when the child component resource is modified. I'd need to add a separate updated() handler to all other components. This gets tedious very fast.
I'm imagining something like a global updated() handler that would fire any time any component has registered a change. Or better, a way to specify that update should fire on component's children changes as well.
edit: I have reworded parts of the question to clarify what I'm trying to accomplish.
Some of you already suggested Vuex. But, from what I understood, Vuex enforces being the single source of truth -- I already have a single source of truth. How is Vuex different / better?
You're going to need a serialized version of lancelot to write out. You can do that with a computed. Then you can watch the computed to see when anything changes.
Alternatively, you could watch each individual trait, and write it out as it changes.
class TCharacter {
constructor() {
this.name = "Lancelot"
this.hp = new TResource(20)
this.magic = new TResource(10)
}
}
class TResource {
constructor(limit) {
this.available = limit
this.limit = limit
}
consume() {
if (this.available > 0) this.available--;
}
}
let lancelot = new TCharacter()
Vue.component('character', {
template: '#character',
props: ['ch'],
})
Vue.component('resource', {
template: '#resource',
props: ['attr'],
})
const vm = new Vue({
el: "#main",
data() {
return {
lancelot
}
},
computed: {
serializedLancelot() {
return JSON.stringify(this.lancelot);
}
},
watch: {
serializedLancelot(newValue) {
console.log("Save update:", newValue);
}
}
});
setTimeout(() => {
vm.lancelot.hp.consume();
}, 500);
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<div id="main">
</div>
Am not sure I understand the use case in entirety, but if my assumption is right, you need to update components based on an object's update (updates to properties of an object), for that you could use Vuex . Although am not sure if you are restricted to use an additional library
Here as an example, you could add a state value named character which is an object, something along the lines of
const state = {
character = {};
}
And now you can mutate this using vuex mutations.
commit('set_character', your_new_value)
Now since you said you need to update all or some components based on any mutation to character, use vuex plugins to listen to any mutation to that object, and update the state of the components.
store.subscribe(mutation => {
if (mutation.type === 'set_character') {
// do whatever you want here
}
})
All of the above is just an outline based on what you mentioned, but this is just a starter, you may or may not want to add character into the store's state but simply the properties such as magic or hp.
Using Mithril, a Javascript framework, I am trying to add new elements after the initial body has been created and rendered.
Here is my problem in it's most basic form:
let divArray = [];
let newDivButton = m('button', { onclick: ()=> {
divArray.push(m('div', "NEW DIV PUSHED"));
}}, "Add new div");
divArray.push(newDivButton);
divArray.push(m('div', "NEW DIV PUSHED"));
let mainDiv = {
view: () => {
return divArray;
}
}
m.mount(document.body, mainDiv);
The code above will create a button and one line of text saying NEW DIV PUSHED. The button adds one new text element exactly like the 1st one. The problem here is that those additional elements are simply not rendered even if the view function is called. I stepped in the code and clearly see that my divArray is being populated even if they are not rendered.
One thing I noticed is that the initial text element (the one that is rendered) has it's dom property populated by actual div object. All the subsequent text elements in my array have their dom property set to undefined. I don't know how to fix this but I am convinced it is related to my problem.
Mithril has an optimization built into the render lifecycle - it won't re-render a DOM tree if the tree is identical to the last tree. Since divArray === divArray is always true the nodes are never re-rendering.
The simple, but non-ideal solution is to slice your array so you're always returning a new array from mainDiv#view and therefore, Mithril will always re-render the top-level array:
let mainDiv = {
view: () => {
return divArray.slice();
}
};
The more correct way to do this is to map over the data, creating vnodes at the view layer, rather than keeping a list of vnodes statically in your module scope:
let data = ["Data Available Here"];
let mainDiv = {
view: () => {
return [
m(
'button',
{ onclick: () => data.push("Data Pushed Here") },
"Add new div"
);
].concat(
data.map(datum => m('div', datum))
);
}
};