Jelajahi Sumber

feat: add `onchange` parameter to some input functions

wangweimin 4 tahun lalu
induk
melakukan
de8ae4d92c

+ 1 - 0
docs/spec.rst

@@ -79,6 +79,7 @@ The ``inputs`` field is a list of input items, each input item is a ``dict``, th
 * label: Label of input field, required.
 * label: Label of input field, required.
 * type: Input type, required.
 * type: Input type, required.
 * name: Identifier of the input field, required.
 * name: Identifier of the input field, required.
+* onchange: bool, whether to push input value when input change
 * auto_focus: Set focus automatically. At most one item of ``auto_focus`` can be true in the input item list
 * auto_focus: Set focus automatically. At most one item of ``auto_focus`` can be true in the input item list
 * help_text: Help text for the input
 * help_text: Help text for the input
 * Additional HTML attribute of the input element
 * Additional HTML attribute of the input element

+ 27 - 23
pywebio/input.py

@@ -63,7 +63,7 @@ import os.path
 import logging
 import logging
 from collections.abc import Mapping
 from collections.abc import Mapping
 
 
-from .io_ctrl import single_input, input_control, output_register_callback
+from .io_ctrl import single_input, input_control, output_register_callback, send_msg
 from .session import get_current_session, get_current_task_id
 from .session import get_current_session, get_current_task_id
 from .utils import Setter, is_html_safe_value, parse_file_size
 from .utils import Setter, is_html_safe_value, parse_file_size
 from .platform import utils as platform_setting
 from .platform import utils as platform_setting
@@ -103,8 +103,8 @@ def _parse_args(kwargs, excludes=()):
     return kwargs, valid_func
     return kwargs, valid_func
 
 
 
 
-def input(label='', type=TEXT, *, validate=None, name=None, value=None, action=None, placeholder=None, required=None,
-          readonly=None, datalist=None, help_text=None, **other_html_attrs):
+def input(label='', type=TEXT, *, validate=None, name=None, value=None, action=None, onchange=None, placeholder=None,
+          required=None, readonly=None, datalist=None, help_text=None, **other_html_attrs):
     r"""Text input
     r"""Text input
 
 
     :param str label: Label of input field.
     :param str label: Label of input field.
