VueJS 2 - Submitting form with Stripe Checkout response? - vuejs2

I'm attempting to integrate VueJS (2.4.2) with Stripe Checkout, but my form is not submitting the updated values for the token and email returned from Stripe.
The basic flow: mount a Vue instance to a form, select a "plan" from a JSON object, open the Stripe Checkout modal populated with the plan's info, bind a couple of the form inputs to the values returned by Stripe, and submit the form. All goes according to plan EXCEPT that the data that actually hits the server is NOT the updated values.
I have tried v-bind and v-model and neither seems to work. I can see the form being updated with the correct values from the Stripe response, but when it actually submits, the originally bound data is submitted.
The HTML (Laravel Blade)
#extends('layouts.master')
#section('page_meta')
<title>{{ page_title('Checkout') }}</title>
#endsection
#section('content')
<div class="container">
#include('errors.list')
{!! Form::open([
'url' => '/subscriptions',
'id' => 'checkoutForm',
]) !!}
#foreach ($plans as $plan)
<div>
{{ $plan->name }} {{ $plan->description }}
<button v-on:click.prevent="subscribe({{ $plan->id }})">Select</button>
</div>
#endforeach
<input name="selected_plan" :value="selectedPlanId">
<input name="stripe_email" :value="stripeEmail">
<input name="stripe_token" :value="stripeToken">
{!! Form::close() !!}
</div>
#endsection
#push('scripts.body')
<script>
var plans = {!! $plans !!}; // JSON from the controller
</script>
<script src="https://checkout.stripe.com/checkout.js"></script>
<script src="/js/checkout.js"></script>
#endpush
The JavaScript:
var vm = new Vue({
el: "#checkoutForm",
data: {
plans: plans, // From a global JSON array
selectedPlan: null, // Default value
stripeEmail: 'foo#example.com', // Initial bindings to test values
stripeToken: 'invalidToken' // Initial bindings to test values
},
computed: {
selectedPlanId() {
if (this.selectedPlan) {
return this.selectedPlan.id;
}
return '';
}
},
methods: {
subscribe(planId) {
let plan = this.findPlanById(planId);
console.log(plan); // Works as expected
this.selectedPlan = plan;
// The following opens a Stripe checkout widget
// with all the correct information.
this.handler.open({
name: plan.name,
description: plan.description,
amount: plan.price * 100, // stored as decimal
token: (token) => {
console.log(token); // Works as expected
this.stripeToken = token.id; // Verified in Vue Dev Tools
this.stripeEmail = token.email; // Verified in Vue Dev Tools
alert(this.stripeToken); // Correct values
alert(this.stripeEmail); // Correct values
// At this point, the form inputs are updated
// with the correct values returned from Stripe.
vm.$el.submit(); // Submits the form to the proper URL
// When the POST request hits the server, the
// token and email fields have their original values
// i.e. "foo#example.com" and "invalidToken"
}
});
},
findPlanById(id) {
return this.plans.find(plan => plan.id == id);
}
},
created() {
this.handler = StripeCheckout.configure({
key: window.Laravel.stripeKey,
locale: 'auto',
});
}
})

UPDATE: It works if I wrap the form submission in a setTimeout. Apparently it needs a little time for the updated values to "take".

Related

How to handle event on html render by computed function in vue js?

