Vue js returning wrong element height - javascript

I need to get the height of an image/element, this is what I did:
mounted() {
this.infoHeight = this.$refs.info.clientHeight + 'px';
}
When I save then on hot reload it works, it gets the correct height but when I refresh the page it returns a smaller/wrong value. I also tried it on created() and it's the same. On other situations it doesn't even return anything.
UPDATE (Temporary solution?)
mounted() {
setTimeout(() => this.infoHeight = this.$refs.info.clientHeight + 'px', 100);
}
I also tried using window.addEventListener('load', () => //todo) but on some components it worked and on others it didn't.

You can do this now in a way cleaner fashion using a ResizeObserver.
data: () => ({
infoHeight: 0,
resizeObserver: null
}),
mounted() {
this.resizeObserver = new ResizeObserver(this.onResize)
this.resizeObserver.observe(this.$refs.info)
this.onResize()
},
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.info)
},
methods: {
onResize() {
this.infoHeight = this.$refs.info.clientHeight + 'px'
}
}

Try with $nextTick which will execute after DOM update.
mounted() {
this.$nextTick(() => { this.infoHeight = this.$refs.info.clientHeight + 'px' });
}

You could use this.$watch with immediate:true option :
mounted () {
this.$watch(
() => {
return this.$refs.info
},
(val) => {
this.infoHeight = this.$refs.info.clientHeight + 'px'
},
{
immediate:true,
deep:true
}
)
}
The above solution works only in the initial mount, the following one use MutationObserver
Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
el: '#app',
data: () => ({
infoHeight: 0,
observer: null,
img: "https://images.ctfassets.net/hrltx12pl8hq/6TOyJZTDnuutGpSMYcFlfZ/4dfab047c1d94bbefb0f9325c54e08a2/01-nature_668593321.jpg?fit=fill&w=480&h=270"
}),
mounted() {
const config = {
attributes: true,
childList: true,
subtree: true
};
this.observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation) {
this.infoHeight = this.$refs.info.clientHeight + 'px'
console.log(" changed ", this.$refs.info.clientHeight)
}
});
});
this.observer.observe(this.$refs.info, config);
},
methods: {
changeImg() {
this.img = "https://i.pinimg.com/originals/a7/3d/6e/a73d6e4ac85c6a822841e449b24c78e1.jpg"
},
}
})
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app" class="container">
<p>{{infoHeight}}</p>
<button class="btn btn-primary" #click="changeImg">Change image</button>
<div ref="info">
<img :src="img" alt="image" />
</div>
</div>

Related

Mutation observer not observing element when placed inside a function

I'm trying to set the liveChatAvailable value to true and isLoading value to false once the cripClient element loads to the page.
When the observer object is within a function the if (crispClient) code never runs.
After some research, it seems that it might have something to do with the code needing to be asynchronous but I don't really know how to go about it so a push in the right direction would be great.
Update:
I made the mixin run the code on mounted() instead of doing it inside of the component to see if that would make a difference but it didn't.
LiveChatAvailability.js
export const LiveChatAvailability = {
data() {
return {
isLoading: true,
liveChatAvailable: false
}
},
methods: {
setLiveChatAvailability() {
const crispClient = document.querySelector('.crisp-client');
const observer = new MutationObserver((mutations, obs) => {
if (crispClient) {
this.loading = false;
this.liveChatAvailable = true;
obs.disconnect();
return;
}
observer.observe(document, {
childList: true,
subtree: true
});
});
}
}
}
LiveChatButton.vue
<template>
<button v-if="liveChatAvailable" #click.prevent="liveChatTrigger">Start a live chat now</button>
</template>
<script>
import {LiveChatAvailability} from '../../../../../public_html/assets/src/js/Vue/Mixins/LiveChatAvailability';
export default {
mixins: [
LiveChatAvailability
],
created() {
this.setLiveChatAvailability();
},
methods: {
liveChatTrigger() {
if (window.$crisp) {
window.$crisp.push(['do', 'chat:open']);
}
}
},
}
</script>
Placing the crispClient const within the MutationObserver code allowed it to work.
export const LiveChatAvailability = {
data() {
return {
isLoading: true,
liveChatAvailable: false
}
},
methods: {
setLiveChatAvailability() {
const observer = new MutationObserver((mutations, obs) => {
if (crispClient) {
const crispClient = document.querySelector('.crisp-client');
this.loading = false;
this.liveChatAvailable = true;
obs.disconnect();
return;
}
});
observer.observe(document, {
childList: true,
subtree: true
});
}
}
}

