This forum has moved to a new location and is in read-only mode. Please visit talk.octobercms.com to access the new location.
I have issue about dependsOn function on 2 level. Please find following yaml file.
income_per_day:
label: Income Per Day
type: number
income_per_week:
label: Income Per Week
type: number
income_per_month:
label: Income Per Month
type: number
income_per_year:
label: Income Per Year
type: number
income_total:
label: Income Total
type: number
dependsOn:
- income_per_day
- income_per_week
- income_per_month
- income_per_year
pay_per_day:
label: Pay Per Day
type: number
pay_per_week:
label: Pay Per Week
type: number
pay_per_month:
label: Pay Per Month
type: number
pay_per_year:
label: Pay Per Year
type: number
pay_total:
label: Pay Total
type: number
dependsOn:
- pay_per_day
- pay_per_week
- pay_per_month
- pay_per_year
balance
label: Balance
type: number
dependsOn:
- pay_total
- income_total
- income_total depends on income_* works fine.
- pay_total depends on pay_* works fine.
- balance depends on pay_total works fine.
- balance depends on income_total works fine.
I would like to update balance value by income_per_day, income_per_week, income_per_month, income_total, pay_per_day, pay_per_week, pay_per_month and pay_per_year by pass pay_total and income_total. But I wouldn't to use following balance config.
balance
label: Balance
type: number
dependsOn:
- income_per_day
- income_per_week
- income_per_month
- income_per_year
- pay_per_day
- pay_per_week
- pay_per_month
- pay_per_year
May I have some advice? Should I try to edit october.form.js?
I don't understand what you mean here:
"I would like to update balance value by income_per_day, income_per_week, income_per_month, income_total, pay_per_day, pay_per_week, pay_per_month and pay_per_year by pass pay_total and income_total. But I wouldn't to use following balance config."
mjauvin said:
I don't understand what you mean here:
"I would like to update balance value by income_per_day, income_per_week, income_per_month, income_total, pay_per_day, pay_per_week, pay_per_month and pay_per_year by pass pay_total and income_total. But I wouldn't to use following balance config."
Example:
If I update value on income_per_day. it should to update value in income_total and effect to value in balance field in the same time.
Same with pay_per_day. If I update value on pay_per_day. it should to update value in pay_total and effect to value in balance field in the same time.
Please find following fields.yaml.
fields:
_field_a:
label: _field_a
type: number
_field_b:
label: _field_b
type: number
_field_c:
label: _field_c
type: number
dependsOn:
- _field_a
- _field_b
readOnly: true
_field_total:
label: _field_total
type: number
dependsOn:
- _field_c
readOnly: true
Function filterFields.
public function filterFields($fields, $context = null)
{
if(isset($fields->_field_a) ||
isset($fields->_field_b) ||
isset($fields->_field_c))
{
$fields->_field_c->value = $fields->_field_a->value+$fields->_field_b->value;
$fields->_field_total->value = $fields->_field_c->value*2;
}
}
Field "_field_total" was not update when I edit "_field_a" or "_field_b". Please advice.
try this instead:
public function getFieldCAttribute()
{
return $this->field_a + $this->field_b;
}
public function getFieldTotalAttribute()
{
return $this->field_c * 2;
}
mjauvin said:
try this instead:
public function getFieldCAttribute() { return $this->field_a + $this->field_b; } public function getFieldTotalAttribute() { return $this->field_c * 2; }
Hi mjauvin,
Thank you for you reply. It works fine but need to reload page. May you have the way to calculate value with AJAX?
Hi mjauvin,
I try to modified /modules/backend/widgets/form/partials/_field_number.htm.
<input
data-request="onAjax" data-track-input
type="number"
step="<?= $step ?>"
name="<?= $field->getName() ?>"
id="<?= $field->getId() ?>"
value="<?= e($field->value) ?>"
placeholder="<?= e(trans($field->placeholder)) ?>"
class="form-control"
autocomplete="off"
<?= $min ? 'min="' . $min . '"' : ''; ?>
<?= $max ? 'max="' . $max . '"' : ''; ?>
<?= $field->hasAttribute('pattern') ? '' : 'pattern="-?\d+(\.\d+)?"' ?>
<?= $field->hasAttribute('maxlength') ? '' : 'maxlength="255"' ?>
<?= $field->getAttributes() ?>
/>
Render:
<input data-request="onAjax" data-track-input="" type="number" step="any" name="Expense[field_a]" id="Form-field-Expense-field_a" value="111" placeholder="" class="form-control" autocomplete="off" pattern="-?\d+(\.\d+)?" maxlength="255">
Result is same.
No need to monkey patch the core files, just add this to the appropriate fields definitions (fields.yaml):
_field_a:
label: _field_a
type: number
attributes:
data-request: onAjax
data-track-input: ''
mjauvin said:
No need to monkey patch the core files, just add this to the appropriate fields definitions (fields.yaml):
_field_a: label: _field_a type: number attributes: data-request: onAjax data-track-input: ''
Thank you for a trick.
mjauvin said:
But it's not refreshing the form either. I'll get back to you on this.
Thank you mjauvin.
Ok, this is working fine but you cannot use the "_" in front of field names you want to dependOn... their value is not sent with the AJAX request.
Note: text/number fields need to lose focus in order for the dependent fields to be refreshed. Also, you don't need to add data-request=onAjax
and data-track-input
in order for this to work.
instead of prefixing the "temporary fields" with "_", you can mark them purgeable in your model like this:
class MyModel extends Model {
use \October\Rain\Database\Traits\Purgeable;
protected $purgeable = [
'field_a',
'field_b',
'field_c',
];
}
mjauvin said:
instead of prefixing the "temporary fields" with "_", you can mark them purgeable in your model like this:
class MyModel extends Model { use \October\Rain\Database\Traits\Purgeable; protected $purgeable = [ 'field_a', 'field_b', 'field_c', ]; }
Thank you for trick.
Result are same. The "field_total" not update when lost focus on "field_a" and "field_b".
Note: "field_c" and "field_total" is read only. I try to removed "readOnly: true" from it but result are same.
fields.yaml:
fields:
field_a:
label: field_a
span: left
type: number
field_b:
label: field_b
span: left
type: number
field_c:
label: field_c
span: left
type: number
dependsOn:
- field_a
- field_b
readOnly: true
field_total:
label: field_total
span: left
type: number
dependsOn:
- field_c
readOnly: true
Model:
public function getFieldCAttribute()
{
$result = $this->field_a + $this->field_b;
return ($result == 0 ? '' : $result);
}
public function getFieldTotalAttribute()
{
if(is_numeric($this->field_c))
{
$result = $this->field_c *2;
}else{
$result = '';
}
return $result;
}
First nesting level definitely works for me, except for field_total which does not update after field_c's value changes (second nesting level).
mjauvin said:
First nesting level definitely works for me, except for field_total which does not update after field_c's value changes (second nesting level).
Yes, That's my purpose.
Actually, I can working with following YAML. But it's so many fields to do in real APP and difficult to maintenence, I would like to have shotcut.
fields:
field_a:
label: field_a
span: left
type: number
field_b:
label: field_b
span: left
type: number
field_c:
label: field_c
span: left
type: number
dependsOn:
- field_a
- field_b
readOnly: true
field_total:
label: field_total
span: left
type: number
dependsOn:
- field_a
- field_b
readOnly: true
OK, I found it. Let me share working solution for 3 levels.
YAML:
fields:
member_id:
label: Member ID
disabled: true
hidden: true
title:
type: partial
path: title
field_a:
label: field_a
span: left
type: number
field_b:
label: field_b
span: left
type: number
field_c:
label: field_c
span: left
type: number
dependsOn:
- field_a
- field_b
readOnly: true
field_d:
label: field_d
span: left
type: number
field_e:
label: field_e
span: left
type: number
field_f:
label: field_f
span: left
type: number
dependsOn:
- field_d
- field_e
readOnly: true
field_total:
label: field_total
span: left
type: number
dependsOn:
- field_c
- field_f
readOnly: true
field_all_total:
label: field_all_total
span: left
type: number
dependsOn:
- field_total
readOnly: true
Class.php
public function getFieldCAttribute()
{
$result = $this->field_a + $this->field_b;
return ($result == 0 ? '' : $result);
}
public function getFieldFAttribute()
{
$result = $this->field_d + $this->field_e;
return ($result == 0 ? '' : $result);
}
public function getFieldTotalAttribute()
{
if(is_numeric($this->field_c)==false) {
$field_c=0;
}else{
$field_c=$this->field_c;
}
if(is_numeric($this->field_f)==false) {
$field_f=0;
}else{
$field_f=$this->field_f;
}
$result = ($field_c*2) + ($field_f*2);
return ($result == 0 ? '' : $result);
}
public function getFieldAllTotalAttribute()
{
if(is_numeric($this->field_total)==false) {
$field_total=0;
}else{
$field_total=$this->field_total;
}
$result = $field_total*2;
return ($result == 0 ? '' : $result);
}
october.form.js
/*
* Form Widget
*
* Dependences:
* - Nil
*/
+function ($) { "use strict";
var Base = $.oc.foundation.base,
BaseProto = Base.prototype
var FormWidget = function (element, options) {
this.$el = $(element)
this.options = options || {}
this.fieldElementCache = null
/*
* Throttle dependency updating
*/
this.dependantUpdateInterval = 300
this.dependantUpdateTimers = {}
$.oc.foundation.controlUtils.markDisposable(element)
Base.call(this)
this.init()
}
FormWidget.prototype = Object.create(BaseProto)
FormWidget.prototype.constructor = FormWidget
FormWidget.prototype.init = function() {
this.$form = this.$el.closest('form')
this.bindDependants()
this.bindCheckboxlist()
this.toggleEmptyTabs()
this.bindLazyTabs()
this.bindCollapsibleSections()
this.$el.on('oc.triggerOn.afterUpdate', this.proxy(this.toggleEmptyTabs))
this.$el.one('dispose-control', this.proxy(this.dispose))
}
FormWidget.prototype.dispose = function() {
this.$el.off('dispose-control', this.proxy(this.dispose))
this.$el.removeData('oc.formwidget')
this.$el = null
this.$form = null
this.options = null
this.fieldElementCache = null
BaseProto.dispose.call(this)
}
/*
* Logic for checkboxlist
*/
FormWidget.prototype.bindCheckboxlist = function() {
var checkAllBoxes = function($field, flag) {
$('input[type=checkbox]', $field)
.prop('checked', flag)
.first()
.trigger('change')
}
this.$el.on('click', '[data-field-checkboxlist-all]', function() {
checkAllBoxes($(this).closest('.field-checkboxlist'), true)
})
this.$el.on('click', '[data-field-checkboxlist-none]', function() {
checkAllBoxes($(this).closest('.field-checkboxlist'), false)
})
}
/*
* Get all fields elements that belong to this form, nested form
* fields are removed from this collection.
*/
FormWidget.prototype.getFieldElements = function() {
if (this.fieldElementCache !== null) {
return this.fieldElementCache
}
var form = this.$el,
nestedFields = form.find('[data-control="formwidget"] [data-field-name]')
return this.fieldElementCache = form.find('[data-field-name]').not(nestedFields)
}
/*
* Bind dependant fields
*/
FormWidget.prototype.bindDependants = function() {
if (!$('[data-field-depends]', this.$el).length) {
return;
}
var self = this,
fieldMap = {},
fieldElements = this.getFieldElements()
/*
* Map master and slave fields
*/
fieldElements.filter('[data-field-depends]').each(function() {
var name = $(this).data('field-name'),
depends = $(this).data('field-depends')
$.each(depends, function(index, depend){
if (!fieldMap[depend]) {
fieldMap[depend] = { fields: [] }
}
fieldMap[depend].fields.push(name)
})
})
$.each(fieldMap, function(index, depend){
if(fieldMap[index].hasOwnProperty('fields')) {
if(fieldMap[depend.fields[0]]) {
fieldMap[index].fieldsNext = [],
fieldMap[index].fieldsNext.push(fieldMap[fieldMap[index].fields[0]])
}
}
})
/*
* When a master is updated, refresh its slaves
*/
$.each(fieldMap, function(fieldName, toRefresh) {
$(document).on('change.oc.formwidget',
'[data-field-name="' + fieldName + '"]',
$.proxy(self.onRefreshDependants, self, fieldName, toRefresh)
);
})
}
/*
* Refresh a dependancy field
* Uses a throttle to prevent duplicate calls and click spamming
*/
FormWidget.prototype.onRefreshDependants = function(fieldName, toRefresh) {
var self = this,
form = this.$el,
formEl = this.$form,
fieldElements = this.getFieldElements()
if(toRefresh.hasOwnProperty('fieldsNext')) {
var fieldNameLv2 = toRefresh.fieldsNext[0].fields[0],
toRefreshLv2 = toRefresh.fieldsNext[0]
if(toRefresh.fieldsNext[0].hasOwnProperty('fieldsNext')) {
var fieldNameLv3 = toRefresh.fieldsNext[0].fieldsNext[0].fields[0],
toRefreshLv3 = toRefresh.fieldsNext[0].fieldsNext[0]
}
}
if (this.dependantUpdateTimers[fieldName] !== undefined) {
window.clearTimeout(this.dependantUpdateTimers[fieldName])
}
this.dependantUpdateTimers[fieldName] = window.setTimeout(function() {
var refreshData = $.extend({},
toRefresh,
paramToObj('data-refresh-data', self.options.refreshData)
)
formEl.request(self.options.refreshHandler, {
data: refreshData
}).success(function() {
self.toggleEmptyTabs()
})
}, this.dependantUpdateInterval)
if(fieldNameLv2) {
if (this.dependantUpdateTimers[fieldNameLv2] !== undefined) {
window.clearTimeout(this.dependantUpdateTimers[fieldNameLv2])
}
this.dependantUpdateTimers[fieldNameLv2] = window.setTimeout(function() {
var refreshData = $.extend({},
toRefreshLv2,
paramToObj('data-refresh-data', self.options.refreshData)
)
formEl.request(self.options.refreshHandler, {
data: refreshData
}).success(function() {
self.toggleEmptyTabs()
})
}, this.dependantUpdateInterval)
if(fieldNameLv3) {
if (this.dependantUpdateTimers[fieldNameLv3] !== undefined) {
window.clearTimeout(this.dependantUpdateTimers[fieldNameLv3])
}
this.dependantUpdateTimers[fieldNameLv3] = window.setTimeout(function() {
var refreshData = $.extend({},
toRefreshLv3,
paramToObj('data-refresh-data', self.options.refreshData)
)
formEl.request(self.options.refreshHandler, {
data: refreshData
}).success(function() {
self.toggleEmptyTabs()
})
}, this.dependantUpdateInterval)
}
}
$.each(toRefresh.fields, function(index, field) {
fieldElements.filter('[data-field-name="'+field+'"]:visible')
.addClass('loading-indicator-container size-form-field')
.loadIndicator()
})
if(toRefreshLv2) {
$.each(toRefreshLv2.fields, function(index, field) {
fieldElements.filter('[data-field-name="'+field+'"]:visible')
.addClass('loading-indicator-container size-form-field')
.loadIndicator()
})
if(toRefreshLv3) {
$.each(toRefreshLv3.fields, function(index, field) {
fieldElements.filter('[data-field-name="'+field+'"]:visible')
.addClass('loading-indicator-container size-form-field')
.loadIndicator()
})
}
}
}
/*
* Render tab form fields once a lazy tab is selected.
*/
FormWidget.prototype.bindLazyTabs = function() {
var tabControl = $('[data-control=tab]', this.$el),
tabContainer = $('.nav-tabs', tabControl)
tabContainer.on('click', '.tab-lazy [data-toggle="tab"]', function() {
var $el = $(this),
handlerName = $el.data('tab-lazy-handler')
$.request(handlerName, {
data: {
target: $el.data('target'),
name: $el.data('tab-name'),
section: $el.data('tab-section'),
},
success: function(data) {
this.success(data)
$el.parent().removeClass('tab-lazy')
// Trigger all input presets to populate new fields.
setTimeout(function() {
$('[data-input-preset]').each(function() {
var preset = $(this).data('oc.inputPreset')
if (preset && preset.$src) {
preset.$src.trigger('input')
}
})
}, 0)
}
})
})
// If initial active tab is lazy loaded, load it immediately
if ($('> li.active.tab-lazy', tabContainer).length) {
$('> li.active.tab-lazy > [data-toggle="tab"]', tabContainer).trigger('click')
}
}
/*
* Hides tabs that have no content, it is possible this can be
* called multiple times in a single cycle due to input.trigger.
*/
FormWidget.prototype.toggleEmptyTabs = function() {
var self = this,
form = this.$el
if (this.toggleEmptyTabsTimer !== undefined) {
window.clearTimeout(this.toggleEmptyTabsTimer)
}
this.toggleEmptyTabsTimer = window.setTimeout(function() {
var tabControl = $('[data-control=tab]', self.$el),
tabContainer = $('.nav-tabs', tabControl)
if (!tabControl.length || !form || !form.length || !$.contains(form.get(0), tabControl.get(0)))
return
/*
* Check each tab pane for form field groups
*/
$('.tab-pane:not(.lazy)', tabControl).each(function() {
$('[data-target="#' + $(this).attr('id') + '"]', tabControl)
.closest('li')
.toggle(!!$('> .form-group:not(:empty):not(.hide)', $(this)).length)
})
/*
* If a hidden tab was selected, select the first visible tab
*/
if (!$('> li.active:visible', tabContainer).length) {
$('> li:visible:first', tabContainer)
.find('> a:first')
.tab('show')
}
}, 1)
}
/*
* Makes sections collapsible by targeting every field after
* up until the next section
*/
FormWidget.prototype.bindCollapsibleSections = function() {
$('.section-field[data-field-collapsible]', this.$form)
.addClass('collapsed')
.find('.field-section:first')
.addClass('is-collapsible')
.end()
.on('click', function() {
$(this)
.toggleClass('collapsed')
.nextUntil('.section-field').toggle()
})
.nextUntil('.section-field').hide()
}
FormWidget.DEFAULTS = {
refreshHandler: null,
refreshData: {}
}
// 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
// ==============
function paramToObj(name, value) {
if (value === undefined) value = ''
if (typeof value == 'object') return value
try {
return ocJSON("{" + value + "}")
}
catch (e) {
throw new Error('Error parsing the '+name+' attribute value. '+e)
}
}
$(document).render(function() {
$('[data-control="formwidget"]').formWidget();
})
}(window.jQuery);
Mainly is "october.form.js" file. May you have any solution without monkey patch the core files?
Last updated
What exactly did you change in core js file? Can you submit a clean PR to october github repo with explanations?
mjauvin said:
What exactly did you change in core js file? Can you submit a clean PR to october github repo with explanations?
I have submitted PR. https://github.com/octobercms/october/pull/5519
Last updated
1-19 of 19