calling javascript class function from onchange attribute - javascript

I have a simple class that has a few methods(for now). I can instantiate it and call the init function, but I cant call another method from the onchange attribute.
here is the class:
var ScheduleViewer = (function() {
var options = {
container: 'trip_stops',
schedule: {}
};
function ScheduleViewer (){};
ScheduleViewer.prototype.init = function(params) {
$.extend(options, params);
// setTimeout(function() {
// ScheduleViewer.addListener(options);
// }, 3000);
this.addListener(options);
}
ScheduleViewer.prototype.getSchedule = function(trip_id) {
var _id = trip_id;
console.log(_id);
}
ScheduleViewer.prototype.addListener = function(options) {
console.log("adding listener");
var _id = options.select_id;
console.log($('#train_select').length);// zero assuming timing issue
$('#'+_id).on('change', function() {
alert("changed");
})
}
return ScheduleViewer;
})();
the call
<div id="trip_stops" class="trip_options">
<% if (trips.length > 0) { %>
<%
var params = {
schedule: schedule
};
var scheduler = new ScheduleViewer();
scheduler.init(params);
%>
<select id="train_select">
<option value="0" selected disabled>Select a train</option>
<% $.each(trips, function(index, val) { %>
<option value="<%= val.trip_id %>">
<%= val.train_num %> Train
</option>
<% }); %>
</select>
<% } else { %>
No trips scheduled.
<% } %>
</div>
Note that Im using jqote for templating.
I get the log from the init call, but then fails in the onchange with Uncaught ReferenceError: scheduler is not defined. Im not sure what Im doing wrong. Any pointers?
Thanks
EDIT: I made an attempt to add a listener that gets called at initialization. Im assuming that because of timing issues the listener is not getting bind. Now this is where my inexperience comes in play I tried to setTimeout but the function call is not happening as is.

I don't know why this may not work, but you shouldn't really use inline styles/functions anyway.
Why don't you try and get the element in your script the apply the onchange function to it.
var scheduler = new ScheduleViewer();
select = document.getElementById('train_select');
scheduler.init(params);
select.onchange = function() {
scheduler.getSchedule(this.value)
};

Try binding the event in javascript and remove the inline event.
var scheduler = new ScheduleViewer();
scheduler.init(params);
$(document).on('change', '#select', function() {
scheduler.getSchedule(this.value)
});

I have never used jQote before, however could it be because of the way jQote handles variable declarations? Try instanciating your scheduler object outside the templating code.
<script>
var scheduler = new ScheduleViewer(),
params = { schedule: schedule };
scheduler.init(params);
</script>
<!-- template code... -->
Also please note that the way you have written your code, all ScheduleViewer instances will share the same options object. I am not sure what you will be doing with this options object, but I guess that's not the desired behaviour.
Finally, like already stated by others, adding event listeners inline is a bad practice and you should register your even handlers programmatically using JavaScript. Either with the native addEventListener function or with helper functions of your favorite library.

I'm not familiar with jqote, but it looks like a scoping problem. If you must use the inline onchange, put the object reference in the window object and access it from there.
window['scheduler'] = new ScheduleViewer();
window['scheduler'].init(params);
And
<select id="train_select" onchange="window['scheduler'].getSchedule(this.value)">

So to solve the issue I ended up adding the class to the window object. I had issues with the listeners as the code that loads the template was not fast enough and the binding was not happening.
The class looks like:
window.ScheduleViewer = (function() {
var options = {
container: 'trip_stops',
schedule: {}
};
this.init = function(params) {
$.extend(options, params);
}
this.getSchedule = function(trip_id) {
var _id = trip_id;
//Do some cool stuff with GTFS data
}
//More functions
return this;
})();
The templated html:
<div id="trip_stops" class="trip_options">
<% if (trips.length > 0) { %>
<%
var params = {
schedule: schedule
};
ScheduleViewer.init(params);
%>
<select id="train_select" onchange="ScheduleViewer.getSchedule(this.value)">
<option value="0" selected disabled>Select a train</option>
<% $.each(trips, function(index, val) { %>
<option value="<%= val.trip_id %>">
<%= val.train_num %> Train
</option>
<% }); %>
</select>
<% } else { %>
No trips scheduled.
<% } %>
</div>
Thank you for all the responses.

