I'm trying to add pagination code in the Vue component and I've tried to add the code in the mounted hook to call the function but it doesn't work. I want to load the code after component loaded completely.Also, jQuery code doesn't load in Vue component. Do I need to change my code to pure javascript for that. Can you guide me how to fix the issue?
// Create a root instance for each block
var vueElements = document.getElementsByClassName('search-bento-block');
var count = vueElements.length;
const store = new Vuex.Store({
state: {
query: drupalSettings.bento.query ? drupalSettings.bento.query : '',
bentoComponents: []
},
mutations: {
add (state, payload) {
state.bentoComponents.push(payload)
}
},
getters: {
getComponents: state => {
return state.bentoComponents
}
}
})
// Loop through each block
for (var i = 0; i < count; i++) {
Vue.component('results', {
template: `
<div v-if="results && results.length > 0">
<div v-for="result in results">
<div class="search-result-item">
<div class="image-holder">
<img src="https://images.unsplash.com/photo-1517836477839-7072aaa8b121?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=750&q=80">
</div>
<div class="container-content">
<a v-bind:href="result.url">
<h3 v-html="result.title"></h3>
</a>
<p>Subjects: <span v-html="result.subjects"></span></p>
</div>
</div>
</div>
</div>
<div v-else>
<p>No results found.</p>
</div>
`,
props: ['results'],
})
new Vue({
el: vueElements[i],
store,
data: {
message: 'Hello There!',
results: [],
total: 0,
bentoSettings: [],
},
methods: {
addComponentToStore: function (type) {
this.$store.commit('add', type);
console.log("test");
console.log(this.results.length);
}
},
mounted: function() {
// console.log(this.$route.query.bentoq);
const id = this.$el.id;
this.bentoSettings = drupalSettings.pdb.configuration[id];
var bentoConfig = drupalSettings.pdb.configuration[id].clients[this.bentoSettings.bento_type] ? drupalSettings.pdb.configuration[id].clients[this.bentoSettings.bento_type].settings : [];
axios
.get('/api/search/' + this.bentoSettings.bento_type, {
params: {
query: this.$store.state.query,
plugin_id: this.bentoSettings.bento_type,
bento_limit: this.bentoSettings.bento_limit,
bento_config: bentoConfig,
}
})
.then(response => {
console.log(response);
this.results = response.data.records;
this.total = response.data.total;
this.addComponentToStore({
title: this.bentoSettings.example_field,
count: this.total
});
})
.catch(error => {
console.log(error.response);
})
}
});
}
// I'm trying to call following function in Vue component.
function baseThemePagination1() {
//Pagination
pageSize = 3;
var pageCount = $('.line-content').length / pageSize;
for (var i = 0; i < pageCount; i++) {
$('#pagin').append('<li><a href=\'#\'>' + (i + 1) + '</a></li> ');
}
$('#pagin li').first().find('a').addClass('current')
showPage = function(page) {
$('.line-content').hide();
$('.line-content').each(function(n) {
if (n >= pageSize * (page - 1) && n < pageSize * page)
$(this).show();
});
}
showPage(1);
$('#pagin li a').click(function() {
$('#pagin li a').removeClass('current');
$(this).addClass('current');
showPage(parseInt($(this).text()))
});
}
What you are trying to do is not the recommended way to use vue, direct DOM manipulation is one of the things that vue is made to avoid (although can be done). The Vue way would be to bind the value you want to a variable with v-model assuming it is an input and then create your pagination based on that.
If you insist on DOM manipulation then try ref="line-content" and then call it like so:
this.refs.line-content.
In terms of reacting to a page change click simply use a method in your methods section there is no reason to use jQuery for that.
See here for a simple explanation:
https://medium.com/#denny.headrick/pagination-in-vue-js-4bfce47e573b
Related
I'm working on an onboarding process that will collect a users name, location, job , etc.
It needs to be one question per page but as an SPA so I currently have around 20 components to conditionally render.
Atm, I have a counter and Prev/Next buttons that decrease/increase the counter respectively. I'm then using v-if to check what number the counter is on and render the appropriate page.
Is there a better way around this that is less repetitive and bulky?
Any ideas appreciated!
data() {
return {
onboardingStep: 0,
}
},
methods: {
prevStep() {
this.onboardingStep -= 1;
},
nextStep() {
this.onboardingStep += 1;
}
}
<intro-step v-if="onboardingStep === 0"></intro-step>
<first-name v-if="onboardingStep === 1"></first-name>
<last-name v-if="onboardingStep === 2"></last-name>
...etc.
Suggestion :
You can make your field components to show or hide based on the prev/next state. Dynamic components provide that platform in an efficient and simple way.
Syntax :
<component :is="componentName"></component>
Then, You can create each component instance dynamically by putting a watcher on components array.
watch: {
components: {
handler() {
this.components.forEach(cName => {
Vue.component(cName, {
template: `template code will come here`
})
});
}
}
}
Live Demo :
new Vue({
el: '#app',
data() {
return {
components: [],
onboardingStep: 0
}
},
mounted() {
this.components = ['intro-step', 'first-name', 'last-name'];
},
watch: {
components: {
handler() {
this.components.forEach(cName => {
Vue.component(cName, {
data() {
return {
modelName: cName
}
},
template: '<input type="text" v-model="modelName"/>'
})
});
}
}
},
methods: {
prevStep() {
this.onboardingStep -= 1;
},
nextStep() {
this.onboardingStep += 1;
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(cName, index) in components" :key="index">
<component :is="cName" v-if="index === onboardingStep"></component>
</div>
<button #click="prevStep" :disabled="onboardingStep < 1">Prev</button>
<button #click="nextStep" :disabled="onboardingStep === components.length - 1">Next</button>
</div>
You could create an array with all your component names in the right order.
const components = ['intro-step', 'first-name', 'last-name' ]
And then with a v-for loop set all the components in your template:
<template v-for="(component, index) in components" :key="component">
<component :is="component" v-if="index === onboardingStep">
</template>
Hope this helps.
Vue version : 3.1.1
Hey guys,
I'm working with dynamic Creation Component, which means a user can add whatever of component he wants.I create it base on this documentation dynamic component creation.
And I use this component vue image uploader.
I need to send an index when the user wants to upload the image, like this :
<div v-for="(line, index) in lines" v-bind:key="index">
{{index}}//if i log the index its 0,1,2,3 and its ok
...
<image-uploader
:preview="true"
:class-name="['fileinput', { 'fileinput--loaded': line.hasImage }]"
:capture="false"
:debug="0"
:auto-rotate="true"
output-format="blob"
accept="image/*"
#input="setImage(output , index)"
:ref="'fileUpload'+index"
>
...
And the setImage funciton :
setImage: function(output,index) {
console.log(index);
console.log(output);
return ;
this.lines[index].hasImage = true;
this.lines[index].image = output;
let formData = new FormData();
formData.append("file", output);
Ax.post(upload_route, formData, {
headers: { "Content-Type": "multipart/form-data" }
})
.then(response => {
// upload successful
})
.catch(error => console.log(error));
}
And the log result is:
The index always is 0 :(
How can i send an index when i want to upload it?
I read this passing event and index and test it but it's not working on component.
Because This is a custom event not a DOM event.
what should I do?
thanks.
Because you're actually passing the return value of setImage to the #input, not the method.
You can't just add extra parameters to setImage, as ImageUploader component just emit an image to the setImage. If you need to add extra parameters to that method, you need to create custom element that wrap ImageUploader.
It's something like this:
ImageUpload.vue
<template>
<image-uploader
:debug="0"
:autoRotate="true"
outputFormat="blob"
:preview="true"
:className="['fileinput', { 'fileinput--loaded' : hasImage }]"
:capture="false"
accept="image/*"
doNotResize="['gif', 'svg']"
#input="setImage"
v-on="listeners" />
</template>
<script>
export default {
props: {
index: {
required: true,
type: Number
}
},
data() {
return {
hasImage: false,
image: null
};
},
computed: {
listeners() {
const listeners = { ...this.$listeners };
const customs = ["input"];
customs.forEach(name => {
if (listeners.hasOwnProperty(name)) {
delete listeners[name];
}
});
return listeners;
}
},
methods: {
setImage(image) {
this.hasImage = true;
this.image = image;
this.$emit("input", this.index, image); // here, we emit two params, as index for the first argument, and the image at the second argument
}
}
};
</script>
Then, you can use that component something like this:
<template>
<div class="container">
<div v-for="(line, index) in lines" :key="index">
<image-upload :index="index" #input="setImage"/>
</div>
</div>
</template>
<script>
import ImageUpload from "./ImageUpload";
export default {
components: {
ImageUpload
},
data() {
return {
lines: ["1", "2", "3", "4"]
};
},
methods: {
setImage(index, image) {
console.log("Result", index, image);
}
}
};
</script>
See the working example: https://codesandbox.io/s/vue-template-ccn0e
Just use $event like this...
#input="setImage($event, index)"
...and you're done!
I store results per page number, see below:
<ul v-for="iten in listingsData" :key="item.id">
<li>{{ item.name }}</li>
</ul>
<button #click="pushPrev">Push Prev Results</button>
<button #click="pushNext">Push Next Results</button>
export default {
data(){
return {
listingsData : [],
page : 1
}
},
methods : {
pushNext(){
var _self = this;
axios.get('https://myapi.com/get/users?page='+this.page+1).then(function(response){
_self.page = _self.page + 1;
_self.listingsData = _self.listingsData.push({
page : _self.page,
results : response.data.results
})
});
},
pushPrev(){
var _self = this;
axios.get('https://myapi.com/get/users?page='+this.page-1).then(function(response){
_self.page = _self.page + 1;
_self.listingsData = _self.listingsData.push({
page : _self.page,
results : response.data.results
})
});
}
}
created(){
//load default data
var _self = this;
axios.get('https://myapi.com/get/users?page='+this.page).then(function(response){
_self.listingsData = {
page : 1,
results : response.data.results
}
});
}
}
Now how I can show or loop only results correspond to the the page number this.page?
_self.listingsData = _self.listingsData.push({
page : _self.page, // page number
results : response.data.results
})
What can I try?
I'm using Vue CLI and webpack.
You should iterate over computed property that will return specific page from listingsData, not over all pages.
Something like that:
<template>
<div v-if="currentPage">
<ul v-for="item in currentPage.results" :key="item.id">
<li>{{ item.name }}</li>
</ul>
</div>
<div v-else><i>Loading...</i></div>
<button #click="fetchPage(-1)" :disabled="page===1">Prev Results</button>
<button #click="fetchPage(1)" :disabled="page===10">Next Results</button>
</template>
<script>
const api = "https://myapi.com/get/users";
export default {
data() {
return {
page: 1,
listingsData: [],
};
},
created() {
this.fetchPage(0);
},
computed: {
currentPage() {
return this.listingsData.find(i => i.page === this.page);
},
},
methods: {
fetchPage(diff) {
this.page += diff;
if (this.currentPage) {
return; // page already loaded
}
const page = this.page;
axios.get(api, { params: { page } })
.then((res) => {
this.listingsData.push({
page,
results: res.data.results,
});
});
},
}
};
</script>
Here, we're loading page only if it hasn't been loaded before, and disable Prev/Next buttons if current page is 1/10 respectively.
Here is jsfiddle (with mockup data instead of actual API calls).
I have a custom component that lets users type in text and sends it to the backend where I do some computation and spit the new text back out with html in it.
My problem is when the user types into this textarea, it reverses all the text and keeps the cursors at the beginning of the textarea. So now 'foo bar' becomes 'rab oof'... This only has happened since I added in watch. I could delete the watcher, but I need it (or need another way) to apply my updates to the textarea, via the foo variable when I set foo equal to something from the parent.
console.log(v) writes out the reverse text.
Any idea how to change this?
Custom componet:
<template>
<div contenteditable="true" #input="updateHTML" class="textareaRoot"></div>
</template>
<script>
export default {
name: 'htmlTextArea',
props:['value'],
mounted: function () {
this.$el.innerHTML = this.value;
},
watch: {
value(v) {
this.$el.innerHTML = v; //v is the reverse text.
}
},
methods: {
updateHTML: function(e) {
this.$emit('input', e.target.innerHTML);
}
}
}
</script>
Parent that uses custom component:
<htmlTextArea id="textarea" v-model="foo"></htmlTextArea>
...
<script>
...
methods: {
triggerOnClick() {
this.foo = 'something';//Without the watcher, when I change this.foo to something the actual textarea does not display the new data that I assigned to foo. But in Vue dev tools I can see the new change.
}
UPDATE:
Vue.component('html-textarea',{
template:'<div contenteditable="true" #input="updateHTML"></div>',
props:['value'],
mounted: function () {
this.$el.innerHTML = this.value;
},
watch: {
value(v) {
this.$el.innerHTML = v;
}
},
methods: {
updateHTML: function(e) {
this.$emit('input', e.target.innerHTML);
}
}
});
new Vue({
el: '#app',
data () {
return {
foo: '',
}
}
});
<script src="https://unpkg.com/vue"></script>
<div id="app">Type here:
<html-textarea spellcheck="false" id="textarea" v-model="foo"> </html-textarea>
</div>
The problem is that when you set the innerHTML of a contenteditable element, you lose the selection (cursor position).
So you should perform the following steps when setting:
save the current cursor position;
set the innerHTML;
restore the cursor position.
Saving and restoring is the tricky part. Luckily I got these two handy functions that do the job for latest IE and newer. See below.
function saveSelection(containerEl) {
var range = window.getSelection().getRangeAt(0);
var preSelectionRange = range.cloneRange();
preSelectionRange.selectNodeContents(containerEl);
preSelectionRange.setEnd(range.startContainer, range.startOffset);
var start = preSelectionRange.toString().length;
return {
start: start,
end: start + range.toString().length
}
}
function restoreSelection(containerEl, savedSel) {
var charIndex = 0, range = document.createRange();
range.setStart(containerEl, 0);
range.collapse(true);
var nodeStack = [containerEl],
node, foundStart = false,
stop = false;
while (!stop && (node = nodeStack.pop())) {
if (node.nodeType == 3) {
var nextCharIndex = charIndex + node.length;
if (!foundStart && savedSel.start >= charIndex && savedSel.start <= nextCharIndex) {
range.setStart(node, savedSel.start - charIndex);
foundStart = true;
}
if (foundStart && savedSel.end >= charIndex && savedSel.end <= nextCharIndex) {
range.setEnd(node, savedSel.end - charIndex);
stop = true;
}
charIndex = nextCharIndex;
} else {
var i = node.childNodes.length;
while (i--) {
nodeStack.push(node.childNodes[i]);
}
}
}
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
Vue.component('htmltextarea', {
template: '#hta',
name: 'htmlTextArea',
props:['value'],
mounted: function () {
this.$el.innerHTML = this.value;
},
watch: {
value(v) {
if (v === 'yes') {
let selection = saveSelection(this.$el);
this.$el.innerHTML = 'no!';
this.$emit('input', 'no!');
restoreSelection(this.$el, selection);
}
}
},
methods: {
updateHTML: function(e) {
this.$emit('input', e.target.innerHTML);
}
}
});
new Vue({
el: '#app',
data: {
foo: 'Clear this and type "yes" (without the quotes). It should become "no!".'
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<htmltextarea id="textarea" v-model="foo"></htmltextarea>
<hr>
Result: <pre>{{ foo }}</pre>
</div>
<template id="hta">
<div contenteditable="true" #input="updateHTML" class="textareaRoot"></div>
</template>
In your app, I recommend you place them in a dedicated .js file, just for better organization.
I am diving into Vue for the first time and trying to make a simple filter component that takes a data object from an API and filters it.
The code below works but i cant find a way to "reset" the filter without doing another API call, making me think im approaching this wrong.
Is a Show/hide in the DOM better than altering the data object?
HTML
<button v-on:click="filterCats('Print')">Print</button>
<div class="list-item" v-for="asset in filteredData">
<a>{{ asset.title.rendered }}</a>
</div>
Javascript
export default {
data() {
return {
assets: {}
}
},
methods: {
filterCats: function (cat) {
var items = this.assets
var result = {}
Object.keys(items).forEach(key => {
const item = items[key]
if (item.cat_names.some(cat_names => cat_names === cat)) {
result[key] = item
}
})
this.assets = result
}
},
computed: {
filteredData: function () {
return this.assets
}
},
}
Is a Show/hide in the DOM better than altering the data object?
Not at all. Altering the data is the "Vue way".
You don't need to modify assets to filter it.
The recommended way of doing that is using a computed property: you would create a filteredData computed property that depends on the cat data property. Whenever you change the value of cat, the filteredData will be recalculated automatically (filtering this.assets using the current content of cat).
Something like below:
new Vue({
el: '#app',
data() {
return {
cat: null,
assets: {
one: {cat_names: ['Print'], title: {rendered: 'one'}},
two: {cat_names: ['Two'], title: {rendered: 'two'}},
three: {cat_names: ['Three'], title: {rendered: 'three'}}
}
}
},
computed: {
filteredData: function () {
if (this.cat == null) { return this.assets; } // no filtering
var items = this.assets;
var result = {}
Object.keys(items).forEach(key => {
const item = items[key]
if (item.cat_names.some(cat_names => cat_names === this.cat)) {
result[key] = item
}
})
return result;
}
},
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<button v-on:click="cat = 'Print'">Print</button>
<div class="list-item" v-for="asset in filteredData">
<a>{{ asset.title.rendered }}</a>
</div>
</div>