Skip to content

Commit

Permalink
add put_scope() to replace output()
Browse files Browse the repository at this point in the history
  • Loading branch information
wang0618 committed Oct 29, 2021
1 parent ebc29a1 commit b1629a0
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 61 deletions.
62 changes: 37 additions & 25 deletions docs/guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -297,29 +297,6 @@ In addition, you can use `put_widget() <pywebio.output.put_widget>` to make your

For a full list of functions that accept ``put_xxx()`` calls as content, see :ref:`Output functions list <output_func_list>`

**Placeholder**

When using combination output, if you want to dynamically update the ``put_xxx()`` content after it has been output,
you can use the `output() <pywebio.output.output>` function. `output() <pywebio.output.output>` is like a placeholder,
it can be passed in anywhere that ``put_xxx()`` can passed in. And after being output, the content can also be modified:

.. exportable-codeblock::
:name: output
:summary: Output placeholder——`output()`

hobby = output('Coding') # equal to output(put_text('Coding'))
put_table([
['Name', 'Hobbies'],
['Wang', hobby] # hobby is initialized to Coding
])
## ----

hobby.reset('Movie') # hobby is reset to Movie
## ----
hobby.append('Music', put_text('Drama')) # append Music, Drama to hobby
## ----
hobby.insert(0, put_markdown('**Coding**')) # insert the Coding into the top of the hobby

**Context Manager**

Some output functions that accept ``put_xxx()`` calls as content can be used as context manager:
Expand Down Expand Up @@ -525,11 +502,46 @@ The above code will generate the following scope layout::
│ └─────────────────────┘ │
└─────────────────────────┘

.. _put_scope:

**put_scope()**

We already know that the scope is a container of output content. So can we use this container as a sub-item
of a output (like, set a cell in table as a container)? Yes, you can use `put_scope() <pywebio.output.put_scope>` to
create a scope explicitly.
The function name starts with ``put_``, which means it can be pass to the functions that accept ``put_xxx()`` calls.

.. exportable-codeblock::
:name: put_scope
:summary: `put_scope()`

put_table([
['Name', 'Hobbies'],
['Tom', put_scope('hobby', content=put_text('Coding'))] # hobby is initialized to coding
])

## ----
with use_scope('hobby', clear=True):
put_text('Movie') # hobby is reset to Movie

## ----
# append Music, Drama to hobby
with use_scope('hobby'):
put_text('Music')
put_text('Drama')

## ----
# insert the Coding into the top of the hobby
put_markdown('**Coding**', scope='hobby', position=0)


.. caution:: It is not allowed to have two scopes with the same name in the application.

**Scope control**

In addition to `use_scope() <pywebio.output.use_scope>`, PyWebIO also provides the following scope control functions:
In addition to `use_scope() <pywebio.output.use_scope>` and `put_scope() <pywebio.output.put_scope>`,
PyWebIO also provides the following scope control functions:

* `set_scope(name) <pywebio.output.set_scope>` : Create scope at current location(or specified location)
* `clear(scope) <pywebio.output.clear>` : Clear the contents of the scope
* `remove(scope) <pywebio.output.remove>` : Remove scope
* `scroll_to(scope) <pywebio.output.scroll_to>` : Scroll the page to the scope
Expand Down
5 changes: 5 additions & 0 deletions docs/spec.rst
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,11 @@ Unique attributes of different types:

* input: input spec, same as the item of ``input_group.inputs``

* type: scope

* dom_id: the DOM id need to be set to this widget
* contents list: list of output spec

pin_value
^^^^^^^^^^^^^^^

Expand Down
60 changes: 44 additions & 16 deletions pywebio/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@
+--------------------+---------------------------+------------------------------------------------------------+
| | **Name** | **Description** |
+--------------------+---------------------------+------------------------------------------------------------+
| Output Scope | `set_scope` | Create a new scope |
| Output Scope | `put_scope` | Create a new scope |
| +---------------------------+------------------------------------------------------------+
| | `get_scope` | Get the scope name in the runtime scope stack |
| | `use_scope`:sup:`†` | Enter a scope |
| +---------------------------+------------------------------------------------------------+
| | `get_scope` | Get the current scope name in the runtime scope stack |
| +---------------------------+------------------------------------------------------------+
| | `clear` | Clear the content of scope |
| +---------------------------+------------------------------------------------------------+
| | `remove` | Remove the scope |
| +---------------------------+------------------------------------------------------------+
| | `scroll_to` | Scroll the page to the scope |
| +---------------------------+------------------------------------------------------------+
| | `use_scope`:sup:`†` | Open or enter a scope |
+--------------------+---------------------------+------------------------------------------------------------+
| Content Outputting | `put_text` | Output plain text |
| +---------------------------+------------------------------------------------------------+
Expand Down Expand Up @@ -95,12 +95,12 @@
* :ref:`Use Guide: Output Scope <output_scope>`
.. autofunction:: set_scope
.. autofunction:: put_scope
.. autofunction:: use_scope
.. autofunction:: get_scope
.. autofunction:: clear
.. autofunction:: remove
.. autofunction:: scroll_to
.. autofunction:: use_scope
Content Outputting
-----------------------
Expand Down Expand Up @@ -232,7 +232,7 @@