@@ -170,6 +170,7 @@ def input(label='', type=TEXT, *, validate=None, name=None, value=None, action=N
 
 
         Note: When using :ref:`Coroutine-based session <coroutine_based_session>` implementation, the ``callback`` function can be a coroutine function.
         Note: When using :ref:`Coroutine-based session <coroutine_based_session>` implementation, the ``callback`` function can be a coroutine function.
 
 
+    :param callable onchange: A callback function which will be called when the value of this input field changed.
     :param str placeholder: A hint to the user of what can be entered in the input. It will appear in the input field when it has no value set.
     :param str placeholder: A hint to the user of what can be entered in the input. It will appear in the input field when it has no value set.
     :param bool required: Whether a value is required for the input to be submittable, default is ``False``
     :param bool required: Whether a value is required for the input to be submittable, default is ``False``
     :param bool readonly: Whether the value is readonly(not editable)
     :param bool readonly: Whether the value is readonly(not editable)
@@ -222,11 +223,11 @@ def input(label='', type=TEXT, *, validate=None, name=None, value=None, action=N
 
 
         return d
         return d
 
 
-    return single_input(item_spec, valid_func, preprocess_func)
+    return single_input(item_spec, valid_func, preprocess_func, onchange)
 
 
 
 
 def textarea(label='', *, rows=6, code=None, maxlength=None, minlength=None, validate=None, name=None, value=None,
 def textarea(label='', *, rows=6, code=None, maxlength=None, minlength=None, validate=None, name=None, value=None,
-             placeholder=None, required=None, readonly=None, help_text=None, **other_html_attrs):
+             onchange=None, placeholder=None, required=None, readonly=None, help_text=None, **other_html_attrs):
     r"""Text input area (multi-line text input)
     r"""Text input area (multi-line text input)
 
 
     :param int rows: The number of visible text lines for the input area. Scroll bar will be used when content exceeds.
     :param int rows: The number of visible text lines for the input area. Scroll bar will be used when content exceeds.
@@ -248,13 +249,13 @@ def textarea(label='', *, rows=6, code=None, maxlength=None, minlength=None, val
 
 
         Some commonly used Codemirror options are listed :ref:`here <codemirror_options>`.
         Some commonly used Codemirror options are listed :ref:`here <codemirror_options>`.
 
 
-    :param - label, validate, name, value, placeholder, required, readonly, help_text, other_html_attrs: Those arguments have the same meaning as for `input()`
+    :param - label, validate, name, value, onchange, placeholder, required, readonly, help_text, other_html_attrs: Those arguments have the same meaning as for `input()`
     :return: The string value that user input.
     :return: The string value that user input.
     """
     """
     item_spec, valid_func = _parse_args(locals())
     item_spec, valid_func = _parse_args(locals())
     item_spec['type'] = TEXTAREA
     item_spec['type'] = TEXTAREA
 
 
-    return single_input(item_spec, valid_func, lambda d: d)
+    return single_input(item_spec, valid_func, lambda d: d, onchange)
 
 
 
 
 def _parse_select_options(options):
 def _parse_select_options(options):
@@ -288,7 +289,7 @@ def _set_options_selected(options, value):
     return options
     return options
 
 
 
 
-def select(label='', options=None, *, multiple=None, validate=None, name=None, value=None, required=None,
+def select(label='', options=None, *, multiple=None, validate=None, name=None, value=None, onchange=None, required=None,
            help_text=None, **other_html_attrs):
            help_text=None, **other_html_attrs):
     r"""Drop-down selection
     r"""Drop-down selection
 
 
@@ -318,44 +319,43 @@ def select(label='', options=None, *, multiple=None, validate=None, name=None, v
        You can also set the initial selected option by setting the ``selected`` field in the ``options`` list item.
        You can also set the initial selected option by setting the ``selected`` field in the ``options`` list item.
     :type value: list or str
     :type value: list or str
     :param bool required: Whether to select at least one item, only available when ``multiple=True``
     :param bool required: Whether to select at least one item, only available when ``multiple=True``
-    :param - label, validate, name, help_text, other_html_attrs: Those arguments have the same meaning as for `input()`
+    :param - label, validate, name, onchange, help_text, other_html_attrs: Those arguments have the same meaning as for `input()`
     :return: If ``multiple=True``, return a list of the values in the ``options`` selected by the user; otherwise, return the single value selected by the user.
     :return: If ``multiple=True``, return a list of the values in the ``options`` selected by the user; otherwise, return the single value selected by the user.
     """
     """
     assert options is not None, 'Required `options` parameter in select()'
     assert options is not None, 'Required `options` parameter in select()'
 
 
-    item_spec, valid_func = _parse_args(locals(), excludes=['value'])
+    item_spec, valid_func = _parse_args(locals(), excludes=['value', 'onchange'])
     item_spec['options'] = _parse_select_options(options)
     item_spec['options'] = _parse_select_options(options)
     if value is not None:
     if value is not None:
         item_spec['options'] = _set_options_selected(item_spec['options'], value)
         item_spec['options'] = _set_options_selected(item_spec['options'], value)
     item_spec['type'] = SELECT
     item_spec['type'] = SELECT
 
 
-    return single_input(item_spec, valid_func, lambda d: d)
+    return single_input(item_spec, valid_func=valid_func, preprocess_func=lambda d: d, onchange_func=onchange)
 
 
 
 
-def checkbox(label='', options=None, *, inline=None, validate=None, name=None, value=None, help_text=None,
-             **other_html_attrs):
+def checkbox(label='', options=None, *, inline=None, validate=None, name=None, value=None, onchange=None,
+             help_text=None, **other_html_attrs):
     r"""A group of check box that allowing single values to be selected/deselected.
     r"""A group of check box that allowing single values to be selected/deselected.
 
 
     :param list options: List of options. The format is the same as the ``options`` parameter of the `select()` function
     :param list options: List of options. The format is the same as the ``options`` parameter of the `select()` function
     :param bool inline: Whether to display the options on one line. Default is ``False``
     :param bool inline: Whether to display the options on one line. Default is ``False``
     :param list value: The value list of the initial selected items.
     :param list value: The value list of the initial selected items.
        You can also set the initial selected option by setting the ``selected`` field in the ``options`` list item.
        You can also set the initial selected option by setting the ``selected`` field in the ``options`` list item.
-    :param - label, validate, name, help_text, other_html_attrs: Those arguments have the same meaning as for `input()`
+    :param - label, validate, name, onchange, help_text, other_html_attrs: Those arguments have the same meaning as for `input()`
     :return: A list of the values in the ``options`` selected by the user
     :return: A list of the values in the ``options`` selected by the user
     """
     """
     assert options is not None, 'Required `options` parameter in checkbox()'
     assert options is not None, 'Required `options` parameter in checkbox()'
 
 
-    item_spec, valid_func = _parse_args(locals())
+    item_spec, valid_func = _parse_args(locals(), excludes=['value'])
     item_spec['options'] = _parse_select_options(options)
     item_spec['options'] = _parse_select_options(options)
     if value is not None:
     if value is not None:
-        del item_spec['value']
         item_spec['options'] = _set_options_selected(item_spec['options'], value)
         item_spec['options'] = _set_options_selected(item_spec['options'], value)
     item_spec['type'] = CHECKBOX
     item_spec['type'] = CHECKBOX
 
 
-    return single_input(item_spec, valid_func, lambda d: d)
+    return single_input(item_spec, valid_func, lambda d: d, onchange)
 
 
 
 
-def radio(label='', options=None, *, inline=None, validate=None, name=None, value=None, required=None,
+def radio(label='', options=None, *, inline=None, validate=None, name=None, value=None, onchange=None, required=None,
           help_text=None, **other_html_attrs):
           help_text=None, **other_html_attrs):
     r"""A group of radio button. Only a single button can be selected.
     r"""A group of radio button. Only a single button can be selected.
 
 
@@ -364,7 +364,7 @@ def radio(label='', options=None, *, inline=None, validate=None, name=None, valu
     :param str value: The value of the initial selected items.
     :param str value: The value of the initial selected items.
        You can also set the initial selected option by setting the ``selected`` field in the ``options`` list item.
        You can also set the initial selected option by setting the ``selected`` field in the ``options`` list item.
     :param bool required: whether to must select one option. (the user can select nothing option by default)
     :param bool required: whether to must select one option. (the user can select nothing option by default)
-    :param - label, validate, name, help_text, other_html_attrs: Those arguments have the same meaning as for `input()`
+    :param - label, validate, name, onchange, help_text, other_html_attrs: Those arguments have the same meaning as for `input()`
     :return: The value of the option selected by the user, if the user does not select any value, return ``None``
     :return: The value of the option selected by the user, if the user does not select any value, return ``None``
     """
     """
     assert options is not None, 'Required `options` parameter in radio()'
     assert options is not None, 'Required `options` parameter in radio()'
@@ -379,7 +379,7 @@ def radio(label='', options=None, *, inline=None, validate=None, name=None, valu
         item_spec['options'][-1]['required'] = required
         item_spec['options'][-1]['required'] = required
     item_spec['type'] = RADIO
     item_spec['type'] = RADIO
 
 
-    return single_input(item_spec, valid_func, lambda d: d)
+    return single_input(item_spec, valid_func, lambda d: d, onchange)
 
 
 
 
 def _parse_action_buttons(buttons):
 def _parse_action_buttons(buttons):
@@ -605,6 +605,7 @@ def input_group(label='', inputs=None, validate=None, cancelable=False):
     spec_inputs = []
     spec_inputs = []
     preprocess_funcs = {}
     preprocess_funcs = {}
     item_valid_funcs = {}
     item_valid_funcs = {}
+    onchange_funcs = {}
     for single_input_return in inputs:
     for single_input_return in inputs:
         try:
         try:
             # 协程模式下,单项输入为协程对象,可以通过send(None)来获取传入单项输入的参数字典
             # 协程模式下,单项输入为协程对象,可以通过send(None)来获取传入单项输入的参数字典
@@ -619,14 +620,15 @@ def input_group(label='', inputs=None, validate=None, cancelable=False):
 
 
         assert all(
         assert all(
             k in (input_kwargs or {})
             k in (input_kwargs or {})
-            for k in ('item_spec', 'preprocess_func', 'valid_func')
+            for k in ('item_spec', 'preprocess_func', 'valid_func', 'onchange_func')
         ), "`inputs` value error in `input_group`. Did you forget to add `name` parameter in input function?"
         ), "`inputs` value error in `input_group`. Did you forget to add `name` parameter in input function?"
 
 
         input_name = input_kwargs['item_spec']['name']
         input_name = input_kwargs['item_spec']['name']
         if input_name in preprocess_funcs:
         if input_name in preprocess_funcs:
-            raise ValueError("Can't use same `name`:%s in different input in input group!!" % input_name)
+            raise ValueError('Duplicated input item name "%s" in same input group!' % input_name)
         preprocess_funcs[input_name] = input_kwargs['preprocess_func']
         preprocess_funcs[input_name] = input_kwargs['preprocess_func']
         item_valid_funcs[input_name] = input_kwargs['valid_func']
         item_valid_funcs[input_name] = input_kwargs['valid_func']
+        onchange_funcs[input_name] = input_kwargs['onchange_func']
         spec_inputs.append(input_kwargs['item_spec'])
         spec_inputs.append(input_kwargs['item_spec'])
 
 
     if all('auto_focus' not in i for i in spec_inputs):  # No `auto_focus` parameter is set for each input item
     if all('auto_focus' not in i for i in spec_inputs):  # No `auto_focus` parameter is set for each input item
@@ -637,5 +639,7 @@ def input_group(label='', inputs=None, validate=None, cancelable=False):
                 break
                 break
 
 
     spec = dict(label=label, inputs=spec_inputs, cancelable=cancelable)
     spec = dict(label=label, inputs=spec_inputs, cancelable=cancelable)
-    return input_control(spec, preprocess_funcs=preprocess_funcs, item_valid_funcs=item_valid_funcs,
+    return input_control(spec, preprocess_funcs=preprocess_funcs,
+                         item_valid_funcs=item_valid_funcs,
+                         onchange_funcs=onchange_funcs,
                          form_valid_funcs=validate)
                          form_valid_funcs=validate)

+ 32 - 9
pywebio/io_ctrl.py

@@ -6,7 +6,7 @@ import json
 import logging
 import logging
 from collections import UserList
 from collections import UserList
 from functools import partial, wraps
 from functools import partial, wraps
-
+from collections.abc import Mapping
 from .session import chose_impl, next_client_event, get_current_task_id, get_current_session
 from .session import chose_impl, next_client_event, get_current_task_id, get_current_session
 from .utils import random_str
 from .utils import random_str
 
 
@@ -176,7 +176,7 @@ def send_msg(cmd, spec=None):
 
 
 
 
 @chose_impl
 @chose_impl
-def single_input(item_spec, valid_func, preprocess_func):
+def single_input(item_spec, valid_func, preprocess_func, onchange_func=None):
     """
     """
     Note: 鲁棒性在上层完成
     Note: 鲁棒性在上层完成
     将单个input构造成input_group,并获取返回值
     将单个input构造成input_group,并获取返回值
@@ -184,10 +184,17 @@ def single_input(item_spec, valid_func, preprocess_func):
     :param valid_func: Not None
     :param valid_func: Not None
     :param preprocess_func: Not None, 预处理函数,在收到用户提交的单项输入的原始数据后用于在校验前对数据进行预处理
     :param preprocess_func: Not None, 预处理函数,在收到用户提交的单项输入的原始数据后用于在校验前对数据进行预处理
     """
     """
+    if onchange_func is not None:
+        item_spec['onchange'] = True
+    else:
+        onchange_func = lambda _: None
+        item_spec.pop('onchange', None)
+
     if item_spec.get('name') is None:  # single input
     if item_spec.get('name') is None:  # single input
         item_spec['name'] = 'data'
         item_spec['name'] = 'data'
     else:  # as input_group item
     else:  # as input_group item
-        return dict(item_spec=item_spec, valid_func=valid_func, preprocess_func=preprocess_func)
+        return dict(item_spec=item_spec, valid_func=valid_func,
+                    preprocess_func=preprocess_func, onchange_func=onchange_func)
 
 
     label = item_spec['label']
     label = item_spec['label']
     name = item_spec['name']
     name = item_spec['name']
@@ -197,23 +204,27 @@ def single_input(item_spec, valid_func, preprocess_func):
     item_spec.setdefault('auto_focus', True)  # 如果没有设置autofocus参数,则开启参数  todo CHECKBOX, RADIO 特殊处理
     item_spec.setdefault('auto_focus', True)  # 如果没有设置autofocus参数,则开启参数  todo CHECKBOX, RADIO 特殊处理
 
 
     spec = dict(label=label, inputs=[item_spec])
     spec = dict(label=label, inputs=[item_spec])
-    data = yield input_control(spec, {name: preprocess_func}, {name: valid_func})
+    data = yield input_control(spec=spec,
+                               preprocess_funcs={name: preprocess_func},
+                               item_valid_funcs={name: valid_func},
+                               onchange_funcs={name: onchange_func})
     return data[name]
     return data[name]
 
 
 
 
 @chose_impl
 @chose_impl
-def input_control(spec, preprocess_funcs, item_valid_funcs, form_valid_funcs=None):
+def input_control(spec, preprocess_funcs, item_valid_funcs, onchange_funcs, form_valid_funcs=None):
     """
     """
     发送input命令,监听事件,验证输入项,返回结果
     发送input命令,监听事件,验证输入项,返回结果
     :param spec:
     :param spec:
     :param preprocess_funcs: keys 严格等于 spec中的name集合
     :param preprocess_funcs: keys 严格等于 spec中的name集合
     :param item_valid_funcs: keys 严格等于 spec中的name集合
     :param item_valid_funcs: keys 严格等于 spec中的name集合
-    :param form_valid_funcs:
+    :param onchange_funcs: keys 严格等于 spec中的name集合
+    :param form_valid_funcs: can be ``None``
     :return:
     :return:
     """
     """
     send_msg('input_group', spec)
     send_msg('input_group', spec)
 
 
-    data = yield input_event_handle(item_valid_funcs, form_valid_funcs, preprocess_funcs)
+    data = yield input_event_handle(item_valid_funcs, form_valid_funcs, preprocess_funcs, onchange_funcs)
 
 
     send_msg('destroy_form')
     send_msg('destroy_form')
     return data
     return data
@@ -241,14 +252,24 @@ def check_item(name, data, valid_func, preprocess_func):
     return True
     return True
 
 
 
 
+def trigger_onchange(event_data, onchange_funcs):
+    name = event_data['name']
+    onchange_func = onchange_funcs[name]
+    try:
+        onchange_func(event_data['value'])
+    except Exception as e:
+        logger.warning('Get %r in onchange function for name:"%s"', e, name)
+
+
 @chose_impl
 @chose_impl
-def input_event_handle(item_valid_funcs, form_valid_funcs, preprocess_funcs):
+def input_event_handle(item_valid_funcs, form_valid_funcs, preprocess_funcs, onchange_funcs):
     """
     """
     根据提供的校验函数处理表单事件
     根据提供的校验函数处理表单事件
     :param item_valid_funcs: map(name -> valid_func)  valid_func 为 None 时,不进行验证
     :param item_valid_funcs: map(name -> valid_func)  valid_func 为 None 时,不进行验证
                         valid_func: callback(data) -> error_msg or None
                         valid_func: callback(data) -> error_msg or None
     :param form_valid_funcs: callback(data) -> (name, error_msg) or None
     :param form_valid_funcs: callback(data) -> (name, error_msg) or None
-    :param preprocess_funcs:
+    :param preprocess_funcs: map(name -> process_func)
+    :param onchange_funcs: map(name -> onchange_func)
     :return:
     :return:
     """
     """
     while True:
     while True:
@@ -260,6 +281,8 @@ def input_event_handle(item_valid_funcs, form_valid_funcs, preprocess_funcs):
                 onblur_name = event_data['name']
                 onblur_name = event_data['name']
                 check_item(onblur_name, event_data['value'], item_valid_funcs[onblur_name],
                 check_item(onblur_name, event_data['value'], item_valid_funcs[onblur_name],
                            preprocess_funcs[onblur_name])
                            preprocess_funcs[onblur_name])
+            elif input_event == 'change':
+                trigger_onchange(event_data, onchange_funcs)
 
 
         elif event_name == 'from_submit':
         elif event_name == 'from_submit':
             all_valid = True
             all_valid = True

+ 2 - 2
webiojs/src/models/input/base.ts

@@ -43,13 +43,13 @@ export class InputItem {
 
 
     }
     }
 
 
-    protected send_value_listener(input_item: this, event: { type: string }) {
+    protected send_value_listener(input_item: this, event: { type: string }, event_name?:string) {
         // let this_elem = $(this);
         // let this_elem = $(this);
         input_item.session.send_message({
         input_item.session.send_message({
             event: "input_event",
             event: "input_event",
             task_id: input_item.task_id,
             task_id: input_item.task_id,
             data: {
             data: {
-                event_name: event.type.toLowerCase(),
+                event_name: event_name || event.type.toLowerCase(),
                 name: input_item.spec.name,
                 name: input_item.spec.name,
                 value: input_item.get_value()
                 value: input_item.get_value()
             }
             }

+ 5 - 0
webiojs/src/models/input/checkbox_radio.ts

@@ -56,6 +56,11 @@ export class CheckboxRadio extends InputItem {
             input_elem.on("blur", (e) => {
             input_elem.on("blur", (e) => {
                 this.send_value_listener(this, e);
                 this.send_value_listener(this, e);
             });
             });
+            if(this.spec.onchange){
+                input_elem.on("change", (e) => {
+                    this.send_value_listener(this, e);
+                });
+            }
             input_elem.val(JSON.stringify(options[idx].value));
             input_elem.val(JSON.stringify(options[idx].value));
             // 将额外的html参数加到input标签上
             // 将额外的html参数加到input标签上
             for (let key in options[idx]) {
             for (let key in options[idx]) {

+ 6 - 1
webiojs/src/models/input/input.ts

@@ -53,12 +53,17 @@ export class Input extends InputItem {
             });
             });
         });
         });
 
 
-        let input_elem = this.element.find('#' + id_name);
+        let input_elem = this.element.find('input');
         // blur事件时,发送当前值到服务器
         // blur事件时,发送当前值到服务器
         input_elem.on("blur", (e) => {
         input_elem.on("blur", (e) => {
             if(this.get_value())
             if(this.get_value())
                 this.send_value_listener(this, e)
                 this.send_value_listener(this, e)
         });
         });
+        if(spec.onchange){
+            input_elem.on("input", (e) => {
+                this.send_value_listener(this, e, 'change');
+            });
+        }
 
 
         // 将额外的html参数加到input标签上
         // 将额外的html参数加到input标签上
         const ignore_keys = {
         const ignore_keys = {

+ 5 - 0
webiojs/src/models/input/select.ts

@@ -38,6 +38,11 @@ export class Select extends InputItem {
         this.element.find('select').on("blur", (e) => {
         this.element.find('select').on("blur", (e) => {
             this.send_value_listener(this, e);
             this.send_value_listener(this, e);
         });
         });
+        if(spec.onchange){
+            this.element.find('select').on("change", (e) => {
+                this.send_value_listener(this, e);
+            });
+        }
         return this.element;
         return this.element;
     }
     }
 
 

+ 11 - 3
webiojs/src/models/input/textarea.ts

@@ -36,14 +36,18 @@ export class Textarea extends InputItem {
         let that = this;
         let that = this;
 
 
         let spec = deep_copy(this.spec);
         let spec = deep_copy(this.spec);
-        const id_name = spec.name + '-' + Math.floor(Math.random() * Math.floor(9999));
-        spec['id_name'] = id_name;
+        spec['id_name'] = spec.name + '-' + Math.floor(Math.random() * Math.floor(9999));
         let html = Mustache.render(textarea_input_tpl, spec);
         let html = Mustache.render(textarea_input_tpl, spec);
         this.element = $(html);
         this.element = $(html);
-        let input_elem = this.element.find('#' + id_name);
+        let input_elem = this.element.find('textarea');
 
 
         // blur事件时,发送当前值到服务器
         // blur事件时,发送当前值到服务器
         // input_elem.on('blur', this.send_value_listener);
         // input_elem.on('blur', this.send_value_listener);
+        if (spec.onchange) {
+            input_elem.on("input", (e) => {
+                this.send_value_listener(this, e, 'change');
+            });
+        }
 
 
         // 将额外的html参数加到input标签上
         // 将额外的html参数加到input标签上
         const ignore_keys = make_set(['value', 'type', 'label', 'invalid_feedback', 'valid_feedback', 'help_text', 'rows', 'code']);
         const ignore_keys = make_set(['value', 'type', 'label', 'invalid_feedback', 'valid_feedback', 'help_text', 'rows', 'code']);
@@ -88,6 +92,10 @@ export class Textarea extends InputItem {
         if (first_show && this.spec.code) {
         if (first_show && this.spec.code) {
             this.code_mirror = CodeMirror.fromTextArea(this.element.find('textarea')[0], this.code_mirror_config);
             this.code_mirror = CodeMirror.fromTextArea(this.element.find('textarea')[0], this.code_mirror_config);
             CodeMirror.autoLoadMode(this.code_mirror, this.code_mirror_config.mode);
             CodeMirror.autoLoadMode(this.code_mirror, this.code_mirror_config.mode);
+            if (this.spec.onchange)
+                this.code_mirror.on('change', (instance: object, changeObj: object) => {
+                    this.send_value_listener(this, null, 'change');
+                })
             this.code_mirror.setSize(null, 20 * this.spec.rows);
             this.code_mirror.setSize(null, 20 * this.spec.rows);
         }
         }
     };
     };