Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mutate_reactive not updating screen (w/ reactive list) #4799

Closed
ghost-tp6 opened this issue Jul 24, 2024 · 6 comments · Fixed by #4802
Closed

mutate_reactive not updating screen (w/ reactive list) #4799

ghost-tp6 opened this issue Jul 24, 2024 · 6 comments · Fixed by #4802

Comments

@ghost-tp6
Copy link

ghost-tp6 commented Jul 24, 2024

Problem
RadioSet is not updating as expected when a dynamic list is built with reactive

MRE Expectation
on_mount appends all files in directory to a reactive list. mutate_reactive will cause the RadioSet to be rebuild with the updated list values.

Versions

textual==0.73.0
textual-dev==1.5.1
Python 3.11.0
Windows 11

MRE
assumes directory called configs exists; assumes files within config directory

from textual.app import App,ComposeResult
from textual.screen import Screen
from textual.widgets import Header, Footer, Static, RadioButton, RadioSet
from textual.reactive import reactive
from textual.widget import Widget

import os

CONFIG_DIR="configs"


class Profile(Static):
    choices: reactive[list[str]] = reactive(list, recompose=True) 

    def compose(self) -> ComposeResult:
        yield RadioSet(*self.choices)

    def on_mount(self) -> None:
        path = CONFIG_DIR
        dir_list = os.listdir(path)
        for config_name in dir_list:
            print("CONFIG: ",config_name)
            self.choices.append(config_name)    
            self.mutate_reactive(Profile.choices)
        

class Landing(Screen):
    def compose(self) -> ComposeResult:
        """Create child widgets for the app."""
        yield Header()
        yield Static(" Profile ", id="title")
        yield Profile()
        yield Footer()

class ForecastApp(App):
    """A Textual app to forecast Financials."""
    CSS_PATH = "main.tcss"
    BINDINGS = [("d", "toggle_dark", "Toggle dark mode"),("q","quit","Quit")]

    def compose(self) -> ComposeResult:
        """Create child widgets for the app."""
        yield Header()
        yield Footer()

    def action_toggle_dark(self) -> None:
        """An action to toggle dark mode."""
        self.dark = not self.dark

    def on_mount(self) -> None:
        self.install_screen(Landing(), name="landing")
        self.push_screen('landing')

if __name__ == "__main__":
    app = ForecastApp()
    app.run()

Current Workaround
data_bind in Screen

from textual.app import ComposeResult
from textual.screen import Screen
from textual.widgets import Header, Footer, Static, RadioButton, RadioSet
from textual.reactive import reactive
from textual.widget import Widget

import os

CONFIG_DIR="configs"


class Profile(Widget):
    choices: reactive[list[str]] = reactive(list, recompose=True) 

    def compose(self) -> ComposeResult:
        yield RadioSet(*self.choices)

class Landing(Screen):
    choices: reactive[list[str]] = reactive(list, recompose=True) 

    def compose(self) -> ComposeResult:
        """Create child widgets for the app."""
        yield Header()
        yield Static(" Profile ", id="title")
        yield Profile().data_bind(Landing.choices)
        yield Footer()
    def on_mount(self) -> None:
        path = CONFIG_DIR
        dir_list = os.listdir(path)
        for config_name in dir_list:
            self.choices.append(config_name)    
            self.mutate_reactive(Profile.choices)
Copy link

Thank you for your issue. Give us a little time to review it.

PS. You might want to check the FAQ if you haven't done so already.

This is an automated reply, generated by FAQtory

@willmcgugan
Copy link
Collaborator

There is a fix coming soon. BTW you only need to call mutate_reactive once after adding multiple items.

Copy link

Don't forget to star the repository!

Follow @textualizeio for Textual updates.

@Banbury
Copy link

Banbury commented Jul 25, 2024

I think this isn't fixed. Here is a minimal example:

from textual.app import App, ComposeResult
from textual.widgets import Static, Button
from textual.reactive import reactive

class TestWidget(Static):
    msg: reactive[list[str]] = reactive(list, recompose=True)
    def compose(self) -> ComposeResult:
        yield Static(" ".join(self.msg))


class TestApp(App):
    msg: reactive[list[str]] = reactive(list, recompose=True)
    def compose(self) -> ComposeResult:
        yield TestWidget()
        yield Button("Click me", id="button").data_bind(TestApp.msg)

    def on_button_pressed(self, event: Button.Pressed) -> None:
        self.msg.append("Hello")
        self.mutate_reactive(TestApp.msg)

if __name__ == "__main__":
    app = TestApp()
    app.run()

Running this I get the error ReactiveError: Unable to bind non-reactive attribute 'msg' on Button(id='button').
But 'msg' is reactive.

@TomJGooding
Copy link
Contributor

@Banbury Did you intend to call data_bind on your TestWidget rather than the Button?

@Banbury
Copy link

Banbury commented Jul 25, 2024

Oops. Don't type while tired. :)
Now it's working. Thanks for the correction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants