Flux/ReactJS leveraging spinJS in component - javascript

I am using the flux architecture with reactjs. I am trying to implement control over displaying a spinjs spinner.
This is my component view that is using the spinner:
var ItemListing = React.createClass({
render: function(){
if (this.state.loading === true) {
return (
<div className="pill-pane active item-list-pane">
<LoadSpin />
</div>
)
}
// .. more non-related logic here
return (
<div className="pill-pane active item-list-pane">
{ items }
<Paginator
paginationClickHandler={ this.paginationClickHandler }
offset={ offset }
limit={ limit }
store={ AppStore }
/>
</div>
);
},
getInitialState: function(){
return {
items: AppStore.getAssetDetails().assetList
};
},
componentWillMount: function(){
AppStore.addChangeListener(this.changeHandler);
AppStore.addChangeListener(this.toggleActivity);
},
componentDidMount: function(){
this.setState({loading: false});
AppActions.queryAssets();
},
toggleActivity: function() {
this.setState({loading: true});
}
});
As shown in componentWillMount I add this.toggleActivity which ultimately sets the loading state to true. Then in componentDidMount I set the loading state to false. But for some reason my spinner remains spinning indefinitely. That is, it never gets set to false when laid out like this. How can I change this so that it will change to false?
Edit - added AppStore.js as requested:
"use strict";
var AppDispatcher = require('../AppDispatcher');
var assetConstants = require('../constants/AssetConstants');
var EventEmitter = require('events').EventEmitter;
var CHANGE_EVENT = 'assetChange';
// Application State
var _assetDetails = {
assetList: [],
requestStatus: undefined,
totalCount: 0
};
var _queryProgress;
var AppStore = $.extend({}, EventEmitter.prototype, {
emitChange: function(){
this.emit(CHANGE_EVENT);
},
addChangeListener: function(callback){
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback){
this.removeListener(CHANGE_EVENT, callback);
},
getTotalCount: function(){
if(_assetDetails.meta){
return _assetDetails.meta.total_count;
}
return 0;
},
getAssetDetails: function(){
return _assetDetails;
},
getRequestStatus: function(){
return _assetDetails.requestStatus;
},
getQueryProgress: function(){
return _queryProgress;
},
dispatcherIndex: AppDispatcher.register(function(payload){
var action = payload.action.actionType;
if(payload.source === "ASSET_ACTION"){
switch(action){ // eslint-disable-line default-case
case assetConstants.QUERY_ASSETS:
_assetDetails = payload.action.assetDetails;
break;
case assetConstants.QUERY_PROGRESS:
_queryProgress = payload.action.progress;
break;
}
AppStore.emitChange();
_assetDetails.requestStatus = undefined;
}
return true;
})
});
module.exports = AppStore;
Re-edit added AppActions.js:
"use strict";
var assetConstants = require('../constants/AssetConstants');
var AppDispatcher = require('../AppDispatcher');
var config = require('../../config');
var ITEM_V2_MAP = require('./apiRemapping').ITEM_V2_MAP;
var getRemappedAssets = require('./apiRemapping').getRemappedAssets;
var AssetActions = {
queryAssets: function(filters){
var assetDetails;
$.ajax({
method: "GET",
url: config.endpoints.itemAPIV2,
contentType: "application/json",
dataType: "json",
data: filters,
beforeSend: function(){
AppDispatcher.handleAssetAction({
actionType: assetConstants.QUERY_PROGRESS,
progress: "querying"
});
},
success: function(data) {
var remappedAssets = getRemappedAssets(
data.objects, ITEM_V2_MAP
);
assetDetails = {
assetList: remappedAssets,
currentFilters: filters,
meta: data.meta
};
},
error: function() {
assetDetails = {
assetList: [],
totalCount: 0
};
},
complete: function(data) {
assetDetails.requestStatus = data.status;
AppDispatcher.handleAssetAction({
actionType: assetConstants.QUERY_ASSETS,
assetDetails: assetDetails
});
AppDispatcher.handleAssetAction({
actionType: assetConstants.QUERY_PROGRESS,
progress: "finished querying"
});
}
});
}
};
module.exports = AssetActions;

Placing your AppStore.addChangeListener(this.toggleActivity); in the componentWillUnmount() lifecycle should solve the issue.

