Search code examples
pythonnicegui

How to get updated values in NiceGUI


I'm writing a NiceGUI frontend for my Python code. This is my first time writing frontend, and I'm not very used to web-framework concepts.

Essentially, the use of this frontend is to ask to user to insert the following variables (I know range is not a good variable name):

variants_text: str
keywords_text: str
seed: int | None
min_articles: int | None
max_articles: int | None
range: int | None
limit: int | None

This is the code I have written so far (the only dependecy is nicegui, and you need a dummy .txt file to pass Step1):

from nicegui import ui

class ArticleRetrieval:

    def __init__(self):

        self.page_title = "Article Retrieval"
        self.headers()
        
        self.stepper = ui.stepper().props("vertical").classes("w-full")

        self.step1_title = "Add input files"
        self.step2_title = "Add optional arguments"
        self.step3_title = "Retrieve articles"

        self.variants_uploaded = False
        self.variants_text = None

        self.keywords_uploaded = False
        self.keywords_text = None

        self.seed = None
        self.min_articles = None
        self.max_articles = None
        self.range = None
        self.limit = None

        self.positive_integer_validator = {
            "You can only insert a non-negative integer": self._acceptable_num
        }
    
    def _acceptable_num(self, n) -> bool:
        return (n is None) or (n == int(n) and n >=0)
    
    def _variants_uploaded(self, e):
        self.variants_uploaded = True
        self.variants_text = e.content.readlines()
        if self.keywords_uploaded is True:
            self.step1_next_button.visible = True  

    def _keywords_uploaded(self, e):
        self.keywords_uploaded = True
        self.keywords_text = e.content.readlines()
        if self.variants_uploaded is True:
            self.step1_next_button.visible = True

    def _update_seed(self, e):
        self.seed = e.value
        self._step2_next_button_visibility()

    def _update_min_articles(self, e):
        self.min_articles = e.value
        self._step2_next_button_visibility()

    def _update_max_articles(self, e):
        self.max_articles = e.value
        self._step2_next_button_visibility()

    def _update_range(self, e):
        self.range = e.value
        self._step2_next_button_visibility()

    def _update_limit(self, e):
        self.limit = e.value
        self._step2_next_button_visibility()

    def _step2_next_button_visibility(self):
        if all(
            self._acceptable_num(x) for x in [
                self.seed,
                self.min_articles,
                self.max_articles,
                self.max_articles,
                self.limit,
            ]
        ):
            self.step2_next_button.set_visibility(True)
        else:
            self.step2_next_button.set_visibility(False)

    def _ask_confirm(self):
        ui.markdown(
            f"""
            ##### DO YOU CONFIRM THIS DATA?<br>

            **variants**: {self.variants_text}<br>
            **keywords**: {self.keywords_text}<br>
            **seed**: {self.seed}<br>
            **min\\_articles**: {self.min_articles}<br>
            **max\\_articles**: {self.max_articles}<br>
            **range**: {self.range}<br>
            **limit**: {self.limit}<br>
            """
        )

    def headers(self):
        ui.page_title(self.page_title)
        ui.markdown(f"#{self.page_title}")
    
    def step1(self):
        with ui.step(self.step1_title):
            ui.upload(
                label="Variants (.txt file)",
                max_files=1,
                auto_upload=True,
                on_upload=self._variants_uploaded,
            ).props("accept=.txt").classes("max-w-full")

            ui.upload(
                label="Keywords (.txt file)",
                max_files=1,
                auto_upload=True,
                on_upload=self._keywords_uploaded,
            ).props("accept=.txt").classes("max-w-full")
        
            with ui.stepper_navigation():
                self.step1_next_button = ui.button(
                    "NEXT",
                    icon="arrow_forward_ios",
                    on_click=self.stepper.next
                )
                self.step1_next_button.visible = False

    def step2(self):

        with ui.step(self.step2_title):
            
            ui.number(
                label="Seed",
                validation=self.positive_integer_validator,
                on_change=self._update_seed
            ).props("clearable")
            ui.number(
                label="Minimum articles",
                validation=self.positive_integer_validator,
                on_change=self._update_min_articles
            ).props("clearable")
            ui.number(
                label="Maximum articles",
                validation=self.positive_integer_validator,
                on_change=self._update_max_articles
            ).props("clearable")
            ui.number(
                label="Range",
                validation=self.positive_integer_validator,
                on_change=self._update_range
            ).props("clearable")
            ui.number(
                label="Limit",
                validation=self.positive_integer_validator,
                on_change=self._update_limit
            ).props("clearable")

            with ui.stepper_navigation():
                self.step2_back_button = ui.button(
                    "BACK",
                    icon="arrow_back_ios",
                    on_click=self.stepper.previous,
                )
                self.step2_next_button = ui.button(
                    "NEXT",
                    icon="arrow_forward_ios",
                    on_click=self.stepper.next
                )
                self.step2_next_button.visibility = False

    def step3(self):
        with ui.step(self.step3_title):
            self._ask_confirm()
            with ui.stepper_navigation():
                ui.button(
                    "BACK",
                    icon="arrow_back_ios",
                    on_click=self.stepper.previous,
                )
                ui.button(
                    "RUN",
                    icon="rocket_launch",
                    on_click=lambda x: None  # HERE I WILL PERFORM THE ACTUAL JOB
                )
    
    def run(self):
        with self.stepper:
            self.step1()
            self.step2()
            self.step3()
        ui.run()

