The content not shown properly in function callback in Vue.js - javascript

I've got two problems here. The first is that I can't get the star rendered properly. I can do it if I change the value in the data() function but if I want to do it in a function callback way, it doesn't work (see comments below). What's going wrong here? Does it have something to do with Vue's lifecycle?
The second one is that I want to submit the star-rate and the content of the textarea and when I refresh the page, the content should be rendered on the page and replace the <textarea></textarea> what can I do?
I want to make a JSFiddle here but I don't know how to make it in Vue's single-file component, really appreciate your help.
<div class="order-comment">
<ul class="list-wrap">
<li>
<span class="comment-label">rateA</span>
<star-rating :data="dimensionA"></star-rating>
</li>
</ul>
<div>
<h4 class="title">comment</h4>
<textarea class="content" v-model="content">
</textarea>
</div>
<mt-button type="primary" class="mt-button">submit</mt-button>
</div>
</template>
<script>
import starRating from 'components/starRating'
import dataService from 'services/dataService'
export default {
data () {
return {
dimensionA: '' //if I changed the value here the star rendered just fine.
}
},
components: {
starRating
},
methods: {
getComment (id) {
return dataService.getOrderCommentList(id).then(data => {
this.dimensionA = 1
})
}
},
created () {
this.getComment(1) // not working
}
}
</script>

What it seems is scope of this is not correct in your getComment method, you need changes like following:
methods: {
getComment (id) {
var self = this;
dataService.getOrderCommentList(id).then(data => {
self.dimensionA = 1
})
}
},
As you want to replace the <textarea> and render the content if present, you can use v-if for this, if content if available- show content else show <textarea>
<div>
<h4 class="title">comment</h4>
<span v-if="content> {{content}} </span>
<textarea v-else class="content" v-model="content">
</textarea>
</div>
See working fiddle here.
one more problem I have observed in your code is you are using dynamic props, but you have assigned the prop initially to the data variable value in star-rating component, but you are not checking future changes in the prop. One way to solve this, assuming you have some other usage of value variable is putting following watch:
watch:{
data: function(newVal){
this.value = newVal
}
}
see updated fiddle.

Related

How to pass dynamic data to a single bootstrap-vue modal instance and reuse it multiple times inside a loop?

I am trying to pass data to a single modal instance and reuse it multiple times inside a loop. It is working but whenever I'm clicking over an image, I'm getting an error - You may have an infinite update loop in a component render function.
This is the html where I'm looping over an array of images and passing it to a modal instance.
<li v-for="(source, i) in sources()" :key="i+'K'">
<span v-b-modal.fancybox #click="fancyBox(source)">
<p>click me</p>
</span>
</li>
This is the html for bootstrap-vue modal -
<b-modal
id="fancybox"
:modal-class="myclass"
hide-footer
centered
>
<img class="fancybox" :src="resource">
</b-modal>
Following is the vue js code -
export default {
data: () => ({
myclass: ['myclass'],
srcItems: [],
resource: ''
}),
methods: {
sources () {
this.srcItems = [
'https://picsum.photos/id/0/5616/3744',
'https://picsum.photos/id/1/5616/3744',
'https://picsum.photos/id/10/2500/1667',
'https://picsum.photos/id/100/2500/1656',
'https://picsum.photos/id/1000/5626/3635'
]
return this.srcItems
},
fancyBox (source) {
this.resource = source
}
}
}
I couldn't able to figure out what is wrong here! Please help me. Thank you for your time.
Hope this will help you to get started
**[LIVE DEMO](https://codepen.io/boussadjra/pen/xxERwZm)**

Vue.js - Transfer focus to component

I have a form fragment wrapped in a component that is hidden by v-if. When the user clicks a button, it toggles the boolean, revealing the hidden component, and when that happens I'd like to transfer focus to the first form input on the fragment.
I've tried using aria-live to no avail. I suspect the nature of the SPA interferes with the registration of those live regions (meaning my guess is that they must be registered when the page is rendered, as they don't seem responsive when injected into the DOM). I did not however, chase the answer down a rabbit hole so that is speculative. So then I added a class to the target input and tried to use HTMLElement.focus()
document.querySelector('.focus')[0].focus();
This also did not work. Does anyone know of a reason why I cannot seem to focus on an element that was recently injected into the DOM and is visible on the page at the time?
I think what's needed is for your form component to have something defined for when it's mounted:
Vue.config.productionTip = false;
const template = `<div>
<div>
<inner v-if="showInner" />
<button #click="toggle">Toggle inner component</button>
</div>
</div>`
const inner = {
name: 'inner',
template: '<form><input ref="textInput" type="text"/></form>',
mounted() {
this.$refs.textInput.focus()
}
};
new Vue({
template,
data: function() {
return {
showInner: false
};
},
methods: {
toggle() {
this.showInner = !this.showInner;
}
},
components: {
inner
}
}).$mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>

