I am new at ruby on rails and backbone as well. I have an existing code in backbone and ruby which is working fine which creates some boards and one board have many lists and so on. I will provide code for that one also if needed. Now I am going to add some feature of categories like user can create and delete categories and each categories have many boards. Following data is displayed on browser from Database which is correct. I am not getting it properly why proper views are not displaying on browser.
Following is the code. Kindly suggest me some code changes so that it will work. It will be really appreciated. Thanks in advance.
/app/assets/javascripts/collections/categories.js
Kanban.Collections.Categories = Backbone.Collection.extend({
model: Kanban.Models.Category,
url: "/categories",
});
app/assets/javascripts/models/category.js
Kanban.Models.Category = Backbone.RelationalModel.extend({
urlRoot: "/categories",
relations: [{
type: Backbone.HasMany,
key: "boards",
relatedModel: "Kanban.Models.Board",
collectionType: "Kanban.Collections.Boards",
reverseRelation: {
key: "category"
}
}]
});
app/assets/javascripts/views/categories/categories_index.js
Kanban.Views.CategoriesIndex = Backbone.View.extend({
template: JST['categories/index'],
tagName: "section",
className: "categories-index",
events: {
"submit form.create_category": "createCategory"
},
initialize: function () {
var that = this;
that.collection.on("all", that.render, that);
},
render: function () {
var that = this;
var renderedContent = that.template({
categories: that.collection
});
that.$el.html(renderedContent);
console.log("categories ka render");
return that;
},
createCategory: function (event) {
event.defaultPrevented();
var that = this;
// get form attrs, reset form
var $form = $(event.target);
var attrs = $form.serializeJSON();
$form[0].reset();
var category = new Kanban.Models.Category();
// fail if no category name
if (!attrs.category.name) {
var $createContainer = $("div.create_category");
var $nameInput = that.$el.find("input.category_name");
$nameInput.hide();
$createContainer.effect("shake", {
distance: 9,
times: 2,
complete: function () {
$nameInput.show();
$nameInput.focus();
}
}, 350);
return false;
}
// save list
category.save(attrs.category, {
success: function (data) {
// category.get("users").add(Kanban.currentUser);
that.collection.add(category);
// keep focus on list input
that.$el.find("input.category_name").focus();
}
});
}
});
app/assets/javascripts/routers/main_routers.js
Kanban.Routers.Main = Backbone.Router.extend({
initialize: function (options) {
this.$rootEl = options.$rootEl;
},
routes: {
"": "index",
"/login": "login",
"categories/:id": "showCategoryBoards",
"boards/:id": "showBoardLists"
//"boards/:id": "deleteBoard"
},
index: function () {
var that = this;
var categoriesIndex = new Kanban.Views.CategoriesIndex({
collection: Kanban.categories
});
that.$rootEl.html(categoriesIndex.render().$el);
},
//index: function () {
//var that = this;
//var boardsIndex = new Kanban.Views.BoardsIndex({
//collection: Kanban.boards
//});
//that.$rootEl.html(boardsIndex.render().$el);
//},
showCategoryBoards: function (id) {
var that = this;
var category = Kanban.categories.get(id);
var categoryShow = new Kanban.Views.BoardIndex({
model: category
});
that.$rootEl.html(categoryShow.render().$el);
},
showBoardLists: function (id) {
var that = this;
var board = Kanban.boards.get(id);
var boardShow = new Kanban.Views.BoardShow({
model: board
});
that.$rootEl.html(boardShow.render().$el);
}
});
app/assets/javascripts/kanban.js
window.Kanban = {
Models: {},
Collections: {},
Views: {},
Routers: {},
initialize: function() {
var that = this;
that.$rootEl = $("#content");
Kanban.currentUser = new Kanban.Models.CurrentUser();
Kanban.currentUser.fetch({
success: function (response) {
// console.log("got user");
Kanban.category = new Kanban.Models.Category();
Kanban.category.fetch({
success: function (response) {
Kanban.boards = new Kanban.Collections.Boards();
Kanban.boards.fetch({
success: function (response) {
// console.log("got boards");
new Kanban.Routers.Main({ $rootEl: that.$rootEl });
Backbone.history.start();
}
});
},
error: function (response) {
// console.log("please log in");
}
});
}
});
}
};
$(document).ready(function(){
Kanban.initialize();
});
//BOARD DELETION METHOD... !
$(document).on("click", ".delete-icon", function() {
var board_id = $(this).parent().attr('id');
$.ajax({
url: "/api/boards/"+board_id,
type: 'DELETE',
success: function(result) {
$("#"+board_id).remove();
}
});
});
//LIST DELETION METHOD... !
$(document).on("click", ".list-delete-icon", function() {
var listId = $(this).parent().attr('id').replace(/list_/, '');
// alert(listId);
//var id = $("div").attr('id').replace(/button/, '');
$.ajax({
url: "/api/lists/"+listId,
type: 'DELETE',
success: function(result) {
alert("success!!!");
$("#list_"+listId).remove();
}
});
});
//card DELETION METHOD... !
app/assets/templates/categories/index.jst.ejs
<header class="categories-index">
<span class=""></span>
<h2>My Categories</h2>
</header>
<div>
<ul class="nav pull-left navbar-nav">
<% categories.each(function (category) { %>
<li id="<%= category.id %>" class="boxes">
<a href="/#categories/<%= category.id %>">
<%= category.escape("title") %>
</a>
<div class="delete-icon">
<span class="glyphicon glyphicon-trash">
</div>
</li>
<% }); %>
<li class="boxes">
<p>Create New Category</p>
<form class="create_category" id="myform">
<input type="text"
id="customInput"
class="category_name"
name="category[title]"
placeholder="Category Name ..." />
<input type="text"
class="category_description"
name="category[description]"
placeholder="Category Description ..." />
<input type="submit" value="Create Category" />
</form>
</li>
</ul>
</div>
/app/controllers/categories_controller
class CategoriesController < ApplicationController
# before_action :set_category, only: [:show, :edit, :update, :destroy]
respond_to :json
def index
#categories = Category.all
# respond_with(#categories)
# #categories = current_user.categories.includes(:boards)
end
def show
respond_with(#category)
end
def new
#category = Category.new
respond_with(#category)
end
def edit
end
def create
category = Category.new(params[:category])
if category.save
# board.members << current_user
render json: category, status: :ok
else
render json: category.errors, status: :unprocessable_entity
end
end
def update
#category.update(category_params)
respond_with(#category)
end
def destroy
#category.destroy
respond_with(#category)
end
private
def set_category
#category = Category.find(params[:id])
end
def category_params
params.require(:category).permit(:title, :description)
end
end
app/models
class Category < ActiveRecord::Base
attr_accessible :title, :description
has_many :boards, dependent: :destroy
end
app/views/categories/index.rabl
collection #categories
attributes :id, :title, :description
config/routes.rb
Kanban::Application.routes.draw do
devise_for :users
resources :users
# root to: "categories#index"
root to: "root#index"
resource :root, only: [:index]
resources :categories
# resource :session, only: [:new, :create, :destroy]
#get "login" => "sessions#new"
# get "logout" => "sessions#destroy"
# resources :users, only: [:show]
namespace :api do
resources :users, only: [:show] do
collection do
get :current
end
end
resources :boards, only: [:index, :show, :create, :update, :destroy]
resources :lists , except: [:edit] do
collection do
post :sort
end
end
resources :cards, except: [:edit] do
collection do
post :sort
end
end
resources :card_comments, only: [:index, :create, :destroy]
end
end
you're trying to initialize a "collection" instead of a "model" in your CategoriesIndex file:
initialize: function () {
var that = this;
that.collection.on("all", that.render, that);
which is being called from your Route file calling constructor with collection.
index: function () {
var that = this;
var categoriesIndex = new Kanban.Views.CategoriesIndex({
collection: Kanban.categories,
});
that.$rootEl.html(categoriesIndex.render().$el);
},
so, see your kanban.js file
it should be:
Kanban.categories = new Kanban.Collections.Categories();
Kanban.categories.fetch({
Related
I'm trying to learn Backbone and can't seem to match data from the fetch function into my Underscore template. How can can I get the children array in my JSON and match it to the template?
The Backbone.View looks like this:
var Projects = Backbone.Collection.extend({
url: '/tree/projects'
});
var Portfolio = Backbone.View.extend({
el: '.page',
render: function () {
var that = this;
var projects = new Projects();
projects.fetch({
success: function (projects) {
var template = _.template($('#projects-template').html());
that.$el.html(template({projects: projects.models}));
}
})
}
});
At the url: http://localhost:3000/portfolio/api/tree/projects
The JSON returned looks like this:
{
id:"projects",
url:"http://localhost:8888/portfolio/projects",
uid:"projects",
title:"Projects",
text:"",
files:[
],
children:[
{
id:"projects/example-1",
url:"http://localhost:8888/portfolio/projects/example-1",
uid:"example-1",
title:"Example 1",
images:"",
year:"2017",
tags:"Website",
files:[
],
children:[
]
},
{
id:"projects/example-2",
url:"http://localhost:8888/portfolio/projects/example-2",
uid:"example-2",
title:"Example #"2
text:"Example 2's text",
year:"2016",
tags:"Website",
files:[
{
url:"http://localhost:8888/portfolio/content/1-projects/4-example-2/example_ss.png",
name:"example_ss",
extension:"png",
size:244845,
niceSize:"239.11 kB",
mime:"image/png",
type:"image"
}
],
children:[
]
},
]
}
My Underscore file looks like this:
<script type="text/template" id="projects-template">
<h4>tester</h4>
<div>
<% _.each(projects.children, function (project) { %>
<div>
<div><%= project.get('year') %></div>
<div><%= project.get('title') %></div>
<div><%= project.get('tags') %></div>
</div>
<% }); %>
</div>
</script>
You can define a parse method on the collection:
var Projects = Backbone.Collection.extend({
url: '/tree/projects',
parse: function(response){
/* save other data from response directly to collection if needed.
for eg this.title = response.title; */
return response.children; // now models will be populated from children array
}
});
Do not use parse
While I usually agree with TJ, using parse on the collection is more like a hack than a definite solution. It would work only to get the children projects of a project and nothing more.
The parse function shouldn't have side-effects on the collection and with this approach, changing and saving fields on the parent project wouldn't be easily possible.
It also doesn't deal with the fact that it's a nested structure, it's not just a wrapped array.
This function works best when receiving wrapped data:
{
data: [{ /*...*/ }, { /*...*/ }]
}
Models and collections
What you have here are projects that have nested projects. A project should be a model. You also have files, so you should have a File model.
Take each resource and make a model and collection classes with it. But first, get the shared data out of the way.
var API_ROOT = 'http://localhost:8888/';
File
var FileModel = Backbone.Model.extend({
defaults: {
name: "",
extension: "png",
size: 0,
niceSize: "0 kB",
mime: "image/png",
type: "image"
}
});
var FileCollection = Backbone.Collection.extend({
model: FileModel
});
Project
var ProjectModel = Backbone.Model.extend({
defaults: function() {
return {
title: "",
text: "",
files: [],
children: []
};
},
getProjects: function() {
return this.get('children');
},
setProjects: function(projectArray, options) {
return this.set('children', projectArray, options);
},
getFiles: function() {
return this.get('files');
},
getSubProjectUrl: function() {
return this.get('url');
}
});
var ProjectCollection = Backbone.Collection.extend({
model: ProjectModel,
url: API_ROOT + '/tree/projects'
});
Project view
Then, make a view for a project. This is a simple example, see the additional information for tips on optimizing the rendering.
var ProjectView = Backbone.View.extend({
template: _.template($('#projects-template').html()),
initialize: function(options) {
this.options = _.extend({
depth: 0, // default option
}, options);
// Make a new collection instance with the array when necessary
this.collection = new ProjectCollection(this.model.getProjects(), {
url: this.model.getSubProjectUrl()
});
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
this.$projectList = this.$('.list');
// use the depth option to avoid rendering too much projects
if (this.depth > 0) this.collection.each(this.renderProject, this);
return this;
}
renderProject: function(model) {
this.$projectList.append(new ProjectView({
model: model,
depth: depth - 1
}).render().el);
}
});
With a template like this:
<script type="text/template" id="projects-template">
<h4><%= title %></h4>
<span><%= year %></span><span><%= tags %></span>
<p><%= text %></p>
<div class="list"></div>
</script>
Using the view:
var model = new ProjectModel({ id: "project" });
model.fetch({
success: function() {
var project = new ProjectView({
model: model,
depth: 2
});
}
});
Additional info
Nested models and collections
Efficiently rendering a list
I have a component called Items that lives within a parents called ItemsContainer. When a button in Items is clicked an Ajax function is called to delete that Item.
At the moment however I am receiving a 500 error message and am not sure why.
Item Component
class Item extends React.Component{
constructor(props) {
super()
this.state = {
name: '',
price: 0,
code: '',
id: ''
}
}
componentWillMount() {
this.setState({
name: this.props.data.name,
price: this.props.data.price,
code: this.props.data.code,
id: this.props.data.id
})
}
deleteItem() {
let finalUrl = '/items/' + this.state.id;
$.ajax({
type: "DELETE",
url: finalUrl, /* THIS URL IS CALLING CORRECTLY ie. /items/8 */
dataType: "json",
success: function(response) {
console.log("successfully deleted");
},
error: function () {
console.log("error");
}
})
}
render(){
let itemName = this.props.data.name
let itemCode = this.props.data.code
let itemQuantity = 1
let itemPrice = (this.props.data.price * itemQuantity).toFixed(2)
const itemId = this.props.data.id
return(
<tr>
<td>{itemName}</td>
<td>{itemCode}</td>
<td>{itemQuantity}</td>
<td><button className="btn btn-warning" onClick={this.deleteItem.bind(this)}>Remove</button></td>
<td>£{itemPrice}</td>
</tr>
)
}
}
Rails Items Controller
class ItemsController < ApplicationController
def create
#item = Item.new(item_params)
if #item.save
render partial: 'items/item', locals: {item: #item}
else
render json: #item.errors.to_json
end
end
def destroy
if #item.destroy
render partial: 'items/item', locals: {item: #item}
else
render json: #item.errors.to_json
end
end
private
def item_params
params.require(:item).permit(
:name,
:price,
:code,
:id
)
end
end
Creating a new Item is working as expected but I can't work out why I am receiving my 500 error for my delete action. Any help would be much appreciated.
Please check your destroy method in rail controller.
There is no definition for #item hence 500 internal server error :)
I've been tasked with refactoring the login functionality of a Rails application to use Backbone.js. The Rails app is using Devise for authentication. I'm very new to Backbone and I'm not sure of the components and functions that are required to execute this. This is what I have so far (Disclaimer: I've been doing a good deal of copy and pasting from other portions of Backbone we currently have in this app so if something seems unecessary, it probably is):
.js files:
LogInTemplate.hbs:
<div class="span4 offset1">
<h3>{{ title }}</h3>
<form class="homepage-login-form" id="login-form">
<input type="text" id="email" placeholder="Email" class="input-block-level" name="email">
<input type="password" id="password" placeholder="Password" class="input-block-level" name="password">
{{ forgot_password }}
<input type="checkbox" id"remeber-me" name="{{ remember_me }}">
<label name="remember-user">Keep me logged in</label>
<div class="control-group error-group"></div>
<input type="submit" id="submit" value="Log In" class="btn btn-primary">
</form>
<button class="icon-cross_mark hidden-phone"></button>
</div>
log_in.js:
window.LogIn = {
'models': {},
'collections': {},
'views': {},
'routers': {},
'mixins': {},
'init': function(data) {
this.router = new LogIn.routers.LogInRouter();
Backbone.history.start();
}
}
_.extend(LogIn, Backbone.Events);
LogInRouter.js:
LogIn.routers.LogInRouter = Backbone.Router.extend({
'routes': {
'': 'index'
},
'views': {},
'index': function() {
var router = this;
router.views.activeUsersView = new LogIn.views.ActiveUsersView();
router.views.logInView = new LogIn.views.LogInView();
},
});
UsersSessionModel.js:
LogIn.models.UserSessionModel = Backbone.Model.extend({
'url': '/',
'defaults': {
'title': 'Welcome back to This Application',
'email': '',
'password': '',
'password_retreive': 'Forgot your password?',
'keep_logged_in': 'Keep me logged in'
}
});
LogInView.js
LogIn.views.LogInView = Backbone.View.extend({
'events': {
'submit form': 'login'
},
'el': '.login-container',
'template': HandlebarsTemplates['log_in/templates/LogInTemplate'],
'model': new LogIn.models.UserSessionModel(),
'initialize': function() {
view.model.fetch({
'success': function() {
view.render();
view.listenTo(view.model, 'change', view.render);
}
});
},
'render': function() {
var view = this;
var viewData = {
'title': view.model.get('title'),
'email': view.model.get('email'),
'password': view.model.get('password'),
'password_retreive': view.model.get('password_retreive'),
'keep_logged_in': view.model.get('keep_logged_in'),
'log_in_btn': view.model.get('log_in_btn')
};
view.$el.html(view.template(viewData));
}
});
Rails files:
class Users::SessionsController < Devise::SessionsController
layout 'home'
before_filter :set_cms_content
before_filter :set_first_login_flag, :only => [:create]
respond_to :json
def new
#user_count = Rails.cache.fetch("current_total_user_count", :expires_in => 1.hour) do
(User.count / 100) * 100
end
super
end
def create
resource = warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#failure")
sign_in(resource_name, resource)
render :json => {:success => true, :successUrl => after_sign_in_path_for(resource)}, :status => :ok
end
def failure
render :json => {:success => false, :error => I18n.t('devise.failure.invalid') }, :status => :unprocessable_entity
end
private
def set_first_login_flag
flash[:is_first_login] = true
end
def set_cms_content
last_update = CmsPage.maximum(:updated_at)
slug = 'landing-page'
if last_update.present?
cache_key = "cms_pages/#{slug}/#{last_update.strftime('%Y%m%d%H%M%S')}"
#landing_page = Rails.cache.fetch(cache_key) do
CmsPage.find_by_slug!(slug)
end
else
#landing_page = CmsPage.find_by_slug!(slug)
end
end
end
routes.rb
WebApp::Application.routes.draw do
#removed registration routes, only focusing on login right now
devise_scope :user do
root to: 'devise/sessions#new'
get :login, :to => 'users/logins#new'
post :login, :to => 'users/logins#create'
post 'register/get_started', :as => 'registration/get_started', :to => 'users/registrations#get_started'
end
end
Questions I have:
Should I have two separate models, one for the login view information and one for the new_user_session route?
Do I need to create success and error functions in my LogInView.js if I already have them in my sessions Rails controller?
Should I be using a gem or plug-in to make this easier (such as backbone-forms: https://github.com/powmedia/backbone-forms)?
I've looked around for tutorials on this with little success. Any help would be super appreciated. Thank you!
I am using Stripe to:
Create Customer
Create Purchase
To do this, I am using this javascript code in my purchases.js (for my purchases controller):
$('document').ready(function() {
jQuery.externalScript = function(url, options) {
options = $.extend(options || {}, {
dataType: "script",
cache: true,
url: url
});
return jQuery.ajax(options);
};
$.externalScript('https://js.stripe.com/v1/').done(function(script, textStatus) {
Stripe.setPublishableKey($('meta[name="stripe-key"]').attr('content'));
var purchase = {
setupForm: function() {
return $('.card_form').submit(function() {
$('input[type=submit]').prop('disabled', true);
if ($('#card_number').length) {
purchase.processCard();
return false;
} else {
return true;
}
});
},
processCard: function() {
var card;
card = {
name: $('#name_on_card').val(),
number: $('#card_number').val(),
cvc: $('#card_code').val(),
expMonth: $('#card_month').val(),
expYear: $('#card_year').val()
};
return Stripe.createToken(card, purchase.handleStripeResponse);
},
handleStripeResponse: function(status, response) {
if (status === 200) {
$('#purchase_stripe_token').val(response.id)
$('.card_form')[0].submit()
} else {
$('#stripe_error').text(response.error.message).show();
return $('input[type=submit]').prop('disabled', false);
}
}
};
return purchase.setupForm();
});
});
This works fine.
My issue is, I am trying to create a recipient. To do this, I basically use the same code, swapping out the respective fields for the recipient creation. BUT before I do this, I attempt to submit the form (essentially creating a purchase from a different controller), but in the second controller, it does not pass the stripe_token.
What would be the cause for this -- the code is basically the same? I'm not versed in JS.
UPDATE
THIS IS THE JAVASCRIPT THAT DOES NOT WORK.
$('document').ready(function() {
jQuery.externalScript = function(url, options) {
options = $.extend(options || {}, {
dataType: "script",
cache: true,
url: url
});
return jQuery.ajax(options);
};
$.externalScript('https://js.stripe.com/v1/').done(function(script, textStatus) {
Stripe.setPublishableKey($('meta[name="stripe-key"]').attr('content'));
var bank = {
setupForm: function() {
return $('.recipient_form').submit(function() {
$('input[type=submit]').prop('disabled', true);
if ($('#account_number').length) {
bank.processBankAccount();
return false;
} else {
return true;
}
});
},
processBankAccount: function() {
var details;
details = {
country: $('#country').val(),
routingNumber: $('#routing_number').val(),
accountNumber: $('#account_number').val()
};
return Stripe.createToken(details, bank.handleStripeResponse);
},
handleStripeResponse: function(status, response) {
if (status === 200) {
$('#bank_stripe_token').val(response.id)
$('.recipient_form')[0].submit()
} else {
$('#stripe_error').text(response.error.message).show();
return $('input[type=submit]').prop('disabled', false);
}
}
};
return bank.setupForm();
});
});
THIS IS THE FORM.
<%= form_for([#user, #bank], role: "form", html:{class: "recipient_form"}) do |f| %>
<%= label_tag :country, "Country"%>
<%= text_field_tag :country, nil, name: nil, required: true %>
<%= label_tag :routing_number, "Routing Number"%>
<%= text_field_tag :routing_number, nil, name: nil, required: true %>
<%= label_tag :account_number, "Accounting Number"%>
<%= text_field_tag :account_number, nil, name: nil, required: true %>
<%= f.hidden_field :stripe_token %>
<%= f.submit value: "ADD" %>
<% end %>
THE CONTROLLER.
class BanksController < ApplicationController
def new
#user = User.find(params[:user_id])
#bank = #user.build_bank
end
def create
#user = User.find(params[:user_id])
#bank = #user.build_bank
stripe_token = params[:bank][:stripe_token]
begin
if !stripe_token.present?
raise "Stripe token not present. Cannot process transaction."
end
recipient = Stripe::Recipient.create(
:name => "Person Name",
:type => "individual",
:bank_account => stripe_token )
end
end
end
FORM DATA:
{"utf8"=>"✓",
"authenticity_token"=>"[FILTERED]",
"bank"=>{"stripe_token"=>""},
"commit"=>"ADD",
"user_id"=>"1680"}
template code :
<script type="text/template" id="baseTemplate">
<% collection.each(function() { %>
<div>
<%= collection %>
</div>
<% }); %>
</script>
<div id="baseContainer"></div>
and other code is :
//model
var PageModel = Backbone.Model.extend({
defaults: {
"id":null,
"title":"",
"content":""
},
});
//collection
var PageCollection = Backbone.Collection.extend({
defaults: {
model: PageModel
},
model: PageModel,
url: 'api/get_page/?id=6'
});
//view
var PageView = Backbone.View.extend({
el: $('#baseContainer'),
initialize: function () {
this.collection = new PageCollection();
this.collection.bind("reset", this.render, this);
this.collection.bind("change", this.render, this);
this.collection.fetch();
},
render: function () {
var html = _.template($("#baseTemplate").html(), { collection: this.collection });
this.$el.html(html);
console.log(html);
return this;
}
});
var page = new PageView();
problem is that its return and object how can i get values from object? api link is http://furqankhanzada.com/backbonejs/api/get_page/?id=6 and here you can see object in browser console http://furqankhanzada.com/backbonejs/
i need to get title, content , attachments -> images -> Gallery Large -> url (attachments using each() ).
Not sure whether this is proper solution or not, but you can give it a try.
An alternative can be like,
var html = _.template($("#baseTemplate").html(), { models: this.collection.models });
Pass models instead of directly passing collection. And in the template you can do something like this,
<script type="text/template" id="baseTemplate">
<% _.each(models, function(mdl) { %>
<div>
<%= mdl.get('title') %>
<%= mdl.get('content') %>
<% _.each(mdl.get('page').attachments, function(attachment) { %>
<%= attachment.images["Gallery Large"].url %>
<% }) %>
</div>
<% }); %>
</script>
<div id="baseContainer"></div>
Please modify the markup as per your needs. But this solution is too specific to the problem :( :(