flet icon indicating copy to clipboard operation
flet copied to clipboard

Updating `SearchBar.controls` doesn't trigger it's rebuild

Open kXborg opened this issue 4 months ago • 9 comments

The example of SearchBar provided in the documentation opens a static dropdown. As it apparent from the controls that ListTile titles are obtained using a loop.

            ft.ListTile(title=ft.Text(f"Color {i}"), on_click=close_anchor, data=i)
            for i in range(10)
        ]

I am not very clear about the use of for loop inside controls. Can't find any syntax in the documentation.

I want the dropdown list to change based on text entered in the text filed. My approach was to update the ListTile controls during on_change event. However, not sure how to access the items in a loop and update the controls. My code is attached below. Please help.

from flet import *
import asyncio 

class SearchApp(UserControl):
    def __init__(self):
        self.get_data = [
            "design 1",
            "design 2",
            "design 3",
            "design 4",
            "design 5"
        ]
        self.control_list = {}
        self.filtered_list = []
        super().__init__()
    
    async def filter_table(self, e):
        self.filtered_list = []
        entered_text = self.control_list["search"].content.controls[0].value
        for i in self.get_data:
            if entered_text.lower() in i.lower() and i != "":
                self.filtered_list.append(i)
        # Update the TextButton here. Not sure how to do.
        self.control_list["search"].content.controls[0].update()
        print(self.filtered_list)
    
    def search_ui(self):
        _obj = Container(
            bgcolor="white10",
            border_radius=10,
            height=50,
            padding=10,
            content=Row(
                controls=[
                    SearchBar(
                        height=50,
                        on_change=lambda e: asyncio.run(self.filter_table(e)),
                        controls=[
                            TextButton(
                                text=item, 
                                on_click=lambda e: print(item), 
                                data=item
                            )
                            for item in self.filtered_list
                        ]
                    )
                ]
            )
        )

        self.control_list["search"] = _obj
        return _obj
    
    def build(self):
        return Column(
            controls=[
                self.search_ui()
            ]
        )

def main(page: Page):
    page.window_width=500
    page.horizontal_alignment = "center"
    page.vertical_alignment = "top"
    page.add(
        SearchApp(),
    )
    page.update()


if __name__ == "__main__":
    app(target=main)

You will notice that, my filtered list is being generated as expected. However, I am not able to update the SearchBar controls.

kXborg avatar Mar 13 '24 18:03 kXborg

@ndonkoHenri,

Please check once if it is within your scope.

kXborg avatar Mar 15 '24 05:03 kXborg

While trying to solve this i noticed two issues:

  1. All the suggestions go invisible when I type 2-3 chars (although they present when I inspect the search bar controls).

For those trying to repro:

  • click on the bar, to open the search view
  • type in "desi"
  • while typing you will notice that the suggestions all dissapear at some point
  1. when modifying the suggestions/controls list, the changes are not reflected in the UI - Issue + Solution

Below is the updated code:

from flet import *
import asyncio 

class SearchApp(UserControl):
    def __init__(self):
        super().__init__()
        self.get_data = [
            "design 1",
            "design 2",
            "design 3",
            "design 4",
            "design 5"
        ]
        self.control_list = {}
        self.filtered_list = []

        self.searchbar_ref = Ref[SearchBar]()

    
    async def filter_table(self, e):
        self.filtered_list = []
        entered_text = self.searchbar_ref.current.value
        for i in self.get_data:
            if entered_text.lower() in i.lower() and i != "":
                self.filtered_list.append(i)

        self.searchbar_ref.current.controls=[
            TextButton(
                text=item, 
                on_click=lambda e: print(item), 
                data=item
            )
            for item in self.filtered_list
        ]
        self.update()
        print(self.searchbar_ref.current.controls)
    
    def search_ui(self):
        _obj = Container(
            bgcolor="white10",
            border_radius=10,
            height=50,
            padding=10,
            content=Row(
                controls=[
                    SearchBar(
                        ref=self.searchbar_ref,
                        height=50,
                        on_change=lambda e: asyncio.run(self.filter_table(e)),
                        controls=[
                            TextButton(
                                text=item, 
                                on_click=lambda e, item=item: print(item), 
                                data=item
                            ) 
                            for item in self.get_data
                        ]
                    )
                ]
            )
        )

        self.control_list["search"] = _obj
        return _obj
    
    def build(self):
        return Column(
            controls=[
                self.search_ui()
            ]
        )

def main(page: Page):
    page.window_width=500
    page.horizontal_alignment = "center"
    page.vertical_alignment = "top"
    page.add(
        SearchApp(),
    )
    page.update()


app(main)

ndonkoHenri avatar Mar 15 '24 11:03 ndonkoHenri

Also at on_click event it prints only "design 5". No matter what is clicked.

kXborg avatar Mar 15 '24 12:03 kXborg

I updated the code to fix that.