Related

How to wait to render view in react.js until $.get() is complete?

Hello I'm writing a chat client in reactjs and want to render my components with data retrieved from a REST call. However, my component is rendered before the REST request returns with data; this causes errors as I am calling this.props within my children components.
var MainChat = React.createClass({
getInitialData: function(){
var c = []
$.get("http://127.0.0.1:8888/test/sampledata.php?user=123", function(data, status) {
console.log("Data: "+ data+ "\nStatus: " + status);
c = data
})
},
getInitialState: function() {
return {
chatId : "",
chats : this.getInitialData(),
//chats: this.props.chats
};
},
handleClickOnLeftUser: function(data){
console.log("handleClickOnLeftUser");
// console.log("chat id is");
// console.log(data.chatID);
this.setState({chatID: data.chatID});
},
render: function() {
console.log("main:render")
console.log(this.props.chats);
var theThreadIWantToPass = {};
for(var i = 0; i < this.state.chats.length; i++)
{
console.log("chat: " + this.state.chats[i].chatID);
if (this.state.chats[i].chatID === this.state.chatID) {
theThreadIWantToPass = this.state.chats[i];
break;
}
}
return (
<div className="chatapp">
<div className="thread-section">
<div className="thread-count">
</div>
<LeftUserList
chats={this.state.chats}
clickFunc={this.handleClickOnLeftUser} // ***important
/>
</div>
<div>
<RightMessageBox chat={theThreadIWantToPass} />
</div>
</div>
);
}
});
In your case you need use method componentDidMount, like so
var MainChat = React.createClass({
getInitialData: function() {
var url = 'http://127.0.0.1:8888/test/sampledata.php?user=123';
$.get(url, function(data, status) {
this.setState({ chats: data });
}.bind(this))
},
componentDidMount: function () {
this.getInitialData();
},
getInitialState: function() {
return {
chatId: '',
chats: []
};
},
handleClickOnLeftUser: function(data){
this.setState({ chatID: data.chatID });
},
render: function() {
var theThreadIWantToPass = {};
for(var i = 0; i < this.state.chats.length; i++) {
if (this.state.chats[i].chatID === this.state.chatID) {
theThreadIWantToPass = this.state.chats[i];
break;
}
}
return (
<div className="chatapp">
<div className="thread-section">
<div className="thread-count"></div>
<LeftUserList
chats={this.state.chats}
clickFunc={this.handleClickOnLeftUser} />
</div>
<div>
<RightMessageBox chat={theThreadIWantToPass} />
</div>
</div>
);
}
});
Note - using "async": false this is bad practice
Found a hackish fix for the time being (at least until someone answers). My current solution is to prevent jQuery from using callbacks by setting
$.ajaxSetup({ "async": false})
before I call Jquery's get() function.
getInitialData: function(){
var c = []
$.ajaxSetup({ "async": false})
$.get("http://127.0.0.1:8888/test/sampledata.php?user=123", function(data, status) {
console.log("Data: "+ data+ "\nStatus: " + status);
c = data
})
return c
},
getInitialState: function() {
return {
chatId : "",
chats : this.getInitialData(),
//chats: this.props.chats
};
},

React filtering data error: this.props.updateFilter is not a function

