LoginSignup
1
0

More than 3 years have passed since last update.

Dashでdcc.CheckListのチェック状態を全選択・全解除するボタンの実装

Posted at

日本語と英語で検索しても全然ロクな文献がヒットしなかったので、検索ひっかかるように併記しておきます

Adding a 'Select All' and 'Remove All' Button to a dcc.CheckList.

達成したいこと

  • 複数個のチェックボックスを、別に用意したボタンによって全選択・全解除する

結果だけ知りたい方は下へスクロールしてください

やったこと

dcc.CheckListのvalueをcallbackで制御する

チェックリストとボタンだけのさっぱりした環境で調査

スクリーンショット 2020-02-19 15.43.06.png

当初のソース / at first
python {ファイル名.py} access localhost:{任意のポート}
で起動できます

import dash
import dash_core_components as dcc
import dash_html_components as html
import dash_table
from dash.dependencies import Input, Output, State
from flask import Flask, request


server = Flask(__name__)
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets, server=server)

app.title = 'checklist-test'

selected_key = None


checklists = dcc.Checklist(
    id='checklist-states',
    options=[
        {'label': 'New York City', 'value': 'NYC'},
        {'label': 'Montréal', 'value': 'MTL'},
        {'label': 'San Francisco', 'value': 'SF'}
    ],
    value=['MTL', 'SF']
)


app.layout = html.Div(id='main',children=[
    html.H1(children='チェックリストのテスト'),
    dcc.Location(id='location', refresh=False),
    html.Div(className='main-block', children=[checklists]),
    html.Div(className='second', children=[
        html.Button('全選択', id='filter-check-button', className='filter_button', n_clicks=0),
        html.Button('全解除', id='filter-remove-button', className='filter_button', n_clicks=0)
        ])
    ])


@app.callback(
    [Output('checklist-states', 'value')],
    [Input('filter-check-button', 'n_clicks'),
    Input('filter-remove-button', 'n_clicks')]
)
def remove_check(all_check, all_remove):

    if all_check:
        return ['NYC', 'MTL', 'SF']
    if all_remove:
        return []



if __name__ == '__main__':
    app.run_server(host='0.0.0.0', debug=True)

CheckListのvalueへ任意のリストを返却すればいけるっしょ!と思ったが違う

全選択ボタンを押下したとき return ['NYC', 'MTL', 'SF']

dash.exceptions.InvalidCallbackReturnValue: Invalid number of output values for ..checklist-states.value...
 Expected 1 got 3

全解除ボタンを押下したとき return []

dash.exceptions.InvalidCallbackReturnValue: Invalid number of output values for ..checklist-states.value...
 Expected 1 got 0

どうも勝手に皮むきするっぽいので応急処置

if all_check:
    return [['NYC', 'MTL', 'SF']]
if all_remove:
    return [[]]

動くもののエラーは出る
どうもボタンがロードされる時?に一度実行されるっぽく、もろもろが未定義な状態で返り値を渡してしまうためのエラーっぽい

dash.exceptions.InvalidCallbackReturnValue: The callback ..checklist-states.value.. is a multi-output.
Expected the output type to be a list or tuple but got None.

クリック以外で呼ばれたときを明示的に対処する

checklistのvalueをStateを使って読み込む

callbackにState
    @app.callback(
    [Output('checklist-states', 'value')],
    [Input('filter-check-button', 'n_clicks'),
    Input('filter-remove-button', 'n_clicks')],
    [State('checklist-states', 'value')]    # callbackが発生したときに値を取得する
)
def update_check(all_check, all_remove, value_checking):

どっちもNoneなら元々valueに入ってる値を返す
皮むき対策も忘れずに

    if all_check:
        return [['NYC', 'MTL', 'SF']]

    if all_remove:
        return [[]]

    if all_check is None and all_remove is None:
        return [value_checking]    # 皮むき対策

ボタンを何回もクリックする

n_clicksがダメっぽい

いけた!と思ったらクリック1発目しか機能しない
調べてみるとn_clickはクリック回数をカウントするという機能らしく、クリックすると0,1,2,3,4...と値が増えていくらしい

これはこの野郎…!と思いながら撮ったSS
スクリーンショット 2020-02-19 17.38.41.png

現在Dashには、そもそもクリックされたことだけを検知する機能は実装されていないらしい
代わりに クリックしたunixtimestamp(ミリ秒まで)を取得する n_clicks_timestampというものが用意されているので、それを使用する

n_clicks_timestampを使う

イベントが発生したタイミングと今が1秒以内ならオッケーという処理にしておく

    if not all_check is None:
        if (time.time() * 1000 - all_check) < 1000:
            return [['NYC', 'MTL', 'SF']]

    if not all_remove is None:
        if (time.time() * 1000 - all_remove) < 1000:
            return [[]]

    if all_check is None and all_remove is None:
        return [checking]

結果

𝐬𝐮𝐜𝐜𝐞𝐬𝐬

8nd91-kgmbx.gif

最終的なソース / in the end

import dash
import dash_core_components as dcc
import dash_html_components as html
import dash_table
from dash.dependencies import Input, Output, State
from flask import Flask, request

import time

server = Flask(__name__)
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
app = dash.Dash(__name__, external_stylesheets=external_stylesheets, server=server)

app.title = 'checklist-test'

selected_key = None


checklists = dcc.Checklist(
    id='checklist-states',
    options=[
        {'label': 'New York City', 'value': 'NYC'},
        {'label': 'Montréal', 'value': 'MTL'},
        {'label': 'San Francisco', 'value': 'SF'}
    ],
    value=['MTL', 'SF']
)


app.layout = html.Div(id='main',children=[
    html.H1(children='チェックリストのテスト'),
    dcc.Location(id='location', refresh=False),
    html.Div(className='main-block', children=[checklists]),
    html.Div(className='second', children=[
        html.Button('全選択', id='filter-check-button', className='filter_button'),
        html.Button('全解除', id='filter-remove-button', className='filter_button')
        ])
    ])


@app.callback(
    [Output('checklist-states', 'value')],
    [Input('filter-check-button', 'n_clicks_timestamp'),
    Input('filter-remove-button', 'n_clicks_timestamp')],
    [State('checklist-states', 'value')]
)
def update_check(all_check, all_remove, checking):

        if not all_check is None:
            if (time.time() * 1000 - all_check) < 1000:
                return [['NYC', 'MTL', 'SF']]

        if not all_remove is None:
            if (time.time() * 1000 - all_remove) < 1000:
                return [[]]

        if all_check is None and all_remove is None:
            return [checking]



if __name__ == '__main__':
    app.run_server(host='0.0.0.0', debug=True)
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0