DomainErrors when dynamically refreshing a Widget¶
Simple calculator¶
If your on_refresh Action
can result in a DomainException
, handle such an error and make sure
the Widget
can be created fully regardless of it having inputs causing the error.
This example shows a simple calculator, that can add and divide two numbers. As soon as the user tabs out of one of the inputs or changes the operator, the screen is refreshed and recalculated to show the answer.
Choosing the combination of ÷ and 0 results in an operation this calculator cannot do. It handles such an error and reports it back to the user while also showing the answer as “—” instead of its usual numeric answer.
The calculator logic¶
In the Calculator’s recalculate method, raise a DomainException
to signal the error condition, but at the same time
ensure that result is always set. In Calculator.recalculate result is set to None for invalid input:
def recalculate(self):
if self.is_divide_by_zero:
self.result = None
raise DomainException(message='I can\'t divide by 0')
if self.operator == 'plus':
self.result = self.operand_a + self.operand_b
elif self.operator == 'divide':
self.result = int(self.operand_a / self.operand_b)
The display logic¶
CalculateForm is the Widget
responsible for showing the Calculator. The usual call to enable_refresh()
automatically
also calls your on_refresh Action
, so be sure to handle the DomainException
it can raise:
def __init__(self, view):
super().__init__(view, 'dynamic_content_error_form')
self.use_layout(FormLayout())
self.calculator = Calculator.for_current_session()
try:
self.enable_refresh(on_refresh=self.calculator.events.inputs_changed)
except DomainException as ex:
self.layout.add_alert_for_domain_exception(ex)
controls = self.add_child(FieldSet(view).use_layout(InlineFormLayout()))
self.add_inputs(controls)
self.display_result(controls)
When adding the result for the current nonsensical input, take into account whether or not a result could be calculated:
def display_result(self, controls):
if self.calculator.result is not None:
message = '= %s' % self.calculator.result
else:
message = '= ---'
controls.add_child(Span(self.view, text=message))
Full source code¶
# Copyright 2021, 2022, 2023 Reahl Software Services (Pty) Ltd. All rights reserved.
#
# This file is part of Reahl.
#
# Reahl is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from reahl.component.modelinterface import ExposedNames, Field, Event, Action, Choice, IntegerField
from reahl.component.exceptions import DomainException
from reahl.web.fw import UserInterface
from reahl.web.layout import PageLayout
from reahl.web.bootstrap.page import HTML5Page
from reahl.web.bootstrap.ui import FieldSet, Span
from reahl.web.bootstrap.grid import Container, ColumnLayout, ColumnOptions, ResponsiveSize
from reahl.web.bootstrap.forms import Form, FormLayout, TextInput, ChoiceField, InlineFormLayout, SelectInput
from sqlalchemy import Column, Integer, UnicodeText, orm
from reahl.sqlalchemysupport import Base, session_scoped
class DynamicUI(UserInterface):
def assemble(self):
self.define_page(HTML5Page).use_layout(PageLayout(document_layout=Container(),
contents_layout=ColumnLayout(
ColumnOptions('main', size=ResponsiveSize(lg=7))).with_slots()))
home = self.define_view('/', title='Dynamic error demo')
home.set_slot('main', CalculatorForm.factory())
class CalculatorForm(Form):
def __init__(self, view):
super().__init__(view, 'dynamic_content_error_form')
self.use_layout(FormLayout())
self.calculator = Calculator.for_current_session()
try:
self.enable_refresh(on_refresh=self.calculator.events.inputs_changed)
except DomainException as ex:
self.layout.add_alert_for_domain_exception(ex)
controls = self.add_child(FieldSet(view).use_layout(InlineFormLayout()))
self.add_inputs(controls)
self.display_result(controls)
def add_inputs(self, controls):
operand_a_input = TextInput(self, self.calculator.fields.operand_a, refresh_widget=self)
controls.layout.add_input(operand_a_input, hide_label=True)
operator_input = SelectInput(self, self.calculator.fields.operator, refresh_widget=self)
controls.layout.add_input(operator_input, hide_label=True)
operand_b_input = TextInput(self, self.calculator.fields.operand_b, refresh_widget=self)
controls.layout.add_input(operand_b_input, hide_label=True)
def display_result(self, controls):
if self.calculator.result is not None:
message = '= %s' % self.calculator.result
else:
message = '= ---'
controls.add_child(Span(self.view, text=message))
@session_scoped
class Calculator(Base):
__tablename__ = 'dynamic_content_error_calculator'
id = Column(Integer, primary_key=True)
operand_a = Column(Integer)
operand_b = Column(Integer)
operator = Column(UnicodeText)
result = Column(Integer)
fields = ExposedNames()
fields.operand_a = lambda i: IntegerField(label='A', required=True)
fields.operand_b = lambda i: IntegerField(label='B', required=True)
fields.operator = lambda i: ChoiceField([Choice('plus', Field(label='+')),
Choice('divide', Field(label='÷'))], required=True)
events = ExposedNames()
events.inputs_changed = lambda i: Event(action=Action(i.recalculate))
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.operand_a = 1
self.operand_b = 1
self.operator = 'plus'
self.recalculate()
@property
def is_divide_by_zero(self):
return self.operator == 'divide' and self.operand_b == 0
def recalculate(self):
if self.is_divide_by_zero:
self.result = None
raise DomainException(message='I can\'t divide by 0')
if self.operator == 'plus':
self.result = self.operand_a + self.operand_b
elif self.operator == 'divide':
self.result = int(self.operand_a / self.operand_b)