I'm trying to implement a filter for an image list that works as you type and getting the above error. I think it's to do with the way I've mapped the data I'm importing, but can't figure it out. I'm still learning React.
var Filters = React.createClass({
handleFilterChange: function() {
var value = this.refs.filterInput.getDOMNode().value;
this.props.updateFilter(value);
},
render: function() {
return <input type="text" ref="filterInput" onChange={this.handleFilterChange} placeholder="Filter" />;
}
});
/** #jsx React.DOM */
var ListItem = React.createClass({
render: function() {
return (
<li className="col-lg-3" style={listStyle}>
<h2>{this.props.filename}</h2>
<p><img className="img-thumbnail" src={this.props.photoLink}/></p>
<p>
<small>Camera model: <strong>{this.props.model}</strong></small><br/>
<small>Camera make: <strong>{this.props.make}</strong></small>
</p>
</li>
)
}
});
var ListContainer = React.createClass({
loadNotificationsFromServer: function() {
$.ajax({
url: this.props.url,
dataType: 'json',
success: function(data) {
this.setState({data: this.state.data.concat([data])});
clearInterval(this.interval);
}.bind(this)
});
},
getInitialState: function() {
return {
data: [],
nameFilter: ''
};
},
componentWillMount: function() {
this.loadNotificationsFromServer();
this.interval = setInterval(this.loadNotificationsFromServer, this.props.pollInterval);
},
render: function() {
if (this.state.data) {
return (
<div>
{this._hasData()}
</div>
)
} else {
return <div>Loading...</div>;
} return false
},
_handleFilterUpdate: function(filterValue) {
this.setState({
nameFilter: filterValue
});
},
_hasData: function() {
var data = this.state.data.map(function(item) {
return item.works.work;
});
if (data.length > 0) {
var imageDetails = _.map(data[0], function(result, n, key){
var image = _(result.urls.url).find({'_type': 'small'});
return {
id: result.id,
filename: result.filename,
_type: image ? image._type : null,
url: image ? image.__text : null,
model: result.exif.model,
make: result.exif.make
};
}, []);
var listItems = imageDetails.map(function(result, index){
return <ListItem key={index}
filename={result.filename}
photoLink={result.url}
model={result.model}
make={result.make} />
})
return listItems
} else return false
}
});
// INITIALISE PAGE
var imageList= React.createClass({
render: function() {
/* component composition == function componsition */
return (
<div>
<Filters updateFilter={this._handleFilterUpdate} />
<ListContainer url="/scripts/works.json" pollInterval={2000}/>
</div>
);
}
});
module.exports = imageList;

How to update Backbone JS model attribute?

UPDATE: I've updated my views to show how I resolved this question using information from the accepted answer.
I'd like to update/increment an attribute ('video_views') of my Backbone JS model via a click event from my view. But, as a Backbone rookie, I'm not sure how to accomplish this exactly.
I'd like the 'video_views' attribute to increment by one with the playVideo event (click).
Thanks for the help!
Here is the structure of my JSON from my API:
{
"id": 8,
"name": "Bike to work day",
"slug": "bike-work-day",
"tagline": "A brief tagline about the video.",
"description": "This is a test.",
"created": "2015-02-06T15:22:26.342658Z",
"website": "http://thevariable.com/",
"logo": "http://dev.thevariable.com/media/brands/logos/test_logo.jpeg",
"video": "http://dev.thevariable.com/media/brands/videos/3D463BC3-38B8-4A6F-BE93-3F53E918EC3B-3533-00000118880074BA_1.1.mp4",
"video_thumbnail": "http://dev.thevariable.com/media/brands/video_thumbnails/3D463BC3-38B8-4A6F-BE93-3F53E918EC3B-3533-00000118880074BA_1.1.mp4.jpg",
"links": {
"self": "http://dev.thevariable.com/api/brands/bike-work-day"
},
"status_display": "published",
"video_views": 0
}
Here are my Backbone views:
var TemplateView = Backbone.View.extend({
templateName: '',
initialize: function () {
this.template = _.template($(this.templateName).html());
},
render: function () {
var context = this.getContext(), html = this.template(context);
this.$el.html(html);
},
getContext: function () {
return {};
}
});
var HomePageView = TemplateView.extend({
templateName: '#home-template',
events: {
'click video': 'updateCounter',
'click .video video': 'playVideo',
'click .sound': 'muteVideo',
'click .js-open-card': 'openCard'
},
initialize: function (options) {
var self = this;
TemplateView.prototype.initialize.apply(this, arguments);
app.collections.ready.done(function () {
app.brands.fetch({success: $.proxy(self.render, self)});
});
},
getContext: function () {
return {brands: app.brands || null};
},
updateCounter: function (e) {
var id = $(e.currentTarget).data('id');
var item = self.app.brands.get(id);
var views = item.get('video_views');
var video = this.$('.video video');
// Only update the counter if the video is in play state
if (video.prop('paused')) {
item.save({video_views: views + 1}, {patch: true});
this.render();
}
},
playVideo: function () {
var video = this.$('.video video');
if (video.prop('paused')) {
video[0].play();
} else {
video.get(0).pause();
}
},
muteVideo: function (e) {
e.preventDefault();
var video = this.$el.parent().find('video');
video.prop('muted', !video.prop('muted'));
this.$('.sound').toggleClass('is-muted');
},
openCard: function (e) {
e.preventDefault();
this.$el.toggleClass('is-open');
this.$el.closest('.card-container').toggleClass('is-open');
}
});
And my Backbone models:
var BaseModel = Backbone.Model.extend({
url: function () {
var links = this.get('links'),
url = links && links.self;
if (!url) {
url = Backbone.Model.prototype.url.call(this);
}
return url;
}
});
app.models.Brand = BaseModel.extend({
idAttributemodel: 'slug'
});
var BaseCollection = Backbone.Collection.extend({
parse: function (response) {
this._next = response.next;
this._previous = response.previous;
this._count = response.count;
return response.results || [];
},
getOrFetch: function (id) {
var result = new $.Deferred(),
model = this.get(id);
if (!model) {
model = this.push({id: id});
model.fetch({
success: function (model, response, options) {
result.resolve(model);
},
error: function (model, response, options) {
result.reject(model, response);
}
});
} else {
result.resolve(model);
}
return result;
}
});
app.collections.ready = $.getJSON(app.apiRoot);
app.collections.ready.done(function (data) {
app.collections.Brands = BaseCollection.extend({
model: app.models.Brand,
url: data.brands
});
app.brands = new app.collections.Brands();
});
Just increment that attribute on the model and save it.
var views = model.get('video_views');
model.set({video_views: views + 1});
model.save();