VueJS detecting if a button was clicked in Watch method

I am creating undo/redo functionality in VueJS. I watch the settings and add a new element to an array of changes when the settings change. I also have a method for undo when the undo button is clicked.
However, when the button is clicked and the last setting is reverted, the settings are changed and the watch is fired again.
How can I prevent a new element being added to the array of changes if the settings changed but it was because the Undo button was clicked?
(function () {
var Admin = {};
Admin.init = function () {
};
var appData = {
settings: {
has_border: true,
leave_reviews: true,
has_questions: true
},
mutations: [],
mutationIndex: null,
undoDisabled: true,
redoDisabled: true
};
var app = new Vue({
el: '#app',
data: appData,
methods: {
undo: function() {
if (this.mutations[this.mutationIndex - 1]) {
let settings = JSON.parse(this.mutations[this.mutationIndex - 1]);
this.settings = settings;
this.mutationIndex = this.mutations.length - 1;
console.log (settings);
}
},
redo: function() {
}
},
computed: {
border_class: {
get: function () {
return this.settings.has_border ? ' rp-pwb' : ''
}
},
undo_class: {
get: function () {
return this.undoDisabled ? ' disabled' : ''
}
},
redo_class: {
get: function () {
return this.redoDisabled ? ' disabled' : ''
}
}
},
watch: {
undoDisabled: function () {
return this.mutations.length;
},
redoDisabled: function () {
return this.mutations.length;
},
settings: {
handler: function () {
let mutation = JSON.stringify(this.settings),
prevMutation = JSON.stringify(this.mutations[this.mutations.length-1]);
if (mutation !== prevMutation) {
this.mutations.push(mutation);
this.mutationIndex = this.mutations.length - 1;
this.undoDisabled = false;
}
},
deep: true
}
}
});
Admin.init();
})();
Since you make the changes with a button click, you can create a method to achieve your goal instead of using watchers.
methods: {
settings() {
// call this method from undo and redo methods if the conditions are met.
// move the watcher code here.
}
}
BTW,
If you don't use setter in computed properties, you don't need getters, so that is enough:
border_class() {
return this.settings.has_border ? ' rp-pwb' : ''
},
These watchers codes look belong to computed:
undoDisabled() {
return this.mutations.length;
},
redoDisabled() {
return this.mutations.length;
},

How to set a state to refresh a google-map-content automatically

