program.es
program.es

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?

mjauvin
mjauvin

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."

program.es
program.es

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.

program.es
program.es

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.

mjauvin
mjauvin

try this instead:

public function getFieldCAttribute()
{
    return $this->field_a + $this->field_b;
}

public function getFieldTotalAttribute()
{
    return $this->field_c * 2;
}
program.es
program.es

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?

mjauvin
mjauvin

Try adding this to your text input fields:

data-request="onAjax" data-track-input
program.es
program.es

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.

mjauvin
mjauvin

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
mjauvin

But it's not refreshing the form either. I'll get back to you on this.

program.es
program.es

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.

mjauvin
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.

mjauvin
mjauvin

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',
   ];
}
program.es
program.es

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;
}
mjauvin
mjauvin

First nesting level definitely works for me, except for field_total which does not update after field_c's value changes (second nesting level).

program.es
program.es

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
program.es
program.es

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

mjauvin
mjauvin

What exactly did you change in core js file? Can you submit a clean PR to october github repo with explanations?

program.es
program.es

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