Issues searching backbone collection

I have this bb app that I'm trying to search and return the results of the search, then when cleared, so all results again. I was able to get everything to show before adding the search feature, but now nothing showing up. I think the collection isn't available at the time it's trying to populate, but can't seem to get it to wait. I've tried moving the fetch around to no avail. Any help would be greatly appreciate. For the sake of ease, I've put everything in a fiddle that can be found here...
//Campaign Model w defaults
app.model.Campaign = Backbone.Model.extend({
default: {
title: '',
img: '',
id: '',
slug: '',
image_types: 'small',
tagline: ''
}
});
//Campaign Collection from model
app.collection.Campaign = Backbone.Collection.extend({
//our URL we're fetching from
url: 'https://api.indiegogo.com/1/campaigns.json?api_token=e377270bf1e9121da34cb6dff0e8af52a03296766a8e955c19f62f593651b346',
parse: function(response) {
console.log('parsing...');
return response.response; //get array from obj to add to collection based on model
},
currentStatus: function(status){
return _(this.filter(function(data){
console.log('currentStats', status);
return data.get('_pending') == status;
}));
},
search: function(searchVal) {
console.log('search...');
if (searchVal == '') {
return this;
}
var pattern = new RegExp(searchVal, 'gi');
return _(this.filter(function(data) {
return pattern.test(data.get('title'));
}));
}
});
app.collection.campaigns = new app.collection.Campaign();
app.collection.campaigns.fetch({
success: function(){
console.log('Success...');
var sHeight = window.screen.availHeight - 200 + 'px';
$('#container ul').css('height', sHeight);
},
error: function() {
console.log('error ',arguments);
}
});
//List view for all the campaigns
app.view.CampaignList = Backbone.View.extend({
events: {
'keyup #searchBox': 'search'
},
render: function(data) {
console.log('campaignList',$(this.el).html(this.template));
$(this.el).html(this.template);
return this;
},
renderAll: function(campaigns) {
console.log('campList renderAll', campaigns, $('#campaignList'));
$('#campaignList').html('');
campaigns.each(function(campaign){
var view = new app.view.CampaignItem({
model: campaign,
collection: this.collection
});
console.log(view);
$('#campaignList').append(view.render().el);
});
return this;
},
initialize: function() {
console.log('init campList',app);
this.template = _.template($('#campaignList-tmp').html());
this.collection.bind('reset', this.render, this);
},
search: function(e) {
console.log('listView search');
var searchVal = $('#searchBox').val();
this.renderAll(this.collection.search(searchVal));
},
sorts: function() {
var status = $('#campSorting').find('option:selected').val();
if(status == '') {
status = false;
};
this.renderAll(this.collection.currentStatus(status));
}
});
//Item view for single campaign
app.view.CampaignItem = Backbone.View.extend({
events: {},
render: function(data){
console.log('campItem render...', data);
this.$el.html(this.template(this.model.toJSON()));
return this;
},
initialize: function(){
console.log('campItem init');
this.template = _.template( $('#campaignItem-tmp').html());
}
});
//Router
app.router.Campaign = Backbone.Router.extend({
routes: {
'': 'campaigns'
},
campaigns: function(){
this.campListView = new app.view.CampaignList({
collection: app.collection.campaigns
});
$('#container').append(this.campListView.render().el);
this.campListView.sorts();
}
});
app.router.campaigns = new app.router.Campaign();
Backbone.history.start();
http://jsfiddle.net/skipzero/xqvrpyx8/

