Form fields can now define "depends" to refresh individual fields when they are changed

Form field options can now be deferred using a closure
This commit is contained in:
Sam Georges 2014-06-29 13:35:47 +10:00
parent dd3bb5d918
commit 4a9fe06d50
15 changed files with 296 additions and 48 deletions

View File

@ -8749,11 +8749,13 @@ html.cssanimations .loading-indicator.transparent > span {
margin-top: -10px;
background-size: 20px 20px;
}
.loading-indicator-container.size-form-field .loading-indicator,
.loading-indicator-container.size-input-text .loading-indicator {
background-color: transparent;
padding: 0;
margin: 0;
}
.loading-indicator-container.size-form-field .loading-indicator > span,
.loading-indicator-container.size-input-text .loading-indicator > span {
padding: 0;
margin: 0;
@ -8764,6 +8766,13 @@ html.cssanimations .loading-indicator.transparent > span {
height: 23px;
background-size: 23px 23px;
}
.loading-indicator-container.size-form-field .loading-indicator > span {
top: 0;
right: 0;
width: 20px;
height: 20px;
background-size: 20px 20px;
}
html.cssanimations .cursor-loading-indicator {
background: transparent url(../images/loading-indicator-transparent.svg) no-repeat 50% 50%;
-webkit-animation: spin 1s linear infinite;
@ -10179,7 +10188,7 @@ body.dropdown-open .dropdown-overlay {
.control-tabs.content-tabs.tabs-inset ul.nav-tabs li {
border-top: 1px solid transparent;
}
.control-tabs.content-tabs.tabs-inset ul.nav-tabs li .active {
.control-tabs.content-tabs.tabs-inset ul.nav-tabs li.active {
border-top: 1px solid #e3e5e7;
}
.control-tabs.content-tabs.tabs-inset ul.nav-tabs li:first-child {

View File

@ -11,7 +11,8 @@
*
* JavaScript API:
*
* $('#buttons').loadIndicator({text: 'Saving...', 'opaque': true}) - display the indicator
* $('#buttons').loadIndicator({text: 'Saving...', opaque: true}) - display the indicator in a solid (opaque) state
* $('#buttons').loadIndicator({text: 'Saving...'}) - display the indicator in a transparent state
* $('#buttons').loadIndicator('hide') - display the indicator
*/
+function ($) { "use strict";

View File

@ -66,6 +66,8 @@ html.cssanimations {
}
}
.loading-indicator-container.size-form-field .loading-indicator,
.loading-indicator-container.size-input-text .loading-indicator {
background-color: transparent;
padding: 0;
@ -83,6 +85,14 @@ html.cssanimations {
}
}
.loading-indicator-container.size-form-field .loading-indicator > span {
top: 0;
right: 0;
width: 20px;
height: 20px;
background-size: 20px 20px;
}
//
// Cursor loading indicator
// --------------------------------------------------

View File

@ -250,7 +250,7 @@
padding-left: 20px;
li {
border-top: 1px solid transparent;
.active {
&.active {
border-top: 1px solid @color-tab-content-border;
}
}

View File

@ -50,7 +50,7 @@ class FormField
/**
* @var string Field options.
*/
public $options = [];
public $options;
/**
* @var string Specifies a side. Possible values: auto, left, right, full
@ -127,6 +127,11 @@ class FormField
*/
public $config;
/**
* @var array Other field names this field depends on, when the other fields are modified, this field will update.
*/
public $depends;
public function __construct($columnName, $label)
{
$this->columnName = $columnName;
@ -167,9 +172,23 @@ class FormField
* @param array $value
* @return self
*/
public function options($value = [])
public function options($value = null)
{
$this->options = $value;
if ($value === null) {
if (is_array($this->options)) {
return $this->options;
}
elseif (is_callable($this->options)) {
$callable = $this->options;
return $callable();
}
return [];
}
else {
$this->options = $value;
}
return $this;
}
@ -209,6 +228,7 @@ class FormField
if (isset($config['default'])) $this->defaults = $config['default'];
if (isset($config['cssClass'])) $this->cssClass = $config['cssClass'];
if (isset($config['attributes'])) $this->attributes = $config['attributes'];
if (isset($config['depends'])) $this->depends = $config['depends'];
if (isset($config['path'])) $this->path = $config['path'];
if (array_key_exists('required', $config)) $this->required = $config['required'];

View File

@ -120,7 +120,7 @@ class Form extends WidgetBase
*/
public function loadAssets()
{
$this->addJs('js/form.js', 'core');
$this->addJs('js/october.form.js', 'core');
}
/**
@ -188,7 +188,7 @@ class Form extends WidgetBase
if (!$this->model)
throw new ApplicationException(Lang::get('backend::lang.form.missing_model', ['class'=>get_class($this->controller)]));
$this->data = (object)$this->getConfig('data', $this->model);
$this->data = (object) $this->getConfig('data', $this->model);
return $this->model;
}
@ -205,6 +205,57 @@ class Form extends WidgetBase
$this->vars['secondaryTabs'] = $this->secondaryTabs;
}
/**
* Sets or resets form field values.
* @param array $data
* @return array
*/
public function setFormValues($data = null)
{
if ($data == null)
$data = $this->getSaveData();
$this->model->fill($data);
$this->data = (object) array_merge((array) $this->data, (array) $data);
foreach ($this->allFields as $field)
$field->value = $this->getFieldValue($field);
return $data;
}
/**
* Event handler for refreshing the form.
*/
public function onRender()
{
$this->setFormValues();
$this->prepareVars();
$result = [];
/*
* If an array of fields is supplied, update specified fields individually.
*/
if (($updateFields = post('fields')) && is_array($updateFields)) {
foreach ($updateFields as $field) {
if (!isset($this->allFields[$field]))
continue;
$fieldObject = $this->allFields[$field];
$result['#' . $fieldObject->getId('container')] = $this->renderField($fieldObject);
}
}
/*
* Update the whole form
*/
if (empty($result))
$result = ['#'.$this->getId() => $this->makePartial('form')];
return $result;
}
/**
* Creates a flat array of form fields from the configuration.
* Also slots fields in to their respective tabs.
@ -400,14 +451,25 @@ class Form extends WidgetBase
*/
$optionModelTypes = ['dropdown', 'radio', 'checkboxlist'];
if (in_array($field->type, $optionModelTypes)) {
$fieldOptions = (isset($config['options'])) ? $config['options'] : null;
$fieldOptions = $this->getOptionsFromModel($field, $fieldOptions);
$field->options($fieldOptions);
/*
* Defer the execution of option data collection
*/
$field->options(function() use ($field, $config) {
$fieldOptions = (isset($config['options'])) ? $config['options'] : null;
$fieldOptions = $this->getOptionsFromModel($field, $fieldOptions);
return $fieldOptions;
});
}
return $field;
}
/**
* Check if a field type is a widget or not
* @param string $fieldType
* @return boolean
*/
private function isFormWidget($fieldType)
{
if ($fieldType === null)
@ -521,6 +583,22 @@ class Form extends WidgetBase
return $result;
}
/**
* Returns a HTML encoded value containing the other fields this
* field depends on
* @param use Backend\Classes\FormField $field
* @return string
*/
public function getFieldDepends($field)
{
if (!$field->depends)
return;
$depends = is_array($field->depends) ? $field->depends : [$field->depends];
$depends = htmlspecialchars(json_encode($depends), ENT_QUOTES, 'UTF-8');
return $depends;
}
/**
* Returns postback data from a submitted form.
*/

View File

@ -1,4 +0,0 @@
/*
* Form Behavior
*/

View File

@ -0,0 +1,116 @@
/*
* Form Widget
*
* Dependences:
* - Nil
*/
+function ($) { "use strict";
var FormWidget = function (element, options) {
var $el = this.$el = $(element);
this.options = options || {};
this.bindDependants()
}
FormWidget.DEFAULTS = {
refreshHandler: null
}
/*
* Bind dependant fields
*/
FormWidget.prototype.bindDependants = function() {
var self = this,
form = this.$el,
formEl = form.closest('form'),
fieldMap = {}
/*
* Map master and slave field map
*/
form.find('[data-field-depends]').each(function(){
var name = $(this).data('column-name'),
depends = $(this).data('field-depends')
$.each(depends, function(index, depend){
if (!fieldMap[depend])
fieldMap[depend] = { fields: [] }
fieldMap[depend].fields.push(name)
})
})
/*
* When a master is updated, refresh its slaves
*/
$.each(fieldMap, function(columnName, toRefresh){
form.find('[data-column-name="'+columnName+'"]')
.on('change', 'select, input', function(){
formEl.request(self.options.refreshHandler, {
data: toRefresh
})
$.each(toRefresh.fields, function(index, field){
form.find('[data-column-name="'+field+'"]')
.addClass('loading-indicator-container size-form-field')
.loadIndicator()
})
})
})
// dependants.on('change', 'select, input', function(){
// var depends = $(this).closest('[data-field-depends]').data('field-depends'),
// form = $(this).closest('form')
// if (!form.length || !self.options.refreshHandler)
// return
// form.request(self.options.refreshHandler)
// })
}
// FORM WIDGET PLUGIN DEFINITION
// ============================
var old = $.fn.formWidget
$.fn.formWidget = function (option) {
var args = arguments,
result
this.each(function () {
var $this = $(this)
var data = $this.data('oc.formwidget')
var options = $.extend({}, FormWidget.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.formwidget', (data = new FormWidget(this, options)))
if (typeof option == 'string') result = data[option].call($this)
if (typeof result != 'undefined') return false
})
return result ? result : this
}
$.fn.formWidget.Constructor = FormWidget
// FORM WIDGET NO CONFLICT
// =================
$.fn.formWidget.noConflict = function () {
$.fn.formWidget = old
return this
}
// FORM WIDGET DATA-API
// ==============
$(document).render(function(){
$('[data-control="formwidget"]').formWidget();
})
}(window.jQuery);

View File

@ -1,24 +1,21 @@
<div class="form-group <?= $this->previewMode ? 'form-group-preview' : '' ?> <?= $field->type ?>-field span-<?= $field->span ?> <?= $field->required?'is-required':'' ?> <?= $field->stretch?'layout-relative':'' ?> <?= $field->cssClass ?>">
<?php if (in_array($field->type, ['checkbox', 'switch'])): ?>
<?php if (in_array($field->type, ['checkbox', 'switch'])): ?>
<?= $this->makePartial('field_'.$field->type, ['field' => $field]) ?>
<?= $this->makePartial('field_'.$field->type, ['field' => $field]) ?>
<?php else: ?>
<?php if ($field->label): ?>
<label for="<?= $field->getId() ?>"><?= e(trans($field->label)) ?></label>
<?php endif ?>
<?php if ($field->comment && $field->commentPosition == 'above'): ?>
<p class="help-block before-field"><?= e(trans($field->comment)) ?></p>
<?php endif ?>
<?= $this->renderFieldElement($field) ?>
<?php if ($field->comment && $field->commentPosition == 'below'): ?>
<p class="help-block"><?= e(trans($field->comment)) ?></p>
<?php endif ?>
<?php else: ?>
<?php if ($field->label): ?>
<label for="<?= $field->getId() ?>"><?= e(trans($field->label)) ?></label>
<?php endif ?>
</div>
<?php if ($field->comment && $field->commentPosition == 'above'): ?>
<p class="help-block before-field"><?= e(trans($field->comment)) ?></p>
<?php endif ?>
<?= $this->renderFieldElement($field) ?>
<?php if ($field->comment && $field->commentPosition == 'below'): ?>
<p class="help-block"><?= e(trans($field->comment)) ?></p>
<?php endif ?>
<?php endif ?>

View File

@ -1,7 +1,10 @@
<?php
$fieldOptions = $field->options();
?>
<!-- Balloon selector -->
<div data-control="balloon-selector" id="<?= $field->getId() ?>" <?= HTML::attributes($field->attributes) ?>>
<ul>
<?php foreach ($field->options as $value => $text): ?>
<?php foreach ($fieldOptions as $value => $text): ?>
<li data-value="<?= e($value) ?>" class="<?= $value == $field->value ? 'active' : '' ?>"><?= e($text) ?></li>
<?php endforeach ?>
</ul>

View File

@ -1,10 +1,11 @@
<!-- Checkbox List -->
<?php
$fieldOptions = $field->options();
$checkedValues = (is_array($field->value)) ? $field->value : [$field->value];
?>
<?php if (count($field->options)): ?>
<!-- Checkbox List -->
<?php if (count($fieldOptions)): ?>
<?php if (count($field->options) > 10): ?>
<?php if (count($fieldOptions) > 10): ?>
<!-- Quick selection -->
<small>
<?= e(trans('backend::lang.form.select')) ?>:
@ -17,7 +18,7 @@
<div class="control-scrollbar" data-control="scrollbar">
<?php endif ?>
<?php $index = 0; foreach ($field->options as $value => $option): ?>
<?php $index = 0; foreach ($fieldOptions as $value => $option): ?>
<?php
$index++;
$checkboxId = 'checkbox_'.$field->columnName.'_'.$index;
@ -42,7 +43,7 @@
</div>
<?php endforeach ?>
<?php if (count($field->options) > 10): ?>
<?php if (count($fieldOptions) > 10): ?>
</div>
</div>
<?php endif ?>

View File

@ -1,6 +1,9 @@
<?php
$fieldOptions = $field->options();
?>
<!-- Dropdown -->
<?php if ($this->previewMode): ?>
<div class="form-control"><?= (isset($field->options[$field->value])) ? e($field->options[$field->value]) : '' ?></div>
<div class="form-control"><?= (isset($fieldOptions[$field->value])) ? e($fieldOptions[$field->value]) : '' ?></div>
<?php else: ?>
<select
id="<?= $field->getId() ?>"
@ -10,7 +13,7 @@
<?php if ($field->placeholder): ?>
<option value=""><?= e(trans($field->placeholder)) ?></option>
<?php endif ?>
<?php foreach ($field->options as $value => $text): ?>
<?php foreach ($fieldOptions as $value => $text): ?>
<option
<?= $value == $field->value ? 'selected="selected"' : '' ?>
value="<?= $value ?>">

View File

@ -1,7 +1,10 @@
<?php
$fieldOptions = $field->options();
?>
<!-- Radio List -->
<?php if (count($field->options)): ?>
<?php if (count($fieldOptions)): ?>
<?php $index = 0; foreach ($field->options as $value => $option): ?>
<?php $index = 0; foreach ($fieldOptions as $value => $option): ?>
<?php
$index++;
if (is_string($option))

View File

@ -1,4 +1,9 @@
<div class="form-widget form-elements layout" role="form" id="<?= $this->getId() ?>">
<div
data-control="formwidget"
data-refresh-handler="<?= $this->getEventHandler('onRender') ?>"
class="form-widget form-elements layout"
role="form"
id="<?= $this->getId() ?>">
<?php if ($outsideFields): ?>
<?= $this->makePartial('form_outside_fields') ?>

View File

@ -1,3 +1,9 @@
<?php foreach ($fields as $field): ?>
<?= $this->makePartial('field', ['field' => $field]) ?>
<?php endforeach ?>
<div
class="form-group <?= $this->previewMode ? 'form-group-preview' : '' ?> <?= $field->type ?>-field span-<?= $field->span ?> <?= $field->required?'is-required':'' ?> <?= $field->stretch?'layout-relative':'' ?> <?= $field->cssClass ?>"
<?php if ($depends = $this->getFieldDepends($field)): ?>data-field-depends="<?= $depends ?>"<?php endif ?>
data-column-name="<?= $field->columnName ?>"
id="<?= $field->getId('group') ?>">
<?= $this->renderField($field) ?>
</div>
<?php endforeach ?>