logger = logging.getLogger(__name__)

__all__ = ['Position', 'remove', 'scroll_to', 'put_tabs',
__all__ = ['Position', 'remove', 'scroll_to', 'put_tabs', 'put_scope',
'put_text', 'put_html', 'put_code', 'put_markdown', 'use_scope', 'set_scope', 'clear', 'remove',
'put_table', 'put_buttons', 'put_image', 'put_file', 'PopupSize', 'popup', 'put_button',
'close_popup', 'put_widget', 'put_collapse', 'put_link', 'put_scrollable', 'style', 'put_column',
Expand Down Expand Up @@ -1390,10 +1390,30 @@ def put_grid(content, cell_width='auto', cell_height='auto', cell_widths=None, c
return put_widget(template=tpl, data=dict(contents=content), scope=scope, position=position)


@safely_destruct_output_when_exp('content')
def put_scope(name, content=[], scope=None, position=OutputPosition.BOTTOM) -> Output:
"""Output a scope
:param str name:
:param list/put_xxx() content: The initial content of the scope, can be ``put_xxx()`` or a list of it.
:param int scope, position: Those arguments have the same meaning as for `put_text()`
"""
if not isinstance(content, list):
content = [content]

dom_id = scope2dom(name, no_css_selector=True)

spec = _get_output_spec('scope', dom_id=dom_id, contents=content, scope=scope, position=position)
return Output(spec)


@safely_destruct_output_when_exp('contents')
def output(*contents):
"""Placeholder of output
.. deprecated:: 1.5
See :ref:`User Guide <put_scope>` for new way to set css style for output.
``output()`` can be passed in anywhere that ``put_xxx()`` can passed in. A handler it returned by ``output()``,
and after being output, the content can also be modified by the handler (See code example below).
Expand Down Expand Up @@ -1431,6 +1451,10 @@ def output(*contents):
"""

import warnings
warnings.warn("`pywebio.output.output()` is deprecated since v1.5 and will remove in the future version, "
"use `pywebio.output.put_scope()` instead", DeprecationWarning, stacklevel=2)

class OutputHandler(Output):
"""
与 `Output` 的不同在于, 不会在销毁时(__del__)自动输出
Expand Down Expand Up @@ -1687,17 +1711,16 @@ def show_msg():
clear_scope = clear


def use_scope(name=None, clear=False, create_scope=True, **scope_params):
"""Open or enter a scope. Can be used as context manager and decorator.
def use_scope(name=None, clear=False, **kwargs):
"""use_scope(name=None, clear=False)
Open or enter a scope. Can be used as context manager and decorator.
See :ref:`User manual - use_scope() <use_scope>`
:param str name: Scope name. If it is None, a globally unique scope name is generated.
(When used as context manager, the context manager will return the scope name)
:param bool clear: Whether to clear the contents of the scope before entering the scope.
:param bool create_scope: Whether to create scope when scope does not exist.
:param scope_params: Extra parameters passed to `set_scope()` when need to create scope.
Only available when ``create_scope=True``.
:Usage:
Expand All @@ -1711,17 +1734,22 @@ def app():
put_xxx()
"""
# For backward compatible
# :param bool create_scope: Whether to create scope when scope does not exist.
# :param scope_params: Extra parameters passed to `set_scope()` when need to create scope.
# Only available when ``create_scope=True``.
create_scope = kwargs.pop('create_scope', True)
scope_params = kwargs

if name is None:
name = random_str(10)
else:
assert is_html_safe_value(name), "Scope name only allow letter/digit/'_'/'-' char."

def before_enter():
if create_scope:
set_scope(name, **scope_params)

if clear:
clear_scope(name)
if_exist = 'clear' if clear else None
set_scope(name, if_exist=if_exist, **scope_params)

return use_scope_(name=name, before_enter=before_enter)

Expand Down
13 changes: 13 additions & 0 deletions test/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,19 @@ def edit_row(choice, row):
hobby.append(put_text('Music'), put_text('Drama'))
hobby.insert(0, put_markdown('**Coding**'))

put_table([
['Name', 'Hobbies'],
['Tom', put_scope('hobby', content=put_text('Coding'))]
])

with use_scope('hobby', clear=True):
put_text('Movie') # hobby is reset to Movie

with use_scope('hobby'):
put_text('Music')
put_text('Drama')

put_markdown('**Coding**', scope='hobby', position=0)


def background_output():
Expand Down
21 changes: 19 additions & 2 deletions webiojs/src/handlers/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,27 @@ import {body_scroll_to} from "../utils";

import {getWidgetElement} from "../models/output"
import {CommandHandler} from "./base";
import {AfterPinShow} from "../models/pin";

const DISPLAY_NONE_TAGS = ['script', 'style'];

let after_show_callbacks: (() => void) [] = [];

// register a callback to execute after the current output widget showing
export function AfterCurrentOutputWidgetShow(callback: () => void){
after_show_callbacks.push(callback);
}

export function trigger_output_widget_show_event() {
for (let cb of after_show_callbacks) {
try {
cb.call(this);
} catch (e) {
console.error('Error in callback of pin widget show event.');
}
}
after_show_callbacks = [];
}

export class OutputHandler implements CommandHandler {
session: Session;

Expand Down Expand Up @@ -79,7 +96,7 @@ export class OutputHandler implements CommandHandler {
else if (state.AutoScrollBottom && output_to_root)
this.scroll_bottom();
}
AfterPinShow();
trigger_output_widget_show_event();
} else if (msg.command === 'output_ctl') {
this.handle_output_ctl(msg);
}
Expand Down
4 changes: 2 additions & 2 deletions webiojs/src/handlers/popup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {Command, Session} from "../session";

import {render_tpl} from "../models/output"
import {CommandHandler} from "./base";
import {AfterPinShow} from "../models/pin";
import {trigger_output_widget_show_event} from "./output";


export class PopupHandler implements CommandHandler {
Expand Down Expand Up @@ -32,7 +32,7 @@ export class PopupHandler implements CommandHandler {

let elem = PopupHandler.get_element(msg.spec);
this.body.append(elem);
AfterPinShow();
trigger_output_widget_show_event();

// 弹窗关闭后就立即销毁
elem.on('hidden.bs.modal', function (e) {
Expand Down
4 changes: 3 additions & 1 deletion webiojs/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ const translations: { [lang: string]: { [msgid: string]: string } } = {
"submit": "Submit",
"reset": "Reset",
"cancel": "Cancel",
"duplicated_pin_name": "This pin widget has expired (due to the output of a new pin widget with the same name ).",
"duplicated_pin_name": "This pin widget has expired (due to the output of a new pin widget with the same name).",
"browse_file": "Browse",
"duplicated_scope_name": "Error: The name of this scope is duplicated with the previous one!",
},
"zh": {
"disconnected_with_server": "与服务器连接已断开,请刷新页面重新操作",
Expand All @@ -28,6 +29,7 @@ const translations: { [lang: string]: { [msgid: string]: string } } = {
"cancel": "取消",
"duplicated_pin_name": "该 Pin widget 已失效(由于输出了新的同名 pin widget)",
"browse_file": "浏览文件",
"duplicated_scope_name": "错误: 此scope与已有scope重复!",
},
"ru": {
"disconnected_with_server": "Соединение с сервером потеряно, пожалуйста перезагрузите страницу",
Expand Down
31 changes: 29 additions & 2 deletions webiojs/src/models/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {b64toBlob, randomid} from "../utils";
import * as marked from 'marked';
import {pushData} from "../session";
import {PinWidget} from "./pin";
import {t} from "../i18n";
import {AfterCurrentOutputWidgetShow} from "../handlers/output";

export interface Widget {
handle_type: string;
Expand Down Expand Up @@ -199,14 +201,39 @@ let TabsWidget = {
}
};


const SCOPE_TPL = `<div>
{{#contents}}
{{& pywebio_output_parse}}
{{/contents}}
</div>`;
let ScopeWidget = {
handle_type: 'scope',
get_element: function (spec: {dom_id:string, contents: any[]}) {
let elem = render_tpl(SCOPE_TPL, spec);
// need to check the duplicate id after current output widget shown.
// because the current widget may have multiple sub-widget which have same dom id.
AfterCurrentOutputWidgetShow(()=>{
if($(`#${spec.dom_id}`).length !== 0){
let tip = `<p style="color: grey; border:1px solid #ced4da; padding: .375rem .75rem;">${t("duplicated_scope_name")}</p>`;
elem.empty().html(tip);
}else{
elem.attr('id', spec.dom_id);
}
})
return elem;
}
};


let CustomWidget = {
handle_type: 'custom_widget',
get_element: function (spec: { template: string, data: { [i: string]: any } }) {
return render_tpl(spec.template, spec.data);
}
};

let all_widgets: Widget[] = [Text, Markdown, Html, Buttons, File, Table, CustomWidget, TabsWidget, PinWidget];
let all_widgets: Widget[] = [Text, Markdown, Html, Buttons, File, Table, CustomWidget, TabsWidget, PinWidget, ScopeWidget];


let type2widget: { [i: string]: Widget } = {};
Expand Down Expand Up @@ -283,7 +310,7 @@ export function render_tpl(tpl: string, data: { [i: string]: any }) {
let sub_elem = getWidgetElement(spec);
elem.find(`#${dom_id}`).replaceWith(sub_elem);
} catch (e) {
console.error('Error when render widget: \n%s', JSON.stringify(spec));
console.error('Error when render widget: \n%s\nSPEC:%s', e, JSON.stringify(spec));
}
}
return elem;
Expand Down
Loading

0 comments on commit b1629a0

Please sign in to comment.