Skip to content

Latest commit

 

History

History
590 lines (467 loc) · 16.2 KB

README.md

File metadata and controls

590 lines (467 loc) · 16.2 KB

django-knockout

django-knockout makes it super easy to use knockout.js with your Django models. It's great for project with objects that have lots of different models, or models with lots of different fields, or both. It can be used in both prototyping complex applications and directly in the templates of simple ones. Supports forms and formsets via Knockout pre-rendered. Supports Django Rest Framework and jQuery by default, but these can be disabled.

Forked from django-knockout-modeler.

Table of Contents

Requirements

It's most likely that django-knockout works with older versions than the following, if it does, please let me know.

Optional

Preview

django-knockout (with Django Rest Framework) turns this:

# models.py
class MyObject(models.Model):
    my_number = models.IntegerField()
    my_name = models.CharField()

# views.py
myobject_class = MyObject

# serializers.py
class MyObjectSerializer(serializers.ModelSerializer):
    class Meta:
        model = models.MyObject
        fields = '__all__'

# api.py
class MyObjectViewSet(viewsets.ModelViewSet):
    serializer_class = serializers.MyObjectSerializer
    metadata_class = metadata.KnockoutMetadata
    queryset = models.MyObject.objects.all()
    
# urls.py
router = routers.DefaultRouter()
router.register(r'myobjects', api.MyObjectViewSet, 'myobject')
urlpatterns += urls.url(r'^api/', urls.include(router.urls)),

Into this:

var MyObjectViewModel = function (data) {
    var self = this;
    self.my_number = ko.observable(data.my_number);
    self.my_name = ko.observable(data.my_name);
}

var MyObjectListViewModel = function(data) {
    var self = this;
    self.myobjects = ko.observableArray(data);
}

var myobject_data = $.getJSON("/app/api/myobjects/").then(function(data) {
    var ko_data = ko.mapping.fromJS(data)();
    return new MyObjectListViewModel(ko_data);
});

function ko_bind_myobjectviewmodel() {
    var element = document.body;
    person_data.done(function(view_model) {
        ko.applyBindings(view_model, element);
    });    
}

person_options.done(ko_bind_myobjectviewmodel);

With this!

