Intercept an async action in redux - redux

I'm building a simple notes app using react with redux. My create notes action looks like this:
import axios from "axios";
const URL = 'http://localhost:3002/api/';
export function createNote(data = {}) {
return {
type: 'CREATE_NOTE',
payload: axios.post(URL + 'notes/', data),
};
}
And I've got the following in my reducer:
// Create
case 'CREATE_NOTE_PENDING':
{
return {
...state,
creating: true
};
}
case 'CREATE_NOTE_FULFILLED':
{
return {
...state,
creating: false,
notes: action.payload.data
};
}
case 'CREATE_NOTE_REJECTED': {
return {
...state,
creating: false,
error: action.payload
};
}
And this is my Notes class:
function mapStateToProps(store) {
return {
data: store.note.notes,
fetching: store.note.fetching,
creating: store.note.creating,
error: store.note.error,
};
}
class Notes extends Component {
componentWillMount() {
this.props.dispatch(fetchNotes());
}
create() {
this.props.dispatch(createNote({
title: this.refs.title.value,
content: this.refs.content.value,
}));
this.refs.title.value = '';
this.refs.content.value = '';
}
render() {
return (
<div className="App">
<h1 className="text-center">Notepad</h1>
<div className="row">
<div className="col-md-6 col-md-offset-3">
<div className="form-group">
<input ref="title" placeholder="Create a note" className="form-control" disabled={this.props.creating} />
</div>
<div className="form-group">
<textarea ref="content" className="form-control" disabled={this.props.creating} />
</div>
<button onClick={this.create.bind(this)}
type="submit"
className="btn btn-primary"
style={{float: 'right'}}
disabled={this.props.creating} >Save</button>
</div>
</div>
No on create I'm going to disable the form till I get an answer from the server and reset the content inside the form. My question is how to reset the content of the form when I've got response, i.e. when the form is enabled?

If you want to control the content of input and textarea, you should use controlled components. That is to provide a value attribute to, e.g. input
<input value={this.state.title} placeholder="Create a note" className="form-control" disabled={this.props.creating} />
You can use either internal state or redux state for the value. Then you are able to control the value by setState or redux actions.

Related

NextJS Display API message on the page

I have a login form. The user types their username/password into the form and submits it. The form uses fetch() to send a request to the backend API and get a response back.
I'd like to display a message from the API on the page. It'll be {apiErrorMessage}
For example, the user's account could be locked. The Password could be wrong. The email address could not be confirmed yet.
In the old days I would do this using Partial Page updates AJAX using C# Razor pages.
This is a NextJS project and have no idea how to do this.
Should I be using Server Side Rendering?
Should I be using Client Side Rendering?
UseEffect() hook?
I'd still like the page to look good for SEO purposes.
I'm thinking maybe we have to use a second page to do this with SSR? LoginResult page or something?
Any Help Appreciated :)
Thanks!
const Login = () => {
let apiErrorMessage = "";
const loginFormSubmit = async (event) => {
event.preventDefault()
// Get data from the form.
const data = {
username: event.target.username.value,
password: event.target.password.value,
}
const JSONdata = JSON.stringify(data)
const response = await fetch(`/api/login`)
const result = await response.json()
if (response.ok){
router.push('/dashboard');
}
else {
// Get the error message from API
apiErrorMessage = result.message;
}
}
return (
<div>
<form onSubmit={loginFormSubmit}>
<div className="mb-4">
<label htmlFor="username"><b>Username</b></label>
<input type="text" id="username" required/>
</div>
<div className="mb-4">
<label htmlFor="password"><b>Password</b></label>
<input type="password" id="password" required/>
</div>
<button type="submit">Login</button>
</form>
</div>
<div>
<p>{apiErrorMessage}</p>
</div>
)
}
export default Login;
you don't need to create a page for that you can create a simple useState hook :
const [errorMsg,setErrorMsg] = useState('');
when the user fails to login :
else {
// Get the error message from API
setErrorMsg(result.message)
}
Now under your <form> you create a <div> that will be only shown when errorMsg !== '' :
{errorMsg === '' ? '' : (<div>
<p>login failed : ${errorMsg}</p>
</div>)}
as simple as that.
I can't think of a reason you'd need good SEO for a login component that just accepts user input and returns an error response or forwards them to a page.
Since everything inside /pages is a Client component by default, I think your best bet is simply using state and effects. Here's an example with some slight modifications to your code:
import {useState} from 'react';
import { useRouter } from 'next/navigation';
const Login = () => {
const [apiErrorMessage, setApiErrorMessage] = useState('');
const router = useRouter();
const loginFormSubmit = async (event: any) => {
event.preventDefault();
// Get data from the form.
const data = {
username: event.target.username.value,
password: event.target.password.value,
};
const JSONdata = JSON.stringify(data);
const response = await fetch(`/api/login`);
const result = await response.json();
if (response.ok) {
router.push("/dashboard");
} else {
// Get the error message from API
setApiErrorMessage(result.message);
}
};
return (
<div>
<div>
<form onSubmit={loginFormSubmit}>
<div className="mb-4">
<label htmlFor="username">
<b>Username</b>
</label>
<input type="text" id="username" required />
</div>
<div className="mb-4">
<label htmlFor="password">
<b>Password</b>
</label>
<input type="password" id="password" required />
</div>
<button type="submit">Login</button>
</form>
</div>
<div>
<p>{apiErrorMessage}</p>
</div>
</div>
);
};
export default Login;

Updating user profile information with redux in firebase

I am trying to use Redux in my React application to update the user profile within my Firebase database from my react component.
This is my component:
import { connect } from "react-redux";
import { Redirect } from "react-router-dom";
import { firestoreConnect } from "react-redux-firebase";
import { compose } from "redux";
import { editProfile } from "../../store/actions/editProfileActions";
class UserProfile extends Component {
state = {
firstName:"",
initials:"",
lastName:""
};
onChange = e => {
this.setState({
[e.target.id]: e.target.value
});
};
onSubmit = e => {
e.preventDefault();
console.log(this.state);
this.props.editProfile(this.state);
}
render() {
const { auth, profile } = this.props;
console.log(profile);
if (auth.isEmpty) return <Redirect to="/home" />;
return (
<div className="container">
<form onSubmit={this.onSubmit} className="white">
<h5 className="grey-text text-darken-3">Edit Profile</h5>
<div className="input-field">
<label htmlFor="title">First Name: {profile.firstName}</label>
<input type="text" id="firstName" onChange={this.onChange} />
</div>
<div className="input-field">
<label htmlFor="title">Initials: {profile.initials}</label>
<input type="text" id="initials" onChange={this.onChange} />
</div>
<div className="input-field">
<label htmlFor="title">Last Name: {profile.lastName}</label>
<input type="text" id="lastName" onChange={this.onChange} />
</div>
<div className="input-field">
<button className="btn black z-depth-0">Submit</button>
{ }
</div>
</form>
</div>
)
}
};
const mapStateToProps = state => {
return {
auth: state.firebase.auth,
profile: state.firebase.profile,
};
};
const mapDispatchToProps = dispatch => {
return {
editProfile: edit => dispatch(editProfile(edit))}
}
export default compose(
connect(mapStateToProps, mapDispatchToProps),
firestoreConnect([
{ collection: "profile"}
])
)(UserProfile);
The component correctly displays the current user information.
This is the action I have set up:
return async (dispatch, getState, { getFirestore, getFirebase }) => {
const firebase = getFirebase();
const user = await firebase
.auth()
.currentUser
.updateProfile({
firstName: profile.firstName
});
dispatch({ type: "EDITPROFILE_SUCCESS", user })
console.log("user = " + profile.firstName);
};
}
When I log the entered profile.firstName I get the entered data.
And my reducer:
const editProfileReducer = (state, action) => {
switch (action.type) {
case "EDITPROFILE_ERROR":
return {
...state,
editError: action.error
};
case "EDITPROFILE_SUCCESS":
return {
...state
};
default:
return state;
}
}
export default editProfileReducer;
Any idea what I am missing here?
In your reducer change the like below
case "EDITPROFILE_SUCCESS":
return {
...state,
user:action.user
};
Above is if you want to update the whole user object
If you want to change only name then
Let’s assume that profileName is in user object then
case "EDITPROFILE_SUCCESS":
return {
...state,
user:Object.assign({}, state.user, profileName:action.user.profileName)
};

Set input class if two variables are equal with VueJS

I'd like my <input> class to be set to saved when its value $event.target.value is equal to my Vuex state variable this.$store.state.company.name.
How should I set it ?
Check out Class and Style Bindings in the docs, you could bind saved class to a computed property which checks if the input equals Vuex state variable, here is an example:
new Vue({
el:"#app",
data: {
inputData : '',
},
computed: {
validated(){
// Here we should compare data from vuex and the user input
return this.inputData == "mohd"
}
}
})
.saved {
border: 1px green solid;
}
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.13/dist/vue.min.js"></script>
<div id="app">
<label> write mohd to validate it</label>
<input type="text" :class="{saved: validated}" v-model="inputData">
</div>
InputSave.vue
<template>
<input type="text" v-model="myInput" :disabled="isEqual" :class="isEqual ? 'saved" : ''" />
</template>
<script>
import { mapGetters } from 'vuex'
export default {
data() {
myInput: ''
},
methods: {
...mapGetters
},
computed: {
isEqual: function() {
return this.myInput === this.myGgetter
}
}
}
</script>
Solved here : https://jsfiddle.net/9a9cvu7b/10/ , in fact you can solve it with plain JavaScript.
Just instanciate the check method on all your dynamics generated inputs attached to event #input (who is thrown each time the input value change on keyup)
<div id="app">
<input
type="text"
#input="check">
</div>
const app = new Vue({
el:'#app',
data() {
return {
wordToCheck: 'toto'
}
},
methods: {
check(event) {
const input = event.target;
// replace this.wordToCheck by this.$store.state.company.name
if(input.value === this.wordToCheck) {
input.classList.add("saved")
} else {
input.classList.remove("saved")
}
}
}
})

Using param in redux form hidden field

I am looking to add reset password ability to an auth app in redux. The server sends out an email and puts a token in the params. I am trying to use a hidden field to store the param value that will need to be submitted back to the server. The problem is redux form doesn't seem to be picking up the value and is passing undefined. I am alittle new to redux form and know this isnt yet at v6. My plan is to get all the functionality for the auth built first then upgrade
import React, { Component } from 'react';
import { reduxForm } from 'redux-form';
import * as actions from '../../actions';
class Resetpassword extends Component {
componentWillMount() {
this.props.clearErrorMsg();
}
handleFormSubmit(formProps) {
// Call action creator to sign up the user!
this.props.resetPassword(formProps);
}
renderAlert() {
if (this.props.errorMessage) {
return (
<div className="alert alert-danger">
<strong>Oops!</strong> {this.props.errorMessage}
</div>
);
}
}
render() {
const { handleSubmit, fields: { password, passwordConfirm, token }} = this.props;
console.log(this.props.location.query.reset);
return (
<form onSubmit={handleSubmit(this.handleFormSubmit.bind(this))}>
<fieldset className="form-group">
<label>Password:</label>
<input className="form-control" {...password} type="password" />
{password.touched && password.error && <div className="error">{password.error}</div>}
</fieldset>
<fieldset className="form-group">
<label>Confirm Password:</label>
<input className="form-control" {...passwordConfirm} type="password" />
{passwordConfirm.touched && passwordConfirm.error && <div className="error">{passwordConfirm.error}</div>}
</fieldset>
<input className="form-control" value={this.props.location.query.reset} {...token} type="hidden" />
{this.renderAlert()}
<button action="submit" className="btn btn-primary">Sign up!</button>
</form>
);
}
}
function validate(formProps) {
const errors = {};
if (!formProps.password) {
errors.password = 'Please enter a password';
}
if (!formProps.passwordConfirm) {
errors.passwordConfirm = 'Please enter a password confirmation';
}
if (formProps.password !== formProps.passwordConfirm) {
errors.password = 'Passwords must match';
}
return errors;
}
function mapStateToProps(state) {
return { errorMessage: state.auth.error };
}
export default reduxForm({
form: 'resetpassword',
fields: ['password', 'passwordConfirm','token'],
validate
}, mapStateToProps, actions)(Resetpassword);
The solution I found which may not follow best practices is to remove the hidden field and pass the query string prop into the action handleFormSubmit
handleFormSubmit(formProps) {
// Call action creator to sign up the user!
this.props.resetPassword(formProps,this.props.location.query.reset);}
Then having the action deal with it

Meteor 1.3 + React Stripe Subscription

I'm following along TheMeteorChef's Building a SaaS with Meteor: Stripe which is built with blaze templates. Tried to use react instead but I think I failed somewhere along the way. I've gotten to about half of the part 1 of 2 but enough to test if signing up with plan should work or not. Well, it doesn't work but also doesn't give any errors in console... I have very little experience, just started actually, so I'm hoping I could get some help. Thank you.
~/client/helpers/stripe.js
Meteor.startup(function() {
const stripeKey = Meteor.settings.public.stripe.testPublishableKey;
Stripe.setPublishableKey(stripeKey);
STRIPE = {
getToken: function(domElement, card, callback) {
Stripe.card.createToken(card, function(status, response) {
if(response.error) {
Bert.alert(response.error.message, "danger");
} else {
STRIPE.setToken(response.id, domElement, callback);
}
});
},
setToken: function(token, domElement, callback) {
$(domElement).append($('<input type="hidden" name="stripeToken" />').val(token));
callback();
}
}
});
~/client/components/SignUp.jsx
import React, {Component} from 'react';
import PlanSelectForm from '../components/PlanSelectForm.jsx';
import CreditCardForm from '../components/CreditCardForm.jsx';
export default class SignUp extends Component {
componentDidMount() {
$.validator.addMethod('usernameRegex', function(value, element) {
return this.optional(element) || /^[a-zA-Z0-9-_]+$/i.test(value);
}, "Username must contain only letters, numbers, underscores and dashes.");
$('#application-signup').validate({
rules: {
username: {
required: true,
usernameRegex: true,
minlength: 6
},
emailAddress: {
required: true,
email: true
},
password: {
required: true,
minlength: 6
}
},
messages: {
username: {
required: 'You can\'t leave this empty',
usernameRegex: 'You can use letter, numbers, underscores, and dashes.',
minlength: 'Too short. Use at least 6 characters.'
},
emailAddress: {
required: 'You can\'t leave this empty',
email: 'Email is invalid or already taken.'
},
password: {
required: 'You can\'t leave this empty',
minlength: 'Too short. Use at least 6 characters.'
}
},
handleSubmit: function() {
STRIPE.getToken('#application-signup', {
number: $('[data-stripe="cardNumber"]').val(),
exp_month: $('[data-stripe="expMo"]').val(),
exp_year: $('[data-stripe="expYr"]').val(),
cvc: $('[data-stripe="cvc"]').val()
}, function() {
const customer = {
username: $('[name="username"]').val(),
emailAddress: $('[name="emailAddress"]').val(),
password: $('[name="password"]').val(),
plan: $('[name="selectPlan"]:checked').val(),
token: $('[name="stripeToken"]').val()
};
const submitButton = $('input[type="submit"]').button('loading');
Meteor.call('createTrialCustomer', customer, function(error, response) {
if(error) {
alert(error.reason);
submitButton.button('reset');
} else {
if(response.error) {
alert(response.message);
submitButton.button('reset');
} else {
Meteor.loginWithPassword(customer.emailAddress, customer.password, function(error) {
if(error) {
alert(error.reason);
submitButton.button('reset');
} else {
Router.go('/chart');
submitButton.button('reset');
}
});
}
}
});
});
}
});
}
render() {
console.log(this);
return (
<form id="application-signup" className="signup">
<h4>Account details</h4>
<div className="form-group">
<label for="username">Username</label>
<input type="text"
name="username"
className="form-control"
placeholder="Username" />
</div>
<div className="form-group">
<label for="emailAddress">Email Address</label>
<input type="email"
name="emailAddress"
className="form-control"
placeholder="Email Address" />
</div>
<div className="form-group">
<label for="password">Password</label>
<input type="password"
name="password"
className="form-control"
placeholder="Password" />
</div>
<h4 className="page-header">Payment Information</h4>
<label>Which plan sounds <em>amazing</em>?</label>
<PlanSelectForm />
<div className="form-group">
<CreditCardForm />{/* data={signup} /> */}
</div>
<div className="form-group">
<input type="submit"
className="btn btn-success btn-block"
data-loading-text="Setting up your trial..."
value="Put me on the rocketship" />
</div>
</form>
)
}
}
Note: In the tutorial, TheMeteorChef uses a dynamic template for CreditCardForm with data="signup" context. I think he mentions the CC template will be used again after but I haven't gone that far yet. Anyways, I didn't know what "signup" means... so I left it commented out. If you know, please let me know about that as well.
~/client/components/PlanSelectForm.jsx
import React, {Component} from 'react';
export default class PlanSelectForm extends Component {
componentDidMount() {
const firstPlanItem = $('.select-plan a:first-child');
firstPlanItem.addClass('active');
firstPlanItem.find('input').prop('checked', true);
}
plans() {
return Meteor.settings.public.plans;
}
handleClickItem(e) {
const parent = $(e.target).closest('.list-group-item');
console.log(parent);
parent.addClass('active');
$('.list-group-item').not(parent).removeClass('active');
$('.list-group-item').not(parent).find('input[type="radio"]').prop('checked', false);
parent.find('input[type="radio"]').prop('checked', true);
}
render() {
let plans = this.plans();
if(!plans) {
return(<div>loading...</div>);
}
return (
<div className="list-group select-plan">
{plans.map((plan) => {
return (
<a key={plan.id}
href="#"
className="list-group-item"
onClick={this.handleClickItem.bind(this)}>
<input key={plan.id}
type="radio"
ref="selectPlan"
id={`selectPlan_${plan.id}`}
value={plan.name} />
{plan.name} {plan.amount.usd}/{plan.interval}
</a>
)
})}
</div>
)
}
}
~/client/components/CreditCardForm.jsx
import React, {Component} from 'react';
export default class CreditCardForm extends Component {
render() {
return (
<div>
<div className="row">
<div className="col-xs-12">
<div className="form-group">
<label className="text-success">
<i className="fa fa-lock"></i> Card Number
</label>
<input type="text"
data-stripe="cardNumber"
className="form-control card-number"
placeholder="Card Number" />
</div>
</div>
</div>
<div className="row">
<div className="col-xs-4">
<label>Exp. Mo.</label>
<input type="text"
data-stripe="expMo"
className="form-control exp-month"
placeholder="Exp. Mo." />
</div>
<div className="col-xs-4">
<label>Exp. Yr.</label>
<input type="text"
data-stripe="expYr"
className="form-control exp-year"
placeholder="Exp. Yr." />
</div>
<div className="col-xs-4">
<label>CVC</label>
<input type="text"
data-stripe="cvc"
className="form-control cvc"
placeholder="CVC" />
</div>
</div>
</div>
)
}
}
~/server/signup.js
Meteor.methods({
createTrialCustomer: function(customer) {
check(customer, {
name: String,
emailAddress: String,
password: String,
plan: String,
token: String
});
const emailRegex = new RegExp(customer.emailAddress, 'i');
const usernameRegex = new RegExp(customer.username, 'i');
const lookupEmail = Meteor.users.findOne({'emails.address': emailRegex});
const lookupUser = Meteor.users.findOne({'username': usernameRegex});
if(!lookupEmail) {
if(!lookupUser) {
const newCustomer = new Future();
Meteor.call('stripeCreateCustomer', customer.token, customer.emailAddress, function(error, stripeCustomer) {
if(error) {
console.log(error);
} else {
const customerId = stripeCustomer.id,
plan = customer.plan;
Meteor.call('stripeCreateSubscription', customerId, plan, function(error, response) {
if(error) {
console.log(error);
} else {
try {
const user = Accounts.createUser({
username: customer.username,
email: customer.emailAddress,
password: customer.password
});
const subscription = {
customerId: customerId,
subscription: {
plan: {
name: customer.plan,
used: 0
},
payment: {
card: {
type: stripeCustomer.sources.data[0].brand,
lastFour: stripeCustomer.sources.data[0].last4
},
nextPaymentDue: response.current_period_end
}
}
}
Meteor.users.update(user, {
$set: subscription
}, function(error, response) {
if(error) {
console.log(error);
} else {
newCustomer.return(user);
}
});
} catch(exception) {
newCustomer.return(exception);
}
}
});
}
});
return newCustomer.wait();
} else {
throw new Meteor.Error('username-exists', 'Sorry, that username is already active!');
}
} else {
throw new Meteor.Erro('email-exists', 'Sorry, that email is already active!')
}
},
})
~/server/stripe.js
const secret = Meteor.settings.private.stripe.testSecretKey;
const Stripe = StripeAPI(secret);
Meteor.methods({
stripeCreateCustomer: function(token, email) {
check(token, String);
check(email, String);
const stripeCustomer = new Future();
Stripe.customers.create({
source: token,
email: email
}, function(error, customer) {
if(error){
stripeCustomer.return(error);
} else {
stripeCustomer.return(customer);
}
});
return stripeCustomer.wait();
},
stripeCreateSubscription: function(customer, plan) {
check(customer, String);
check(plan, String);
const stripeSubscription = new Future();
Stripe.customers.createSubscription(customer, {
plan: plan
}, function(error, subscription) {
if(error) {
stripeSubscription.return(error);
} else {
stripeSubscription.return(subscription);
}
});
return stripeSubscription.wait();
}
})
packages
"dependencies": {
"meteor-node-stubs": "~0.2.0",
"react": "^15.0.2",
"react-addons-css-transition-group": "^15.0.2",
"react-dom": "^15.0.2",
"react-mounter": "^1.2.0"
},
accounts-base
accounts-password
session
check
random
kadira:flow-router
ultimatejs:tracker-react
meteortoys:allthings
fourseven:scss
fortawesome:fontawesome
themeteorchef:bert
themeteorchef:jquery-validation
momentjs:moment
mrgalaxy:stripe
Thanks for reading, I hope that it wasn't painful.
did you import mrgalaxy:stripe ?
it will be something like
import { StripeAPI } from 'meteor/mrgalaxy:stripe'
I guess.
anw, you can install by npm :
npm install stripe
import Stripe from 'stripe'
then
Stripe('your_secret_key')

Resources