I built a small boat visualizer. I am using AISHub APIs. After fetching data from the APIs I am able to obtain a json file with the vessels I am interested in and inject these vessels inside a table.
The user has to manually update the page pushing the refresh button on top left of the page to see the new updated table.
The problem: How to set a state to refresh the google-map content automatically every minute instead of the user doing it manually?
Below the code:
GoogleMap.js
class BoatMap extends Component {
constructor(props) {
super(props);
this.state = {
buttonEnabled: true,
buttonClickedAt: null,
progress: 0,
ships: [],
type: 'All',
shipTypes: [],
activeShipTypes: [],
logoMap: {}
};
this.updateRequest = this.updateRequest.bind(this);
this.countDownInterval = null;
}
async componentDidMount() {
this.countDownInterval = setInterval(() => {
if (!this.state.buttonClickedAt) return;
const date = new Date();
const diff = Math.floor((date.getTime() - this.state.buttonClickedAt.getTime()) / 1000);
if (diff < 90) {
this.setState({
progress: diff,
buttonEnabled: false
});
} else {
this.setState({
progress: 0,
buttonClickedAt: null,
buttonEnabled: true
});
}
}, 500);
await this.updateRequest();
const shipTypeResults = await Client.getEntries({
content_type: 'competitors'
});
console.log(shipTypeResults);
const shipTypes = shipTypeResults.items.map((data) => data.fields);
const logoMap = shipTypes.reduce((acc, type) => {
return {
...acc,
[type.name]: type.images.fields.file.url
};
}, {});
console.log({ shipTypes });
this.setState({
logoMap
});
}
componentDidUpdate(prevProps, prevState) {
if (this.state.type !== prevState.type) {
}
}
componentWillUnmount() {
clearInterval(this.countdownInterval);
}
async updateRequest() {
const url = 'http://localhost:3001/hello';
console.log(url);
const fetchingData = await fetch(url);
const ships = await fetchingData.json();
console.log(ships);
this.setState({
buttonEnabled: false,
buttonClickedAt: new Date(),
progress: 0,
ships
});
setTimeout(() => {
this.setState({ buttonEnabled: true });
});
}
render() {
return (
<div className="google-map">
<GoogleMapReact
bootstrapURLKeys={{ key: 'KEY' }}
center={{
lat: this.props.activeShip ? this.props.activeShip.latitude : 42.4,
lng: this.props.activeShip ? this.props.activeShip.longitude : -71.1
}}
zoom={8}
>
</GoogleMapReact>
</div>
);
}
}
What I have done so far:
A good way would be using a setTimeout() but would that be correct? Where should that be applied and how?
setTimeout(function () {
location.reload();
}, 60 * 1000);
Or maybe setting an interval as a refresh rate?
I am a bit confused on what would the best way to approach this.
On your request function i guess u want to disable the button while the api doesn't return, so maybe move this piece above the requests:
this.setState({
buttonEnabled: false,
buttonClickedAt: new Date(),
progress: 0,
ships
});
If im wrong you could remove the timeout from the second setState and call as a callback on the first like this:
this.setState({
buttonEnabled: false,
buttonClickedAt: new Date(),
progress: 0,
ships
}, () => {
this.setState({ buttonEnabled: true });
});
on the last part instead of location.reload() set a interval calling the update on ur componentDidMount:
let updateInterval = setInterval(() => {
this.updateRequest();
}, 60 * 1000);
this.setState({updateInterval})
then on the componentWillUnmount you clear the interval this.state.updateInterval

How to insert vue.js computed data into form data?

