React render/re-render two different versions of state - render

I have an app that is rendering correctly on initial load. It is rendering data from a json obj I'm passing in as a prop. I'm saving the data in an array in state. Later async calls occur. Using setState I update the state accordingly. When React re-renders however it does two passes -- one with the correct, new state that I can see when I step through, then one with the initial state. The initial state render occurs last so I'm left with the same initial state. What gives?
updateTab = React.addons.update(this.state, {
tabResults: {0: {docs: {$set: data.response.docs}}},
counts: {$set: data.facet_counts.facet_fields.categories},
tab: { $set: b }
});
this.setState(updateTab, this.saveTab);
Then my component looks like:
var TabControls = React.createClass({
render: function(){
var tabsum = 0;
countrow = [];
function addit(d){
tabsum = tabsum + d;
}
for(o = 0; o < 5; o++){
if(this.props.counts[o] != null){
addit(this.props.counts[o]);
}
}
if(this.props.tabID == 0){
countrow.push(<p className="result-count-large col-sm-6">{tabsum} Results</p>);
}
else{
countrow.push(<p className="result-count-large col-sm-6">{this.props.tabCount} Results</p>);
}
return (
<div className="search-tab-controls container-fluid">
{countrow}
<div className="col-sm-6">
<p>Sort by</p>
<select className="form-control">
<option>Relevance</option>
<option>Recent</option>
</select>
<img src="/img/search/print-icon.jpg" alt="print search results" />
</div>
</div>
);
}
});
Like I said, I can step through and watch this happen. I watch it set the proper updateTab variable to get pushed to setState. Then I watch my component re-render. The first re-rendering the state is correct. The second re-rendering has the initial all-zero state. I tried putting unique keys on the LIs in the countrow array, that did nothing.

Related

Remove Item From Array after ajax response

I'm a frustrated Vue.js noobie coming from jQuery.
I'm trying to do something very basic for a simple project: Delete an article but only after ajax response. While waiting, for the response there's a spinner. (I don't want components or vue files. It's a simple, single file app)
I've been hacking away at it for a few days now and I can't grasp some basic concepts it seems. (Or I want it to behave like jquery)
Fiddle
window.app = new Vue({
el: '#app',
data: {
name: '',
posts: [],
loadingItems: [],
},
created() {
this.fetchData();
},
methods:{
fetchData() {
axios.get('https://jsonplaceholder.typicode.com/posts').then(response => {
this.posts = response.data.slice(0,20);
});
},
removeItem(item) {
var index = this.posts.indexOf(item);
//var loadingIndex = this.loadingItems.push(item) - 1;
//console.log(loadingIndex);
item.isLoading = true;
//Update vue array on the fly hack - https://vuejs.org/2016/02/06/common-gotchas/
this.posts.splice(index, 1,item);
axios.post('//jsfiddle.net/echo/json/', "json='{status:success}'&delay=2")
.then(response => {
this.posts.splice(index, 1);
//this.loadingItems.splice(loadingIndex, 1);
//this.loadingItems.pop(item);
//item.isLoading = false;
//this.posts.splice(index, 1,item);
});
}
},
computed: {
showAlert() {
return this.name.length > 4 ? true : false
}
}
})
<div id="app">
<div v-for="(post,index) in posts" :key="post.id">
<b-card class="mb-2">
<b-card-title>{{post.title}}</b-card-title>
<b-card-text>{{post.body}}</b-card-text>
<a href="#" #click.prevent="removeItem(post)" class="card-link">
Remove
<span v-show="post.isLoading" class="spinner"></span>
</a>
</b-card>
</div>
</div>
Works fine for deleting them 1 by 1 one but not when you click on multiple at the same time, since the index is different by the time the request comes back and it splices the wrong item.
What I've tried so far:
First, it took me a day to figure out that item.isLoading = true; won't work if it wasn't present when the data was first observed (or fetched). However, I don't want to add the property to the database just for a loading animation. So the workaround was to do this.posts.splice(index, 1,item); to "force" Vue to notice my change. Already feels hacky.
Also tried using an array LoadingItems and pushing them while waiting. Didn't work due to the same problem: don't know which one to remove based on index alone.
Studying the TODO app didn't help since it's not quite addressing handling async ajax responses or adding properties at runtime.
Is the best way to do it by using post.id and then trying to pass it and find it back in the array? Really confused and thinking jQuery would have been easier.
Any help will be appreciated. Thanks!
Works fine for deleting them 1 by 1 one but not when you click on multiple at the same time, since the index is different by the time the request comes back and it splices the wrong item.
Don't save the index in a variable. Calculate it every time you need it:
removeItem(item) {
item.isLoading = true;
this.posts.splice(this.posts.indexOf(item), 1, item);
axios.post('/echo/json/', "json='{status:success}'&delay=2")
.then(response => {
this.posts.splice(this.posts.indexOf(item), 1);
});
}