Calling a Vue mixin directly with a constant?

I have a footer component with three links. When a user clicks a link, besides taking the user to a new page, I am trying to use a mixin to track the click event. When I set a breakpoint in chrome devtools, it appears that this implementation is not working. I imported my constants file, and the mixin.
footer, one link for brevity
<template>
<footer>
<div class="container">
<div class="row">
<div class="col align-center">
<a
href="/"
target="_blank"
class="btn"
name="item"
#click="logButtonClick(ANALYTICS.ITEM)">{{ $t('footer.item') }}</a>
</div>
</div>
</div>
</footer>
</template>
<script>
import analytics from '#/mixins/analytics'
import { ANALYTICS } from "#/constants"
export default {
name: 'PageFooter',
mixins: [analytics]
}
</script>
mixin
methods: {
logButtonClick (buttonType) { // breakpoint here, get nothing
this.$analytics.track({
identifier: `Consumer ${this.$options.name} - ${buttonType} Button`
})
}
}
Am I missing something? Should this implementation work or should I have a method such as:
methods: {
selectLink(str) {
if (str === item) {
this.logButtonClick(ANALYTICS.ITEM)
}
}
}
The original error I received was
"Property or method ANALYTICS not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class based components, by initializing the property."
and
"Uncaught TypeError: Cannot read property ITEM of undefined at click event...."
Essentially this means I did not define ANALYTICS either in PageFooter (because this is a dumb component, I did not want to add a data object to it, I wanted to keep it strictly presentational) or on the vue instance at a root level. Since ANALYTICS is undefined, ITEM then throws another error because it can not be a property of undefined.
This is my solution, I used a switch case in the and in the template tag added #click="selectLink('asd')"
methods: {
selectLink (str) {
switch (true) {
case str === 'asd':
this.logButtonClick(ANALYTICS.ITEM)
break
case str === 'efg':
this.logButtonClick(ANALYTICS.ITEM2)
break
case str === 'hij':
this.logButtonClick(ANALYTICS.ITEM3)
break
}
}
}
and the unit test:
it('[positive] should track analytics if `asd` is passed to selectLink()', () => {
const str = 'asd'
const mockFn = jest.fn()
jest.spyOn(wrapper.vm, 'logButtonClick')
wrapper.find('a').trigger('click')
mockFn(str)
expect(wrapper.vm.logButtonClick).toHaveBeenCalledWith(ANALYTICS.COOKIE_CONSENT)
})
https://v2.vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties
Moral of the story, question things when senior engineers tell you to do something funky in a code review.

How to control order of rendering in vue.js for sibling component

I have following kind of code:
<div>
<compA />
<compB />
</div>
How do I make sure that first compA is rendered only after it compB is rendered.
Why I want is I have some dependency on few elements of compA, and style of compB depends on presence of those elements.
Why in details:
I have some complex UI design, where one box will become fixed when you scroll. SO It will not go above the screen when you scroll, it will be fixed once you start scrolling and it start touching the header. So I am using jquery-visible to find if a div with a particular id is visible on the screen, if it is not visible, I change the style and make that box fixed. Following code should give the idea what I am doing:
methods: {
onScroll () {
if ($('#divId').visible(false, false, 'vertical')) { // This is div from the compA, so I want to make sure it is rendered first and it is visible
this.isFixed = false
} else {
this.isFixed = true
}
}
},
mounted () {
window.addEventListener('scroll', this.onScroll() }
},
destroyed () {
window.removeEventListener('scroll', this.onScroll)
}
I dont want to make those in same component as one reason is it dont make sense as the nature of these components, and other I use compA at many places, while compB is specific to only one page. Also layout of these does not allow me to make compB child of compA as suggested in comments.
Any suggestions are welcome.
An option with events:
<!-- Parent -->
<div>
<comp-a #rendered="rendered = true"></comp-a>
<component :is="compB"></component>
</div>
<script>
// import ...
export default {
components: { CompA, CompB },
watch: {
rendered: function (val) {
if (val) this.compB = 'comp-b';
}
},
data() {
return {
rendered: false,
compB: null
}
}
}
</script>
<!-- Component B -->
<script>
export default {
mounted() {
this.$emit('rendered');
}
}
</script>
After going through the edit I realised that the dependency is not data driven but event driven (onscroll). I have tried something and looks like it works (the setTimeout in the code is for demonstration).
My implementation is slightly different from that of Jonatas.
<div id="app">
RenderSwitch: {{ renderSwitch }} // for demonstration
<template v-if='renderSwitch'>
<comp-a></comp-a>
</template>
<comp-b #rendered='renderSwitchSet'></comp-b>
</div>
When the component-B is rendered it emits an event, which just sets a data property in the parent of both component-A and component-B.
The surrounding <template> tags are there to reduce additional markup for a v-if.
The moment renderSwitch is set to true. component-a gets created.

