I'm retrieving an endDate field from Django model using forloop on HTML page.
I want to check whether all the endDate coming are less that today's date or not. For that I'm using JS code.
My code is able to perform checking only on first date retrieved but it's not working on other dates.
Here's my HTML code for retrieving data:
{% for p in object_list %}
<h4>{{ p.jobName }}</h4>
<p><b style="color: red">Expires On: </b><b>{{ p.endDate }}</b></p>
View info
<input type="hidden" id="endDate" name="variable" value="{{ p.endDate }}">
<span id="check"></span>
{% endfor %}
JavaScript code to check date:
<script type="text/javascript">
var endDate = document.getElementById("endDate").value;
var ToDate = new Date();
if(new Date(endDate).getTime()<=ToDate.getTime()){
document.getElementById("check").innerHTML = "Expired";
document.getElementById("check").className = "label danger";
}
else{
document.getElementById("check").innerHTML = "Active";
document.getElementById("check").className = "label success";
}
</script>
I have 2 {{ p.jobName }} right now. First one should show Expired while second should show Active.
My code is working only for first date i.e., for Computer Admin only.
Here's what I'm getting the output:
Can anybody tell me what's the issue?
Thanks
To expand on what I've already said in the comments:
The essence of your problem is here:
{% for p in object_list %}
...
<input type="hidden" id="endDate" name="variable" value="{{ p.endDate }}">
<span id="check"></span>
...
{% endfor %}
An HTML document can only ever have one element of a particular ID (otherwise it doesn't "ID" the element very well - this is part of the HTML spec). In your case, whenever the loop has more than one element, you will end up with multiple elements, all with the endDate ID, as well as multiples with the check ID.
Repeating an ID like this, as well as being wrong in itself, can have a major functional impact - as you're seeing in your code. Namely that since there is only ever supposed to be (at most) one element of a given ID, any Javascript code that runs on the page is entitled to assume that this is the case. And in particular, document.getElementById returns an object representing a single HTML element, as opposed to similar methods such as document.getElementsByClassName which return a "collection" of DOM nodes. (Because a class, unlike an ID, can be applied to multiple elements in the same document.)
So that, in a nutshell, is how to solve the problem you're having. Simply replace the id="endDate" in the above to class="endDate", similarly with check, and alter your javascript to take this into account. Not only do you have to change the getElementById method to getElementsByClassName, you also have to take into account that this returns a collection (which you can loop over with .forEach), and that you need to target the neighbouring .check span rather than the first, or any old random one. The following code is one way to make it work (note the use of ES6 Array.from to convert the collection to a genuine Array so you can use .forEach):
var endDateElts = Array.from(document.getElementsByClassName("endDate"));
var ToDate = new Date();
endDateElts.forEach(function(dateElt) {
var endDate = dateElt.value;
var check = dateElt.nextSibling;
if(new Date(endDate).getTime()<=ToDate.getTime()){
check.innerHTML = "Expired";
check.className = "label danger";
}
else{
check.innerHTML = "Active";
check.className = "label success";
}
});
Based on
{% for p in object_list %}
<h4>{{ p.jobName }}</h4>
<p><b style="color: red">Expires On: </b><b>{{ p.endDate }}</b></p>
View info
<input type="hidden" id="endDate" name="variable" value="{{ p.endDate }}">
<span id="check"></span>
{% endfor %}
I would assume that you have multiple objects with the same ID hence the reason why the JS is only working on the first endDate and check object.
<input type="hidden" id="endDate" name="variable" value="{{ p.endDate }}">
<span id="check"></span>
What you should do:
Use document.querySelectorAll() to get multiple DOM elements
with the same class name.
Iterate through the NodeList and perform the operations that you wish to perform.
const endDates = document.querySelectorAll('.endDate');
const checks = document.querySelectorAll('.check');
let ToDate = new Date();
let i = 0;
for (const endDate of endDates){
if(new Date(endDate.value).getTime()<=ToDate){
checks[i].innerHTML = "Expired";
checks[i].className = "label danger";
}
else{
checks[i].innerHTML = "Active";
checks[i].className = "label";
}
i++;
}
<div class="container-1">
<input type="text" class="endDate" name="variable" value="2019-11-29">
<span class="check"></span>
<br>
</div>
<div class="container-2">
<input type="text" class="endDate" name="variable" value="2019-11-24">
<span class="check"></span>
<br>
</div>
<div class="container-3">
<input type="text" class="endDate" name="variable" value="2019-11-27">
<span class="check"></span>
</div>
Notes
Not very sure why you would want to do the validation in the front-end if you can do it on your views.py. #Robin Zigmond gave a very good suggestion of just doing it on the backend and populate the template with the result.
Related
I am trying to compare two fields generated with Jinja, however, without the need of pressing the submit button.
I have used the included validators, standard if cases, etc. None of them work as expected, so I tried Javascript.
In general, I want to:
Compare two fields (domain/server). If their IP addresses are not exactly a 'Confirm' button should be shown.
In order to do that, I need to reload the server-side HTML.
Additionally, I am unable to figure out how to compare the two fields' IP addresses via Javascript.
Here is a sample code:
<div id="home" class="text-center">
Hello, {{ username }}
</div>
<body>
<input type="text" id="domain" size="30">
<input type="text" id="server" size="30">
{% if not compareArecords %}
<button type="button" class="btn btn-danger">Danger</button><br>
{%endif %}
</body>
<script>
document.getElementById("domain").addEventListener("keyup", compareArecords);
document.getElementById("server").addEventListener("keyup", compareArecords);
function compareArecords() {
var text1 = document.getElementById("domain");
var text2 = document.getElementById("server");
if (text1.value === text2.value)
return true;
else
return false;
}
</script>
What's typically done in order not to have to reload the html is to have the element hidden with css until you need to display it. In your example would be something like
<body>
<input type="text" id="domain" size="30">
<input type="text" id="server" size="30">
<button type="button" class="btn btn-danger hidden" id="btn-danger">Danger</button><br>
</body>
<script>
document.getElementById("domain").addEventListener("keyup", compareArecords);
document.getElementById("server").addEventListener("keyup", compareArecords);
function compareArecords() {
var text1 = document.getElementById("domain");
var text2 = document.getElementById("server");
if (text1.value === text2.value)
// Nothing to do here!
else {
var danger_btn = document.getElementById("btn-danger");
danger_btn.classList.toggle("hidden");
}
}
</script>
Css:
.hidden { display: none; }
It should be noted that you can also generate the Danger button entirely with javascript, and that's not overly complicated either, should you want to do that, but it has some drawbacks like separation of concerns.
Context
I am building a simple "todo" flask app with an SQLAchemy database.
The tasks are sorted by sections (check the image below to see how it is organized).
Once I implemented all the functionalities I wanted, I ran into the issue which was the whole page
got refreshed each time I triggered a button (add/edit/delete/update_status).
Then I found out hotwire which is amazing to handle this.
This my taskManager.html organization:
<!--New task button-->
<turbo-frame id="data_frame-add">
<button class="btn-add" onclick="openAddForm()">...</button>
<div class="form-popup" id="myForm">
<form name="AddTaskForm" action="{{ url_for('add_task') }}" class="form-container" method="POST">
<label for="section"><b>Section</b></label>
<input type="text" id="section" name="section" required>
<label for="content"><b>Task</b></label>
<input type="text" id="content" name="content required>
<button type="submit" class="btn">Add</button>
</form>
</div>
</turbo-frame>
<!--Display sections and tasks-->
<div class="flex-row">
{% for section in sections %}
<turbo-frame id="data_frame-{{ section }}">
<div class="flex-column">
<h2>{{ section }}</h2>
{% for task in tasks %}
{% if task.section == section %}
<p>{{ task }}</p>
<button class="satus"></button>
<button class="edit"></button>
<div class="form-popup" id="form-{{ task.id }}">...</div>
<button class="delete"></button>
<div class="form-popup" id="form-{{ task.id }}">...</div>
{% endif %}
{% endfor %}
</div>
</turbo-frame>
{% endfor %}
</div>
Using a turbo frame with id data_frame-{{ section }} (one for each section) allowed to refresh only the concerned section when hitting status, edit and delete buttons (for example, hitting delete button of task 2 of Section 2 will only refresh the turbo frame data_frame-Section 2). However as the New task button is out of theses turbo frames, It works differently and this is a different challenge...
Issue
When adding a new task, I would like the section of the task (entered here <input type="text" id="section" name="section"...>) to be saved in a variable which will be used to target a specific <turbo-frame id="data_frame-{{ section }}"> and refresh it without refreshing the whole page.
At the moment as the New task button is wrapped with <turbo-frame id="data_frame-add"> it is self contained (meaning if I'm adding a task 5 to Section 1 only the turbo frame with id data_frame-add is refreshed not the data_frame-Section 1 so I need to manually refresh the page to see changes)
What I tried
I added data-turbo-frame to the form:
<form name="AddTaskForm" action="{{ url_for('add_task') }}" class="form-container" method="POST" data-turbo-frame="data_frame-Section 1">
in order to be able to refresh the "data_frame-Section 1" when I add a New task in section Section 1, and it works! But I would like to make this data-turbo-frame="data_frame-<section>" with <section> a variable that get the value of <input type="text" id="section" name="section"...>
To achieve this I removed data-turbo-frame="data_frame-Section 1" in the form:
<form name="AddTaskForm" action="{{ url_for('add_task') }}" class="form-container" method="POST">
and added a Javascript part:
var sectionValVar = document.getElementById("section").value;
const sectionValPref = "data_frame-";
let sectionVal = sectionValPref + sectionValVar;
$(".AddTaskForm").attr("data-turbo-frame", sectionVal);
sectionVal is supposed to get the variable value "data_frame-<section>" and last line add "data-turbo-frame" = "data_frame-<section>" to the <form name="AddTaskForm"...>
But this doesn't work. I'm not sure if this even possible to make as it looks tricky...
But if someone has any hint or fix for this It would be amazing !
Thank you !
Other ressources
This is my add_task route in my python flask app:
#app.route('/add', methods=['GET', 'POST'])
def add_task():
content = request.form.get('content')
section = request.form.get('section')
task = Task(content=content, section=section)
db.session.add(task)
db.session.commit()
return redirect(url_for('taskManager'))
This looks like it would work but I don't see an event listener to set the form data-turbo-frame attribute whenever the input value changes.
You need to update the attribute either before the form submits or whenever the input gets updated.
this is how you could do it with jquery
$("#section").change(function() {
let sectionAndPrefix = "data_frame-" + $("#section").val()
$(".AddTaskForm").attr("data-turbo-frame", sectionAndPrefix);
})
in vanilla javascript
const sectionInput = document.querySelector("#section")
sectionInput.addEventListener("input", function() {
const taskForm = document.querySelector(".AddTaskForm")
const sectionAndPrefix = "data_frame-" + sectionInput.value
taskForm.setAttribute("data-turbo-frame", sectionAndPrefix)
})
I have a form in which I need to add a button that will allow the user to add a new field.
I'm trying to do it with JavaScript, but since I don't know much this language I'm lost with some concepts.
The form I use is one where the data is completed in steps, and the fields I want to add are the following
<div class="job-form row mb-3">
{% for form in add_trabajo %}
<div class="col-md-6">
<label for="ref" class="">Nombre</label>
{% render_field form.trabajo class="form-control" type="text" placeholder="Ej: ReparaciĆ³n de envolvente delantero" %}
</div>
<div class="col-md-2">
<label for="ref" class="">Descuento</label>
{% render_field form.descuento class="form-control" type="text" %}
</div>
<div class=" col-md-2">
<label for="ref" class="">Precio</label>
{% render_field form.precio class="form-control" type="text" %}
</div>
<div class="col-md-2 d-flex align-items-end">
<button id="addButton" type="button" class="btn-icon btn-icon-only btn btn-success p-2"><i class="pe-7s-plus btn-icon-wrapper"> </i></button>
</div>
{% endfor %}
</div>
I'm trying to add these fields after existing ones that are identical
I already have the button and some code in javascript but it indicates the following error
Uncaught DOMException: Failed to execute 'insertBefore' on 'Node': The node before
which the new node is to be inserted is not a child of this node.
at HTMLButtonElement.addForm
This is my JavaScript code
let jobForm = document.querySelectorAll(".job-form")
let container = document.querySelector("#presupuestoForm")
let addButton = document.querySelector("#addButton")
let totalForms = document.querySelector("#id_trabajosarealizar_set-TOTAL_FORMS")
let formNum = jobForm.length-1 // Get the number of the last form on the page with zero-based indexing
addButton.addEventListener('click', addForm)
function addForm(e) {
e.preventDefault()
let newForm = jobForm[0].cloneNode(true) //Clone the bird form
let formRegex = RegExp(`trabajosarealizar_set-(\\d){1}-`,'g') //Regex to find all instances of the form number
formNum++ //Increment the form number
newForm.innerHTML = newForm.innerHTML.replace(formRegex, `trabajosarealizar_set-${formNum}-`) //Update the new form to have the correct form number
container.insertBefore(newForm, addButton) //Insert the new form at the end of the list of forms
totalForms.setAttribute('value', `${formNum+1}`) //Increment the number of total forms in the management form
}
and this would be the structure of my form
that's where I try to add the new fields, between the job-form class and the divider with id div-form
This error means "addButton is not a child of container".
The nodes to provide are parentNode.insertBefore(newNode, referenceNode).
parentNode has to be the direct parent of the referenceNode.
So try:
jobForm.parentNode.insertBefore(newForm, addButton)
But that may not be correct either... since #addButton is a child of .job-form... I think that will have two add buttons.
But now that you know what .insertBefore() needs... You can fix it.
JavaScript work only in first iteration, doesn't affect on the field from second iteration.
forloop Code Sample
{% for i in userlist %}
<input id="a" type="email" value="{{i.email}}">
<input id="b" type="text" value="{{i.date_of_birth}}">
<input id="c" type="text" value="{{i.mobile}}">
{% endfor %}
Button for disable editing
<button onclick="disableProfileEditing()" type="button"> Disable </button>
Button for enable editing
<button onclick="enableProfileEditing()"> Edit</button>
JavaScript function
function disableProfileEditing() {
document.getElementById(a).disabled = true;
document.getElementById(b).disabled = true;
document.getElementById(c).disabled = true;
}
function disableProfileEditing() {
document.getElementById(a).disabled = false;
document.getElementById(b).disabled = false;
document.getElementById(c).disabled = false;
}
Ok regardless of how the for loop is made, what it is doing is making many elements with the same I'd, which is a problem. Instead you can give them all a unique class, or data attribute, but class is simpler and more widely supported, then in your function you have to get all elements with that class (or data attribute) and do the hiding etc.
So for the for loop
{% for i in userlist %}
<input class="email" type="email" value="{{i.email}}">
<input class="dob" type="text" value="{{i.date_of_birth}}">
<input class="mobile" type="text" value="{{i.mobile}}">
{% endfor %}
Just changed the IDs to class, so you can have more than one. Now for the first function just get all of each class (and the second one I'll leave for you to figure out)
function disableProfileEditing() {
Array.apply(0,document.getElementsByClassName("email")).forEach(x=> x.disabled = true);
Array.apply(0,document.getElementsByClassName("dob")).forEach(x=> x.disabled = true);
Array.apply(0,document.getElementsByClassName("mobile")).forEach(x=> x.disabled = true);
}
OR you could have just made a surrounding container div with a unique ID then just made the for loop add elements into that, probably would have been simpler if you want to show or hide them all together
I have the following situation: the CVCFormType is a collection of BenefiItemsFormType. Each BenefitItemFormType has one field that is a collection of BenefitGroupFormType.
I want to be able to dynamically add and remove elements.
I followed the instructions here. Of course they must be tweaked as we talk about nested collections.
On the "fixed" side everything is ok. On the dynamic side (to add and remove elements) so far I've implemented only the inner side (adding BenefitGroups) and only for adding fields.
Here is what I get (which is not right). I have a double link on the top Benefit Item (I should have only one), plus the two group of links (of the first benefit
item and of the second one) are not independent (I click on the second of the one above and it adds a field to the one below). I think I'll have to dynamically change the ul class name.
Any help?
Here is a screenshot:
And here is the code:
{% extends "internal.html.twig" %}
{% block content %}
{{ form_start(form) }}
<br><b>CVC</b>
{% for benefititem in form.benefititems %}
<br><b>Benefit Item</b>
{{ form_row(benefititem.comment) }}
<br><b>Benefit Groups</b>
{# <ul class="benefitgroups"> #}
<ul class="benefitgroups" data-prototype="{{ form_widget(benefititem.benefitgroups.vars.prototype)|e }}">
{% for benefitgroup in benefititem.benefitgroups %}
<li>{{ form_row(benefitgroup.name) }}</li>
{% endfor %}
</ul>
{% endfor %}
{{ form_end(form) }}
{% block javascripts %}
<script>
var $collectionHolder;
// setup an "add a benefitgroup" link
var $addBenefitGroupLink = $('Add a Group');
var $newLinkLi = $('<li></li>').append($addBenefitGroupLink);
jQuery(document).ready(function() {
// Get the ul that holds the collection of benefit groups
$collectionHolder = $('ul.benefitgroups');
// add the "add a benefitgroup" anchor and li to the benefitgroups ul
$collectionHolder.append($newLinkLi);
// count the current form inputs we have (e.g. 2), use that as the new
// index when inserting a new item (e.g. 2)
$collectionHolder.data('index', $collectionHolder.find(':input').length);
$addBenefitGroupLink.on('click', function(e) {
// prevent the link from creating a "#" on the URL
e.preventDefault();
// add a new tag form (see next code block)
addBenefitGroupForm($collectionHolder, $newLinkLi);
});
});
function addBenefitGroupForm($collectionHolder, $newLinkLi) {
// Get the data-prototype explained earlier
var prototype = $collectionHolder.data('prototype');
// get the new index
var index = $collectionHolder.data('index');
// Replace '__name__' in the prototype's HTML to
// instead be a number based on how many items we have
var newForm = prototype.replace(/__name__/g, index);
// increase the index with one for the next item
$collectionHolder.data('index', index + 1);
// Display the form in the page in an li, before the "Add a BenefitGroup" link li
var $newFormLi = $('<li></li>').append(newForm);
$newLinkLi.before($newFormLi);
}
</script>
{% endblock %}
{% endblock content %}
If it can help here is the generated HTML:
<html>
<head>
<meta charset="UTF-8" />
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<title>Welcome!</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
</head>
<body>
<h1>Here you are inside</h1>
<form name="CVC" method="post" action="">
<br><b>CVC</b>
<br><b>Benefit Item</b>
<div> <label for="CVC_benefititems_0_comment" class="required">Comment</label> <input type="text" id="CVC_benefititems_0_comment" name="CVC[benefititems][0][comment]" required="required" maxlength="400" value="b1" /></div>
<br><b>Benefit Groups</b>
<ul class="benefitgroups" data-prototype="<div id="CVC_benefititems_0_benefitgroups___name__"><div> <label for="CVC_benefititems_0_benefitgroups___name___name" class="required">Name</label> <input type="text" id="CVC_benefititems_0_benefitgroups___name___name" name="CVC[benefititems][0][benefitgroups][__name__][name]" required="required" maxlength="100" /></div></div>">
<li><div> <label for="CVC_benefititems_0_benefitgroups_0_name" class="required">Name</label> <input type="text" id="CVC_benefititems_0_benefitgroups_0_name" name="CVC[benefititems][0][benefitgroups][0][name]" required="required" maxlength="100" value="c1b1" /></div></li>
<li><div> <label for="CVC_benefititems_0_benefitgroups_1_name" class="required">Name</label> <input type="text" id="CVC_benefititems_0_benefitgroups_1_name" name="CVC[benefititems][0][benefitgroups][3][name]" required="required" maxlength="100" value="c2b1" /></div></li>
</ul>
<br><b>Benefit Item</b>
<div> <label for="CVC_benefititems_1_comment" class="required">Comment</label> <input type="text" id="CVC_benefititems_1_comment" name="CVC[benefititems][4][comment]" required="required" maxlength="400" value="b2" /></div>
<br><b>Benefit Groups</b>
<ul class="benefitgroups" data-prototype="<div id="CVC_benefititems_1_benefitgroups___name__"><div> <label for="CVC_benefititems_1_benefitgroups___name___name" class="required">Name</label> <input type="text" id="CVC_benefititems_1_benefitgroups___name___name" name="CVC[benefititems][5][benefitgroups][__name__][name]" required="required" maxlength="100" /></div></div>">
<li><div> <label for="CVC_benefititems_1_benefitgroups_0_name" class="required">Name</label> <input type="text" id="CVC_benefititems_1_benefitgroups_0_name" name="CVC[benefititems][6][benefitgroups][0][name]" required="required" maxlength="100" value="c2b2" /></div></li>
</ul>
<div><button type="submit" id="CVC_submit" name="CVC[submit]">Do Something</button></div><input type="hidden" id="CVC__token" name="CVC[_token]" value="MEUAU3VawkCDJ5jTHo5hSTGrgrWS6XUm-UXeEI9onT8" /></form>
Instead of a list I wish to do all this with tables (so adding and removing rows from the table).
The final goal (adding an additional layer) will be the following:
Well, as you say, your problem is on the dynamic side, aka, Client Side.
I gonna post my shot on what you're trying to do here.
But before, a pro-tip: NEVER EVER print html on an attribute, use another technique, like the one I'm using for example, templates (to replace your prototype attr). There are a ton more techniques to do this, I'll just explain mine.
var $outerTemplate;
var $innerTemplate;
var $outerContainer;
jQuery(document).ready(function($) {
$outerTemplate = $('#top-form-template');
$innerTemplate = $('#inner-form-template');
$outerContainer = $("#row-container");
$("#addRow").on('click', function(e){
addOuterForm();
});
$outerContainer.on('click', '.addItem', function (e) {
addInnerForm(e.target.dataset.rowId);
})
$outerContainer.on('click', '.destroy-row', function (e) {
destroyRow(e.target.dataset.rowId);
});
$outerContainer.on('click', '.destroy-item', function (e) {
destroyItem(e.target.dataset.itemId);
});
});
function addOuterForm () {
var compiled = _.template($outerTemplate.html());
var html = compiled({
outerId: _.uniqueId(),
innerId: _.uniqueId()
});
$outerContainer.append(html);
}
function addInnerForm (outerId) {
var compiled = _.template($innerTemplate.html());
var html = compiled({
outerId: outerId,
innerId: _.uniqueId()
});
$outerContainer.find('#row-'+outerId).find('.benefitgroups').append(html);
}
function destroyRow(id){
$("#row-"+id).remove();
}
function destroyItem(id){
$("#item-"+id).remove();
}
What I did is create 2 templates, one for the outer form (the one with benefit item & group) and other with an inner form (the extra benefit items). Then attach & removing them using some buttons. I encorage you to take a look at templating engines on client side (I notice you know how to use Twig templating engine maybe Handlebars will be easy to catch for you).
I'm using lodash templating engine, because its quite simple and lodash tools are very powerful and useful.
To embed multiple collection of forms usually i use self-widget to control templating. For example:
{{form.name}}
<ul id="benefit-items" data-prototype="{{_self.widget_prototype(form.benefitItems.vars.prototype}|e}}">
{% for benefitItem in form.benefitItems %}
{{_self.widget_prototype(benefitItem)}}
{% endfor %}
<li id="add-benefit-item" onclick="addBenefitItem(this);">add benefit</li>
</ul>
{% macro widget_prototype(form) %}
<li class="benefitItem">
{{form.title}}
<ul class="benefit-group" data-prototype="{{form_widget(form.benefitGroups.vars.prototype)|e}}">
</ul>
</li>
{% endmacro %}
It's how i'm used to create multiple collection forms.