I'm using vue 2. I have a text get from api.
"Hello everyone! My name is [input]. I'm [input] year old".
Now, I have to replace the [input] with an html input and handle the onKeyUp for this input.
What I have to do?
I used computed render html, but it not work with v-on:xxx.
content.replaceAll('[answer]', '<input type="text" class="input_answer" v-on:click="handleInput()"/>')
Thanks!
After spending an hour and so on this requirement, I came up with the solution.
Here you go (I added all the descriptive comments/steps in the below code snippet itself) :
// Template coming from API
var textFromAPI = "<p>Hello everyone! My name is [input]. I'm [input] year old</p>";
// getting the array of input tags. So that we can loop and create the proper input element.
const matched = textFromAPI.match(/(input)/g);
// Iterating over an array of matched substrings and creating a HTML element along with the required attributes and events.
matched.forEach((el, index) => {
textFromAPI = textFromAPI.replace('[input]', `<input type="text" id="${index + 1}" v-model="inputValue[${index}]" v-on:keyup="getValue"/>`);
})
// Here, we are compiling the whole string so that it will behave in a Vue way.
var res = Vue.compile(textFromAPI)
var app = new Vue({
el: '#app',
data: {
compiled: null,
inputValue: []
},
render: res.render,
staticRenderFns: res.staticRenderFns,
mounted() {
setTimeout(() => {
this.compiled = res;
})
},
methods: {
getValue() {
// Here you will get the updated values of the inputs.
console.log(this.inputValue);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
</div>
Thanks # Rohit Jíndal, but!
When I use vue2 and it doesn't work. And there is an error:
TypeError: Cannot read properties of undefined (reading 'string')
I build this to a component and use anywhere in my project.
<render-html :text="question.quest_content" #handleAnswer="handleAnswer"></render-html>
I used and it's work.
this.$options.staticRenderFns = string.staticRenderFns;
return string.render.bind(this)(h)
Thanks so much!

In Cypress how to found count a selection with same ID and get the length?

I have a such HTML code.
<div id ='pages'>
<div id='wrapper'>1 </div>
<div id='wrapper'>2 </div>
</div>
I am want to find elements count with id wrapper.
I using Cypress. I'm starting to learn Cypress.
If I try:
cy.get('div#wrapper').should('have.length', 2)
I get AssertionError:
CypressError: Timed out retrying: expected 1 to equal 2
As jonrsharpe pointed out, it's invalid HTML to have multiple elements with identical id attribute.
That being said, DOM is quite smart and can recover and work even with invalid HTML. Duplicate-id elements shouldn't cause much trouble.
If you e.g. try doing document.querySelectorAll('#wrapper') it should return list of 2 elements (in your case).
Problem is, Cypress is using jQuery to query the DOM instead of using native DOM methods and I guess jQuery isn't as smart (or it's more pedantic).
That being said, I can't reproduce that error when running:
// succeeds
cy.get('div#wrapper').should('have.length', 2)
Only when querying #wrapper directly (without the preceding div):
// fails
cy.get('#wrapper').should('have.length', 2)
I reckon this is because jQuery uses a heuristic of exiting early when a selector string (#wrapper) contains only a single id (and that's why div#wrapper returns both elements).
Also, your solution in comments (cy.get('#pages') .find('div#wrapper') .should(($div) => { expect($div).to.have.length(2) })), while working, isn't ideal because it won't retry. Let me demonstrate:
In the following code, the 2nd #wrapper will appear in the DOM only after 1 sec.
describe( 'test', () => {
beforeEach(() => {
cy.document().then( doc => {
doc.body.innerHTML = `
<div id='pages'>
<div id='wrapper'>1</div>
</div>
`;
setTimeout(() => {
doc.body.innerHTML = `
<div id='pages'>
<div id='wrapper'>1</div>
<div id='wrapper'>2</div>
</div>
`;
}, 1000 );
});
});
// will fail
it('solution A', () => {
cy.get('#pages') // <- won't be retried
.find('div#wrapper') // <- only this command will be retried
.should( $div => expect($div).to.have.length(2) );
});
// will pass
it('solution B', () => {
cy.get('#pages #wrapper') // <- will be retried and succeed in 1sec
.should( $div => {
expect($div).to.have.length(2);
});
});
// will pass
it('solution C', () => {
cy.get('#pages')
.should($pages => {
// using native DOM querying
expect($pages[0].querySelectorAll('#wrapper').length).to.eq(2);
});
});
});
Thus, you should go with solution similar to B or C.

Invert boolean on click with v-for?

Beginner to JS and VueCLI so I'll try to explain as best as I can. I'm using Express as my back-end.
I'm trying to change the boolean in my array of objects on click. I'm able to accomplish that but when I click on a different list item in my v-for loop it's flipping the boolean in all other indexes of my array. Here's my code:
Express: /routes:
// fake data store
const tasks = [
{ id: 1, task: 't1', completed: false},
{ id: 2, task: 't2', completed: false},
{ id: 3, task: 't3', completed: false}
];
/**
* GET handler for /tasks route
* #returns {Array.<{id: Number, task: String, completed: Boolean}>} array of task objects
*/
router.get('/', (req, res) => {
res.send(tasks);
});
Webapp:
/**
* GET /tasks
* #returns Promise => {Array.<{id: Number, task: String, completed: Boolean}>} array of task objects
*/
export function getTasks() {
return request('tasks');
}
and now my Vue component:
<template>
<div id="tasks">
<h2>Movies to Add</h2>
<ul class="todo-list">
<li v-for='task in tasks' :id="task.id" v-on:click="completeMovie($event)" :key='task.id' class="todo-list__li">
<input class="todo-list__input" type="checkbox" :name='task.task' :id="task.task">
<div class="todo-list__checkbox">
<span class="todo-list__checkbox-inner"><i></i></span>
</div>
<label :for='task.task'>{{ task.task }}</label>
</li>
</ul>
</div>
</template>
<script>
import {getTasks} from './../../services/tasks';
export default {
name: 'TaskList',
data: function() {
return {
tasks: []
};
},
created: function() {
getTasks()
.then(res => this.tasks = res);
},
methods: {
completeMovie: function (event) {
var taskId = event.currentTarget.id -1;
getTasks()
.then((res) => {
this.tasks = res;
res[taskId].completed = !res[taskId].completed;
});
}
}
}
</script>
So when I click on my first list item it changes the Task: t1 to True but if I click on the second list item it changes t1 back to False and t2 to True. I'm not sure exactly what I'm doing wrong. I'm not even sure this is the best way to do this. My main issue is I'm not sure why it's happening.
Any help is much appreciated!
You're probably over-complicating this.
All you need is
<li v-for="task in tasks" :key="task.id"
#click="task.completed = !task.completed"
class="todo-list__li">
Demo ~ https://jsfiddle.net/xy1q0auL/2/
There's no (obvious) need to re-fetch the tasks every time you click on one. This is why your previous changes are reset; it's because you overwrite all the data with the unmodified values from getTasks().
When completeMovie() is called, you send an HTTP request to your server that is gonna send you back unaltered task list. I don't understand clearly what's you're trying to achieve but your callback in the promise has no sens. In the callback you reaffect the "tasks" list in your vue :
In this case, here is the event timeline you have to do :
When client page is loaded, Vue Initialized, the vue component calls your webservice to get the task list then print it.
Then when I click on a task, you have to make a another HTTP call to your webservice (on another route), that will update the task list and will return it.
Then in the call of your vue component you reaffect the new task list.
You can make its simply:
<li v-for='task in tasks' :id="task.id" v-on:click="completeMovie(task.id)" :key='task.id' class="todo-list__li">
<input class="todo-list__input" type="checkbox" :name='task.task' :id="task.task">
<div class="todo-list__checkbox">
<span class="todo-list__checkbox-inner"><i></i></span>
</div>
<label :for='task.task'>{{ task.task }}</label>
</li>
Ans for the method
completeMovie: function ($taskId) {
this.tasks[$taskId].completed = !this.tasks[$taskId].completed;
}
}

how to do pagination in vuejs

hi i want to do pagination in my view page.can anyone tell me how to do that in vuejs..
Here is my view page:
<div class="container">
<div class="row">
<el-row :gutter="12">
<el-col>
<p>View Candidates</p>
</el-col>
</el-row>
<el-row :gutter="12">
<template v-for="c in candidates">
<el-col :span="6">
<Candidate :c="c" :key="c.id"></Candidate>
</el-col>
</template>
</el-row>
</div>
here is my js page:
const app = new Vue({
el: '#app',
data() {
return {
candidates: window.data.candidates,
}
},
components: { Candidate }
});
i am working on laravel5.4 and vuejs 2
Please can anyone help me..how to do this..
For real pagination you will need to ensure that your endpoints (from your post I'd say something like /candidates) will return json AND that it will return a pagniated object ofcourse.
In Laravel you'd do it like
public function index() {
return Candidates::paginate(10);
}
EDIT: for more information regarding laravel pagination you can take a look at their examples and docs: https://laravel.com/docs/5.4/pagination
A full example is rather hard to give but here a really short one
routes/web.php
Route::get('candidates', 'CandidateController#index');
app/http/controller/CandidateController
public function index() {
$candidates = App\Candidate::paginate(10);
return $candidates;
}
For a more detailed version of the laravel part you should provide your Controller, Migration, Routing setup.
In Vue I'd suggest you load all your data from within Vue and not with blade. Even though you could keep it as it is - it would be more "unified".
data: function() {
return { paginator: null }
},
created: function() {
// load initial first 10 entries
axios.get('/candidates').then(function(response) {
this.paginator = response.data;
}.bind(this));
}
Ok so now you have the initial load as you had it before. You can loop through pagniator.data which is your actual list now. Small example:
<ul v-if="paginator"><!-- important !not loaded on initial render-->
<li v-for="paginator.data as candidate">{{ candidate.name }}</li>
</ul>
Now to the load more. Let's say you want a button for that. The paginator has a pro called next_page_url to give you the next http endpoint. If it's null - now data is left to load.
<button v-if="paginator && paginator.next_page_url" #click.prevent="loadMore">Load more</button>
Button is setup - now the load more
methods: {
loadMore: function() {
// load next 10 elements
axios.get(this.paginator.next_page_url).then(function(response) {
// you have two options now. Either replace the paginator fully - then you will actually "page" your results.
// only 10 will be visible at any time
this.paginator = response.data;
}.bind(this));
}
}
There you go this is an actual pagination. If you want to loadMore to add 10 elements to your current list it is a little bit more tricky because you don't want to replace the paginator.data with the new loaded stuff. You want to concat it.
...
axios.get(this.paginator.next_page_url).then(function(response) {
response.data.data = this.paginator.data.concat(response.data.data);
this.paginator = response.data;
}.bind(this));

vue interpolated value not updating

I'm using Vue JS 2.0 and this is a table I'm trying to display. Everything displays properly initially but when I update(in a method called editPlayerStat) editedPlayers[any-Player-Id], it doesn't update the interpolated value in the respective small tag.
At the editPlayerStat() I check to see if editedPlayers[any-Player-Id] is changing properly and it is. But for some reason it isn't getting updated in the small tag. Also, in the span tag's v-if doesn't update and has the same problem.
While in the same method editPlayerStat(), I manually update player.stats.month.goals you see interpolated, to see if also has the same problem and IT DOESN'T. It works just fine.
What am I doing wrong?
<tr v-if="showMonth" v-for="player in players">
<td><input type="checkbox"></td>
<td>{{ player.name }}</td>
<td><a #click.prevent="editPlayerStat(player._id,'goals',false)" href=""><i class="fa fa-arrow-down"></i></a>
{{ player.stats.month.goals }}
<a #click.prevent="editPlayerStat(player._id,'goals',true)" href=""><i class="fa fa-arrow-up"></i></a>
<span id="changed"> <small>({{ editedPlayers[player._id].stats.goals }})</small></span>
</td>
<td><a #click.prevent="editPlayerStat(player._id,'assists',false)" href=""><i class="fa fa-arrow-down"></i></a>
{{ player.stats.month.assists }}
<a #click.prevent="editPlayerStat(player._id,'assists',true)" href=""><i class="fa fa-arrow-up"></i></a>
<span id="changed" v-if="editedPlayers[player._id].stats.assists > 0"> <small>({{ editedPlayers[player._id].stats.assists }})</small></span>
</td>
</tr>
This is my editPlayerStat() method:
editPlayerStat(id, type, isIncrease) {
console.log('editPlayerStat...');
console.log('editedPlayers[player._id].stats.goals...' + this.editedPlayers[id].stats.goals );
if(isIncrease) {
console.log('increase');
this.editedPlayers[id].stats[type]++;
this.editedPlayers[id].isEdited = true;
} else if(!isIncrease && this.editedPlayers[id].stats[type] > 0) {
console.log('decrease');
this.editedPlayers[id].stats[type]--;
this.editedPlayers[id].isEdited = true;
}
}
ANOTHER instance:
I use v-model on an input and display the interpolated value beside and it updates just fine when I update the input tag value. But when I 'watch' for changes in the object player, the watch function doesn't run except for the first time.
<input class="form-control" id="name" placeholder="Name" type="text" v-model="player.name">{{ player.name }}
watch: {
player: function(player) {
console.log('from watch... '+JSON.stringify(player));
}
},
methods: {
getPlayerInfo() {
var vm = this;
axios.get('http://localhost:3000/admin/players/info/'+'5847a093a5e13203a4d0455f')
.then(function (response) {
vm.player = response.data;
console.log(response);
console.log(JSON.stringify(vm.player));
console.log('showForm: ' + vm.showForm);
})
.catch(function (error) {
console.log(error);
});
},
created: {
this.getPlayerInfo();
}
Output:
Object { data: Object, status: 200, statusText: "OK", headers: Object, config: Object, request: XMLHttpRequest }
{"_id":"5847a093a5e13203a4d0455f","name":"Michael","age":25,"dateOfBirth":"03-04-1998","phone":7987997799,"image":"imgur::djnwkndkjnkn","preferredFoot":"Right","isActive":true,"positions":["CB","RB","RW"]}
showForm: true
from watch... {"_id":"5847a093a5e13203a4d0455f","name":"Michael","age":25,"dateOfBirth":"03-04-1998","phone":7987997799,"image":"imgur::djnwkndkjnkn","preferredFoot":"Right","isActive":true,"positions":["CB","RB","RW"]}
As you can see, even though the interpolated player.name is updated when I edit the value in the input tag, the watch function for player property doesn't run except for that first time when player was initialized with an Object.
Thanks to #vbranden and participants in an issue I raised here. So here's what was going wrong.
I was running into this behavior in Vue 2.0, which according to the documentation, "cannot detect property addition or deletion". You will run into this sooner or later.
The thing was that, I was adding properties to an Object directly (or as I thought 'string indexed array' values to an array. Note: also see issue; javascript doesn't allow associative arrays) and Vue couldn't watch for changes in it due to this caveat.
Solution: Instead of directly assigning properties to an Object, you need to use Vue.set() or this.$set() to assign them. This allows Vue to track for changes in them.
Original Problem: fiddle
editedPlayers: []
this.editedPlayers['someID'] = { name: 'John', stats: { goals: 10, assists: 10 }};
this.editedPlayers['someOtherID'] = { name: 'Flint', stats: { goals: 10, assists: 10 }};
Solution: fiddle
editedPlayers: {}
Vue.set(this.editedPlayers, 'someID', { name: 'John', stats: { goals: 10, assists: 10 }});
Vue.set(this.editedPlayers, 'someOtherID', { name: 'Flint', stats: { goals: 10, assists: 10 }});