How to fire function after v-model change?

I have input which I use to filter my array of objects in Vue. I'm using Salvattore to build a grid of my filtered elements, but it doesn't work too well. I think I have to call rescanMediaQueries(); function after my v-model changes but can't figure how.
Here is my Vue instance:
var articlesVM = new Vue({
el: '#search',
data: {
articles: [],
searchInput: null
},
ready: function() {
this.$http.get('posts').then(function (response) {
this.articles = response.body;
});
}
});
And here is how I have built my search
<div class="container" id="search">
<div class="input-field col s6 m4">
<input v-model="searchInput" class="center-align" id="searchInput" type="text" >
<label class="center-align" for="searchInput"> search... </label>
</div>
<div id="search-grid" v-show="searchInput" data-columns>
<article v-for="article in articles | filterBy searchInput">
<div class="card">
<div class="card-image" v-if="article.media" v-html="article.media"></div>
<div class="card-content">
<h2 class="card-title center-align">
<a v-bind:href="article.link">{{ article.title }}</a>
</h2>
<div class="card-excerpt" v-html="article.excerpt"></div>
</div>
<div class="card-action">
<a v-bind:href="article.link"><?php _e('Read More', 'sage'); ?></a>
</div>
</div>
</article>
</div>
I did get the grid system working by adding watch option to my Vue, but every time I wrote something to my input and then erase it my filterBy method wouldn't work at all. It didn't populate any data even if I tried to retype the same keyword as earlier. Here is the watch option I used:
watch: {
searchInput: function (){
salvattore.rescanMediaQueries();
}
}
I think your problem is with the scoping of this in your success handler for http. Your articles object in Vue component is not getting any values from your http.get(..) success handler.
Inside your ready function, your http success handler should be as follows:
this.$http.get('posts').then(response => {
this.articles = response.body; // 'this' belongs to outside scope
});`
Alternatively you can also do:
var self = this; // self points to 'this' of Vue component
this.$http.get('posts').then(response => {
self.articles = response.body; // 'self' points to 'this' of outside scope
});`
Another similar issue: https://stackoverflow.com/a/40090728/654825
One more thing - it is preferable to define data as a function, as follows:
var articlesVM = new Vue({
el: '#search',
data: function() {
return {
articles: [],
searchInput: null
}
},
...
}
This ensures that your articles object is unique to this instance of the component (when you use the same component at multiple places within your app).
Edited after comment #1
The following code seems to work alright, the watch function works flawlessly:
var vm = new Vue({
el: '#search',
template: `<input v-model="searchInput" class="center-align" id="searchInput" type="text" >`,
data: {
searchInput: ""
},
watch: {
searchInput: function() {
console.log("searchInput changed to " + this.searchInput);
}
}
})
The input in template is an exact copy of your version - I have even set the id along with v-model, though I do not see the reason to set an id
Vue.js version: 2.0.3
I am unable to see any further, based on details in the question. Can you check if your code matches with the one above and see if you can get the console debugging messages?
Edited after comment #4, #5
Here is another thought which you need to verify:
Role of vue.js: Render the DOM
Role of salvattore plugin: Make the DOM layouts using CSS only
Assuming the above is true for salvattore plugin, and hopefully it does not mess with vue.js observers / getters / setters, then you can do the following: provide a time delay of about 50 ms so that vue.js completes the rendering, and then call the salvattore plugin to perform the layouts.
So your watch function needs to be as follows:
watch: {
searchInput: function (){
setTimeout(function(){
salvattore.rescanMediaQueries();
}, 50);
}
}
Alternatively you may also use Vue.nexttick() as follows:
Vue.nextTick(function () {
// DOM updated
})
The nextTick is documented here: https://vuejs.org/api/#Vue-nextTick
I do not know if you may need to provide a little bit of extra time for salvattore plugin to start the layouts, but one of the above should work out.
Let me know if it works!

Categories