Add support for Buefy autocomplete; several other form tweaks

at least the Edit User form should work now, for instance
This commit is contained in:
Lance Edgar 2019-06-08 13:46:00 -05:00
parent d7e19865de
commit 2b6d88105c
16 changed files with 390 additions and 42 deletions

View file

@ -760,6 +760,29 @@ class Form(object):
context['render_field_readonly'] = self.render_field_readonly
return render(template, context)
def get_vuejs_model_value(self, field):
"""
This method must return "raw" JS which will be assigned as the initial
model value for the given field. This JS will be written as part of
the overall response, to be interpreted on the client side.
"""
if isinstance(field.schema.typ, colander.Date):
# TODO: don't recall why "always null" here?
return 'null'
if isinstance(field.schema.typ, deform.FileData):
# TODO: don't recall why "always null" here?
return 'null'
if isinstance(field.schema.typ, colander.Set):
if field.cstruct is colander.null:
return '[]'
if field.cstruct is colander.null:
return 'null'
return json.dumps(field.cstruct)
def messages_json(self, messages):
dump = json.dumps(messages)
dump = dump.replace("'", ''')

View file

@ -0,0 +1,91 @@
const TailboneAutocomplete = {
template: '#tailbone-autocomplete-template',
props: {
name: String,
serviceUrl: String,
value: String,
initialLabel: String,
},
data() {
let selected = null
if (this.value) {
selected = {
value: this.value,
label: this.initialLabel,
}
}
return {
data: [],
selected: selected,
isFetching: false,
autocompleteValue: this.value,
}
},
methods: {
clearSelection() {
this.selected = null
this.autocompleteValue = null
this.$nextTick(function() {
this.$refs.autocomplete.focus()
})
// TODO: should emit event for caller logic (can they cancel?)
// $('#' + oid + '-textbox').trigger('autocompletevaluecleared');
},
// TODO: should we allow custom callback? or is event enough?
// function (oid) {
// $('#' + oid + '-textbox').on('autocompletevaluecleared', function() {
// ${cleared_callback}();
// });
// }
selectionMade(option) {
this.selected = option
// TODO: should emit event for caller logic (can they cancel?)
// $('#' + oid + '-textbox').trigger('autocompletevalueselected',
// [ui.item.value, ui.item.label]);
},
// TODO: should we allow custom callback? or is event enough?
// function (oid) {
// $('#' + oid + '-textbox').on('autocompletevalueselected', function(event, uuid, label) {
// ${selected_callback}(uuid, label);
// });
// }
itemSelected(value) {
this.$emit('input', value)
},
// TODO: buefy example uses `debounce()` here and perhaps we should too?
// https://buefy.org/documentation/autocomplete
getAsyncData: function (entry) {
if (entry.length < 3) {
this.data = []
return
}
this.isFetching = true
this.$http.get(this.serviceUrl + '?term=' + encodeURIComponent(entry))
.then(({ data }) => {
this.data = data
})
.catch((error) => {
this.data = []
throw error
})
.finally(() => {
this.isFetching = false
})
},
},
}
Vue.component('tailbone-autocomplete', TailboneAutocomplete)

View file

@ -10,6 +10,8 @@ const OnceButton = {
':title="title"',
':disabled="buttonDisabled"',
'@click="clicked"',
'icon-pack="fas"',
':icon-left="iconLeft"',
'>',
'{{ buttonText }}',
'</b-button>'
@ -22,6 +24,7 @@ const OnceButton = {
href: String,
text: String,
title: String,
iconLeft: String,
working: String,
workingText: String,
disabled: Boolean

View file

@ -13,3 +13,12 @@
white-space: nowrap;
width: 18em;
}
.field.is-horizontal .field-body {
min-width: 30em;
}
.field.is-horizontal .field-body .select,
.field.is-horizontal .field-body .select select {
width: 100%;
}

View file

@ -1,4 +1,5 @@
## -*- coding: utf-8 -*-
## -*- coding: utf-8; -*-
## TODO: This function signature is getting out of hand...
<%def name="autocomplete(field_name, service_url, field_value=None, field_display=None, width='300px', select=None, selected=None, cleared=None, change_clicked=None, options={})">
<div id="${field_name}-container" class="autocomplete-container">
@ -56,3 +57,31 @@
});
</script>
</%def>
<%def name="tailbone_autocomplete_template()">
<script type="text/x-template" id="tailbone-autocomplete-template">
<div>
<b-autocomplete ref="autocomplete"
:name="name"
v-show="!selected"
v-model="autocompleteValue"
:data="data"
@typing="getAsyncData"
@select="selectionMade"
@input="itemSelected"
keep-first>
<template slot-scope="props">
{{ props.option.label }}
</template>
</b-autocomplete>
<b-button v-if="selected"
style="width: 100%; justify-content: left;"
@click="clearSelection()">
{{ selected.label }} (click to change)
</b-button>
</div>
</script>
</%def>

View file

@ -2,10 +2,14 @@
css_class css_class|field.widget.css_class;
oid oid|field.oid;
field_display field_display;
style style|field.widget.style"
style style|field.widget.style;
url url|field.widget.service_url;
use_buefy use_buefy|0;"
tal:omit-tag="">
<div tal:condition="not use_buefy"
id="${oid}-container"
class="autocomplete-container">
<input type="hidden"
name="${name}"
id="${oid}"
@ -98,5 +102,16 @@
}
);
</script>
</div>
<div tal:condition="use_buefy"
tal:define="vmodel vmodel|'field_model_' + name;"
tal:omit-tag="">
<tailbone-autocomplete name="${name}"
service-url="${url}"
v-model="${vmodel}"
initial-label="${field_display}">
</tailbone-autocomplete>
</div>
</div>

View file

@ -1,13 +1,26 @@
<div class="checkbox">
<input tal:define="name name|field.name;
<div tal:define="name name|field.name;
true_val true_val|field.widget.true_val;
css_class css_class|field.widget.css_class;
style style|field.widget.style;
oid oid|field.oid"
type="checkbox"
oid oid|field.oid;
use_buefy use_buefy|0;"
tal:omit-tag="">
<div tal:condition="not use_buefy" class="checkbox">
<input type="checkbox"
name="${name}" value="${true_val}"
id="${oid}"
tal:attributes="checked cstruct == true_val;
class css_class;
style style;" />
</div>
<div tal:condition="use_buefy"
tal:define="vmodel vmodel|'field_model_' + name;">
<b-checkbox name="${name}"
v-model="${vmodel}"
native-value="${true_val}">
{{ ${vmodel} }}
</b-checkbox>
</div>
</div>

View file

@ -0,0 +1,60 @@
<div i18n:domain="deform" tal:omit-tag=""
tal:define="oid oid|field.oid;
name name|field.name;
css_class css_class|field.widget.css_class;
style style|field.widget.style;
use_buefy use_buefy|0;">
<div tal:condition="not use_buefy" tal:omit-tag="">
${field.start_mapping()}
<div>
<input type="password"
name="${name}"
value="${field.widget.redisplay and cstruct or ''}"
tal:attributes="class string: form-control ${css_class or ''};
style style;
attributes|field.widget.attributes|{};"
id="${oid}"
i18n:attributes="placeholder"
placeholder="Password"/>
</div>
<div>
<input type="password"
name="${name}-confirm"
value="${field.widget.redisplay and confirm or ''}"
tal:attributes="class string: form-control ${css_class or ''};
style style;
confirm_attributes|field.widget.confirm_attributes|{};"
id="${oid}-confirm"
i18n:attributes="placeholder"
placeholder="Confirm Password"/>
</div>
${field.end_mapping()}
</div>
<div tal:condition="use_buefy">
${field.start_mapping()}
<b-input type="password"
name="${name}"
value="${field.widget.redisplay and cstruct or ''}"
tal:attributes="class string: form-control ${css_class or ''};
style style;
attributes|field.widget.attributes|{};"
id="${oid}"
i18n:attributes="placeholder"
placeholder="Password">
</b-input>
<b-input type="password"
name="${name}-confirm"
value="${field.widget.redisplay and confirm or ''}"
tal:attributes="class string: form-control ${css_class or ''};
style style;
confirm_attributes|field.widget.confirm_attributes|{};"
id="${oid}-confirm"
i18n:attributes="placeholder"
placeholder="Confirm Password">
</b-input>
${field.end_mapping()}
</div>
</div>

View file

@ -58,12 +58,14 @@
<div tal:condition="use_buefy"
tal:define="vmodel vmodel|'field_model_' + name;"
tal:omit-tag="">
>
<input type="hidden" name="__start__" value="${name}:sequence"
tal:condition="multiple" />
<b-select tal:attributes="name name;
id oid;
placeholder '(please choose)';
class string: form-control ${css_class or ''};
multiple multiple;
:multiple str(multiple).lower();
size size;
style style;
v-model vmodel;
@ -88,6 +90,8 @@
</tal:loop>
</b-select>
<input type="hidden" name="__end__" value="${name}:sequence"
tal:condition="multiple" />
</div>
</div>

View file

@ -0,0 +1,27 @@
<div tal:define="rows rows|field.widget.rows;
cols cols|field.widget.cols;
css_class css_class|field.widget.css_class;
oid oid|field.oid;
name name|field.name;
style style|field.widget.style;
use_buefy use_buefy|0;"
tal:omit-tag="">
<div tal:condition="not use_buefy" tal:omit-tag="">
<textarea tal:attributes="rows rows;
cols cols;
class string: form-control ${css_class or ''};
style style;
attributes|field.widget.attributes|{};"
id="${oid}"
name="${name}">${cstruct}</textarea>
</div>
<div tal:condition="use_buefy"
tal:define="vmodel vmodel|'field_model_' + name;">
<b-input type="textarea"
name="${name}"
v-model="${vmodel}">
</b-input>
</div>
</div>

View file

@ -0,0 +1,32 @@
<span tal:define="name name|field.name;
css_class css_class|field.widget.css_class;
oid oid|field.oid;
mask mask|field.widget.mask;
mask_placeholder mask_placeholder|field.widget.mask_placeholder;
style style|field.widget.style;
use_buefy use_buefy|0;"
tal:omit-tag="">
<div tal:condition="not use_buefy" tal:omit-tag="">
<input type="text" name="${name}" value="${cstruct}"
tal:attributes="class string: form-control ${css_class or ''};
style style;
attributes|field.widget.attributes|{};"
id="${oid}"/>
<script tal:condition="mask" type="text/javascript">
deform.addCallback(
'${oid}',
function (oid) {
$("#" + oid).mask("${mask}",
{placeholder:"${mask_placeholder}"});
});
</script>
</div>
<div tal:condition="use_buefy"
tal:define="vmodel vmodel|'field_model_' + name;"
tal:omit-tag="">
<b-input name="${name}"
v-model="${vmodel}">
</b-input>
</div>
</span>

View file

@ -11,7 +11,38 @@
${form.render(buttons=capture(self.render_form_buttons))|n}
</%def>
<%def name="render_buefy_form()">
<div class="form">
<tailbone-form></tailbone-form>
</div>
</%def>
<%def name="render_form_complete()">
% if use_buefy:
${self.render_form()}
<script type="text/x-template" id="form-page-template">
<div style="display: flex; justify-content: space-between;">
<div class="form-wrapper">
${self.render_buefy_form()}
</div>
<div style="display: flex; align-items: flex-start;">
<div class="object-helpers">
${self.object_helpers()}
</div>
<ul id="context-menu">
${self.context_menu_items()}
</ul>
</div>
</div>
</script>
<div id="form-page-app">
<form-page></form-page>
</div>
% else:
<div style="display: flex; justify-content: space-between;">
<div class="form-wrapper">
@ -29,6 +60,7 @@
</div>
</div>
% endif
</%def>
<%def name="modify_tailbone_form()">
@ -43,8 +75,14 @@
Vue.component('tailbone-form', TailboneForm)
const FormPage = {
template: '#form-page-template'
}
Vue.component('form-page', FormPage)
new Vue({
el: '#tailbone-form-app'
el: '#form-page-app'
})
</script>
@ -53,6 +91,6 @@
${self.render_form_complete()}
% if form.use_buefy:
% if use_buefy:
${self.make_tailbone_form_app()}
% endif

View file

@ -91,21 +91,10 @@
% for field in form.fields:
% if field in dform:
<% field = dform[field] %>
% if isinstance(field.schema.typ, colander.Date):
field_model_${field.name}: null,
% elif isinstance(field.schema.typ, deform.FileData):
field_model_${field.name}: null,
% else:
field_model_${field.name}: ${'null' if field.cstruct is colander.null else json.dumps(field.cstruct)|n},
% endif
field_model_${field.name}: ${form.get_vuejs_model_value(field)|n},
% endif
% endfor
% endif
}
</script>
<div id="tailbone-form-app">
<tailbone-form></tailbone-form>
</div>

View file

@ -20,16 +20,28 @@
% endif
</%def>
<%def name="render_form()">
<%def name="render_buefy_form()">
<br />
% if use_buefy:
<b-notification type="is-danger" :closable="false">
You are about to delete the following ${model_title} and all associated data:
</b-notification>
% else:
<p>You are about to delete the following ${model_title} and all associated data:</p>
% endif
${parent.render_form()}
${parent.render_buefy_form()}
</%def>
<%def name="render_form_buttons()">
<br />
% if use_buefy:
<b-notification type="is-danger" :closable="false">
Are you sure about this?
</b-notification>
% else:
<p>Are you sure about this?</p>
% endif
<br />
${h.form(request.current_route_url(), class_=None if form.use_buefy else 'autodisable')}
@ -39,14 +51,12 @@
<once-button tag="a" href="${form.cancel_url}"
text="Whoops, nevermind...">
</once-button>
% else:
<a class="button" href="${form.cancel_url}">Whoops, nevermind...</a>
% endif
% if form.use_buefy:
<once-button type="is-primary" native-type="submit"
<once-button type="is-primary is-danger"
native-type="submit"
text="Yes, please DELETE this data forever!">
</once-button>
% else:
<a class="button" href="${form.cancel_url}">Whoops, nevermind...</a>
${h.submit('submit', "Yes, please DELETE this data forever!", class_='button is-primary')}
% endif
</div>

View file

@ -141,13 +141,12 @@
<input type="hidden"
name="uuids"
:value="checkedRowUUIDs()" />
<b-button type="is-primary"
<once-button type="is-primary"
native-type="submit"
icon-pack="fas"
icon-left="object-ungroup"
:disabled="checkedRows.length != 2">
Merge 2 ${model_title_plural}
</b-button>
:disabled="checkedRows.length != 2"
text="Merge 2 ${model_title_plural}">
</once-button>
% else:
${h.hidden('uuids')}
<button type="submit" class="button">Merge 2 ${model_title_plural}</button>

View file

@ -1,6 +1,7 @@
## -*- coding: utf-8; -*-
<%namespace file="/grids/nav.mako" import="grid_index_nav" />
<%namespace file="/feedback_dialog_buefy.mako" import="feedback_dialog" />
<%namespace file="/autocomplete.mako" import="tailbone_autocomplete_template" />
<%namespace name="base_meta" file="/base_meta.mako" />
<!DOCTYPE html>
<html lang="en">
@ -30,6 +31,11 @@
</head>
<body>
## TODO: should move template to JS, then can postpone the JS
${tailbone_autocomplete_template()}
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.buefy.autocomplete.js') + '?ver={}'.format(tailbone.__version__))}
<header>
<nav class="navbar" role="navigation" aria-label="main navigation">