I'm trying to remove a row in a table with Backbone. I'm using Rails 4 and the backbone-on-rails gem for this project.
I'm using a addEntryevent to add row in my Movies table. This adds a new row containing a id, movie_title and user_id.
The index view
class Movieseat.Views.MovieseatsIndex extends Backbone.View
template: JST['movieseats/index']
initialize: ->
#collection.on('update', #render, this)
#collection.on('add', #appendEntry, this)
render: ->
$(#el).html(#template())
#collection.each(#appendEntry)
this
events: ->
"click li": "addEntry"
"click .remove": "removeEntry"
addEntry: (e) ->
movie_title = $(e.target).text()
#collection.create title: movie_title
removeEntry: (e) ->
thisid = #$(e.currentTarget).closest('div').data('id')
console.log thisid
#collection.remove thisid
appendEntry: (entry) ->
view = new Movieseat.Views.Entry(model: entry)
$('#entries').append(view.render().el)
When I click on the .remove element I get a console log result showing the id stored in the element I want to remove.
The Entry view
class Movieseat.Views.Entry extends Backbone.View
template: JST['movieseats/entry']
className: 'movie-frame'
render: ->
$(#el).html(#template(entry: #model))
this
This is my index.jst.eco template
<h1>Movies</h1>
<div id="entries"></div>
This is the Entry template I render in the index.jst.eco template (i'm doing this so I only rerender the movie movies added to to the view.
<div class="movie-frame" data-id="<%= #entry.get('id') %>">
<p><%= #entry.get('title') %></p>
<p class="remove">Remove</p>
</div>
And my Backbone routes,
class Movieseat.Routers.Movieseats extends Backbone.Router
routes:
'': 'index'
initialize: ->
#collection = new Movieseat.Collections.Movieseats()
#collection.fetch()
index: ->
view = new Movieseat.Views.MovieseatsIndex(collection: #collection)
$('#container').html(view.render().el)
But there's no network activity when I click on the .remove element. There is network activity when the addEntry event is triggerd.
So apparently #collection.remove doesn't put out a HTTP request. Changing this to #collection.get(thisid).destroy() does the trick. But you will still need to create a destroy method in your Rails controller,
The removeEntry event in the Backbone view index,
removeEntry: (e) ->
thisid = #$(e.currentTarget).closest('div').data('id')
#collection.get(thisid).destroy()
The destroy method in the Rails controller,
def destroy
movie = Movie.find_by_id(params[:id])
movie.destroy
respond_to do |format|
format.json {render :json => {:msg => "item deleted!"},:status => 200}
end
end
Related
I created one nested form by watching gorails tutorial It is fine and i done it. Issue started when i want to creat nested model under on other nested model. I have Survey model and it is main model. Then i added Question model and made form with vue.js. So I added Choice model under question ( you can notice in survey controller params) First problem is; i don't know how i can define/implemen in vue.js control.(hello_vue.js) And second importan point is: how i can create form elements in new.html
This is my survey.rb model:
class Survey < ApplicationRecord
has_many :questions, dependent: :destroy
accepts_nested_attributes_for :questions, allow_destroy: true
belongs_to :user
end
and surveys_controller.rb
class SurveysController < ApplicationController
before_action :set_survey, only: [:show, :edit, :update, :destroy]
def survey_params
params.require(:survey).permit(:user_id, :name, questions_attributes:[:id,:survey_id, :title, :qtype, :_destroy, choices_attributes:[:id,:question, :ctext]])
end
end
This is nested model of Survey : question.rb:
class Question < ApplicationRecord
enum qtype: [:multiple_choice, :check_boxes, :short_answer]
belongs_to :survey
has_many :choices
accepts_nested_attributes_for :choices, allow_destroy: true
end
So finaly vue.js file:
import TurbolinksAdapter from 'vue-turbolinks'
import Vue from 'vue/dist/vue.esm'
import VueResource from 'vue-resource'
Vue.use(VueResource)
Vue.use(TurbolinksAdapter)
Vue.component('app', App)
document.addEventListener('turbolinks:load', () => {
Vue.http.headers.common['X-CSRF-Token'] = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
var element = document.getElementById("survey-form")
if (element != null){
var survey = JSON.parse(element.dataset.survey)
var questions_attributes = JSON.parse(element.dataset.questionsAttributes)
var choices_attributes = JSON.parse(element.dataset.choicesAttributes)
questions_attributes.forEach(function(question) { question._destroy = null })
survey.questions_attributes = questions_attributes
var app = new Vue({
el: element,
//mixins: [TurbolinksAdapter],
data: function(){
return { survey: survey }
},
methods:{
addQuestion: function(){
this.survey.questions_attributes.push({
id: null,
title:"",
qtype:"",
_destroy: null
})
},
removeQuestion: function(index) {
var question = this.survey.questions_attributes[index]
if (question.id == null) {
this.survey.questions_attributes.splice(index, 1)
} else {
this.survey.questions_attributes[index]._destroy = "1"
}
},
undoRemove: function(index) {
this.survey.questions_attributes[index]._destroy = null
},
saveSurvey: function() {
// Create a new survey
if (this.survey.id == null) {
this.$http.post('/surveys', { survey: this.survey }).then(response => {
Turbolinks.visit(`/surveys/${response.body.id}`)
}, response => {
console.log(response)
})
// Edit an existing survey
} else {
this.$http.put(`/surveys/${this.survey.id}`, { survey: this.survey }).then(response => {
Turbolinks.visit(`/surveys/${response.body.id}`)
}, response => {
console.log(response)
})
}
},
existingSurvey: function() {
return this.survey.id != null
}
}
})
}
})
_form.html.erb
<%= content_tag :div,
id: "survey-form",
data: {
survey: survey.to_json(except: [:created_at, :updated_at]),
questions_attributes: survey.questions.to_json,
} do %>
<label>Survey Name</label>
<input qtype="text" v-model="survey.name">
<h4>Questions</h4>
<div v-for="(question, index) in survey.questions_attributes">
<div v-if="question._destroy == '1'">
{{ question.title }} will be removed. <button v-on:click="undoRemove(index)">Undo</button>
</div>
<div v-else>
<label>Question</label>
<input qtype="text" v-model="question.title" />
<label>Qestion qtype</label>
<select v-model="question.qtype">
<option v-for="qtype in <%= Question.qtypes.keys.to_json %>"
:value=qtype>
{{ qtype }}
</option>
</select>
<button v-on:click="removeQuestion(index)">Remove</button>
</div>
<hr />
</div>
<button v-on:click="addQuestion">Add Question</button>
<br>
<button v-on:click="saveSurvey" >Save Survey</button>
<% end %>
I followed this same tutorial and started running into issues using JSON.parse with more complex nested attributes. Try using Jbuilder to build your JSON objects and look into the gon gem to pass your Rails variables into Javascript. It'll be much easier to query your database and pass the results into your Javascript file using the nested naming that Rails needs. For example...
survey = #survey
json.id survey.id
json.survey do
json.(survey, :user_id, :name)
json.questions_attributes survey.questions do |question|
json.(question, :id, :title, :qtype, :_destroy)
json.choices_attributes question.choices do |choice|
json.(choice, :id, :ctext)
end
end
end
It allows you to do things like...
var survey = gon.survey
Instead of...
var survey = JSON.parse(element.dataset.survey)
And you can pass gon.jbuilder from your controller action and have your defined JSON object ready and available in Vue.
I have an employee dropdown that lists all the employees. I want to be able to select an employee and get the address of the employee from the model so that I may display it. the following is the code of my collection_select.
<div class="form-group col-md-2 field">
<%= form.label :employee_id %>
<%= form.collection_select :employee_id, Employee.all, :id, :full_name,{:prompt=>"Select Employee"},{:id=>"emp_select",class:"form-control",:onchange=>"getEmployee();"} %>
</div>
Next is the code I am using to grab the value of the employee that was selected and it does work.
function getEmployee() {
var selectedVal=$('#emp_select option:selected').val();}
From here what do I do to get the address of the employee that was selected?
You will have to retrieve the employee's address via ajax call. Here are the steps:
Define an action in your rails app to return employee's address by json.
Make an ajax request to that action and get the info needed.
Render result into view.
For more information, take a look at this link:
https://guides.rubyonrails.org/working_with_javascript_in_rails.html
routes.rb
controller :ajax do
get 'ajax/get_employee_address/:employee_id', action: :get_employee_address, as: :get_employee_address
end
ajax_controller.rb
class AjaxController < ActionController::Base
def get_employee_address
employee = Employee.find(params[:employee_id])
render json: employee.address.to_json
rescue ActiveRecord::RecordNotFound
render json: 'Employee not found', status: 422
end
end
Your js code
function getEmployee() {
var selectedVal=$('#emp_select option:selected').val();
$.ajax({
url: '/ajax/get_employee_address/' + selectedVal,
success: function (address) {
// Render your address to view
},
error: function () {
// Handle error here or just return nothing
return null;
}
})
}
Note: This ajax endpoint will expose your employee address to outside so be sure to make authentication to prevent leaking info.
Add address to option data-attribute:
<%= form.select :employee_id,
options_for_select(Employee.all.map {
|e| [e. full_name, e.id, { 'data-address' => e.address }]
}),
{ prompt: "Select Employee" },
{ id: "emp_select", class: "form-control", onchange: "getEmployee();" } %>
On change get it with js:
function getEmployee() {
var selectedVal=$('#emp_select option:selected').data("address");}
And insert it to needed place
I have a page that does a search, using javascript, and I want to take the list of users that it comes up with, and send that as a submit to the next page. What I have, is:
.search_client_users
= form_tag admin_clients_path, method: "get" , class: "search_form" do
= label_tag 'search_term', 'Old domain name:'
= text_field_tag 'search_term', nil, autocomplete: "off", size: "50"
.main_form.client_emails
= simple_form_for(:domainNameSwap, url: { action: "update" }, html: { method: :put }) do |f|
.input-row
= f.hidden_field :users, value: #clients
.submit-row
.row
.col-xs-5
.submit
= f.submit "Update domains", id: "submit", :class => "btn btn-primary submit"
.client_list
- content_for :javascript do
= javascript_include_tag 'admin/search_client_users'
[some of the formatting may not be quite right due to cut and paste, sorry]
The admin/search_client_users creates an #clients, I'm pretty sure, at least, with:
class App.ClientUserList
constructor: ->
#incrementalSearchAttempts = 0
search: (searchTerm, completeCallback) =>
handleResponseWithOrderAwareness = (attemptNumber, response) =>
if attemptNumber >= #incrementalSearchAttempts
completeCallback(response)
#incrementalSearchAttempts++
onComplete = _.partial(handleResponseWithOrderAwareness, #incrementalSearchAttempts)
$.get('/admin/manage_clients/client_list', { search_term: searchTerm }).complete(onComplete)
class App.Views.SearchClientUsers extends Backbone.View
events:
"keyup input[name='search_term']": "search",
"click .profile_attribute": "showClientUserProfile"
initialize: =>
#clientUserList = new App.ClientUserList()
search: =>
searchTerm = $('.search_form input[name=search_term]').val()
#clientUserList.search(searchTerm, #render)
showClientUserProfile: (event) =>
window.location = $(event.currentTarget).closest('tr').data('client-path')
render: (response) =>
#$el.find('.client_list').html(response.responseText)
$ ->
new App.Views.SearchClientUsers(el: $('.search_client_users')).search()
so, I'm trying to take the list of clients, and send it to the update method in the controller. However, due to when javascript and ruby take place, it doesn't seem to be working... is there a way to do this? or do I have to figure out how to do this in Ajax?
ETA: An alternative idea is, I suppose to just turn the initial text_field into a form, so that the text field is used both for the javascript, and THEN submitted to the form, and then the update can re-do the search... My dataset is small enough that doing the search twice is not a huge problem I suppose...
But I'm not quite sure exactly how to merge the two forms...
I am using require.js with backbone and backbone.localstorage and I am trying to figure out how to make use of the data after calling fetch, not sure how to go about it... I am trying to pass the data into my view and make use of it.
Here is the example of the data stored in localstorage:
[{"artist":"Hits 1 Entertainment 4-1-1","title":"Hear Katy's Perry's New Album!"}, ...]
So it is objects within an array.
This is my code for backbone...
var songz = new Songs();
songz.localStorage = new Backbone.LocalStorage("music");
songz.fetch({dataType: 'json'});
var songV = new SongV({collection: songz});
songV.render();
Songs is a collection, that looks like this in the collections file, SongV is the view for each song.
Here is the view with the code above included:
define([
'jquery',
'underscore',
'backbone',
'models/song',
'collections/songs',
'views/song',
'text!templates/page.html'
], function($, _, Backbone, Song, Songs, SongV, PageT){
var Page = Backbone.View.extend({
el: $("#page"),
render: function () {
this.$el.html( PageT );
var songz = new Songs();
songz.localStorage = new Backbone.LocalStorage("music");
songz.fetch({dataType: 'json'});
var songV = new SongV({collection: songz});
songV.render();
}
});
return Page;
});
Here is the collection file:
define([
'jquery',
'underscore',
'backbone',
'models/song',
], function($, _, Backbone, Song){
var Songs = Backbone.Collection.extend({
model: Song,
initialize: function () {
}
});
return Songs;
});
Here is the model file:
define([
'underscore',
'backbone'
], function(_, Backbone) {
var Song = Backbone.Model.extend({
});
return Song;
});
Here is the template file:
<tr>
<th> Number </th>
<th> Title </th>
<th> Artist </th>
<th> Date_Added </th>
<th> Video </th>
</tr>
<% _.each(songs, function(song){ %>
<tr>
<td> <%= song.get("number") %> </td>
<td> <%= song.get("title") %> </td>
<td> <%= song.get("artist") %> </td>
<td> <%= song.get("added_on") %> </td>
<td> <%= song.get("video") %> </td>
</tr>
<% }); %>
You need to fetch and then bind to the reset event on the collection to see when it was successfully pulled the data from the server.
Page = Backbone.View.extend
el: $('#page')
render: ->
songz = new Songs()
# Initialize view
songV = new SongV({collection: songz})
# Render view
songV.render()
# Fetch collection
songz.fetch()
SongV = Backbone.View.extend
initialize: ->
#listenTo #collection, "reset", #onReset
onReset: (collection) ->
# Use populated collection data
...
Songs = Backbone.Collection.extend
model: Song
localStorage: new Backbone.LocalStorage("music")
initialize: ->
This is how Backbone.LocalStorage stores the collections and models --> here
See the table at the bottom, the key for the chain block is you local storage name and then each model has a unique key.
So this means if you have data sitting in local storage that you have put there yourself, you should take it out with a cross-browser local storage device like store.js and then use it to populate your Backbone.Collection.
Alternatively, you could fetch from the server (recommended) and that will populate your collection. Or you could bootstrap the data on page load and reset your collection that way.
I have a collection view and a model view, like so:
EventListView
|-- EventView
EventListView must display many EventViews in a one-to-many relationship. I am using the underscore _.template() function to build my views templates.
Here is my EventView template:
<h1>
<span class="date"><%= prefix %><%= dateString %></span>
<span class="title"><%= title %></span>
</h1>
<div class="caption"><%= caption %></div>
My EventView render method:
render: function () {
this.$el.html(this.template(this.model.attributes));
return this;
}
And here is my EventListView template:
<h1>
<% if(typeof(title) != "undefined") { print(title) } %>
</h1>
<%= events %>
And it's render method:
// this._EventViews is an array of eventView objects
render: function() {
var templateData = {
events: _.reduce(this._EventViews, function(memo, eventView) { return memo + eventView.$el.html(); }, "")
}
this.$el.html(this.template(templateData));
return this;
}
The problem I am having is that eventView.$el.html() contains only the HTML in my template, but I need to take advantage of the tagName, className and id attributes that Backbone views support.
Consider if I set up EventView like so:
return Backbone.View.extend({
model: EventModel
, tagName: 'article'
, className: 'event'
, template: _.template(templateText)
, render: function () {
this.$el.html(this.template(this.model.attributes));
return this;
}
});
I want to insert:
<article class="event" id="someID342">
<h1>
<span class="date">01/02/2010</span>
<span class="title"></span>
<div class="caption></div>
</h1>
</article>
but eventView.$el returns:
<h1>
<span class="date">01/02/2010</span>
<span class="title"></span>
<div class="caption></div>
</h1>
How do I insert the entire eventView element? Not just it's innerHTML.
Just reserve placeholder in your EvenListView's template
<h1><%- title %></h1>
<div class="js-events"></div>
And then render and append child views
render: function() {
this.$el.html(this.template({title: 'Title'}));
this.$events = this.$('.js-events');
_.each(this._EventViews, function (eventView) {
this.$events.append(eventView.render().$el);
}, this);
return this;
}
The render() function shouldn't be responsible for handling the setup of the view.el. This is done by Backbone in the _ensureElement function that is called when you initialize the view.
Also, the $.fn.html() function is only supposed to return the contents of the selected element.
You have many options but I think the most flexible and sustainable approach is to get each sub view to define its own template. The parent view simply appends the child elements .el property.
The advantages of this approach, your template is only compiled once. And updates to children do not require re-rendering parent and neighbouring elements.
Here is a JSBin
Example:
var ContainerView = Backbone.View.extend({
tagName: "article",
className: "event",
id: "someID342",
initialize: function(options){
//the template will now be rendered
this.childView = new ChildView()
//the rendered child will now appear within the parent view
this.el.appendChild( this.childView.el )
}
})
var ChildView = Backbone.View.extend({
tagName: "h1",
dateString:"01/02/2010",
prefix: "Date: ",
caption: "What a wonderful date!:",
title: "I am a title",
template: _.template([
'<h1>',
'<span class="date"><%= prefix %><%= dateString %></span>',
'<span class="title"><%= title %></span>',
'</h1>',
'<div class="caption"><%= caption %></div>'
].join("")),
initialize: function(){
this.render()
},
render: function(){
// because you are only altering innerHTML
// you do not need to reappend the child in the parent view
this.el.innerHTML = this.template(this)
}
})
I'd personally caution against using templates in Backbone at all. I've found that simply having a Backbone view for every component of your app becomes a lot easier to edit later. Sharing templates is a lot harder than sharing views. Of course it depends on the requirements of your project.