Django Forms with three-way data-binding
Point a second browser onto the same URL and observe form synchronization
This example shows, how to propagate the form's model-scope to a foreign browser using Websockets.
How does it work?
With django-angular and the additional Django app django-websocket-redis, one can extend two-way data-binding to propagate all changes on a model, back and forward with a corresponding object stored on the server. This means, that the server “sees” whenever the model changes on the client and can by itself, modify values on the client side at any time, without having the client to poll for new messages.
This can be useful, when the server wants to inform the clients about asynchronous events such as sport results, chat messages or multi-player game events.
In this example no submit buttons are available, because the server receives the Form's data on
every change event. Apart from initializing the angular module, the only JavaScript code required
for this example, is the statement djangoWebsocket.connect
, which bi-directionally
binds the object subscribe_data
from our Controller's $scope
object, to an
equally named data bucket inside the remote Redis datastore.
from django.forms import widgets
from django.core.exceptions import ValidationError
from djng.forms import fields
from djng.styling.bootstrap3.forms import Bootstrap3Form
def validate_password(value):
# Just for demo. Do not validate passwords like this!
if value != 'secret':
raise ValidationError('The password is wrong.')
class SubscribeForm(Bootstrap3Form):
use_required_attribute = False
CONTINENT_CHOICES = [('am', 'America'), ('eu', 'Europe'), ('as', 'Asia'), ('af', 'Africa'),
('au', 'Australia'), ('oc', 'Oceania'), ('an', 'Antartica')]
TRAVELLING_BY = [('foot', 'Foot'), ('bike', 'Bike'), ('mc', 'Motorcycle'), ('car', 'Car'),
('public', 'Public Transportation'), ('train', 'Train'), ('air', 'Airplane')]
NOTIFY_BY = [('email', 'EMail'), ('phone', 'Phone'), ('sms', 'SMS'), ('postal', 'Postcard')]
first_name = fields.CharField(
label='First name',
min_length=3,
max_length=20)
last_name = fields.RegexField(
r'^[A-Z][a-z -]?',
label='Last name',
error_messages={'invalid': 'Last names shall start in upper case'})
sex = fields.ChoiceField(
choices=(('m', 'Male'), ('f', 'Female')),
widget=widgets.RadioSelect,
error_messages={'invalid_choice': 'Please select your sex'})
email = fields.EmailField(
label='E-Mail',
required=True,
help_text='Please enter a valid email address')
subscribe = fields.BooleanField(
label='Subscribe Newsletter',
initial=False, required=False)
phone = fields.RegexField(
r'^\+?[0-9 .-]{4,25}$',
label='Phone number',
error_messages={'invalid': 'Phone number have 4-25 digits and may start with +'})
birth_date = fields.DateField(
label='Date of birth',
widget=widgets.DateInput(attrs={'validate-date': '^(\d{4})-(\d{1,2})-(\d{1,2})$'}),
help_text='Allowed date format: yyyy-mm-dd')
continent = fields.ChoiceField(
label='Living on continent',
choices=CONTINENT_CHOICES,
error_messages={'invalid_choice': 'Please select your continent'})
weight = fields.IntegerField(
label='Weight in kg',
min_value=42,
max_value=95,
error_messages={'min_value': 'You are too lightweight'})
height = fields.FloatField(
label='Height in meters',
min_value=1.48,
max_value=1.95,
step=0.05,
error_messages={'max_value': 'You are too tall'})
traveling = fields.MultipleChoiceField(
label='Traveling by',
choices=TRAVELLING_BY,
help_text='Choose one or more carriers',
required=True)
notifyme = fields.MultipleChoiceField(
label='Notify by',
choices=NOTIFY_BY,
widget=widgets.CheckboxSelectMultiple,
help_text='Must choose at least one type of notification')
annotation = fields.CharField(
label='Annotation',
required=True,
widget=widgets.Textarea(attrs={'cols': '80', 'rows': '3'}))
agree = fields.BooleanField(
label='Agree with our terms and conditions',
initial=False,
required=True)
password = fields.CharField(
label='Password',
widget=widgets.PasswordInput,
validators=[validate_password],
help_text='The password is "secret"')
confirmation_key = fields.CharField(
max_length=40,
required=True,
widget=widgets.HiddenInput(),
initial='hidden value')
from django.views.generic.edit import FormView
class SubscribeView(FormView):
template_name = 'three-way-data-binding.html'
form_class = SubscribeForm
<script type="text/javascript">
angular.module('djangular-demo', ['djng.websocket']).config(function(djangoWebsocketProvider) {
djangoWebsocketProvider.setURI('{{ WEBSOCKET_URI }}');
djangoWebsocketProvider.setHeartbeat({{ WS4REDIS_HEARTBEAT }});
});
</script>
<form ng-controller="MyWebsocketCtrl">
{{ form.as_div }}
</form>
angular.module('djangular-demo')
.controller('MyWebsocketCtrl', function($scope, djangoWebsocket) {
djangoWebsocket.connect($scope, 'subscribe_data', 'subscribe_data', ['subscribe-broadcast', 'publish-broadcast']);
});
Note that AngularJS directives are configured inside HTML, since only Django templates can expand server variables.