VueJS - manipulate DOM after computed - javascript

I'm getting posts and comments from an API, using Vuex. Right now, I have:
mounted () {
this.$store.dispatch('getComments', this.post);
},
computed: {
comments () {
return this.$store.getters.orderedComments;
},
The comments body is a string that contains HTML tags. I need to strip out the href attribute from a tags of a given class.
cleanUnwantedLinks () {
const link = document.getElementsByClassName('link-class')[0];
link.removeAttribute('href');
}
I'm not sure how to call cleanUnwantedLinks. It should be called just after the component is mounted (when I have the comments already). Is there a way to do it?

If you will return a promise from your getComments action, you could do:
this.$store
.dispatch('getComments', this.post)
.then(() => {
cleanUnwantedLinks()
/*
Can also try wrapping your call in $nextTick
this.$nextTick(() => { cleanUnwantedLinks() })
*/
});

Related

Wait for data in mapstate to finish loading

I have stored a userProfile in Vuex to be able to access it in my whole project. But if I want to use it in the created() hook, the profile is not loaded yet. The object exists, but has no data stored in it. At least at the initial load of the page. If I access it later (eg by clicking on a button) everything works perfectly.
Is there a way to wait for the data to be finished loading?
Here is how userProfile is set in Vuex:
mutations: {
setUserProfile(state, val){
state.userProfile = val
}
},
actions: {
async fetchUserProfile({ commit }, user) {
// fetch user profile
const userProfile = await fb.teachersCollection.doc(user.uid).get()
// set user profile in state
commit('setUserProfile', userProfile.data())
},
}
Here is the code where I want to acess it:
<template>
<div>
<h1>Test</h1>
{{userProfile.firstname}}
{{institute}}
</div>
</template>
<script>
import {mapState} from 'vuex';
export default {
data() {
return {
institute: "",
}
},
computed: {
...mapState(['userProfile']),
},
created(){
this.getInstitute();
},
methods: {
async getInstitute() {
console.log(this.userProfile); //is here still empty at initial page load
const institueDoc = await this.userProfile.institute.get();
if (institueDoc.exists) {
this.institute = institueDoc.name;
} else {
console.log('dosnt exists')
}
}
}
}
</script>
Through logging in the console, I found out that the problem is the order in which the code is run. First, the method getInstitute is run, then the action and then the mutation.
I have tried to add a loaded parameter and played arround with await to fix this issue, but nothing has worked.
Even if you make created or mounted async, they won't delay your component from rendering. They will only delay the execution of the code placed after await.
If you don't want to render a portion (or all) of your template until userProfile has an id (or any other property your users have), simply use v-if
<template v-if="userProfile.id">
<!-- your normal html here... -->
</template>
<template v-else>
loading user profile...
</template>
To execute code when userProfile changes, you could place a watcher on one of its inner properties. In your case, this should work:
export default {
data: () => ({
institute: ''
}),
computed: {
...mapState(['userProfile']),
},
watch: {
'userProfile.institute': {
async handler(institute) {
if (institute) {
const { name } = await institute.get();
if (name) {
this.institute = name;
}
}
},
immediate: true
}
}
}
Side note: Vue 3 comes with a built-in solution for this pattern, called Suspense. Unfortunately, it's only mentioned in a few places, it's not (yet) properly documented and there's a sign on it the API is likely to change.
But it's quite awesome, as the rendering condition can be completely decoupled from parent. It can be contained in the suspensible child. The only thing the child declares is: "I'm currently loading" or "I'm done loading". When all suspensibles are ready, the template default is rendered.
Also, if the children are dynamically generated and new ones are pushed, the parent suspense switches back to fallback (loading) template until the newly added children are loaded. This is done out of the box, all you need to do is declare mounted async in children.
In short, what you were expecting from Vue 2.

How to focus on a specific input text after the component is rendered?

I'm struggling to make this work properly every time I rendered the component by pushing it using the router object something like this this.$router.push('/Home/mypath'); which will focus only on the first input text element with index = 0 after the component is rendered even if I passed another value for index. Basically, I passed an index value to the ref of the input text element which is inside the v-for loop of a component and so at mounted(), I have something like this
mounted() {
this.$nextTick(() =>
{
this.$refs.newInp[index].focus();
});
}
but it keeps focusing on the first input element even though I passed a value of 1 or 2. When I looked at the console log, it shows this error on the console.
TypeError: Cannot read property '0' of undefined
pointing on this line this.$refs.newInp[index].focus();
Sample Code to fetch the data in the v-for
async GetContentDetails() {
let testRes =
await axios.get('myUrl/api', {
params: {
ContentId: this.$cookie.get('OpV-ContId')
}
}).then(res => this.contentItems = res.data)
.then()
.catch(error => console.log(error));
this.testData = testRes;
}
Template:
<div v-for="(contItem, index) in contentItems" :key="contItem.commentId">
<textarea class="reply-commented" ref="newInp"></textarea>
</div>
How to fix this type of issues? What is the solution for this?
Thanks.
From what I understood, you want to focus a textarea after fetching some data, that said trying to focus inside the mounted method wont work because you can't tell if the data has been fetch and the textareas already exist in the DOM.
So the best way to do this is to focus after being sure the data has been fetched, inside the then callback.
new Vue({
el: '#app',
data: {
posts: [],
index: 3 // Change this to focus whatever textarea you want
},
mounted () {
this.fetchItems();
},
methods: {
fetchItems () {
const url = 'https://jsonplaceholder.typicode.com/posts'
axios.get(url).then(response => {
this.posts = response.data
this.$nextTick(_ => {
this.$refs.newInp[this.index].focus()
})
})
}
}
});
<div id="app">
<div v-for="(post, index) in posts" :key="post.id">
<textarea class="reply-commented" ref="newInp" v-text="post.body"></textarea>
</div>
</div>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
For few days of research and thorough testing and observation on DOM behavior on how vue.js renders component and of course basing on the suggestions from other folks on this thread. I realized you can't really focus in the created/mounted properties on a specific index of an element within the for loop particulary in this case the input text element if the data being fetch to bind on the component is coming from the server due to its asynchronous behavior and you have to wait until the component is completely rendered. So I found a solution at least on my case to use a dynamic watcher either in the created or mounted properties and set a dummy or duplicate data properties for the default change of the data properties for the purpose of only to activate the watcher to focus on the specific element after the component has been rendered. This how it looks like. Hope this help to folks that encountering the same scenario as me.
created() {
this.GetContentDetails();
this.$watch('commentItems2', function () {
this.$nextTick(() => {
this.$refs.newRep[mapCredential.state.index2].focus();
});
});
},
methods: {
async GetComment2() {
let testRes =
await axios.get('myUrl/api/GetContent/GetComments')
.then(this.GetReply2())
.catch(error => console.log(error));
this.commentItems = testRes.data;
this.commentItems2 = testRes.data;
},
}

Framework 7 Vue how to stop Firebase from listening to changes when on different pages?

Suppose I have pageA where I listen for a firebase document changes
export default {
mounted() {
this.$f7ready(() => {
this.userChanges();
})
},
methods: {
userChanges() {
Firebase.database().ref('users/1').on('value', (resp) => {
console.log('use data has changed');
});
}
}
}
Then I go to pageB using this..$f7.views.current.router.navigate('/pageB/')
If on pageB I make changes to the /users/1 firebase route I see this ,message in the console: use data has changed, even though I'm on a different page.
Any way to avoid this behavior, maybe unload the page somehow?
I tried to stop the listener before navigating away from pageA using Firebase.off() but that seems to break a few other things.
Are you properly removing the listener for that specific database reference? You'll have to save the referenced path on a dedicated variable to do so:
let userRef
export default {
mounted() {
this.$f7ready(() => {
this.userChanges();
})
},
methods: {
userChanges() {
userRef = Firebase.database().ref('users/1')
userRef.on('value', (resp) => {
console.log('use data has changed');
});
},
detachListener() {
userRef.off('value')
}
}
}
That way you only detach the listener for that specific reference. Calling it on the parent, would remove all listeners as the documentation specifies:
You can remove a single listener by passing it as a parameter to
off(). Calling off() on the location with no arguments removes all
listeners at that location.
More on that topic here: https://firebase.google.com/docs/database/web/read-and-write#detach_listeners

Category component do api call on created

Currently my category component is making http api call on its creation.
So when I go from my index to category the result of http call is efficient.
But now when I change category so I go from localhost:8080/category/1 to localhost:8080/category/2 my component isn't re-rendered so it's not created again and my http call function doesn't fire again how I would like to.
async created() {
try {
console.log(this.id);
const response = await axios.get(`/api/category/getOne.php?id=${this.id}`, axiosConfig);
console.log(response.data.records[0].name);
this.category = [response.data.records[0].name];
} catch (e) {
this.errors.push(e);
console.log(this.errors);
}
}
What do you guys suggest for me, should I place this http call not in created () but for example in methods {} and then detect if url id change and execute this function whenever it change? Or there is some better, vue approach for that?
I moved above code to methods {} and call it on created() and on id change like so:
created() {
this.getData(this.id);
},
watch: {
'$route.params.id'(newId, oldId) {
this.getData(newId);
}
}
as #Belmin Bedak suggested in comments section.

how to add returned data to the existing template

I am using ember. I intercept one component's button click in controller. The click is to trigger a new report request. When a new report request is made, I want the newly made request to appear on the list of requests that I currently show. How do I make ember refresh the page without obvious flicker?
Here is my sendAction code:
actions: {
sendData: function () {
this.set('showLoading', true);
let data = {
startTime: date.normalizeTimestamp(this.get('startTimestamp')),
endTime: date.normalizeTimestamp(this.get('endTimestamp')),
type: constants.ENTERPRISE.REPORTING_PAYMENT_TYPE
};
api.ajaxPost(`${api.buildV3EnterpriseUrl('reports')}`, data).then(response => {
this.set('showLoading', false);
return response.report;
}).catch(error => {
this.set('showLoading', false);
if (error.status === constants.HTTP_STATUS.GATEWAY_TIMEOUT) {
this.notify.error(this.translate('reports.report_timedout'),
this.translate('reports.report_timedout_desc'));
} else {
this.send('error', error);
}
});
}
There are few think you should consider. Generaly you want to have variable that holds an array which you are render in template in loop. For example: you fetch your initial set of data in route and pass it on as model variable.
// route.js
model() { return []; }
// controller
actions: {
sendData() {
foo().then(payload => {
// important is to use pushObjects method.
// Plain push will work but wont update the template.
this.get('model').pushObjects(payload);
});
}
}
This will automatically update template and add additional items on the list.
Boilerplate for showLoading
You can easily refactor your code and use ember-concurency. Check their docs, afair there is example fitting your usecase.

Categories

Resources