React setState with multiple AJAX calls

I want the view to always be updated with the result of the latest call to searchWiki(). I've used a module that resolves repeated calls to $.ajax to the value returned by the most recent call.
It still seems to go out of sync and show the result of previous calls however. I'm guessing this is because setState is async? How should I keep the two async operations in sync?
In addition, I realize I should put in a debounce somewhere, but I'm not sure what I should debounce. handleChange, searchWiki or latestAjax?
Here is a demo: http://codepen.io/prashcr/pen/obXvWv
Try typing stuff then backspacing to see what I mean.
Search component
<div style={style}>
<input
value={this.props.search}
// this calls searchWiki with e.target.value
onInput={this.handleChange}
/>
</div>
searchWiki function of parent component
searchWiki (search) {
console.log('searchWiki called with: ' + search);
if (!search) {
this.setState({results: []});
}
else {
// "latest" taken from https://github.com/bjoerge/promise-latest
let latestAjax = latest($.ajax);
let options = {
url: this.props.url + search,
dataType: 'jsonp'
};
latestAjax(options)
.then(data => {
var results = data.query.search.map(res => {
res.url = 'http://en.wikipedia.org/wiki/' + encodeURIComponent(res.title);
return res;
});
this.setState({results: results});
})
.error(e => console.log(e.message));
}
}
The handleChange function is too generic to be debounced with a hardcoded value as you might want to use this search component elsewhere. However, you still want to catch the repeating action as early as possible and ensure that it never does any unnecessary work.
Therefore I would suggest that you debounce the handleChange function with an optional prop, defaulting to 0ms.
getDefaultProps() {
return {
debounce: 0
};
},
render() {
// ...
return (
<div style={style}>
<input
// ...
onInput={debounce(this.handleChange, this.props.debounce)}/>
</div>
);
}
Then make sure you pass this prop whenever you want to debounce the handler.
<Search onSearch={this.searchWiki} debounce={1000} />
Your other problem is happening because you are calling latest inside your searchWiki function and you only call the returned function once! Each time you call searchWiki you create a new latestAjax function.
For it to work, you'll need to call the returned function multiple times.
This means defining the wrapped $.ajax function outside of searchWiki function.
latestAjax: latest($.ajax),
searchWiki(search) {
// ...
this.latestAjax(options)
.then(data => {
});
}

History inside state

