Refresh (variable) part of an html page with hotwire (Flask app) - javascript

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)
})

Related

How can I modify this JS function so that it runs whenever the field is equal a certain length?

I have a function that currently runs whenever the user clicks/tabs out of the employee_number field. I would like it to run whenever the length of the numbers entered is equal to 6, without having to leave the field, since when I try using the tab, it conflicts with loading the next field which is a drop-down that is part of the function ran.
I tried by running it using .change and putting the constraint within the function, but it did not work and I don't know what else to try.
enter_exit.html
{% extends "base.html" %}
{% load core_tags staticfiles %}
{% block main %}
<form id="warehouseForm" action="" method="POST" data-employee-activity-lookup-url="{% url 'operations:employee_activity_search' %}" novalidate >
{% csrf_token %}
<div>
<div>
<div id="employee-name" style="margin-bottom: 10px"> </div>
<label>Employee #</label>
{{ form.employee_number }}
</div>
<div=>
<label>Work Area</label>
{{ form.work_area }}
</div>
<div style="display: none" id="my-hidden-div">
<label>Station</label>
{{ form.station_number }}
</div>
</div>
<div>
<div>
<button>Enter Area</button>
<button>Exit Area</button>
</div>
</div>
</form>
<script>
// Grab the employee name and their current active work log (if any)
$(document).on('blur', "#{{ form.employee_number.id_for_label }}", function(){
var url = $("#warehouseForm").attr('data-employee-activity-lookup-url');
var employeeId = $(this).val();
# ... more fields ...
if (employeeId !== "") {
# .. Rest of function ...
})
</script>
{% endblock main %}
Instead of using on blur have you tried using .keyup or .keydown?
Here's a simple version of it working in CodePen:
$("#test1").keydown(()=>{
if($("#test1").val().length>6)
console.log('6+');
})
https://codepen.io/orunnals/pen/JjoRGLX

Django: How can I create a dynamic form that changes on user click?

I'm making a workout calendar website where a user can add workouts with varying amounts of lift, sets and reps, etc. Thus, I need a form that adds a field when a user clicks a button. I've made a template and some javascript to describe what it is I want to achieve exactly:
url:
url(r'^add/(?P<year>[0-9]+)/(?P<month>[0-9]+)/(?P<day>[0-9]+)/$', views.add_workout, name = 'add_workout')
template:
{% block hidden %}
{% include "workoutcal/liftrow.html" %} {# To be used by Javascript #}
{% include "workoutcal/cardiorow.html" %}
{% endblock %}
<form action="{% url 'add_workout' date.year date.month date.day %}" method="post">
<div class="row">
<div class="col-xs-2">
<p id="date">{{ date.year }}-{{ date.month }}-{{ date.day }}</p>
<input type="hidden" name="date" value="{{ date }}">
</div>
</div>
<h2 class="col-xs-12">Lifts</h2>
<div id="liftrows">
{% for i in range %}
{% include "workoutcal/liftrow.html" %}
{% endblock %}
</div>
<div class="row">
<div class="col-xs-0"></div>
<label class="col-xs-2"><button type="button" id="addliftbutton">One more lift</button></label>
</div>
<h2 class="col-xs-12">Cardio</h2>
<div id="cardiorows">
{% include "workoutcal/cardiorow.html" %}
</div>
<div class="row">
<label class="col-xs-2"><button type="button" id="addcardiobutton">One more cardio</button></label>
</div>
<div class="row">
<div class="col-xs-10"></div>
<label class="col-xs-2"><input type="submit" id="submitbutton" value="Save Workout"></label>
</div>
</form>
javascript:
//Adding onclick to buttons
document.getElementById('addliftbutton').onclick = addLiftRow;
document.getElementById('addcardiobutton').onclick = addCardioRow;
for (var i=0; i<setsBoxes.length; i++){
setsBox = setsBoxes[i];
setsBox.onchange = insertRepFields;
}
function addLiftRow(){
var liftRowElements = document.getElementById('liftrows');
var hidden_liftrow = document.getElementById('hidden').getElementsByClassName('lift')[0];
var new_liftrow = hidden_liftrow.cloneNode(true);
liftRowElements.appendChild(new_liftrow);
}
function addCardioRow(){
var cardiorows = document.getElementById('cardiorows');
var hidden_cardiorow = document.getElementById('hidden').getElementsByClassName('cardio')[0];
var new_cardiorow = hidden_cardiorow.cloneNode(true);
cardiorows.appendChild(new_cardiorow);
}
function insertRepFields(){} // big function that inserts as many input fields as the number inside the box whose event called the function.
2 questions:
1. Is there a better way to do this in Django?
2. If this is the best way, how do I go about sending the data of my massive form back to django? Since I don't know exactly how many fields there will be, I don't know how to create a form that accepts a variable amount of fields, and fields within fields.
Here's how a filled-in form could look:
The best way to accomplish that is inserting inputs with the same name and then in Django get all those inputs as a list like:
def view(request):
inputs = request.POST.getlist('your_input_name')
for i in inputs:
Model.objects.create() # Save your model

Colorbox + Form, Submit only working on Enter button (not onclick)

I integrated a popup upon landing on our page http://www.showstye.lu (best seen in an incognito window due to cookie reset). In this popup you have a newsletter signup form which is a copy of the one available in the footer.
I had to duplicate css to adapt to the newsletter popup so that it styled correctly which is fine, I am using Colorbox for the popup itself.
Now, the form itself when I click on the submit button (S'Abonner) it doesn't trigger the submit correctly. I always get a false return telling me to resubmit a new e-mail address.
However, the newsletter form itself works completely fine in the footer and ALSO when I use the keyboard stroke "Enter" instead of clicking on the submit button.
I don't comprehend why one form works in the footer but the identical copy does not (only with Enter keystroke), I looked at the styling/JS/html of the form and it is identical across both.
Any ideas how to resolve this so that the submit button works successfully?
<div style='display:none'>
<div id='subscribe_popup'>
<div id="spop_left">
<div id="spop_top"></div>
<div id="spop_sign">
<div class="popbox">
{% if theme.setting_newsletter %}
<form id="formNewsletter" action="{{ 'account/newsletter' | url }}" method="post">
<input type="hidden" name="key" value="{{ page.key }}" />
<input type="email" name="email" id="formNewsletterEmail" value="" placeholder="{{ 'E-mail' | t }}"/>
<a class="btn glyphicon glyphicon-send" href="#" onclick="$('#formNewsletter').submit(); return false;" title="{{ 'Subscribe' | t }}" {% if shop.language == 'de' %}style="padding: 0px 10px;"{% endif %}><span>{{ 'Subscribe' | t }}</span></a>
</form>
</div> {% endif %}
</div>
</div>
<div id="spop_right"></div>
</div>
</div>
<!-- END subscribe popup-->
Here is the JS behind it:
<script> // popup script
$("document").ready(function (){
// load the overlay
if (document.cookie.indexOf('visited=true') == -1) {
var fifteenDays = 1000*60*60*24*15;
var expires = new Date((new Date()).valueOf() + fifteenDays);
document.cookie = "visited=true;expires=" + expires.toUTCString();
$.colorbox({width:"553px", inline:true, href:"#subscribe_popup"});
}
$(".open_popup").colorbox({width:"553px", inline:true, href:"#subscribe_popup"});
});
</script>
Ok that was it, I was integrating a submit into the hyperlink instead of using a simple inputtype submit, keystroke was obviously working because of how the browser automatically identifies the keystroke enter as a submit!

Embedding multiple levels collection of forms in Symfony2

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.

How to write JQuery function to display text in special location of Django template

I'm trying to create my first ajax function in Django.
I want to change my code using JQuery, the idea is pretty simple:
User type a subject name and this name is displayed in subject-list below the form,
The problem is I don't really know what to type in JQuery function.
JQuery:
function create_subject() {
$("input").focus(function(){
var subject = $(this).val();
$(".btn-create").click(function(){
/* What I need to write in here */
});
});
}
In HTML "subjects" refer to database.
HTML
<div id="subjects-list">
{% if user.username %}
<ul>
{% if subjects %}
<form method="post" action=".">{% csrf_token %}
{% for subject in subjects %}
-------- TYPED TEXT SHOULD BE HERE --------> <li>{{ subject.name }}</li>
{% endfor %}
</form>
{% else %}
<p>No Subjects for this user</p>
{% endif %}
</ul>
{% else %}
You are in else
{% endif %}
</div>
That's how HTML looks in "View Page Source"
<div id="create-subject">
<form method="post" action="."> <div style='display:none'><input type='hidden' name='csrfmiddlewaretoken' value='cfbd1893742c3ab9936bacaae9653051' /></div>
<p><label for="id_name">Subject Name:</label> <input id="id_name" type="text" name="name" size="9" /></p>
<input type="button" name="subject-create-b" value="Create Subject" class="btn-create"/>
</form>
</div>
<div id="subjects-list">
<ul>
<form method="post" action="."><div style='display:none'><input type='hidden' name='csrfmiddlewaretoken' value='cfbd1893742c3ab9936bacaae9653051' /></div>
<li>Math 140<span id="subject-link"></span></li>
</form>
</ul>
</div>
</div>
And that's my form
forms.py
class SubjectCreationForm(forms.Form):
name = forms.CharField(label="Subject Name", widget=forms.TextInput(attrs={'size':9}))
class Meta:
exclude = ('created_by', 'created_time', 'num_of_followers', 'vote')
def clean_name(self):
name = self.cleaned_data['name']
if len(name)>1:
return name
else:
raise forms.ValidationError("Subject name should be longer")
In order to do what (I think) you want to do which is some basic AJAX using Django as your backend, you'll need the following:
A view which returns the data you want to load
There are a number of ways you can represent the data, but to keep it simple, I'll use HTML.
Javascript to load that view (using JQuery if you like)
Your code might look like this for the first part:
urls.py:
...
(r'^get-subjects/$', 'yourapp.views.get_subjects'),
...
views.py:
...
def get_subjects(request):
subjects = # code to fetch your subjects.
return render_to_response('subjects_template.html', {'subjects': subjects})
...
subjects_template.html:
{% for subject in subjects %}
<li>{{ subject.name }}</li>
{% endfor %}
For the second part, it might look like this:
main_template.html:
...
<ul id="subjects-list"></ul>
<script>
function loadSubjects() {
$.ajax({
url: "/get-subjects",
success: function (data) {
$("#subjects-list").html(data);
}
});
}
</script>
...
[1] render_to_response()
[2] jQuery.ajax()
This will get you most the way there. When you want to reload the list, you call the loadSubjects() function.
As far as creating the subjects go, that is a different thing. What you'll want to look into is how to do an HTML form submission without leaving the page. There are plenty of tools and libraries to do that stuff with a nice api. If you want to stick with JQuery, you might consider this plugin for a nicer api.
function create_subject() {
$("input").focus(function(){
var subject = $(this).val();
$(".btn-create").click(function(){
$('#subjects-list').append(subject);
});
});
}
that said, you probably don't want to assign the click handler every time the input is focused. i'd move that out of the focus handler.

Categories