<template>
<form #submit.prevent="uploadMeasurement(measure)">
<input v-model="measure.length">
<input v-model="measure.width">
</form>
</template>
<script>
export default {
data() {
return {
measure: this.createFreshMeasure(),
};
},
computed: {
sqftTotal: function() {
return this.length * this.width;
}
},
methods: {
uploadMeasurement(measure) {
MeasurementService.uploadMeasurement(measure)
.then(...);
this.measure = this.createFreshMeasure();
})
.catch(error => {
this.error = error.response.data.error;
});
},
createFreshMeasure() {
return {
length: this.length,
width: this.width,
sqftTotal: this.sqftTotal
};
}
</script>
On submit, I'd like to calculate a square footage value using the values placed into the length and width inputs and send all three into the Mongo database.
The database is storing a value for sqftTotal when I send a hard-coded value directly over Postman, so it's capable of doing it, but this Vue form isn't accomplishing that task.
methods: {
uploadMeasurement() {
let measure = this.measure;
measure.sqftTotal = this.sqftTotal;
MeasurementService.uploadMeasurement(measure)
...
Got it, thanks to everyone for your input. Had to remove the argument from the method and declare it before the service call.
The easiest way to accomplish this would be something like this.. I have commented different options within the code to help explain things..
new Vue({
el: "#root",
template: `
<div>
<form ref="form">
<!--<input v-model="measure.length">
<input v-model="measure.width">-->
<input v-model="length">
<input v-model="width">
</form>
<button #click.prevent="uploadMeasurement">Submit</button>
</div>
`,
data: {
//measure: ""
length: "",
width: "",
},
computed: {
sqftTotal: function() {
//return this.measure.length * this.measure.width;
return this.length * this.width;
}
},
methods: {
uploadMeasurement() {
/** This is where you would POST the data **/
// You can either submit the form:
// -NOTE: '...$refs.form...' below must match the ref
// you assign to the <form ref="form"> element.
// this.$refs.form.$el.submit();
// ~ OR ~
// manually POST via fetch, etc:
// fetch('/url/to/post/to', {
// method: 'POST',
// body: JSON.stringify({
// length: this.measure.length,
// width: this.measure.width,
// sqftTotal: this.sqftTotal
// })
// })
alert(JSON.stringify({
//length: this.measure.length,
//width: this.measure.width,
length: this.length,
width: this.width,
sqftTotal: this.sqftTotal
}));
},
createFreshMeasure() {
this.length = 10;
this.width = 5;
//return {
// length: 10,
// width: 5
//};
}
},
created() {
this.createFreshMeasure();
//this.measure = this.createFreshMeasure();
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<div id="root"></div>
I recommend cleaning up your code like below, as Vue often has issues when using object properties as a model like that
<template>
<form #submit.prevent="uploadMeasurement()">
<input v-model="length">
<input v-model="width">
</form>
</template>
<script>
export default {
data() {
return {
length: null,
width: null,
};
},
computed: {
sqftTotal: function() {
return this.length * this.width;
}
},
methods: {
uploadMeasurement() {
MeasurementService.uploadMeasurement({
length: this.length,
width: this.width,
sqftTotal: this.sqftTotal
})
.then(() => {
console.log('save success!');
})
.catch(error => {
this.error = error.response.data.error;
});
},
}
</script>

Is there a way to dynamically insert click events in Vue.js?

Trying to achieve inserting some computed methods onto an element depending on mobile viewports only. Here's a basic gist of what I'm working with:
<a class="nav-link float-left p-x-y-16" v-bind:class={active:isCurrentTopicId(t.id)} #click="onTopicClicked($event, m, t)" href="#">{{t.title}}</a>
<script>
export default {
data() {
return {
isClosed: false
}
},
computed: {
toggleMenu() {
return {
isClosed: this.isClosed
}
}
},
watch: {
browserWidth(prevWidth, newWidth) {
console.log('width changed from ' + newWidth + ' to ' + prevWidth);
},
mounted() {
var that = this;
this.$nextTick(function() {
window.addEventListener('resize', function(e) {
that.browserWidth = window.innerWidth;
if(that.browserWidth > 824) {
console.log('Desktop View');
} else {
console.log('Mobile View');
}
})
})
}
}
</script>
I would like to try to use the resize event to determine browser width so that I can dynamically insert the computed function onto that <a> tag
You could either provide two different elements (one for desktop and another for mobile) as stated by Karthikeyan, or conditionally add click event to that element:
v-on="isMobileView ? { mouseover: onTopicClicked($event, m, t) } : {}"
You can add a data that says if the view is mobile or not and use v-if , v-else and have the #click only added to the v-if="isMobileView"
<a v-if="isMobileView" class="nav-link float-left p-x-y-16" v-bind:class={active:isCurrentTopicId(t.id)} #click="onTopicClicked($event, m, t)" href="#">{{t.title}}</a>
<a v-else class="nav-link float-left p-x-y-16" v-bind:class={active:isCurrentTopicId(t.id)} href="#">{{t.title}}</a>
<script>
export default {
data() {
return {
isClosed: false,
isMobileView: false
}
},
computed: {
toggleMenu() {
return {
isClosed: this.isClosed
}
}
},
watch: {
browserWidth(prevWidth, newWidth) {
console.log('width changed from ' + newWidth + ' to ' + prevWidth);
},
mounted() {
var that = this;
function checkIfMobileView() {
that.isMobileView = window.innerWidth <= 824;
}
this.$nextTick(function() {
window.addEventListener('resize', checkIfMobileView);
});
checkIfMobileView();
}
}
</script>

Categories