ArticleRetrieval().run()

My question is about the _ask_confirm method.
It always prints the initial values of attributes, the ones I've defined in the __init__ method (i.e. all None).

enter image description here

I would like that string to be updated every time I update values in Step1 and Step2.

The thing I don't understand is that if, for example, I change the _update_seed method like this

def _update_seed(self, e):
    print(self.seed)
    self.seed = e.value
    print(self.seed)
    self._step2_next_button_visibility()

It prints the updated values.


Solution

  • Ok so I have cut out a few parts of your GUI, but kept the step2() and step3() chunks to validate that this behaves correctly.

    The issue you are facing is that the markdown content is not re-rendered unless you explicitly use the set_content() method within ui.markdown (see here).

    As a result I got this to work for me by creating a callback update_markdown_content that is run on_click of self.step2_next_button, as you can hopefully see below. This new method also calls the self.stepper.next() method to retain the navigation of the button within the stepper.

    MWE

    from nicegui import ui
    
    class ArticleRetrieval:
    
        def __init__(self):
    
            self.page_title = "Article Retrieval"
            self.headers()
            
            self.stepper = ui.stepper().props("vertical").classes("w-full")
    
            self.step2_title = "Add optional arguments"
            self.step3_title = "Retrieve articles"
    
            self.variants_uploaded = False
            self.variants_text = None
    
            self.keywords_uploaded = False
            self.keywords_text = None
            self.seed = None
            
            self.min_articles = None
            self.max_articles = None
            self.range = None
            self.limit = None
    
            self.positive_integer_validator = {
                "You can only insert a non-negative integer": self._acceptable_num
            }
        
        def _acceptable_num(self, n) -> bool:
            return (n is None) or (n == int(n) and n >=0)
        
        def _update_seed(self, e):
            self.seed = e.value
            self._step2_next_button_visibility()
    
        def _update_min_articles(self, e):
            self.min_articles = e.value
            self._step2_next_button_visibility()
    
        def _update_max_articles(self, e):
            self.max_articles = e.value
            self._step2_next_button_visibility()
    
        def _update_range(self, e):
            self.range = e.value
            self._step2_next_button_visibility()
    
        def _update_limit(self, e):
            self.limit = e.value
            self._step2_next_button_visibility()
    
        def _step2_next_button_visibility(self):
            if all(self._acceptable_num(x) for x in [self.seed, self.min_articles, self.max_articles, self.max_articles, self.limit]):
                self.step2_next_button.set_visibility(True)
            else:
                self.step2_next_button.set_visibility(False)
    
        def _ask_confirm(self):
            self.md = ui.markdown(
                f"""
                ##### DO YOU CONFIRM THIS DATA?<br>
    
                **variants**: {self.variants_text}<br>
                **keywords**: {self.keywords_text}<br>
                **seed**: {self.seed}<br>
                **min\\_articles**: {self.min_articles}<br>
                **max\\_articles**: {self.max_articles}<br>
                **range**: {self.range}<br>
                **limit**: {self.limit}<br>
                """
            )
    
        def headers(self):
            ui.page_title(self.page_title)
            ui.markdown(f"#{self.page_title}")
        
        def step2(self):
    
            with ui.step(self.step2_title):
                
                ui.number(label="Seed",             validation=self.positive_integer_validator, on_change=self._update_seed        ).props("clearable")
                ui.number(label="Minimum articles", validation=self.positive_integer_validator, on_change=self._update_min_articles).props("clearable")
                ui.number(label="Maximum articles", validation=self.positive_integer_validator, on_change=self._update_max_articles).props("clearable")
                ui.number(label="Range",            validation=self.positive_integer_validator, on_change=self._update_range       ).props("clearable")
                ui.number(label="Limit",            validation=self.positive_integer_validator, on_change=self._update_limit       ).props("clearable")
    
                with ui.stepper_navigation():
                    self.step2_back_button = ui.button("BACK", icon="arrow_back_ios", on_click=self.stepper.previous)
                    self.step2_next_button = ui.button("NEXT", icon="arrow_forward_ios", on_click=self.update_markdown_content)
                    self.step2_next_button.visibility = False
    
        def step3(self):
            with ui.step(self.step3_title):
                self._ask_confirm()
                with ui.stepper_navigation():
                    ui.button("BACK", icon="arrow_back_ios", on_click=self.stepper.previous)
                    ui.button("RUN", icon="rocket_launch", on_click=lambda x: None)  # HERE I WILL PERFORM THE ACTUAL JOB
                return
        
        def update_markdown_content(self):
            self.md.set_content(
                f"""
                ##### DO YOU CONFIRM THIS DATA?<br>
    
                **variants**: {self.variants_text}<br>
                **keywords**: {self.keywords_text}<br>
                **seed**: {self.seed}<br>
                **min\\_articles**: {self.min_articles}<br>
                **max\\_articles**: {self.max_articles}<br>
                **range**: {self.range}<br>
                **limit**: {self.limit}<br>
                """
                )
            self.stepper.next()
    
        def run(self):
            with self.stepper:
                self.step2()
                self.step3()
            ui.run()
    
    ArticleRetrieval().run()