{# template #}
{% load knockout_tags %}
{% knockout myobject_class %}

Quick Start

  1. Install django-knockout

    via git

    pip install git+https://github.com/AntycSolutions/django-knockout

    or

    git clone github.com/AntycSolutions/django-knockout
    pip install ./django-knockout
  2. Add 'knockout' to your INSTALLED_APPS setting:

    # settings.py
    INSTALLED_APPS = (
      ...
      'knockout',
      # django rest framework, can be disabled
      'restframework',
    )
  3. Include knockout.js in your HTML:

    {# template #}
    <script type='text/javascript' src='//cdnjs.cloudflare.com/ajax/libs/knockout/3.3.0/knockout-min.js'></script>
    <script type='text/javascript' src='//cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.js'></script>
    {# Optionally needed if you're using forms/formsets #}
    <script type='text/javascript' src='//cdnjs.cloudflare.com/ajax/libs/knockout-pre-rendered/0.5.0/knockout-pre-rendered.min.js'></script>
    {# jQuery, can be disabled #}
    <script type='text/javascript' src='//code.jquery.com/jquery-3.1.1.js'></script>
  4. Knockout your QuerySet:

    {# template #}
    {% load knockout_tags %}
    <script type="text/javascript">
        {% knockout my_objects %}
    </script>
  5. Loop over your bound data like so:

    {# template #}
    <div data-bind="foreach: myobjects">
        My Name: <span data-bind="value: my_name"></span>
        My Number: <span data-bind="value: my_number"></span>
    </div>

Simple Usage

django-knockout can be used directly in templates to generate knockout view models.

First, import it!

{# template #}
{% load knockout_tags %}

To get just the list view model, if you prefer to load your data from API's, like this:

{# template #}
{% knockout_list_view_model myobject_class %}

or if you don't need a list view model just a regular view model:

{# template #}
{% knockout_view_model myobject_class %}

And even just the bindings:

{# template #}
{% knockout_bindings myobject_class %}

If you'd like to output the list utils (see below for more information):

{# template #}
{% knockout_list_utils myobject_class %}

Programmatic Usage

First, import it!

from knockout import ko

To get the whole template (like the knockout tag), you can do this:

ko_string = ko.ko(MyObject)

Just the bindings

ko_bindings_string = ko.ko_bindings(MyObject)

Just the List View Model

ko_list_view_model_string = ko.ko_list_view_model(MyObject)

Just the View Model

ko_view_model_string = ko.ko_view_model(MyObject)

list utils (see below for more information):

ko_list_utils_string = ko.ko_list_utils(MyObject)

ModelForm Usage

Want to use Django forms to generate your html and knockout binds as well? Just use KnockoutModelForm:

# forms.py
from knockout import forms

class MyObjectKnockoutModelForm(forms.KnockoutModelForm):
    ...
    class Meta:
        model = models.MyObject
        fields = '__all__'

Since Django forms output values we use Knockout pre-rendered to prevent double binding. This means we don't need django-knockout to get data via ajax.

{# template #}
{% load knockout_tags %}
{% knockout myobject_class disable_ajax_data=True is_list=False %}

<form method="POST">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Submit</button>
</form>

which outputs:

<p>
    <label for="id_my_number">My Number:</label>
    <input data-bind="init, value: my_number" id="id_my_number" maxlength="64" name="my_number" type="text" value="..." />
</p>
<p>
    <label for="id_my_name">My Name:</label>
    <input data-bind="init, value: my_name" id="id_my_name" maxlength="64" name="my_name" type="text" value="..." />
</p>

Custom fieldsets are also allowed at form level (see Access Control for model level):

from knockout import forms

class MyObjectKnockoutModelForm(forms.KnockoutModelForm):
    ...
    @staticmethod
    def knockout_fields():
        return ['id', 'my_name', 'my_number', ...]

Access Control

If you don't want to expose your entire model to Knockout, you can define a function in your model:

class MyObject(models.Model):
    ...
    @staticmethod
    def knockout_fields():
        return ['id', 'my_name', 'my_number', ...]

Sorting

django knockout provides some convenience methods (via knockout_list_utils/ko_list_utils) for manipulating your arrays:

    self.addMyObjectViewModel = function(data) {
        self.persons.push(new MyObjectViewModel(data));
    };

    self.createMyObjectViewModel = function(data) {
        return new MyObjectViewModel(data);
    };

    self.removeMyObjectViewModel = function(data) {
        self.persons.remove(data);
    };

    self.destroyMyObjectViewModel = function(data) {
        self.persons.destroy(data);
    };

    self.deleteMyObjectViewModel = function(data) {
        var index = self.persons.indexOf(data);
        self.persons()[index].DELETE(true);
    }
  • See here for what the destroy function is for
  • The data parameter for add/create is optional
  • The delete function is for formsets

for sorting your data (see below for changing the comparator):

self.sortMyObjectViewModelsAsc = function() {
    self.myobjects.sort(function(a, b) {
        var a_comparator = a.id();
        var b_comparator = b.id();
        if (!a_comparator) { a_comparator = undefined; }
        if (!b_comparator) { b_comparator = undefined; }
        var result = a_comparator>b_comparator?-1:a_comparator<b_comparator?1:0;

        return result;
    });
};

self.sortMyObjectViewModelsDesc = function() {
    self.myobjects.sort(function(a, b) {
        var a_comparator = a.id();
        var b_comparator = b.id();
        if (!a_comparator) { a_comparator = undefined; }
        if (!b_comparator) { b_comparator = undefined; }
        var result = a_comparator<b_comparator?-1:a_comparator>b_comparator?1:0;

        return result;
    });
};

Include a variation of this in your template:

{# template #}
<button data-bind='click: sortMyObjectViewModelsAsc'>Sort Asc</button>
<button data-bind='click: sortMyObjectViewModelsDesc'>Sort Desc</button>

By default, it will use the object's 'id' field, but you can also define your own comparator like so:

class MyObject(models.Model):
    ...
    @staticmethod
    def knockout_comparator(self):
        return 'my_name'  # or whichever field

If you don't define a comparator, 'id' must be available.

Multi-Model Support

django-knockout is all ready set up to be used with multiple types of data at the same time, as bindings can happen to specific objects via this generated function:

function ko_bind() {
    var element_id = "myobjectviewmodel";
    var element = document.getElementById(element_id);
    
    ko.applyBindings(new MyObjectListViewModel(), element);
}
ko_bind();

which means that you somewhere in your HTML template, you will need to have an object with that id, like so:

{# template #}
<div id="myobjectviewmodel">
    <div data-bind="foreach: myobjects">
        User <span data-bind="value: my_name"></span> is number <span data-bind="value: my_number"></span>.
    </div>
</div>

and add the paramter to the knockout tag:

{# template #}
{% load knockout_tags %}
{% knockout myobject_class element_id="myobjectviewmodel" %}

Custom Data Support

Is django-knockout using the wrong url? Pass it into knockout/ko or knockout_bindings/ko_bindings:

{# template #}
{% load knockout_tags %}
{% url 'app:myobject-list' as myobject_list_url %}
{% knockout myobject_class url=myobject_list_url %}
from knockout import ko
ko.ko(MyObject, url='/app/api/myobjects')

Settings

The following settings are currently supported:

# settings.py
DJANGO_KNOCKOUT = {
    'disable_jquery': False,  # default
    'disable_ajax_data': False,  # default
    'disable_ajax_options': False,  # default
}
  • Don't like jQuery (or don't want to include another library)? Set disable_jquery to True and django-knockout will fallback to vanilla javascript XMLHttpRequest
  • Don't like how django-knockout fetches data? Set disable_ajax_data to True and provide your own data (after knockout tags)
var vm = ko.dataFor(document.body); // or
vm = ko.dataFor(document.getElementById(id)); // if you specified an element_id
vm.myobjects(data);
  • Don't like how django-knockout fetches view model's fields? Set disable_ajax_options to True and setup your fields manually (before knockout tags)
myobject_fields = {
    'my_number': null, // null required to become observable
    'my_name': null,
    'my_list': [], // [] required to become observableArray
    ...
}

Advanced Usage

Defaults not working out for you? Here's the parameters to template tags and ko functions:

Knockout Functions

'''
    required:
        model_class: The class of model you want to knockout
        
    optional:
        element_id: The id of the element you want to bind your view model to
        context: We need this to lookup urls requiring app_name, not required if your urls don't require app_name, or you've specified an url
        url: The url to get ajax data/options from
        disable_ajax_data: If True, do not get ajax data (overrides disable_ajax_data setting), defaults to `disable_ajax_data` setting
        disable_ajax_options: If True, do not get ajax options (overrides disable_ajax_options setting), defaults to `disable_ajax_options` setting
        is_list: Controls whether or not to output a List View Model (True, default) or just a View Model (False)
'''
def ko(
    model_class,
    element_id=None,
    context=None,
    url=None,
    disable_ajax_data=None,
    disable_ajax_options=None,
    is_list=True,
)
'''
    required:
        model_class: The class of model you want to knockout
        
    optional:
        element_id: The id of the element you want to bind your view model to
        context: We need this to lookup urls requiring app_name, not required if your urls don't require app_name, or you've specified an url
        url: The url to get ajax data/options from
        disable_ajax_data: If True, do not get ajax data (overrides disable_ajax_data setting), defaults to `disable_ajax_data` setting
        disable_ajax_options: If True, do not get ajax options (overrides disable_ajax_options setting), defaults to `disable_ajax_options` setting
        is_list: Controls whether or not to output a List View Model (True, default) or just a View Model (False)
'''
def ko_bindings(
    model_class,
    element_id=None,
    context=None,
    url=None,
    disable_ajax_data=None,
    disable_ajax_options=None,
    is_list=True,
)
'''
    required:
        model_class: The class of model you want to knockout
        
    optional:
        context: We need this to lookup urls requiring app_name, not required if your urls don't require app_name, or you've specified an url
        url: The url to get ajax data/options from
        disable_ajax_options: If True, do not get ajax options (overrides disable_ajax_options setting), defaults to `disable_ajax_options` setting
        include_list_utils: default True, if False, django-knockout will skip rendering js util functions
'''
def ko_list_view_model(
    model_class,
    context=None,
    url=None,
    disable_ajax_options=None,
    include_list_utils=True,
)
'''
    required:
        model_class: The class of model you want to knockout
        
    optional:
        context: We need this to lookup urls requiring app_name, not required if your urls don't require app_name, or you've specified an url
        url: The url to get ajax data/options from
        disable_ajax_options: If True, do not get ajax options (overrides disable_ajax_options setting), defaults to `disable_ajax_options` setting
'''
def ko_view_model(
    model_class,
    context=None,
    url=None,
    disable_ajax_options=None,
)
'''
    required:
        model_class: The class of model you want to knockout
'''
def ko_list_utils(model_class)

Knockout Tags

Knockout template tags knockout, knockout_list_view_model, knockout_bindings, knockout_view_model, knockout_list_utils match up with their Knockout function equivalent, except their first required arg model_class can be a list of models, a QuerySet, a model's class, or an instance of a model. They also implicitly take in the context when called.

{# template #}
<!--
    required:
        field: a Django Model Form field
        data_bind: variable to assign output to
-->
{% data_bind field as data_bind %}