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:
App.vmRouter.respond(viewmodels);
While single viewmodel may be execuded via the following call:
App.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:
// viewmodel bind context with method
var handler = {
fn: App.MyClass.prototype.myMethod,
context: App.myClassInstance
};
// Subscribe to bound method:
App.vmRouter.addHandler('my_view', handler)
// Subscribe to bound method:
.add('my_view', App.MyClass.prototype.myMethod2, App.myClassInstance)
// Subscribe to unbound function:
.add('my_view', myFunc)
// Subscribe to instantiate a new class via classpath specified:
.addHandler('my_view', 'App.MyClass');
// ...
// Will execute all four handlers attached above with passed viewmodel argument:
App.vmRouter.exec('my_view', {'a': 1, 'b': 2});
// ...
// Unsubscribe handlers. The order is arbitrary.
App.vmRouter.removeHandler('my_view', {fn: App.MyClass.prototype.myMethod2, context: App.myClassInstance})
.removeHandler('my_view', myFunc)
.removeHandler('my_view', handler)
.removeHandler('my_view', 'App.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 App.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 app.js:
[
'redirect_to',
'post',
'alert',
'alert_error',
'confirm',
'trigger',
'append',
'prepend',
'after',
'before',
'remove',
'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:
App.vmRouter.add('my_view', function(viewModel, vmRouter) {
// execute viewmodel here...
})
.add('my_view2', {fn: App.MyClass.prototype.method, context: MyClassInstance})
.add('my_view3', 'App.MyClass');
// or
App.vmRouter.add({
'my_view': function(viewModel, vmRouter) {
// execute viewmodel here...
},
'my_view2': {fn: App.MyClass.prototype.method, context: MyClassInstance},
'my_view3': 'App.MyClass'
});
The following syntax allows to reset previous handlers with the names specified (if any):
App.vmRouter.removeAll('my_view', 'my_view2', 'my_view3')
.add({
'my_view': function(viewModel, vmRouter) {
// execute viewmodel here...
},
'my_view2': {fn: App.MyClass.prototype.method, context: MyClassInstance},
'my_view3': 'App.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 App.vmRouter that was used to process current
viewmodel
. This instance of App.vmRouter could be used to call another viewmodel handler inside the current
handler, or to add / remove handlers via calling vmRouter instance methods:
App.vmRouter.add('my_view1', function(viewModel, vmRouter) {
// dynamically add 'my_view2' viewmodel handler when 'my_view1' handler is executed:
vmRouter.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:
vmRouter.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
App.FieldPopover
in the following code:
App.vmRouter.add('tooltip_error', function(viewModel) {
// Adding .instance property at the client-side to server-side generated viewModel:
viewModel.instance = new App.FieldPopover(viewModel);
});
Every already executed viewmodel is stored in .executedViewModels
property of App.vmRouter instance, which may be
processed later. An example of such processing is App.destroyTooltipErrors static method, which clears form input
Bootstrap tooltips previously set by 'tooltip_error'
viewmodel handler then removes these viewmodels from
.executedViewModels
list via App.ViewModelRouter.filterExecuted() method:
App.destroyTooltipErrors = function(form) {
App.vmRouter.filterExecuted(
function(viewModel) {
if (viewModel.view === 'tooltip_error' &&
typeof viewModel.instance !== 'undefined') {
viewModel.instance.destroy();
return false;
}
return true;
}
);
};
It is possible to chain viewmodel handlers, implementing a code-reuse and a pseudo-inheritance of viewmodels:
App.vmRouter.add('popover_error', function(viewModel, vmRouter) {
viewModel.instance = new App.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 tradidional 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:
$('#my_button').on('click', function(ev) {
$.post(
'/url_to_ajax_handler',
{csrfmiddlewaretoken: App.conf.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>
app.js 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 app.js Javascript:
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:
App.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 App.post()
optional
bindContext
argument:
App.post('button-click', postData, bindContext);
That allows to use method prototypes bound to different instances of the same Javascript class:
App.AjaxDialog = function(options) {
$.inherit(App.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 App.AjaxDialog passed via App.post() this argument
* methods .vm_addReceivedMessage() / .vm_addSentMessage() will be called:
*/
App.post('my_url_name', this.postData, this);
};
// Subscribe to 'add_received_message' / 'add_sent_message' custom viewmodel handlers:
App.vmRouter.add({
'add_received_message': AjaxDialog.vm_addReceivedMessage,
'add_sent_message': AjaxDialog.vm_addSentMessage,
});
})(App.AjaxDialog.prototype);
var ajaxDialog = new App.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 app.js App.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::
# App.vmRouter.add('session_view', function(viewModel) { ... });
# // or:
# App.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 App.vmRouter instance .req()
method:
App.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 app.js App.Dialog
callback via
App.vmRouter.respond()
method conditionally processing returned viewmodels:
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) {
App.vmRouter.respond(cbViewModel);
} else if (typeof self.dialogOptions.cb_cancel === 'object') {
App.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.
App.vmRouter method .saveResponse()
saves received viewmodels:
App.vmRouter.add('popup_modal_error', function(viewModel, vmRouter) {
// Save received response to execute it in the 'shown.bs.modal' event handler (see just below).
vmRouter.saveResponse('popupModal', viewModel);
// Open modal popup to show actual errors (received as viewModel from server-side).
$popupModal.modal('show');
});
App.vmRouter method loadResponse()
executes viewmodels previously saved with .saveResponse()
call:
// Open modal popup.
$popupModal.on('shown.bs.modal', function (ev) {
// Execute viewmodels previously received in 'popup_modal_error' viewmodel handler.
App.vmRouter.loadResponse('popupModal');
});
Multiple save points might be set by calling App.vmRouter .saveResponse()
with the particular name
argument
value, then calling App.vmRouter .loadResponse()
with the matching name
argument value.
AJAX actions¶
Large classes of AJAX viewmodel handlers inherit from ActionsView at server-side and from App.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 App.ModelFormActions and App.GridActions inherit from App.Actions. See Datatables for more info.
Viewmodel router defines own (our) viewmodel name as Python ActionsView class viewmodel_name attribute /
Javascript App.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 App.Actions
directly. Instead, they are routed to
standard viewmodel handlers, added via App.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 app.js), while AJAX form validation errors are processed via form_error
viewmodel handler (see tooltips.js).
The difference between handling AJAX viewmodels with App.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 App.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
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 App.Actions inherited class .perform()
method:
App.ClubActions = function(options) {
// Comment out, when overriding App.ko.Grid actions.
// $.inherit(App.GridActions.prototype, this);
$.inherit(App.Actions.prototype, this);
this.init(options);
};
var clubActions = new App.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 inApp.ClubActions
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; App.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); }, } } ); App.clubActions.ajax( 'member_roles', { club_id: this.club.id, }, // viewmodel response will be returned to the bound method App.clubRolesEditor.updateMemberRoles(): { context: App.clubRolesEditor, fn: App.ClubRolesEditor.updateMemberRoles, } );
Note:
actionOptions
value may be dynamically altered / generated via optionalqueryargs_review_club()
method in case it’s defined inApp.ClubActions
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
App.ClubActions
class.ajax()
method was called, the resulting viewmodel will be passed toApp.ClubActions
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 App.ko.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 App.Actions class.callback_perform_review()
method will be called, no arguments passed to it except the default viewmodel_name;False
- client-side App.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 App.Actions class.callback_perform_review()
method will be called; - The rest of viewmodels (if any) will be processed by the App.vmRouter;
- In case the viewmodel
special case: override callback method by routing to
another_action
Javascript App.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: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() # App.Action.callback_show_readonly() will be called instead of the default # App.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 App.Actions (or from App.GridActions in case of custom grid Datatables):
App.MyModelFormActions = function(options) {
$.inherit(App.Actions.prototype, this);
this.init(options);
};
Client-side part of edit_form
action response, which receives AJAX viewmodel(s) response is defined as:
(function(MyModelFormActions) {
MyModelFormActions.callback_edit_form = function(viewModel) {
viewModel.owner = this.grid;
var dialog = new App.ModelFormDialog(viewModel);
dialog.show();
};
// ... See more sample methods below.
})(App.MyModelFormActions.prototype);
Client-side App.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 App.Actions descendant classes can directly process actions without calling AJAX viewmodel server-side
part (client-only actions) by defining perform_ACTION_NAME
method:
MyFormActions.perform_edit_form = function(queryArgs, ajaxCallback) {
// this.owner may be instance of App.ko.Grid or another class which implements proper owner interface.
new App.ActionTemplateDialog({
template: 'my_form_template',
owner: this.owner,
meta: {
user_id: queryArgs.user_id,
},
}).show();
};
For such client-only actions App.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 App.GridActions
and it’s base class App.Actions
:
App.MyGridActions = function(options) {
$.inherit(App.GridActions.prototype, this);
$.inherit(App.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 App.EditForm, App.EditInline
and by App.ko.Grid client-side components. See also App.EditForm usage in djk-sample
project.