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.


Please enter a valid email address
Allowed date format: yyyy-mm-dd
Choose one or more carriers
Must choose at least one type of notification
The password is "secret"
MyWebsocketCtrl's scope:
subscribe_data = {{ subscribe_data | json }}

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.

Fork me on GitHub