ndonkoHenri avatar Mar 15 '24 13:03 ndonkoHenri

I updated the code to fix that.

Thanks, stupid me 🤦‍♂️.

kXborg avatar Mar 15 '24 16:03 kXborg

controls can be dynamic updated if u do close_view() and open_view() on every single change. like this:

def filter_heroes(e):
        query = e.control.value.lower()
        search_bar.controls.clear()
        filtered_heroes = [(hero["localized_name"], f"dir/img/{hero['localized_name']}.png") for hero in heroes_data if query in hero["localized_name"].lower()]
        for hero_name, icon_path in filtered_heroes:
            search_bar.controls.append(ft.ListTile(
                leading=ft.Image(src=icon_path, width=40, height=40),
                title=ft.Text(hero_name),
                on_click=select_hero,
                # data=hero_name 
            ))
            current_value = search_bar.value
            search_bar.close_view(current_value)
            search_bar.open_view()
            page.update()

but it works only for 1 letter, because it loops=) and u lose an option to choose=/ (close_view is default option for choosing?)

sorry for my beautiful English

denzro avatar Mar 21 '24 19:03 denzro

temporary workaround

code original source

import flet as ft

def main(page):

    def close_anchor(e):
        text = f"Color {e.control.data}"
        print(f"closing view from {text}")
        anchor.close_view(text)

    def handle_change(e):
        print(f"handle_change e.data: {e.data}")
        print(f"handle_change e.data: {e.data}")
        
        lv.controls.clear()
        for i in range(6):
            lv.controls.append(ft.ListTile(title=ft.Text(f"{e.data} {i}"), on_click=close_anchor, data=i))
        lv.update()

    def handle_submit(e):
        print(f"handle_submit e.data: {e.data}")

    def handle_tap(e):
        print(f"handle_tap")

    lv = ft.ListView()
    anchor = ft.SearchBar(
        view_elevation=4,
        divider_color=ft.colors.AMBER,
        bar_hint_text="Search colors...",
        view_hint_text="Choose a color from the suggestions...",
        on_change=handle_change,
        on_submit=handle_submit,
        on_tap=handle_tap,
        controls=[
            lv
            # ft.ListTile(title=ft.Text(f"Color {i}"), on_click=close_anchor, data=i)
            # for i in range(10)
        ],
    )

    page.add(
        ft.Row(
            alignment=ft.MainAxisAlignment.CENTER,
            controls=[
                ft.OutlinedButton(
                    "Open Search View",
                    on_click=lambda _: anchor.open_view(),
                ),
            ],
        ),
        anchor,
    )


ft.app(target=main)

bhushanrathod32 avatar Mar 26 '24 07:03 bhushanrathod32

Nice workaround, @bhushanrathod32 - thanks for sharing!

Using the ListView (or any other container control, ex: Column) as base control of the suggestions list, solves the both issues I mentioned in my last comment. One could conclude by saying that the direct children of SearchBar.controls (the list of suggestions) should not be modified, if you expect to see real-time filtering. (abnormal behaviour to be fixed) I think we should modify the example in the docs with yours. (Feel free to open a PR if you wish to contribute)

@kXborg, I trimmed your code to the below:

from flet import *


class SearchApp(SearchBar):
    def __init__(self):
        super().__init__(on_change=self.filter_table)
        self.get_data = ["design 1", "design 2", "design 3", "design 4", "design 5"]

        self.list_view_ref = Ref[ListView]()

        self.controls = [
            ListView(
                ref=self.list_view_ref,
                controls=[
                    ListTile(
                        title=Text(item),
                        on_click=lambda e, item=item: print(item),
                    )
                    for item in self.get_data
                ],
            )
        ]

    def filter_table(self, e: ControlEvent):
        entered_text = self.value
        filtered_list = []

        for i in self.get_data:
            if entered_text.lower() in i.lower() and i != "":
                filtered_list.append(i)

        self.list_view_ref.current.controls = [
            ListTile(
                title=Text(item),
                on_click=lambda e, item=item: print(item),
            )
            for item in filtered_list
        ]

        self.update()


def main(page: Page):
    page.add(SearchApp())

app(main)

I will keep this issue opened for sometime again to see if I can fix that. :)

ndonkoHenri avatar Mar 26 '24 09:03 ndonkoHenri

if small modify function filter_table, than can filter element of every letters regardless of place in words

def filter_table(self, e: ControlEvent):
    entered_text = self.value
    filtered_list = []

    for i in self.get_data:
        add = []
        for el in entered_text:
            if el.lower() in i.lower() and i != "":
                add.append(True)
        if len(entered_text) == add.count(True):
            filtered_list.append(i)

    self.list_view_ref.current.controls = [
        ListTile(
            title=Text(item),
            on_click=lambda e, item=item: print(item),
        )
        for item in filtered_list
    ]
    self.update()

SergNif avatar Apr 22 '24 15:04 SergNif