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!
Related
I have a use case (below) where I need to mount (if thats the correct term) a Vue.js component template that was inserted into the DOM via jQuery, I can setup a Mutation Observer or react to certain events that are triggered when the mutation happens.
I am using Vue.js v2
Here is a simple example I put together to illustrate the point:
live jsFiddle https://jsfiddle.net/w7q7b1bh/2/
The HTML below contains inlined-templates for two components
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.13/dist/vue.min.js"></script>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<div id="app">
<!-- The use of inline-template is required for my solution to work -->
<simple-counter inline-template>
<button v-bind:style="style" v-on:click="add">clicks: {{ counter }}</button>
</simple-counter>
<simple-counter inline-template>
<button v-on:click="counter += 1">{{ counter }}</button>
</simple-counter>
</div>
<button id="mutate">Mutate</button>
The js:
// simple counter component
Vue.component('simple-counter', {
data: function() {
return {
counter: 0,
style: {
color: 'red',
width: '200px'
}
}
},
methods: {
add: function() {
this.counter = this.counter + 1;
this.style.color = this.style.color == 'red' ? 'green' : 'red';
}
}
})
// create the Vue instance
var initV = () => new Vue({
el: '#app'
});
// expose the instance for later use
window.v = initV();
// click handler that will add a new `simple-counter` template to the Vue.el scope
$('#mutate').click(function(){
$('#app').append(` <div is="simple-counter" inline-template>
<button v-bind:style="style" v-on:click="add">click to add: <span class="inactive" v-bind:class="{ active: true }">{{ counter }}</span></button></div>`)
// do something after the template is incerted
window.v.$destroy()
window.v = initV(); // does not work
})
As mentioned in the code, destroying the re-instantiating the Vue instance does not work, I understand why, the templates for the components are changed on first Vue instantiation to their final HTML, when you try and instantiate a second time, templates are not there, components are not mounted
I'd like to be able to find the newly added components after mutation and mount only those, is that possible? and how?
UPDATE:
I was able to find a way to do it via instantiating a new Vue instance with el set to the specific mutated part of the DOM as opposed to the whole #app tree:
$('#mutate').click(function(){
var appended =
$(`
<div is="simple-counter" inline-template>
<button v-bind:style="style" v-on:click="add">
click to add: {{ counter }}
</button>
</div>`
).appendTo($('#app'));
var newV = new Vue({el: appended[0]});
});
Seems to work, but also looks ugly and I am not sure what other implications this might have..
Use Case:
I am working on a way to write Vue.js components for a CMS called Adobe Experience Manager (AEM).
I write my components using inlined-template which gives me the advantage of SEO as well as server-side rendering using another templating language called HTL.
The way AEM authoring works is that, when a component is edited (via a dialog), that specific component is re-rendered on the server-side then injected back to the DOM to replace the old component, all done via Ajax and jQuery (no browser refresh).
Here is an example
AEM component template:
<button>${properties.buttonTitle}</button>
Here is what an author might do:
author visits the authoring page
opens the button component dialog to edit
changes the buttonTitle to "new button title"
Saves
upon saving, an ajax is sent, the component HTML is re-rendered on the server and returned is the new HTML. That HTML now replaces the old HTML via jQuery (mutates the DOM)
This is fine for static components, but if this was a Vue.js component, how do I dynamically mount it while keeping other components mounted.
An easy solution to this is to refresh the page... but that is just bad experience... There has to be a better way.
Thanks to #liam I was able to find an appropriate solution to my problem
After mutating the DOM with the HTML template, keep a reference to that template's parent element
for example:
var $template = $('<div is="simple-counter" inline-template> ..rest of template here.. <div>').appendTo('#app') // app is the Vue instance el or a child of it
Now you can create a new instance of your component and add $template to it as the el property
if my component was:
var simpleCounterComponent = Vue.component('simple-counter', {
data: function() {
return {
counter: 0,
style: {
color: 'red',
width: '200px'
}
}
},
methods: {
add: function() {
this.counter = this.counter + 1;
this.style.color = this.style.color == 'red' ? 'green' : 'red';
}
}
})
I can do:
var instance = new simpleCounterComponent({
el: $template.get(0) // getting an HTML element not a jQuery object
});
And this way, that newly added template has become a Vue component
Take a look at this fiddle for working example based on the question:
https://jsfiddle.net/947ojvnw/11/
One way to instantiate Vue components in runtime-generated HTML is:
var ComponentClass = Vue.extend({
template: '...',
});
var instance = new ComponentClass({
propsData: { name: value },
});
instance.$mount('#uid'); // HTML contains <... id="uid">
...
instance.$destroy(); // if HTML containing id="uid" is dropped
More here (I am not affiliated with this site)
https://css-tricks.com/creating-vue-js-component-instances-programmatically/
I have worked with VueJS for a while, and it is great. I have been able to integrate it with jQueryUI (for an old looking website) and I created a datepicker component, and a datetime picker component as well, both working correctly.
Now I am trying to create a simple phone number component, which simply provides an input with a mask that helps with the phone number format. The plugin for jquery that provides the masking, works correctly on it's own, but if I try to mask an input inside my component, it does not work.
Here is the example code in jsfiddle:
Simple Masked Phone input component for vuejs 2.4.0 - jsfiddle
Javascript:
Vue.component('phone', {
template: '#phone',
props: {
value : {
type : String,
default: ''
},
mask: {
type : String,
default: '(999) 999-9999'
}
},
data: function() {
return {
internalValue: ''
};
},
created: function() {
this.internalValue = $.trim(this.value);
},
mounted: function() {
$(this.$el).find('.phone:eq(0)').mask('(999) 999-9999');
},
methods: {
updateValue: function (value) {
this.$emit('input', value);
}
}
});
var vueapp = new Vue({
el: '#content',
data: {
myphone: ''
}
});
$('.phonex').mask('(999) 999-9999');
HTML:
<div id="content">
<script type="text/x-template" id="phone">
<input type="text" class="phone" v-model="internalValue" v-on:input="updateValue($event.target.value)" />
</script>
<label>Vue Phone</label>
<phone v-model="myphone"></phone>
<br />
{{ myphone }}
<br />
<label>Simple Phone</label>
<input type="text" class="phonex" />
</div>
This is what I see:
Dependencies:
jquery-2.2.4.min.js
jquery.maskedinput.min.js (1.4.1)
vue.js (2.4.0)
Is there anything I am doing wrong here? Thanks.
You don't need the .find('.phone:eq(0)') in your jquery, removing it seems to fix the masking (as shown here), though this does seem to mess with Vue's data binding.
After doing a bit more digging it looks like this is a known issue.
And is addressed here:
Vue is a jealous library in the sense that you must let it completely
own the patch of DOM that you give it (defined by what you pass to
el). If jQuery makes a change to an element that Vue is managing, say,
adds a class to something, Vue won’t be aware of the change and is
going to go right ahead and overwrite it in the next update cycle.
The way to fix this is to add the event handler when you call .mask on the element.
So for example:
mounted: function() {
var self = this;
$(this.$el).mask('(999) 999-9999').on('keydown change',function(){
self.$emit('input', $(this).val());
})
},
Here is the fiddle with the fix: https://jsfiddle.net/vo9orLx2/
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.
Following an answer to my question on debouncing I am wondering if vue.js and lodash/underscore are compatible for this functionality. The code in the answer
var app = new Vue({
el: '#root',
data: {
message: ''
},
methods: {
len: _.debounce(
function() {
return this.message.length
},
150 // time
)
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.6/vue.js"></script>
<script src="https://unpkg.com/underscore#1.8.3"></script> <!-- undescore import -->
<div id="root">
<input v-model="message">Length: <span>{{ len() }}</span>
</div>
indeed holds on the execution of my function when there is continuous input, but when it is finally executed after some inactivity, the input for function() seems to be wrong.
A practical example after starting the code above:
quick sequence of characters, then no activity:
One extra character (b) added, and no activity -- the length is updated (but wrongly, see below)
Erase all the characters with Backspace in a quick sequence:
Add one character:
It looks like the function is ran on the last but one value of message.
Could it be that _.debounce handles the vue.js data before it is actually updated with the <input> value?
Notes:
tested with both lodash and underscore, with the same results (for both debounceand throttle functions).
I also tested it on JSFiddle in case there would be some interference with the SO snippet
Here's an improved version of #saurabh's version.
var app = new Vue({
el: '#root',
data: {
message: '',
messageLen: 0
},
methods: {
updateLen: _.debounce(
function() {
this.messageLen = this.message.length
}, 300)
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.6/vue.js"></script>
<script src="https://unpkg.com/underscore#1.8.3"></script> <!-- undescore import -->
<div id="root">
<input v-model="message" v-on:keyup="updateLen">Length: <span>{{ messageLen }}</span>
</div>
Why this is happening is because Vue invokes methods only when vue variables used in the methods changes, if there are no changes in the vue varaibles, it will not trigger those methods.
In this case as well, once we stop typing, it will continue to show last called method's output and will only show again once you enter the input again.
One alternate approach if you dont want to call an function on all inputs, you can call a mehtod on blur event, so method will be invoked only when focus goes out of input field, like following:
var app = new Vue({
el: '#root',
data: {
message: '',
messageLen: 0
},
methods: {
updatateLen:
function() {
this.messageLen = this.message.length
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.6/vue.js"></script>
<script src="https://unpkg.com/underscore#1.8.3"></script> <!-- undescore import -->
<div id="root">
<input v-model="message" v-on:blur="updatateLen">Length: <span>{{ messageLen }}</span>
</div>
I am trying to bind the listview to the ViewModel. I have placed some hard coded data into the code to ensure that it is not a problem with the web services. I am not seeing any console errors so I am at a loss for how to troubleshoot this problem.
Ideally I would want to have as much of the code dealing with getting the data in the ViewModel, and I want to stay as close as possible to the way that you are supposed to use the KendoUI Mobile framework.
Html
<div data-role="view" id="contactView" data-model="ContactViewModel" data-init="dataInit">
<h1 id="ContactHallo">Contact Screen</h1>
<ul id="contactDetailList"
data-role="listview"
data-style="inset"
data-template="contactDetailtemplate"
data-bind="source: rdata">
</ul>
</div>
JavaScript
var ContactViewModel = kendo.observable({
rdata: null,
loadData: function () {
var testData = [
{
AssociatedContactType: "n\/a",
AssociatedProperties: [],
EmailAddress: "n\/a",
FName: "User1",
HomeNumber: "n\/a",
LName: "LastName",
MobileNumber: "+27 21 0823219213",
WorkNumber: "n\/a"
}];
var loadedData = new kendo.data.DataSource.create({ data: testData });
loadedData.read();
this.rdata = loadedData;
}
});
function dataInit() {
ContactViewModel.loadData();
}
var app = new kendo.mobile.Application($(document.body));
Template
<div>
<h4>Added Record</h4>
#:data.MobileNumber#
</div>
It would be interesting to know why someone down-voted the original question..
I cover this in one of my blog posts here: Kendo Mobile Gotchas, Tips, and Tricks.
The MVVM data bind actually happens before the init event, so your ContactViewModel.rdata is still null when the bind happens. However, I think if you properly call .set() when setting rdata, it might fix your issue:
loadData: function () {
...
this.set('rdata', loadedData);
}
The set should trigger the ListView to update since rdata is being set.
If that doesn't work, then you can get really tricky and delay the MVVM data bind until the init event by doing it yourself instead of using data-model declaratively.
To do that, you would remove data-model= attribute from your view, and instead manually call kendo.bind() at the end of your init function, like this:
<div data-role="view" id="contactView" data-init="dataInit">
function dataInit(initEvt) {
ContactViewModel.loadData();
kendo.bind($(initEvt.view.element), ContactViewModel, kendo.mobile.ui);
}