I`am created ionic app that has one state called master {url: /master}. There are two buttons on the state view. Each button shows hidden div with text.
I want to implement history tracking by this rules:
When user clicks on first button then url should be appended with firstOpened=true
When user clicks on second button then url should be appended with secondOpened=true
Controllers should stay the same (no re-initialization needed)
Back button of browser should undo last action, for example: If user opens first div then back button should close this div and restore url without page reload.
If history of master state is empty, then back button should restore previous state if it exsits
If i use $state.go to change url, then ionic will reinitialize state and all controllers. It looks like i move from one view to another (from one state to another), but i dont want to change the state, i want to update url and push history. I want to use same state without re-initialization.
I`am created plunker without history tracking. Can somebody help me with implementig history tracking?
script.js (see ):
angular.module('starter', ['ionic'])
.config(function($stateProvider, $urlRouterProvider) {
var controller = 0;
$urlRouterProvider.otherwise("/master");
$stateProvider.state('master', {
url: '/master?firstOpened&secondOpened',
views: {
'default': {
templateUrl: 'content.html',
controller: function($scope, $ionicHistory, $stateParams, $state) {
console.log('Initing controller...');
controller = controller + 1;
$scope.controller = controller;
$scope.firstOpened = $stateParams.firstOpened
$scope.secondOpened = $stateParams.secondOpened
$scope.openFirst = function() {
$scope.firstOpened = true;
};
$scope.openSecond = function() {
$scope.secondOpened = true;
};
}
}
}
})
})
The point is, that you do not change the state on click. You are only changing the scope variables, but that doesn't mean any change to the URL or history.
To get history support, you need to change the state and put a ng-show on your sliders. I did it with ui-sref like this:
<ion-content>
<h1>Controller №{{::controller}}</h1>
<button class="button" ui-sref='master({firstOpened: true, secondOpened: false})'>Open first</button>
<button class="button" ui-sref='master({firstOpened: false, secondOpened: true})'>Open second</button>
<div style="background-color: #FF0000;" ng-show="{{firstOpened}}">
<h1>Slider 1</h1>
</div>
<div style="background-color: #00FF00;" ng-show="{{secondOpened}}">
<h1>Slider 2</h1>
</div>
</ion-content>
And your controller only needs to keep track of the stateParams, like you already did:
controller: function($scope, $ionicHistory, $stateParams, $state) {
console.log('Initing controller...');
controller = controller + 1;
$scope.controller = controller;
$scope.firstOpened = $stateParams.firstOpened;
$scope.secondOpened = $stateParams.secondOpened;
}
I've updated your plunker code here.
As you can see, I had to remove the slide animation, as it did not react properly on firstOpened and secondOpened values. But this is another issue and I am sure it's no big deal to get it to work again.

ColdFusion ajax validation

in cf9 i have a page where i want to check if field value exist in db (via ajax). If it doesn't exist, i want to stop processing (return false). Everything works fine, except i don't know how to pass back to the main function the result of the ajax call
please help
<cfajaximport tags="cfmessagebox, cfwindow, cfajaxproxy">
<cfajaxproxy cfc="reqfunc" jsclassname="jsobj" />
<script language="JavaScript">
function checkRequired() {
var testVal = document.getElementById('myField').value;
return testAjax(testVal);
/* more processing that should stop if ajaxCallBack returns false */
}
function testAjax(testVal) {
var instance = new jsobj();
instance.setCallbackHandler(ajaxCallBack);
instance.checkProfile(testVal);
}
function ajaxCallBack(returns) {
alert(returns);
// returns correctly "true" if value checks against db, "false" if it doesn't
// HOW DO I PASS THIS VALUE BACK TO checkRequired ???
}
</script>
<form>
<input type="text" name="myField" id="myField" value=""><p>
<input type="button" value="Check with Ajax" onClick="return checkRequired()">
</form>
many thanks
Unless you build your main function to 'wait' for the return, you can't return your result to that instance of the function; it has already exited, so to speak. Using cfajax it is probably possible to tweak the main function to call and wait, but the simple solution is to have the callback subsequently recall your main function and treat the existence of the result/return as the flag as to whether to process or call the ajax.
function checkRequired(return) {
if(return != null) {
/* more processing */
} else {
testAjax(testVal);
}
}
function ajaxCB(return) {
checkRequired(return);
}
I would probably refactor a bit more but you should get the idea.
it's really kind of a hack, not what i was looking for, but for what it's worth: if i put this stanza at the very end of my function, with the callBack collapsed within the main function, it would work
function checkRequired() {
var testVal = document.getElementById('myField').value;
var instance = new jsobj();
var r = instance.setCallbackHandler(
function(returns) {
if(returns == 1) {
document.getElementById('testForm').submit();
} else { alert("Something wrong"); }
}
);
instance.checkProfile(testVal);
}

How do I repopulate form fields after validation errors with express-form?

