Client-side viewmodels and AJAX response routing¶
Client-side viewmodels¶
django_jinja_knockout
implements AJAX response routing with client-side viewmodels.
Viewmodels are defined as an array of simple objects in Javascript:
var viewmodels = [
{
'view': 'prepend',
'selector': '#infobar',
'html': '<div class="alert alert-info">Welcome to our site!</div>'
},
{
'view': 'confirm',
'title': 'Please enter <i>your</i> personal data.',
'message': 'After the registration our manager will contact <b>you</b> to validate your personal data.',
'callback': [{
'view': 'redirect_to',
'url': '/homepage/'
}],
'cb_cancel': [{
'view': 'redirect_to',
'url': '/logout/'
}]
}
];
and as the special list (vm_list) of ordinary dicts in Python:
from django_jinja_knockout.viewmodels import vm_list
viewmodels = vm_list(
{
'view': 'prepend',
'selector': '#infobar',
'html': '<div class="alert alert-info">Welcome to our site!</div>'
},
{
'view': 'confirm',
'title': 'Please enter <i>your</i> personal data.',
'message': 'After the registration our manager will contact <b>you</b> to validate your personal data.',
'callback': vm_list({
'view': 'redirect_to',
'url': '/homepage/'
}),
'cb_cancel': vm_list({
'view': 'redirect_to',
'url': '/logout/'
})
}
)
When executed, each viewmodel object (dict) from the viewmodels
variable defined above, will be used as the function
argument of their particular handler:
'view': 'prepend'
: executesjQuery.prepend(viewmodel.html)
function for specified selector#infobar
;'view': 'confirm'
: showsBootstrapDialog
confirmation window with specifiedtitle
andmessage
;'callback'
: when user hitsOk
button ofBootstrapDialog
, nestedcallback
list of client-side viewmodels will be executed, which defines just one command:redirect_to
with the specified url/homepage/
;'cb_cancel
: when user cancels confirmation dialog, redirect to/logout/
url will be performed.
Now, how to execute these viewmodels we defined actually? At Javascript side it’s a simple call:
import { vmRouter } from '../../djk/js/ioc.js';
vmRouter.respond(viewmodels);
While single viewmodel may be executed via the following call:
import { vmRouter } from '../../djk/js/ioc.js';
vmRouter.show({
'view': 'form_error',
'id': $formFiles[i].id,
'messages': [message]
});
However, it does not provide much advantage over performing jQuery.prepend()
and instantiating BootstrapDialog()
manually. Then why is all of that?
First reason: one rarely should execute viewmodels from client-side directly. It’s not the key point of their introduction. They are most useful as foundation of interaction between server-side Django and client-side Javascript via AJAX requests where the AJAX response is the list of viewmodels generated at server-side, and in few other special cases, such as sessions and document.onload viewmodels injecting.
Second reason: It is possible to setup multiple viewmodel handlers and then to remove these. One handler also could call another handler. Think of event subscription: these are very similar, however not only plain functions are supported, but also functions bound to particular instance (methods) and classpath strings to instantiate new Javascript classes:
import { vmRouter } from '../../djk/js/ioc.js';
// viewmodel bind context with method
var handler = {
fn: MyClass.prototype.myMethod,
context: myClassInstance
};
// Subscribe to bound method:
vmRouter.addHandler('my_view', handler)
// Subscribe to bound method:
.add('my_view', MyClass.prototype.myMethod2, myClassInstance)
// Subscribe to unbound function:
.add('my_view', myFunc)
// Subscribe to instantiate a new class via classpath specified:
.addHandler('my_view', 'MyClass');
// ...
// Will execute all four handlers attached above with passed viewmodel argument:
vmRouter.exec('my_view', {'a': 1, 'b': 2});
// ...
// Unsubscribe handlers. The order is arbitrary.
vmRouter.removeHandler('my_view', {fn: MyClass.prototype.myMethod2, context: myClassInstance})
.removeHandler('my_view', myFunc)
.removeHandler('my_view', handler)
.removeHandler('my_view', 'MyClass');
Javascript bind context¶
The bind context is used when the viewmodel response is processed. It is used by add()
/ addHandler()
viewmodel
router methods and as well as AJAX actions callback.
The following types of context arguments of are available:
- unbound function: subscribe viewmodel to that function;
- plain object with optional
fn
andcontext
arguments: to subscribe to bound method; - string: Javascript class name to instantiate;
See ViewModelRouter.applyHandler() for the implementation details.
Viewmodel data format¶
Key 'view'
of each Javascript object / Python dict in the list specifies the value of viewmodel name
, that is
bound to particular Javascript viewmodel handler
. The viewmodel itself is used as the Javascript object argument of
each particular viewmodel handler
with the corresponding keys and their values. The following built-in viewmodel
names currently are available in ioc.js:
[
'redirect_to',
'post',
'alert',
'alert_error',
'confirm',
'trigger',
'append',
'prepend',
'after',
'before',
'remove',
'text',
'html',
'replaceWith',
'replace_data_url'
]
If your AJAX code just needs to perform one of these standard actions, such as display alert / confirm window, trigger an event, redirect to some url or to perform series of jQuery DOM manipulation, then you may just use the list of viewmodels that map to these already pre-defined handlers.
Automatic AJAX POST is available with post
viewmodel and even an AJAX callback is not required for POST because each
post
viewmodel AJAX response will be interpreted (routed) as the list of viewmodels - making chaining / nesting of
HTTP POSTs easily possible.
There are class-based AJAX actions available, which allow to bind multiple methods of the Javascript class instance to single viewmodel handler: to perform multiple actions bound to the one viewmodel name.
Defining custom viewmodel handlers¶
One may add custom viewmodel handlers via Javascript plugins to define new actions. See tooltips.js for the additional bundled viewmodel names and their viewmodel handlers:
'tooltip_error', 'popover_error', 'form_error'
which are primarily used to display errors for AJAX submitted forms via viewmodels AJAX response.
The following methods allows to attach one or multiple handlers to one viewmodel name:
import { vmRouter } from '../../djk/js/ioc.js';
vmRouter.add('my_view', function(viewModel, vmRouter) {
// execute viewmodel here...
})
.add('my_view2', {fn: MyClass.prototype.method, context: MyClassInstance})
.add('my_view3', 'MyClass');
// or
vmRouter.add({
'my_view': function(viewModel, vmRouter) {
// execute viewmodel here...
},
'my_view2': {fn: MyClass.prototype.method, context: MyClassInstance},
'my_view3': 'MyClass'
});
The following syntax allows to reset previous handlers with the names specified (if any):
import { vmRouter } from '../../djk/js/ioc.js';
vmRouter.removeAll('my_view', 'my_view2', 'my_view3')
.add({
'my_view': function(viewModel, vmRouter) {
// execute viewmodel here...
},
'my_view2': {fn: MyClass.prototype.method, context: MyClassInstance},
'my_view3': 'MyClass'
});
When function
handler is called, it’s viewModel
argument receives the actual instance of viewmodel
.
Second optional argument vmRouter
points to the instance of vmRouter that was used to process current
viewmodel
. This instance of vmRouter could be used to call another viewmodel handler inside the current
handler, or to add / remove handlers via calling vmRouter instance methods:
import { vmRouter } from '../../djk/js/ioc.js';
vmRouter.add('my_view1', function(viewModel, currentVmRouter) {
// dynamically add 'my_view2' viewmodel handler when 'my_view1' handler is executed:
currentVmRouter.add('my_view2', function(viewModelNested, vmRouter) {
// will receive argument viewModelNested == {'a': 1, 'b': 2}}
// execute viewModelNested here...
});
// ... skipped ...
// nested execution of 'my_view2' viewmodel from 'my_view1' handler:
currentVmRouter.exec('my_view2', {'a': 1, 'b': 2});
});
New properties might be added to viewmodel for further access, like .instance
property which holds an instance of
FieldPopover
in the following code:
import { vmRouter } from '../../djk/js/ioc.js';
import { FieldPopover } from '../../djk/js/tooltips.js';
vmRouter.add('tooltip_error', function(viewModel) {
// Adding .instance property at the client-side to server-side generated viewModel:
viewModel.instance = new FieldPopover(viewModel);
});
Every already executed viewmodel is stored in .executedViewModels
property of vmRouter instance, which may be
processed later. An example of such processing is destroyFormErrors static method, which clears form input
Bootstrap tooltips previously set by 'tooltip_error'
viewmodel handler then removes these viewmodels from
.executedViewModels
list via ViewModelRouter.filterExecuted() method:
AjaxForm.destroyFormErrors = function() {
var form = this.$form.get(0);
vmRouter.filterExecuted(
function(viewModel) {
if (viewModel.view === 'form_error' && typeof viewModel.instance !== 'undefined') {
viewModel.instance.destroy(form);
return false;
}
return true;
}
);
};
It is possible to chain viewmodel handlers, implementing a code-reuse and a pseudo-inheritance of viewmodels:
import { vmRouter } from '../../djk/js/ioc.js';
import { FieldPopover } from '../../djk/js/tooltips.js';
vmRouter.add('popover_error', function(viewModel, vmRouter) {
viewModel.instance = new FieldPopover(viewModel);
// Override viewModel.name without altering it:
vmRouter.exec('tooltip_error', viewModel);
// or, to preserve the bound context (if any):
vmRouter.exec('tooltip_error', viewModel, this);
});
where newly defined handler popover_error
executes already existing tooltip_error
viewmodel handler to re-use
it’s code.
The purpose of passing this
bind context as an optional third argument of vmRouter.exec()
call is to preserve
currently passed Javascript bind context.
AJAX response routing¶
When one develops mixed web application with traditional server-side generated html responses but also having lots of AJAX interaction, with traditional approach, the developer would have to write a lot of boilerplate code, like this, html:
<button id="my_button" class="button btn btn-default">Save your form template</button>
Javascript:
import { AppConf } from '../../djk/js/conf.js';
$('#my_button').on('click', function(ev) {
$.post(
'/url_to_ajax_handler',
{csrfmiddlewaretoken: AppConf('csrfToken')},
function(response) {
BootstrapDialog.confirm('After the registration our manager will contact <b>you</b> ' +
'to validate your personal data.',
function(result) {
if (result) {
window.location.href = '/another_url';
}
}
);
},
'json'
)
});
Such code have many disadvantages:
- Too much of callback nesting.
- Repeated boilerplate code with
$.post()
numerous arguments, including manual specification$.post()
arguments. - Route url names are hardcoded into client-side Javascript, instead of being supplied from Django server-side. If one
changes an url of route in
urls.py
, and forgets to update url path in Javascript code, AJAX POST will fail. - What if the AJAX response should have finer control over client-side response? For example, sometimes you need
to open
BootstrapDialog
, sometimes to redirect instead, sometimes to perform a custom client-side action for the same HTTP POST url?
Enter client-side viewmodels response routing: to execute AJAX post via button click, the following Jinja2 template code will be enough:
<button class="button btn btn-default" data-route="button-click">
Save your form template
</button>
ajaxform.js AjaxButton class will care itself of setting Javascript event handler, performing AJAX request POST,
then AJAX response routing will execute viewmodels returned from Django view. Define the view path in project
urls.py
:
from my_app.views import button_click
# ...
url(r'^button-click/$', button_click, name='button-click', kwargs={'is_anonymous': True}),
Client-side routes¶
Let’s implement the view. Return the list of viewmodels which will be returned via button click in my_app/views.py:
from django_jinja_knockout.decorators import ajax_required
from django_jinja_knockout.viewmodels import vm_list
@ajax_required
def button_click(request):
return vm_list({
'view': 'confirm',
'title': 'Please enter <i>your</i> personal data.',
'message': 'After the registration our manager will contact <b>you</b> to validate your personal data.',
'callback': vm_list({
'view': 'redirect_to',
'url': '/homepage'
})
})
Register AJAX client-side route (url name) in settings.py
, to make url available in Javascript application:
DJK_CLIENT_ROUTES = {
# True means that the 'button-click' url will be available to anonymous users:
('button-click', True),
}
Register button-click
url mapped to my_app.views.button_click in your urls.py
:
from my_app.views import button_click
# ...
url(r'^button-click/$', button_click, name='button-click', 'allow_anonymous': True, 'is_ajax': True}),
That’s all.
Django view that processes button-click
url (route) returns standard client-side viewmodels only, so it does not
even require to modify a single bit of built-in Javascript code. To execute custom viewmodels, one would have to register
their handlers in Javascript (see Defining custom viewmodel handlers).
It is possible to specify client-side routes per view, not having to define them globally in template context processor:
from django_jinja_knockout.views import create_page_context
def my_view(request):
create_page_context(request).add_client_routes({
'club_detail',
'member_grid',
})
or via decorator:
from django.shortcuts import render
from django_jinja_knockout.views import page_context_decorator
@page_context_decorator(client_routes={
'club_detail',
'member_grid',
})
def my_view(request):
# .. skipped ..
return render(request, 'sample_template.htm', {'sample': 1})
and per class-based view:
class MyGridView(KoGridView):
client_routes = {
'my_grid_url_name'
}
It is possible to specify view handler function bind context via .add()
method optional argument:
import { vmRouter } from '../../djk/js/ioc.js';
vmRouter.add({
'set_context_title': {
fn: function(viewModel) {
// this == bindContext1
this.setTitle(viewModel.title);
},
context: bindContext1
},
'set_context_name': {
fn: function(viewModel) {
// this == bindContext2
this.setName(viewModel.name);
},
context: bindContext2
}
});
It is also possible to override the value of context for viewmodel handler dynamically with AppPost() optional
bindContext
argument:
import { AppPost } from '../../djk/js/url.js';
AppPost('button-click', postData, bindContext);
That allows to use method prototypes bound to different instances of the same Javascript class:
import { inherit } from '../../djk/js/dash.js';
import { vmRouter } from '../../djk/js/ioc.js';
import { AppPost } from '../../djk/js/url.js';
import { Dialog } from '../../djk/js/dialog.js';
AjaxDialog = function(options) {
inherit(Dialog.prototype, this);
this.create(options);
};
(function(AjaxDialog) {
AjaxDialog.receivedMessages = [];
AjaxDialog.sentMessages = [];
AjaxDialog.vm_addReceivedMessage = function(viewModel, vmRouter) {
this.receivedMessages.push(viewModel.text);
};
AjaxDialog.vm_addSentMessage = function(viewModel, vmRouter) {
this.sentMessages.push(viewModel.text);
};
AjaxDialog.receiveMessages = function() {
/**
* When AJAX response will contain one of 'add_received_message' / 'add_sent_message' viewmodels,
* currently bound instance of AjaxDialog passed via AppPost() this argument
* methods .vm_addReceivedMessage() / .vm_addSentMessage() will be called:
*/
AppPost('my_url_name', this.postData, this);
};
// Subscribe to 'add_received_message' / 'add_sent_message' custom viewmodel handlers:
vmRouter.add({
'add_received_message': AjaxDialog.vm_addReceivedMessage,
'add_sent_message': AjaxDialog.vm_addSentMessage,
});
})(AjaxDialog.prototype);
var ajaxDialog = new AjaxDialog(options);
ajaxDialog.receiveMessages();
Django MyView
mapped to 'my_url_name'
(see Context processor) should return vm_list ()
instance with one of it’s elements having the structure like this:
from django.views import View
from django_jinja_knockout.viewmodels import vm_list
# skipped ...
class MyView(View):
def post(self, request, *args, **kwargs):
return vm_list([
{
# Would call .vm_addReceivedMessage() of Javascript ajaxDialog instance with 'text' argument:
'view': 'add_received_message',
'text': 'Thanks, I am fine!'
},
{
# Would call .vm_addSentMessage() of Javascript ajaxDialog instance with 'text' argument:
'view': 'add_sent_message',
'text': 'How are you?'
}
])
to have ajaxDialog
instance .vm_addReceivedMessage()
/ .vm_addSentMessage()
methods to be actually called.
Note that with viewmodels the server-side Django view may dynamically decide which client-side viewmodels will be
executed, the order of their execution and their arguments like the value of ‘text’ dict key in this example.
In case AJAX POST button route contains kwargs / query parameters, one may use data-url
html5 attribute instead
of data-route
:
<button class="btn btn-sm btn-success" data-url="{{
tpl.reverseq('post_like', kwargs={'feed_id': feed.id}, query={'type': 'upvote'})
}}">
Non-AJAX server-side invocation of client-side viewmodels¶
Besides direct client-side invocation of viewmodels via vmrouter.js vmRouter.respond()
method, and AJAX POST /
AJAX GET invocation via AJAX response routing, there are two additional ways to execute client-side viewmodels with
server-side invocation:
Client-side viewmodels can be injected into generated HTML page and then executed when page DOM is loaded. It’s
useful to prepare page / form templates which may require automated Javascript code applying, or to display
BootstrapDialog alerts / confirmations when the page is just loaded. For example to display confirmation dialog when the
page is loaded, you can override class-based view get()
method like this:
from django_jinja_knockout.views ViewmodelView
class MyView(ViewmodelView):
def get(self, request, *args, **kwargs):
load_vm_list = self.page_context.onload_vm_list('client_data')
load_vm_list.append({
'view': 'confirm',
'title': 'Please enter <i>your</i> personal data.',
'message': 'After the registration our manager will contact <b>you</b> to validate your personal data.',
'callback': [{
'view': 'redirect_to',
'url': '/homepage'
}]
})
return super().get(self, request, *args, **kwargs)
Read more about PageContext (page_context).
The second way of server-side viewmodels invocation is similar to just explained one. It stores client-side viewmodels in the current user session, making them persistent across requests. This allows to set initial page viewmodels after HTTP POST or after redirect to another page (for example after login redirect), to display required viewmodels in the next request:
def set_session_viewmodels(request):
last_message = Message.objects.last()
# Custom viewmodel. Define it's handler at client-side with .add() method::
# vmRouter.add('session_view', function(viewModel) { ... });
# // or:
# vmRouter.add({'session_view': {fn: myMethod, context: myClass}});
view_model = {
'view': 'session_view'
}
if last_message is not None:
view_model['message'] = {
'title': last_message.title,
'text': last_message.text
}
page_context = create_page_context(request)
session_vm_list = page_context.onload_vm_list(request.session)
# Find whether 'session_view' viewmodel is already stored in HTTP session vm_list:
idx, old_view_model = session_vm_list.find_by_kw(view='session_view')
if idx is not False:
# Remove already existing 'session_view' viewmodel, otherwise they will accumulate.
# Normally it should not happen, but it's better to be careful.
session_vm_list.pop(idx)
if len(view_model) > 1:
session_vm_list.append(view_model)
To inject client-side viewmodel when page DOM loads just once (function view):
onload_vm_list = create_page_context(request).onload_vm_list('client_data')
onload_vm_list.append({'view': 'my_view'})
In CBV view, inherited from ViewmodelView:
onload_vm_list = self.page_context.onload_vm_list('client_data')
onload_vm_list.append({'view': 'my_view'})
To inject client-side viewmodel when page DOM loads persistently in user session (function view):
session_vm_list = create_page_context(request).onload_vm_list(request.session)
session_vm_list.append({'view': 'my_view'})
In CBV view, inherited from ViewmodelView:
session_vm_list = self.page_context.onload_vm_list(request.session)
session_vm_list.append({'view': 'my_view'})
See PageContext.onload_vm_list() and vm_list.find_by_kw() for the implementation details.
Require viewmodels handlers¶
Sometimes there are many separate Javascript source files which define different viewmodel handlers. To assure that
required external source viewmodel handlers are immediately available, use vmRouter instance .req()
method:
import { vmRouter } from '../../djk/js/ioc.js';
vmRouter.req('field_error', 'carousel_images');
Nested / conditional execution of client-side viewmodels¶
Nesting viewmodels via callbacks is available for automated conditional / event subscribe viewmodels execution. Example
of such approach is the implementation of 'confirm'
viewmodel in ioc.js Dialog
callback via
vmRouter.respond()
method conditionally processing returned viewmodels:
import { vmRouter } from '../../djk/js/ioc.js';
var self = this;
var cbViewModel = this.dialogOptions.callback;
this.dialogOptions.callback = function(result) {
// @note: Do not use alert view as callback, it will cause stack overflow.
if (result) {
vmRouter.respond(cbViewModel);
} else if (typeof self.dialogOptions.cb_cancel === 'object') {
vmRouter.respond(self.dialogOptions.cb_cancel);
}
};
Asynchronous execution of client-side viewmodels¶
There is one drawback of using vm_list: it is execution is synchronous and does not support promises by default. In some complex cases, for example when one needs to wait for some DOM loaded first, then to execute viewmodels, one may “save” viewmodels received from AJAX response, then “restore” (execute) these later in another DOM event / promise handler.
vmRouter method .saveResponse()
saves received viewmodels:
import { vmRouter } from '../../djk/js/ioc.js';
vmRouter.add('popup_modal_error', function(viewModel, currentVmRouter) {
// Save received response to execute it in the 'shown.bs.modal' event handler (see just below).
currentVmRouter.saveResponse('popupModal', viewModel);
// Open modal popup to show actual errors (received as viewModel from server-side).
$popupModal.modal('show');
});
vmRouter method loadResponse()
executes viewmodels previously saved with .saveResponse()
call:
import { vmRouter } from '../../djk/js/ioc.js';
// Open modal popup.
$popupModal.on('shown.bs.modal', function (ev) {
// Execute viewmodels previously received in 'popup_modal_error' viewmodel handler.
vmRouter.loadResponse('popupModal');
});
Multiple save points might be set by calling vmRouter .saveResponse()
with the particular name
argument
value, then calling vmRouter .loadResponse()
with the matching name
argument value.
AJAX actions¶
Large classes of AJAX viewmodel handlers inherit from ActionsView at server-side and from Actions at client-side, which utilize the same viewmodel handler for multiple actions. It allows to structurize AJAX code and to build the client-server AJAX interaction more easily.
ModelFormActionsView and KoGridView inherit from ActionsView, while client-side ModelFormActions and GridActions inherit from Actions. See Datatables for more info.
Viewmodel router defines own (our) viewmodel name as Python ActionsView class viewmodel_name attribute /
Javascript Actions class .viewModelName
property. By default it has the value action
but the derived
classes may change it’s name; for example grid datatables use grid_page
as the viewmodel name.
Viewmodels which have non-matching names are not processed by Actions directly. Instead, they are routed to
standard viewmodel handlers, added via vmRouter methods - see Defining custom viewmodel handlers section.
Such way standard built-in viewmodel handlers are not ignored. For example server-side exception reporting is done with
alert_error
viewmodel handler (see ioc.js), while AJAX form validation errors are processed via form_error
viewmodel handler (see tooltips.js).
The difference between handling AJAX viewmodels with vmRouter (see Defining custom viewmodel handlers) and AJAX actions is that the later shares the same viewmodel handler by routing multiple actions to methods of Actions class or it’s descendant class.
Custom actions at the server-side¶
Server-side part of AJAX action with name edit_form
is defined as ModelFormActionsView method
action_edit_form
:
def action_edit_form(self):
obj = self.get_object_for_action()
form_class = self.get_edit_form()
form = form_class(instance=obj, **self.get_form_kwargs(form_class))
return self.vm_form(
form, verbose_name=self.render_object_desc(obj), action_query={'pk_val': obj.pk}
)
This server-side action part generates AJAX html form, but it can be arbitrary AJAX data passed back to client-side via one or multiple viewmodels.
To implement custom server-side actions, one has to:
- Inherit class-based view class from ActionsView or it’s descendants like ModelFormActionsView or KoGridView (see also Datatables)
- Define the action by overriding the view class
.get_actions()
method - Implement
action_my_action
method of the view class, which usually would return action viewmodel(s).
Here is the example of defining two custom actions, save_equipment
and add_equipment
at the server-side:
class ClubEquipmentGrid(KoGridView):
def get_actions(self):
actions = super().get_actions()
actions['built_in']['save_equipment'] = {}
actions['iconui']['add_equipment'] = {
'localName': _('Add club equipment'),
'css': 'iconui-wrench',
}
return actions
# Creates AJAX ClubEquipmentForm bound to particular Club instance.
def action_add_equipment(self):
club = self.get_object_for_action()
if club is None:
return vm_list({
'view': 'alert_error',
'title': 'Error',
'message': 'Unknown instance of Club'
})
equipment_form = ClubEquipmentForm(initial={'club': club.pk})
# Generate equipment_form viewmodel
vms = self.vm_form(
equipment_form, form_action='save_equipment'
)
return vms
# Validates and saves the Equipment model instance via bound ClubEquipmentForm.
def action_save_equipment(self):
form = ClubEquipmentForm(self.request.POST)
if not form.is_valid():
form_vms = vm_list()
self.add_form_viewmodels(form, form_vms)
return form_vms
equipment = form.save()
club = equipment.club
club.last_update = timezone.now()
club.save()
# Instantiate related EquipmentGrid to use it's .postprocess_qs() method
# to update it's row via grid viewmodel 'prepend_rows' key value.
equipment_grid = EquipmentGrid()
equipment_grid.request = self.request
equipment_grid.init_class()
return vm_list({
'update_rows': self.postprocess_qs([club]),
# return grid rows for client-side EquipmentGrid component .updatePage(),
'equipment_grid_view': {
'prepend_rows': equipment_grid.postprocess_qs([equipment])
}
})
Note that form_action
argument of the .vm_form()
method overrides default action name for the generated form.
See the complete example: https://github.com/Dmitri-Sintsov/djk-sample/blob/master/club_app/views_ajax.py
Separate action handlers for each HTTP method¶
Since v1.1.0 it’s possible to define separate action handlers for each HTTP method:
from django_jinja_knockout import tpl
from django_jinja_knockout.views import ActionsView
from django_jinja_knockout.viewmodels import vm_list
class MemberActions(ActionsView):
template_name = 'member_template.htm'
def get_actions(self):
return {
# action type
'built_in': {
# action definition
# empty value means the action has no options and is enabled by default
'reply': {}
}
}
# will be invoked for HTTP GET action 'reply':
def get_action_reply(self):
return tpl.Renderer(self.request, 'action_reply_template.htm', {
'component_atts': {
'class': 'component',
'data-component-class': 'MemberReplyActions',
'data-component-options': {
'route': self.request.resolver_match.view_name,
'routeKwargs': copy(self.kwargs),
'meta': {
'actions': self.vm_get_actions(),
},
},
}
})()
# will be invoked for HTTP POST action 'reply',
# usually via Javascript MemberReplyActions.ajax('reply'):
def post_action_reply(self):
return vm_list({
'members': Member.objects.filter(club=club, role=role)
})
def get(self, request, *args, **kwargs):
reply = self.conditional_action('reply')
if reply:
return reply
else:
return super().get(request, *args, **kwargs)
However, by default automatic invocation of action handler is performed only for HTTP POST. To perform HTTP GET action,
one has to invoke it manually by calling conditional_action method in get
method view code (see above), or in
member_template.htm
Jinja2 template (in such case custom get
method is not required):
{% set reply = view.conditional_action('reply') -%}
{% if reply %}
{{ reply }}
{% endif -%}
See Component IoC how to register custom Javascript data-component-class
, like
MemberReplyActions
mentioned in this example.
The execution path of the action¶
The execution of action usually is initiated in the browser via the Components DOM event / Knockout.js
binding handler, or is programmatically invoked in Javascript via the Actions inherited class .perform()
method:
import { inherit } from '../../djk/js/dash.js';
import { Actions } from '../../djk/js/actions.js';
// import { GridActions } from '../../djk/js/grid/actions.js';
ClubActions = function(options) {
// Comment out, when overriding Grid actions.
// inherit(GridActions.prototype, this);
inherit(Actions.prototype, this);
this.init(options);
};
var clubActions = new ClubActions({
route: 'club_actions_view',
actions: {
'review_club': {},
}
});
var actionOptions = {'club_id': 1};
var ajaxCallback = function(viewmodel) {
console.log(viewmodel);
// process viewmodel...
};
clubActions.perform('review_club', actionOptions, ajaxCallback);
actionOptions
and ajaxCallback
arguments are the optional ones.
In case there is
perform_review_club()
method defined inClubActions
Javascript class, it will be called first.If there is no
perform_review_club()
method defined,.ajax()
method will be called, executing AJAX POST request withactionOptions
value becoming the queryargs to the Django urlclub_actions_view
.In such case, Django
ClubActionsView
view class should havereview_club
action defined (see Custom actions at the server-side).Since v0.9.0
ajaxCallback
argument accepts Javascript bind context as well as viewmodelbefore
andafter
callbacks, to define custom viewmodel handlers on the fly:var self = this; clubActions.ajax( 'member_names', { club_id: this.club.id, }, { // 'set_members' is a custom viewmodel handler defined on the fly: after: { set_members: function(viewModel) { self.setMemberNames(viewModel.users); }, } } ); clubActions.ajax( 'member_roles', { club_id: this.club.id, }, // viewmodel response will be returned to the bound method clubRolesEditor.updateMemberRoles(): { context: clubRolesEditor, fn: ClubRolesEditor.updateMemberRoles, } );
Note:
actionOptions
value may be dynamically altered / generated via optionalqueryargs_review_club()
method in case it’s defined inClubActions
class.Custom
perform_review_club()
method could execute some client-side Javascript code first then call.ajax()
method manually to execute Django view code, or just perform a pure client-side action only.In case
ClubActions
class.ajax()
method was called, the resulting viewmodel will be passed toClubActions
classcallback_review_club()
method, in case it’s defined. That makes the execution chain of AJAX action complete.
See Client-side routes how to make club_actions_view
Django view name (route) available in Javascript.
See club-grid.js for sample overriding of Grid
actions. See Datatables for more info.
Overriding action callback¶
Possible interpretation of server-side ActionsView class .action\*()
method (eg .action_perform_review()
)
result (AJAX response):
None
- client-side Actions class.callback_perform_review()
method will be called, no arguments passed to it except the default viewmodel_name;False
- client-side Actions class.callback_perform_review()
will be suppressed, not called at all;list
/dict
- the result will be converted to vm_list- In case the viewmodel
view
key is omitted or contains the default Django view viewmodel_name attribute value, the default client-side Actions class.callback_perform_review()
method will be called; - The rest of viewmodels (if any) will be processed by the vmRouter;
- In case the viewmodel
special case: override callback method by routing to
another_action
Javascript Actions class.callback_another_action()
method by providing callback_action key with the valueanother_action
in the viewmodel dict response.For example to conditionally “redirect” to
show_readonly
action callback foredit_inline
action in a KoGridView derived class:from django_jinja_knockout import tpl from django_jinja_knockout.views import KoGridView class CustomGridView(KoGridView): # ... skipped... def action_edit_inline(self): # Use qs = self.get_queryset_for_action() in case multiple objects are selected in the datatable. obj = self.get_object_for_action() if obj.is_editable: if obj.is_invalid: return { 'view': 'alert_error', 'title': obj.get_str_fields(), 'message': tpl.format_html('<div>Invalid object={}</div>', obj.pk) } else: title = obj.get_str_fields() # Action.callback_show_readonly() will be called instead of the default # Action.callback_edit_inline() with the following viewmodel as the argument. return { 'callback_action': 'show_readonly', 'title': title, } else: return super().action_edit_inline()
Custom actions at the client-side¶
To implement or to override client-side processing of AJAX action response, one should define custom Javascript class, inherited from Actions (or from GridActions in case of custom grid Datatables):
import { inherit } from '../../djk/js/dash.js';
import { Actions } from '../../djk/js/actions.js';
MyModelFormActions = function(options) {
inherit(Actions.prototype, this);
this.init(options);
};
Client-side part of edit_form
action response, which receives AJAX viewmodel(s) response is defined as:
import { ModelFormDialog } from '../../djk/js/modelform.js';
(function(MyModelFormActions) {
MyModelFormActions.callback_edit_form = function(viewModel) {
viewModel.owner = this.grid;
var dialog = new ModelFormDialog(viewModel);
dialog.show();
};
// ... See more sample methods below.
})(MyModelFormActions.prototype);
Client-side Actions descendant classes can optionally add queryargs to AJAX HTTP request in a custom
queryargs_ACTION_NAME
method:
MyFormActions.queryargs_edit_form = function(options) {
// Add a custom queryarg to AJAX POST:
options['myArg'] = 1;
};
Client-side Actions descendant classes can directly process actions without calling AJAX viewmodel server-side
part (client-only actions) by defining perform_ACTION_NAME
method:
import { ActionTemplateDialog } from '../../djk/js/modelform.js';
MyFormActions.perform_edit_form = function(queryArgs, ajaxCallback) {
// this.owner may be instance of Grid or another class which implements proper owner interface.
new ActionTemplateDialog({
template: 'my_form_template',
owner: this.owner,
meta: {
user_id: queryArgs.user_id,
},
}).show();
};
For such client-only actions ActionTemplateDialog utilizes Underscore.js templates for one-way binding, or Knockout.js templates when two way binding is required. Here is the sample template
<script type="text/template" id="my_form_template">
<card-default>
<card-body>
<form class="ajax-form" enctype="multipart/form-data" method="post" role="form" data-bind="attr: {'data-url': actions.getLastActionUrl()}">
<input type="hidden" name="csrfmiddlewaretoken" data-bind="value: getCsrfToken()">
<div class="jumbotron">
<div class="default-padding">
The user id is <span data-bind="text: meta.user_id"></span>
</div>
</div>
</form>
</card-body>
</card-default>
</script>
Custom grid actions should inherit from both GridActions and it’s base class Actions:
import { inherit } from '../../djk/js/dash.js';
import { Actions } from '../../djk/js/actions.js';
import { GridActions } from '../../djk/js/grid/actions.js';
MyGridActions = function(options) {
inherit(GridActions.prototype, this);
inherit(Actions.prototype, this);
this.init(options);
};
For more detailed example of using viewmodel actions routing, see the documentation Datatables section
Client-side action routing. Internally, AJAX actions are used by EditForm, EditInline
and by Grid client-side components. See also EditForm usage in djk-sample
project.