Related

Javascript doesn't run on back button or menu item click in my rails application

I have the following Javascript code contained in my profile.js file in Rails 7:
window.addEventListener('load', function () {
updateProfile();
})
window.onpageshow = function(event) {
if (event.persisted) {
window.location.reload();
}
};
window.addEventListener('popstate', function() {
updateProfile();
});
window.addEventListener( "pageshow", function ( event ) {
var historyTraversal = event.persisted ||
( typeof window.performance != "undefined" &&
window.performance.navigation.type === 2 );
if ( historyTraversal ) {
// Handle page restore.
window.location.reload();
}
});
function fileSelected() {
// Get the selected file
const file = document.querySelector('#fileInput').files[0];
if (file == null){
return
}
// Create a new FileReader object
const reader = new FileReader();
// Set the onload event handler for the FileReader object
reader.onload = function(event) {
// Update the src attribute of the profile image
document.querySelector('#profile-image').src = event.target.result;
};
// Read the selected file as a DataURL
reader.readAsDataURL(file);
}
function updateProfile(){
var toggle_switch = document.getElementById('toggle');
var save_button = document.getElementById('save-button');
let nameInput = document.getElementById('name');
nameInput.addEventListener('input', function(event) {
let error_element = document.getElementById('error-message-name');
let regex = /^.{3,}$/; // Regex that requires at least 3 characters
if (regex.test(value)) {
// Value is valid
error_element.classList.remove("visible-error")
error_element.classList.add("invisible-error")
} else {
// Value is invalid
error_element.classList.remove("invisible-error")
error_element.classList.add('visible-error');
}
});
toggle_switch.addEventListener('click', function() {
if (this.getAttribute('data-type') == "influencer"){
this.setAttribute('data-type', "vender");
document.getElementById('profile_type').value = "vender";
}else{
this.setAttribute('data-type', "influencer");
document.getElementById('profile_type').value = "influencer";
}
});
save_button.addEventListener('click', function() {
let new_name = document.getElementById('name').value;
let new_headline = document.getElementById('headline').value;
let new_country = document.getElementById('country').value;
let new_city = document.getElementById('city').value;
let new_about = document.getElementById("about").value;
let new_profile_type = document.getElementById('toggle').getAttribute('data-type');
let div = document.getElementById('user_id');
let user_id = div.getAttribute('data');
var fileInput = document.getElementById("fileInput");
var file = fileInput.files[0];
if (file == null) {
$.ajax({ //A new image was not uploaded for change
type: "PATCH",
url: encodeURI('/users/' + user_id),
beforeSend: function(xhr) {xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'))},
data: { user: { profile_type: new_profile_type, name: new_name, headline: new_headline, country: new_country, city: new_city, about: new_about} },
success: function(response) {
console.log("Update success.")
}
});
} else {
const formData = new FormData();
formData.append("avatar", file);
$.ajax({
url: encodeURI('/users/' + user_id),
type: "PUT",
beforeSend(xhr, options) {
xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'));
xhr.setRequestHeader('image-change', true);
options.data = formData;
},
success: function(response) {
console.log("Image Update success.")
},
error: () => {
alert("An issue occured. Please try again.");
}
});
}
});
}
This code should be run when a user navigates to my profile page with the following html.erb:
<head>
<%= stylesheet_link_tag 'linked_card', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= stylesheet_link_tag 'profile_card', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= stylesheet_link_tag 'alert', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag "profile", "data": { "turbolinks-track": "reload" } %>
</head>
<header>
<%= render partial: "layouts/header" %>
</header>
<body class = "profile_main_body">
<%= render partial: "layouts/profile_card", locals: { user: #user } %>
<br>
<br>
<div class="col-md-6 mx-auto text-center">
<h3 class="heading-black">Link Accounts</h3>
<p class="text-muted lead">Click the button below to link an account to your profile. You will be redirected to the chosen service.</p>
<i class="fa fa-link"></i> Link Account
</div>
<br>
<br>
<div class="container">
<div class="row">
<%= render partial: "layouts/linked_card" %>
<%= render partial: "layouts/linked_card" %>
</div>
</div>
<br>
</body>
<footer>
<%= render partial: "layouts/footer" %>
</footer>
I added the following line:
<%= javascript_include_tag "profile", "data": { "turbolinks-track": "reload" } %>
Because I was thinking this is a turbolinks issue. Nothing has changed though with the line above. Is there anyway I can re-run the my javascript when the user clicks a turbo link? The javascript runs fine on a full page reload. Any help would be great thanks!
Turbolinks is replaced by Turbo in Rails 7. However there is a lot of overlap in how you should think about JS.
Turbolinks/Turbo (or almost any SPA framework for that matter) creates a persistent browser session across pages. Thinking in terms of "when the page is loaded I want to attach an event handler to X that does Y" might have been OK in a basic JS tutorial ten years ago but is actually counter-productive:
It doesn't work when elements are inserted dynamically into the DOM. Like when Turbo Drive or Turbolinks replaces the page contents. Or whenever people try to insert content loaded with AJAX for the first time.
Adding event handlers directly to a bunch of elements adds a lot of overhead.
If you hook into an event like turbolinks:change you might be adding the event handler multiple times to the same element. Just switching out the load/ready event for the Turbo/Turbolinks equivilent won't necissarily fix stinky code and may just introduce new issues.
It's a broken mental model as you build your JS to just "frobnob X on page Y" instead of thinking in terms of reusable UI components or augmenting the behavior of elements in a reusable way.
So what then?
Stop assigning IDs and event handlers directly to everything. You're just going to end up with duplicate IDs and garbage JS.
Instead use delegation to catch the event as it bubbles to the top of the DOM:
// Do something awesome when the user clicks buttons with class="foo"
document.addEventHandler('click', function(event){
let el = event.target;
if (!el.matches('.foo')) return;
// ...
});
Use classes and attributes to target elements. Not ID's. Its not 2010 and querying the DOM is much faster.
Use DOM traversal and the form API to get elements relative to the element that was clicked/changed/etc. Remember that in JS functions can actually take arguments. Use data attributes if you need to pass additional information from the backend to your JS.
This is basically what Stimulus does:
<div data-controller="hello">
<input type="text">
<button>Greet</button>
</div>
// src/controllers/hello_controller.js
import { Controller } from "#hotwired/stimulus"
export default class extends Controller {
connect() {
console.log("Hello, Stimulus!", this.element)
}
}

Returning a partial view with Ajax, MVC 3

First let me say I think this site is a Godsend, and I am extremely grateful for all I've learned. I have an MVC3 component I'm working with. I want to populate a selectlist and when the user selects one of the options, I want a to load a partial view with the data displayed. Everything works so far except I'm missing the piece that refreshes the partial view. When the page originally loads, I see the empty container in the code. I get no JS errors and Firebug shows the properly formatted HTML returned. So what part of the operation am I missing to refresh the partial view on the page? Please advise and thanx in advance.
The View:
<tr>
<th>Select a User to view their Roles:</th>
<td><%= Html.DropDownList("UserListForRoleView", list, "Please choose a User")%></td>
</tr>
<tr>
<td>
<% Html.RenderPartial("ViewUsersInRole");%>
</td>
</tr>
$(document).ready(function () {
$('#UserListForRoleView').change(function () {
var selectedID = $(this).val();
$.get('/UserAdminRepository/ViewUsersInRole/?user=' + selectedID)
});
});
THe Controller:
public ActionResult ViewUsersInRole()
{
string user = Request["user"];
string[] selectedRoles = Roles.GetRolesForUser(user);
List<string> data = new List<string>();
if (selectedRoles.Length > 0)
{
data = selectedRoles.ToList<string>();
}
else
{
data.Add("No data found");
}
ViewData["UsersinRole"] = data;
return PartialView("ViewUsersInRole");
}
The PartialView (in it's entirety):
<%# Page Language="C#" Inherits="System.Web.Mvc.ViewPage<dynamic>" %>
<ul>
<%
List<string> list = ViewData["UsersinRole"] as List<string>;
if (list != null && list.Count > 0)
{
foreach (string item in list)
{ %>
<li><%: item %></li>
<% }
}
%>
</ul>
append html in the td like this:
<td id="partialcontainer">
<% Html.RenderPartial("ViewUsersInRole");%>
</td>
and append html in it:
$(document).ready(function () {
$('#UserListForRoleView').change(function () {
var selectedID = $(this).val();
$.get('/UserAdminRepository/ViewUsersInRole/?user=' + selectedID,function(response){
$('#partialcontainer').html(response);
})
});
});
Little Improved code, always use Url.Action to generate url:
$(document).ready(function () {
$('#UserListForRoleView').change(function () {
var selectedID = $(this).val();
var url = '#Url.Action("ViewUsersInRole","UserAdminRepository")';
url+"?user="+selectedID;
$.get(url,function(response){
$('#partialcontainer').html(response);
});
});
});
I have modified a little your code to make it more clear and readable.
Try RenderAction
<% Html.RenderAction("ViewUsersInRole");%>

Odd behavior when trying to get the contents of a script element

I'm playing around with backbone and underscore templates. When I try to dereference the contents of a script block (using $("#people-templ").html()) I get two different behaviors depending on the context of the call. Inside the render function for the backbone view I get nothing returned. If I get the contents of the script block outside any function, I get the valid HTML contents. I tried this on Chrome, Safari, and Firefox. I stepped through with the debugger and validated that JQuery returned an empty array when calling $("#people-templ") within the callback function. I was wondering if someone had an explanation for this behavior.
In index.html I have a simple template:
<script type="text/template" id="people-templ">
<h1>People</h1>
<table>
<thead>
<tr><th>First Name</th><th>Last Name</th></tr>
</thead>
<tbody>
<% people.each(function(person) { %>
<tr>
<td><%= person.get("firstName") %></td>
<td><%= person.get("lastName") %></td>
</tr>
<% }) %>
</tbody>
</table>
</script>
<script src='/javascripts/lib/jquery-2.0.3.js'></script>
<script src='/javascripts/lib/underscore.js'></script>
<script src='/javascripts/lib/backbone.js'></script>
<script src="/javascripts/main/index.js"></script>
Inside index.js I have the following Backbone view definition:
var peopleHtml = $("#people-templ").html();
var PeopleView = Backbone.View.extend({
el: "#foo",
initialize: function() {
this.render();
},
render: function() {
var people = new People();
var me = this;
this.$el.empty();
people.fetch({
success: function(people) {
var template = _.template(peopleHtml, { people: people });
me.$el.html(template);
},
error: function() {
console.error("Failed to load the collection");
}
});
}
});
This works. The code gets the template from the script tag, Underscore processes it, and inserts the resulting markup into the DOM. If I move var peopleHtml = $("#people-templ").html(); inside of the callback, as such:
var PeopleView = Backbone.View.extend({
el: "#foo",
initialize: function() {
this.render();
},
render: function() {
var people = new People();
var me = this;
this.$el.empty();
people.fetch({
success: function(people) {
var template = _.template($("#people-templ").html(), { people: people });
me.$el.html(template);
},
error: function() {
console.error("Failed to load the collection");
}
});
}
});
Then it seems that nothing is returned from $("people-tempo").html() and the code failed inside of underscore.js when it tries to do text replacement when processing the template.
Does anyone know why this might be the case?
SOLVED thanks to Pointy
<div id="foo"/>
and
<div id="foo"></div>
are not the same thing. If the former is used, everything from the div on down (including all my script elements) were replaced by the templates text. using the latter, only the contents of the div are replaced.
When using elements for underscore templating, it looks like you need to use the
<div id='foo'></div>
form instead of just:
<div id='foo'/>

canjs attach Control for element in Observe.List after render

Generally the problem is that my click action in Control doesn't work.
In codes below you can see situation when i generate list with items using Observe.List, so it's updated when new element occurs on the list automatically (view below) and it's generated in first Control in init func.
Also in first control When new elements are put on the list I attach Control to it '{files} add' method for new generated element.
The problem is that in second control click event won't work. I think that because I try to do that before automatic regenerate Observe.List was finish. The proof is that jquery doesn't element yet.
Is it possible that I attach Contol ('{files} add') before it's render by automatic regenerate Observe.List? How Should I set Control on new element in proper way? Can I do that somehow in view?
define(['canjs/can',
'text!platform/presenter/views/file_list.ejs',
'platform/presenter/file',
'platform/presenter/show',],
function(can,EjsFileList,ControlFile,ControlShow){
var file_list = can.Control({
},{
'init': function(){
"use strict";
can.view.ejs('EjsFileList',EjsFileList);
can.view( "EjsFileList", {
files : this.options.files
}, $.proxy( function( fragment ) {
this.element.html( fragment );
},this));
},
'{files} add': function(list,event ,added){
list.forEach($.proxy(function(el){
console.log($('#file-' + el.id));
// HERE IS PROBLEM
//[context: document, selector: "#file-80", jquery: "1.9.1", constructor: function, init: //function…]
//context: document
//selector: "#file-80"
//__proto__: Object[0]
new ControlFile('#file-' + el.id ,{
'file': el,
'files': list
});
},this));
},
'{files} remove': function(a,b,removed){
removed.forEach(function(element){
$('#file-' + element.id).remove();
});
}
});
return file_list;
});
the view for list:
<% files.each(function(file){ %>
<li id="file-<%= file.id %>" <%= (el) -> can.data(el, 'file', file) %> class="<%= file.attr('public')? '' : ' private ' %>">
<img class="loading" />
</li>
<% }); %>
Control for list element. Here is problem!
define(['canjs/can',
'platform/model/file',
'platform/presenter/show',
'text!platform/presenter/views/file.ejs',
],
function(can, modelFile,ControlShow,EjsFile){
var list = can.Control({
defaults: {
loaderClass: 'loading',
model: modelFile
}
},{
'init': function(){
"use strict";
console.log(this.element);
//[context: document, selector: "#file-73", jquery: "1.9.1", constructor: function, init: function…]
},
'img click': function(){
console.log('should work but never gets here');
}
});
return list;
});
I found the solution.
If you using dynamic list in canjs and want to attach Control on each element you should create the view like below:
<% files.each(function(file){ %>
<li id="file-<%= file.id %>" <%= (el) -> can.data(el, 'file', file) %>
<%= function(element){ new ControlFile(element, {file: file}); } %>>
<img class="loading" />
</li>
<% }); %>
As you can see I set up Control on the list, but I need to pass to view every variable that Control using.

Selecting item dynamically in rails

i am attempting to do a dynamic select here using rails and jquery. the code is as follows
<div class = "line_items">
<%- calc = Hash[Item.all.map{|p| [p.id, p.quantity]}].to_json %>
<div class = "item"><%= f.collection_select :item_id,Item.all,:id,:title, :prompt => "Select a Item", input_html: {data:{calc: calc} %></div>
<div class ="quantity"> <%= f.text_field :quantity %></div>
/*rest of code*/
</div>
javascript for the view is as follows
jQuery(document).ready(function(){
jQuery('.item').bind('change',function() {
var selectElement = jQuery(this);
var itemSelected = jQuery('.item:selected').val();
var wrapperDivElement = selectElement.parent(".line_items");
var quantity= eval(selectElement.data("calc"))[itemSelected];
jQuery(".quantity", wrapperDivElement).val(quantity);
});
});
when i change the item i am getting the following error
eval(selectElement.data("calc"))[itemSelected] is undefined in firebug. Can anyone point out where i am going wrong? also any better way to get the quantity. i feel the method i am doing is crude. any guidance would be helpful. Thanks in advance.
jQuery(document).ready(function(){
jQuery('.item select').bind('change',function() {
var selectElement = jQuery(this);
var itemSelected = selectElement.val();
var wrapperDivElement = selectElement.parents(".line_items");
var quantity= eval(selectElement.data("calc"))[itemSelected];
jQuery(".quantity input", wrapperDivElement).val(quantity);
});
});
i guess the value of itemSelected was not getting detected, hence the error. the above redefined code should work. But i strongly urge you not to get data like this. its better to do a json call or ajax call to get the related data from the controller. and use parents instead parent in the code:)
Ik think you mixup the reference by index and the reference by ID. I'm no Javascript wizard, but you fill the hash with a list of . Then you get the 'val'-value in the change event. That contains an integer, but that is not the index.
Then you request in Javascript the item with index val, not the item with value val. Maybe javascript cannot distinguish between them because both need an Int...
it's this line:
var quantity= eval(selectElement.data("calc"))[itemSelected];

Categories