Using node.js and express (2.5.9) with express-form.
How should I repopulate form fields with the submitted values?
I have a get and a post route. If there are validation errors when the form is posted, I redirect the user back to the get, the problem is that the repopulated locals don't show up (I do have autoLocals: true, so I assume it's because I am redirecting and res is reset.)
So how do you guys repopulate and what's your application flow, do you res.send instead of res.redirect and set up the whole thing again? That seems repetitive.
Here's an example of my post route:
app.post(
'/projects/:id'
, form(field("title").required("title", "Title is required)
, function (req, res){
if (!req.form.isValid){
res.redirect('/project/'+req.params.id+'/edit');
}
else{
// save to db
}
});
I am working with expressjs4.0 to repopulate the forms fields after validation you do:
router.route('/posts/new')
.get(function(req, res) {
res.render('posts/new', new Post({}));
});
The second argument in res.render below will set some variables in the view.
res.render('posts/new', new Post({}));
In my view I then set my form fields as follows:
...
<input type="text" name="title" value="<%- post.title %>">
<textarea name="article"><%- post.article %></textarea>
...
When you submit this form, it should be caught by your router like so:
router.route('/posts')
.post(function(req, res) {
var post = new Post(req.body)
post.save(function(err) {
if (err) {
res.locals.errors = err.errors;
res.locals.post = post;
return res.render('posts/new');
}
return res.redirect('/posts');
});
...
})
This line of code, resets the form fields in your view
res.locals.post = post;
I hope someone finds this useful ;)
Not sure if it's best practice, but when I have validation failure, I don't redirect I just re-render the view (often by passing control to the 'get' callback). Somethign like this:
function loadProject(req,res, id){ /* fetch or create logic, storing as req.model or req.project */}
function editProject(req,res){ /* render logic */ }
function saveProject(req,res){
if(!req.form.isValid){
editProject(req,res);
}else{
saveToDb(req.project);
res.redirect('/project'+req.project.id+'/edit');
}
}
app.param('id', loadProject);
app.get('/projects/:id/edit', editProject);
app.post('/projects/:id', saveProject);
I had to work on similar problem recently and used two node modules: validator and flashify.
In the form view I configured my form fields as follows:
div.control-group
label.control-label Description
div.controls
textarea(name='eventForm[desc]', id='desc', rows='3').input-xxlarge= eventForm.desc
div.control-group
label.control-label Tag
div.controls
select(id='tag', name='eventForm[tag]')
tags = ['Medjugorje', 'Kibeho', 'Lourdes', 'Fatima']
for tag in tags
option(selected=eventForm.tag == tag)= tag
Notice the naming convention of the form fields. Then in my config file I set one global variable, which is really just a placeholder for when the form first loads:
//locals
app.locals.eventForm = []; // placeholder for event form repopulation
The validation logic is in my router file and looks like this:
app.post('/posts', function(req, res){
var formData = req.body.eventForm;
var Post = models.events;
var post = new Post();
post.text = formData.desc;
post.tag = formData.tag;
// run validations before saving
var v = new Validator();
var isPostValid = true;
// custom error catcher for validator, which uses flashify
v.error = function(msg) {
res.flash('error', msg);
isPostValid = false;
}
v.check(post.text, "Description field cannot be empty").notEmpty();
v.check(post.tag, "Tag field cannot be empty").notEmpty();
Then I check to see there are errors, and if so, pass the form data back to the view:
// reject it
res.render('Event.jade', {page: req.session.page, eventForm: formData});
Notice this evenForm data gets passed back to the view, which repopulates the default values.
The final step is to include the flashify component in your form view.
div(style='margin-top: 60px').container-fluid
include flashify
The code for the flashify view looks like this:
if (flash.error != undefined)
div.container
div.alert.alert-error
b Oops!
button(type='button', data-dismiss='alert').close ×
ul
each error in flash.error
li= error
if (flash.success != undefined)
div.container
div.alert.alert-success
b Success!
button(type='button', data-dismiss='alert').close ×
ul
each success in flash.success
li= success

Resources