Symfony 2.0 updating select options with JS? - ajax

I've been googling for hours but surprisingly I didn't find any topic on that subject.
I have the following Form
class propertyType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('city')
->add('district', 'entity', array('class'=>'FlatShanghaidefaultBundle:district',
'property'=>'name',
'query_builder' => function ($repository) {
$qb = $repository->createQueryBuilder('district');
$qb->add('where', 'city = :city');
$qb->setParameter('city', 1);
return $qb;
}
public function getName()
{
return 'property';
}
}
When the user choose a City in the form, I want the options of district to be dynamically updated and limited to that city. With Ajax or JS?
What would be the best practice? Do you know a tutorial on that topic?
If someone can put me on the right tracks, that would help a lot..
Thanks!

The query builder will not solve your problem, you can remove it altogether.
That query is run when the form gets built, once you have it on your browser you need to use javascript to populate the options.
You can have the options stored in a javascript variable, or pull them from the server as needed with ajax (you will need a controller to handle these ajax requests).
You will probably want to use some jquery plugin to handle the cascading logic between the select elements, there are a couple available:
I use this one, but it seems to be offline: http://devlicio.us/blogs/mike_nichols/archive/2008/05/25/jquery-cascade-cascading-values-from-forms.aspx
And there is this one, which I never used really: http://code.google.com/p/jquery-cascade/
There is also at least this Bundle I know of: https://github.com/genemu/GenemuFormBundle, which has ajax field types available for several jquery plugins. This may save you writing the ajax part to handle the data, as it comes built in (it's probably easier to implement the controller your self anyway). I haven't tried this one, and I don't know if it has cascading support.

Jbm is right about the query builder. And his approach is perfecly valid.
Another option could be to dispense the cascade select in favor of an autocomplete field.
Assuming that you save the countries, cities and districts as entities and have a relation between them, you do not even need to save what city/country has been selected because you can just call:
$district->getCity()->getCountry();
I have implemented a similar thing for country/city selection and will link here to the the main involved files.
First, create a custom form type to encapsulate all form stuff, it contains a hidden field to store the selected id and a text field to serve as input for the autocomplete logic:
https://github.com/roomthirteen/Room13GeoBundle/blob/master/Form/LocationFieldType.php
Then theme the form type:
https://github.com/roomthirteen/Room13GeoBundle/blob/master/Resources/views/Form/fields.html.twig
The url of the autocomplete source is passed as data attribute so no JS will be smutching the html code.
Last but not least, the JS functions have to be implemented:
https://github.com/roomthirteen/Room13GeoBundle/blob/master/Resources/public/jquery.ui.location-autocomplete.js
The result can be seen in the image below, see that for clarity the country name will be displayed in braces behind the city name:
--
I favor this solution much more that using cascade selects because the actual value can be selected in one step.
cheers

I'm doing this myself on a form.
I change a field (a product) and the units in which the quantity can be measured are updated.
I am using a macro with parameters to adapt it more easily.
The macro :
{% macro javascript_filter_unit(event, selector) %}
<script>
$(function(){
$('#usersection')
.on('{{ event }}', '{{ selector }}', function(e){
e.preventDefault();
if (!$(this).val()) return;
$.ajax({
$parent: $(this).closest('.child_collection'),
url: $(this).attr('data-url'),
type: "get",
dataType: "json",
data: {'id' : $(this).val(), 'repo': $(this).attr('data-repo'), parameter: $(this).attr('data-parameter')},
success: function (result) {
if (result['success'])
{
var units = result['units'];
this.$parent.find('.unit').eq(0).html(units);
}
}
});
})
});
</script>
{% endmacro %}
The ajax returns an array : array('success' => $value, 'units' => $html). You use the $html code and put it in place of the select you want to change.
Of course the javascript code of the ajax call need to be modfied to match your fields.
You call the macro like you would normally do:
{% import ':Model/Macros:_macros.html.twig' as macros %}
{{ macros.javascript_filter_unit('change', '.unitTrigger') }}
So I have two arguments : the event, often a change of a select. and a selector, the one whose change triggers the ajax call.
I hope that helps.

Related

How to populate a WTForms SelectField with an ajax call (i.e. Select2)?

I'm trying to implement a web app with Flask + WTForms + Select2 (ajax call), but unfortunately without great success. The main problem regards the attribute "choices" in one of my WTForms classes, because I can't understand how to populate the select menu with the ajax call and let WTForms class to manage these data.
This is my workaround:
class insertData(FlaskForm):
...
feature_tag = SelectMultipleField(
u'Features',
choices=[(0, "")],
coerce=int,
render_kw={"multiple": "multiple"},
id="feature_tag_sel2")
...
this is called in a template with:
<div class="col-sm-4">
{{form.feature_tag}}
</div>
managed by Select2:
$("#feature_tag_sel2").select2({
minimumInputLength: 2,
ajax: {
url: "/select/api/feature",
dataType: 'json',
data: function (params) {
return { q: params.term };
},
processResults: function (data) {
return {results: data};
},
cache: false
}
});
and, finally, ajax call refers to this code:
#app.route('/select/api/feature')
def suggestion_feature():
param = request.args.get('q', '')
q = db.session.query(Feature).filter(Feature.nome_f.startswith(param)).all()
value = [dict(id=i.id, text=i.nome_f) for i in q]
return json.dumps(value)
When this code run, whatever the value of the select, validate_on_submit rises an error: I can select the many values from Select2 menu but of course any value different for 0 is not a valid choice. Then I tried to stop the propagation of this error by implementing a new field definition:
class Select2MultipleField(SelectMultipleField):
def pre_validate(self, form):
# Prevent "not a valid choice" error
pass
This code works, but in any case it is a workaround. For instance, when validate_on_submit finds another error in the form and the page is reloaded, all the selected values are lost. The same happens when I need to edit data. In sum, this workaround make me lose the many advantages of WTForms.
The question thus is: is there any method to integrate a WTForms SelectField with data retrieved with an ajax call?
EDITED 10/08/2021
For anyone still interested in the matter, I found the following solution: Flask-Select2. It integrates very well into the code with very little modification. The only flaw is that it works well out of the box only with Select2 ver. 3 (for an update to the current version of Select2 there are several points to rewrite)
From my understanding of your issue, you have got two issue with your ajax select2 with WTForm.
Not a valid choice when submitting a form
and selected value lost and form reload or error
Let me try to solve them as the followings:
Not a valid choice when submitting a form
For the first issue of not a valid choice, you can tell wtf form not to validate it on submitting form, yeah your workaround solution! it works for me too.
class Select2MultipleField(SelectMultipleField):
def pre_validate(self, form):
# Prevent "not a valid choice" error
pass
selected value lost and form reload or error
Quoted from your question
when validate_on_submit finds another error in the form and the page
is reloaded, all the selected values are lost. The same happens when I
need to edit data.
Yeah, this does works as expected.First the select field does not contain any option since all of data from your select field was getting from remote ajax call, meaning the select field was not provided any data choice when you created with WTForm call as your code here
class insertData(FlaskForm):
...
feature_tag = SelectMultipleField(
u'Features',
choices=[(0, "")],
coerce=int,
render_kw={"multiple": "multiple"},
id="feature_tag_sel2")
...
Indeed, you don't have to provide argument choices since your data was called from remote ajax call and you already solve issue Not a valid choice already.
To answer to your question and solve this second issue, you can check if data value was attached with your select field like below, and create it as an option for your select field as this:
<div class="col-sm-4">
{{form.feature_tag}}
</div>
<script>
$(document).ready(function(){
$("#feature_tag_sel2").select2({
minimumInputLength: 2,
ajax: {
url: "/select/api/feature",
dataType: 'json',
data: function (params) {
return { q: params.term };
},
processResults: function (data) {
return {results: data};
},
cache: false
}
});
{# check if select field come with data previously selected #}
{% if form.feature_tag.data %}
{# trigger an ajax call to url with parameter of this value #}
$.ajax("/select/api/feature?q={{form.feature_tag.data}}", {
dataType: "json"
}).done(
function(data) {
text = data.items[0].text // text returned from your url
id = data.items[0].id. // id returned from your url
// created an option for the select field
var newOption = new Option(text, id, false, false)
$('#{{form.feature_tag.id}}').append(newOption).trigger('change')
$('#{{form.feature_tag.id}}').val(id).trigger('change')
}
);
{% endif %}
});
</script>
This way, your select field should already contain a value of your previously selected before either when you viewing it or editing it :)
Hope this help! Thanks
This is a few years old but I had a similar requirement but I wanted a pure javascript (no jquery) alternative to select2. I found this in the form of tom-select https://tom-select.js.org
I created a custom widget based on code from flask-admin.
from wtforms import widgets
from flask.globals import _request_ctx_stack
from flask_admin.babel import gettext, ngettext
from flask_admin import helpers as h
__all__ = ['TomSelectWidget','RenderTemplateWidget', ]
class TomSelectWidget(widgets.Select):
"""
`TomSelect <https://tom-select.js.org/docs/>`_ styled select widget.
You must include tom-select.complete.js,and tom-select stylesheet for it to
work.
"""
def __call__(self, field, **kwargs):
kwargs.setdefault('data-role', u'tom-select')
allow_blank = getattr(field, 'allow_blank', False)
if allow_blank and not self.multiple:
kwargs['data-allow-blank'] = u'1'
return super(TomSelectWidget, self).__call__(field, **kwargs)
class RenderTemplateWidget(object):
"""
WTForms widget that renders Jinja2 template
"""
def __init__(self, template):
"""
Constructor
:param template:
Template path
"""
self.template = template
def __call__(self, field, **kwargs):
ctx = _request_ctx_stack.top
jinja_env = ctx.app.jinja_env
kwargs.update({
'field': field,
'_gettext': gettext,
'_ngettext': ngettext,
'h': h,
})
template = jinja_env.get_template(self.template)
return template.render(kwargs)
Then for my feild I used QuerySelect, here's my example:
followed_by = QuerySelectField(label="Followers",
query_factory=lambda: User.query.order_by(User.full_name.asc()),
widget=TomSelectWidget(),
render_kw={'class': 'form-control'},)
In your jinja template include css and javascript for tom-select as per the documentation (link above). I just grabbed these from jsDelivr.
Then to initalise in your javascript like so:
new TomSelect('#followed_by',{
plugins: ['remove_button'],
maxItems: 10,
maxOptions: 100,
create: false
});
or you if you have multiple selects on the same page initialise by class:
document.querySelectorAll('.select').forEach((el)=>{
let settings = {maxItems: null,};
new TomSelect(el,settings);
});

Update Symfony's form collection prototype through ajax

I've built a form with Symfony, based on a Doctrine entity. The form contains form collections. Let's say the outer form is used to create a task. Many sub-tasks can be added to this task (the form collections part).
The task-form contains a choice field "category". De sub-task forms contain choice field "subcategory". The values of the subcategory field depend on the value chosen for the category field. To achieve this, I want to update the form prototype through AJAX when a main category has been chosen.
The JS-part of this is probably not that hard, so I'm sure I'd manage that. But I've got no idea what to do on the server/Symfony side to achieve this. Any hints in the right direction would be appreciated.
You need to create a route that link to a new function(action) in the controller lets name it "ajaxGetSubCategoriesAction()"
implement it to get all the sub categories, then return them like this :
//get the category id from ajax request
$categoryID = $request->request->get('category_id');
//get all subcategories and return your result like this
return new Response(json_encode($result));
then in the twig that renders the category put a data field inside the form tag or div tag like
this data-path you get it inside the JS file to know the path of the function you created to get the subcategories.
$posturl = $formelement.data('path');
var categoryid = //get it here with from your input field.
$.ajax({
type: "POST",
url: $posturl,
async: false,
dataType: "json",
data: {
category_id: categoryid
}
}).done(function (response) {
inside response you will have all the subcategories returned from your ajaxGetSubCategories function
}

How to use AJAX in Joomla component to load the State field based on the country selected?

Inside a component's view, I have something like this:
<?php echo TestcompHelperFind::loadStates(...); ?>
<?php echo TestcompHelperFind::loadCounties(...); ?>
The above static functions load <select> dropdowns with the state names and countries respectively.
The class TestcompHelperFind is located in the file /administrator/components/com_testcomp/helpers/find.php.
How do I load States dropdown list based on the country selected using AJAX? I'm not sure what url I should provide in the ajax function.
On the client, you will need a function that watches the country select for changes, and when it happens calls the appropriate url with a callback that will populate the counties select.
On the server, you need to output the select content.
Since you have the html output already working, let's use this approach. As an alternative you could have your server method return a json object and use the javascript to parse it and populate the select. But let's stick to html communication, i.e. the server returns the html contents of the select.
1. On the server
1.a. Output the counties select
We only need to return the result of the TestcompHelperFind::loadCounties(...); to the ajax call. This is achieved easily writing a new method in the component's controller, i.e. the controller.php in the root of the component folder or one of the sub-controllers if appropriate. It's up to you to place it in a meaningful spot.
Inside the controller simply add a new public task such as
class SomethingController extends JController
{
public function getCountiesHTML() {
$input = JFactory::getApplication()->input;
$country = $input->getCMD('filter_country');
// load helper if necessary, then:
echo TestcompHelperFind::loadCounties($country);
exit; // this will stop Joomla processing, and not output template modules etc.
}
Please note the exit; at the end, this will make Joomla output only the component's output (our echo) and not the whole template/modules etc.
1.b Add an ID to the country and county selects so that it will be possible to manipulate them on the client; I'll assume filter_country and filter_county ;
2. On the client
you will want to invoke the url
index.php?option=com_something&action=getCountiesHTML&filter_country=UK
when the country select is changed. It will also need to cancel any pending requests to avoid overlapping messages. To keep things simple, let's assume you use a library to handle Ajax, I'll write an example for jQuery:
<script>
var xhr;
jQuery(function($) {
$('#filter_country').change(function(){
var filterCountry = $('#filter_country').val();
if (xhr && xhr.abort) {xhr.abort();xhr=false;}
xhr = jQuery.ajax(
url: 'index.php',
data: 'option=com_something&task=getCountiesHTML&filter_country='+filterCountry,
success: function(data){
jQuery('#filter_county').replaceWith(data);
}
);
});
});
</script>
For cancelling the previous request, please see a dedicated answer such as this one.

What else can I pass to the `form` property of Ext.Ajax.request?

Normally one posts an ExtJS form to the backend using form.submit({...}). I want to make my form submission synchronous now, so I'm switching to using Ext.Ajax.request({async: false, ...}). The form property of Ext.Ajax.request() usually looks like so:
Ext.Ajax.request({
url: 'formsubmit',
form: 'formid',
method:'POST',
success: function(response, opts) {
alert("successfull");
},
failure:function(res,opt) {
alert("request failed");
}
});
I'm dealing with a bunch of anonymous forms right now. Is there any way around this?
Given a var form = {xtype: 'form', items: [...]}
I've tried replacing 'formid' with form.getEl(), form.getForm(), and form.getForm().getFieldValues() which all don't work.
There's no other way around this other than assigning a generated id to each of my anonymous forms, is there.
Thanks for any input
It looks like you could just do this as an alternative to the form attribute:
var form = this.down('form');
Ext.Ajax.request({
url: 'test.xyz',
params: form.getValues()
// etc...
});
getValues gives you the name/value pairs you need for your submission.
It looks like the ExtJS forms do not actually use form elements in the markup. When the submit function is called on an ExtJS form, an HTML form element is crafted as part of the process, and that's the form that is used for the submission.
http://docs.sencha.com/extjs/4.2.1/#!/api/Ext.form.action.Submit-method-buildForm
Ideally, you could modify the options that are used in the Ajax request called within the doSubmit function. Since you can't, you might want to consider overriding Ext.form.action.Submit such that you can, then calling the form.submit() function you mentioned in your question.
http://docs.sencha.com/extjs/4.2.1/#!/api/Ext.form.action.Submit-method-doSubmit

How do I render a view after POSTing data via AJAX?

I've built an app that works, and uses forms to submit data. Once submitted, the view then redirects back to display the change. Cool. Django 101. Now, instead of using forms, I'm using Ajax to submit the data via a POST call. This successfully saves the data to the database.
Now, the difficult (or maybe not, just hard to find) part is whether or not it's possible to tell Django to add the new item that has been submitted (via Ajax) to the current page, without a page refresh. At the moment, my app saves the data, and the item shows up on the page after a refresh, but this obviously isn't the required result.
If possible, I'd like to use exactly the same view and templates I'm using at the moment - essentially I'd like to know if there's a way to replace a normal HTTP request (which causes page refresh) with an Ajax call, and get the same result (using jQuery). I've hacked away at this for most of today, so any help would be appreciated, before I pull all of my hair out.
I had a very similar issue and this is how I got it working...
in views.py
from django.utils import simplejson
...
ctx = {some data to be returned to the page}
if ajax == True:
return HttpResponse(simplejson.dumps(ctx), mimetype='json')
then in the javascript
jQuery.ajax({
target: '#id_to_be_updated',
type: "POST",
url: "/",
dataType: 'json',
contentType: "text/javascript; charset=\"utf-8\"",
data: {
'foo':foo,
'bar':bar,
},
success: function(data){
$("#id_to_be_updated").append(data.foo);
}
});
Here's how I did it:
The page that has the form includes the form like so
contact.html
{% include "contact_form.html" %}
This way it's reusable.
Next I setup my view code (this view code assumes the contact form needs to be save to the db, hence the CreateView):
class ContactView(CreateView):
http_method_names = ['post']
template_name = "contact_form.html"
form_class = ContactForm
success_url = "contact_form_succes.html"
There are a few things to note here,
This view only accepts pots methods, because the form will be received through the contact.html page. For this view I've setup another template which is what we included in contact.html, the bare form.
contact_form.html
<form method="POST" action="/contact">{% crsf_token %}
{{ form.as_p }}
</form>
Now add the javascript to the contact.html page:
$("body").on("submit", 'form', function(event) {
event.preventDefault();
$("#contact").load($(this).attr("action"),
$(this).serializeArray(),
function(responseText, responseStatus) {
// response callback
});
});
This POSTS the form to the ContactView and replaces whatever is in between #contact, which is our form. You could not use jquery's .load function to achieve some what more fancy replacement of the html.
This code is based on an existing working project, but slightly modified to make explaining what happens easier.

Resources