Backbonejs when to initialize collections

I'm building small one page application with rails 3.1 mongodb and backbonejs.
I have two resources available through json api. I created two models and collections in backbone which look like this
https://gist.github.com/1522131
also I have two seprate routers
projects router - https://gist.github.com/1522134
notes router - https://gist.github.com/1522137
I generated them with backbonejs-rails gem from github so code inside is just template. I initialize my basic router inside index.haml file
#projects
:javascript
$(function() {
window.router = new JsonApi.Routers.ProjectsRouter({projects: #{#projects.to_json.html_safe}});
new JsonApi.Routers.NotesRouter();
Backbone.history.start();
});
I don't want fetch notes when application is starting, because there is big chance that user will never look inside notes. So there isn't good reason to fetch it on start. Inside NotesRouter in all action I rely on #notes variable but without .fetch() method this variable is empty. Also I should can reproduce notes view from url like
/1/notes/5
project_id = 1
note_id = 5
What is best practices in backbonejs to solve this kind of problem ?
Why don't you lazy load the notes when it's requested? Here's an example:
var State = Backbone.Model.extend({
defaults: {
ready: false,
error: null
}
});
var Note = Backbone.Model.extend({
initialize: function () {
this.state = new State();
}
});
var Notes = Backbone.Collection.extend({
model: Note,
initialize: function () {
this.state = new State();
}
});
var NoteCache = Backbone.Model.extend({
initialize: function () {
this._loading = false;
this._loaded = false;
this._list = new Notes();
},
_createDeferred: function (id) {
var note = new Note({ id: id });
this._list.add(note);
this._load();
return note;
},
getNote: function (id) {
return this._list.get(id) || this._createDeferred(id);
},
getNotes: function () {
if (!this._loaded)
this._load();
return this._list;
},
_load: function () {
var that = this;
if (!this._loading) {
this._list.state.set({ ready: false, error: null });
this._loading = true;
$.ajax({
url: '/api/notes',
dataType: 'json',
cache: false,
type: 'GET',
success: function (response, textStatus, jqXHR) {
_.each(response.notes, function (note) {
var n = that._list.get(note.id);
if (n) {
n.set(note);
} else {
that._list.add(note, { silent: true });
n = that._list.get(note.id);
}
n.state.set({ ready: true, error: null });
});
that._list.state.set({ ready: true, error: null });
that._list.trigger('reset', that._list);
that._loaded = true;
},
error: function (jqXHR, textStatus, errorThrown) {
that._list.state.set({ error: 'Error retrieving notes.' });
that._list.each(function (note) {
note.state.set({ error: 'Error retrieving note.' });
});
},
complete: function (jqXHR, textStatus) {
that._loading = false;
}
});
}
}
});
In this example, I'm defining a NoteCache object that manages the lazy loading. I also add a "state" property to the Note model and Notes collection.
You'll probably want to initialize NoteCache somewhere (probably inside your route) and whenever you want a note or notes, just do this:
var note = noteCache.getNote(5);
var notes = noteCache.getNotes();
Now inside your view, you'll want to listen for state changes in case the note/notes is not loaded yet:
var NoteView = Backbone.View.extend({
initialize: function(){
this.note.state.bind('change', this.render, this);
},
render: function(){
if (this.note.state.get('error') {
// todo: show error message
return this;
}
if (!this.note.state.get('ready') {
// todo: show loader animation
return this;
}
// todo: render view
return this;
}
});
I haven't tested this, so there may be some bugs, but I hope you get the idea.

Categories

Resources