Changing content without refreshing (Ajax)

Dishing out money

You can make parts of your page refresh in response to having changed the value of an input.

This example is a page on which you choose how to divide an amount you want to invest into two different funds:

AllocationDetailForm.

You enter the total amount, and then the percentage allocated to each fund. Each time you tab out of a percentage input, the amount portion next to it and the totals at the bottom of the table are recalculated.

AllocationDetailForm with totals and amounts recalculated.

You can also change the total amount or elect to rather specify portions as amounts instead of percentages.

AllocationDetailForm specified in amount.

Make an HTMLElement refreshable

You make an HTMLElement (AllocationDetailForm in this example) refreshable by calling enable_refresh() on it:

    def __init__(self, view):
        super().__init__(view, 'investment_order_allocation_details_form')
        self.use_layout(FormLayout())

        self.investment_order = InvestmentOrder.for_current_session()
        self.enable_refresh(on_refresh=self.investment_order.events.allocation_changed)

        self.add_allocation_controls()
        self.add_allocation_table()

        self.define_event_handler(self.investment_order.events.submit)
        self.add_child(Button(self, self.investment_order.events.submit))

A TextInput will trigger a refresh of the HTMLElement passed as its refresh_widget.

    def add_allocation_controls(self):
        allocation_controls = self.add_child(FieldSet(self.view, legend_text='Investment allocation'))
        allocation_controls.use_layout(FormLayout())

        if self.exception:
            self.layout.add_alert_for_domain_exception(self.exception)
        
        total_amount_input = TextInput(self, self.investment_order.fields.amount, refresh_widget=self)
        allocation_controls.layout.add_input(total_amount_input)

        amount_or_percentage_radio = RadioButtonSelectInput(self, self.investment_order.fields.amount_or_percentage, refresh_widget=self)
        allocation_controls.layout.add_input(amount_or_percentage_radio)

Recalculate

Note

The values you recalculate must be attributes of persisted model objects.

Specify an on_refresh Event in your enable_refresh() call to trigger a recalculate Action:

        self.enable_refresh(on_refresh=self.investment_order.events.allocation_changed)

    events = ExposedNames()
    events.submit = lambda i: Event(label='Submit', action=Action(i.submit))
    events.allocation_changed = lambda i: Event(action=Action(i.recalculate))
    def recalculate(self):
        for allocation in self.allocations:
            allocation.recalculate(self.amount)

Validating results

When the user finally submits the InvestmentOrder, recalculate the order before validating the newly recalculated results.

    def submit(self):
        print('Submitting investment')
        self.recalculate()
        self.validate_allocations()

        print('\tAmount: %s' % self.amount)
        print('\tAllocations (%s)' % self.amount_or_percentage)
        for allocation in self.allocations:
            allocation_size = allocation.percentage if self.is_in_percentage else allocation.amount
            print('\t\tFund %s(%s): %s (%s)' % (allocation.fund, allocation.fund_code, allocation_size, allocation.amount))

        self.clear()

If the submitted data is not correct, raise a DomainException to indicate the problem:

    def validate_allocations(self):
        if self.is_in_percentage:
            if self.total_allocation_percentage != 100:
                raise DomainException(message='Please ensure allocation percentages add up to 100')
        else:
            if self.total_allocation_amount != self.amount:
                raise DomainException(message='Please ensure allocation amounts add up to your total amount (%s)' % self.amount)

Communicate the error condition to the user by displaying the DomainException as part of AllocationDetailForm:

    def add_allocation_controls(self):
        allocation_controls = self.add_child(FieldSet(self.view, legend_text='Investment allocation'))
        allocation_controls.use_layout(FormLayout())

        if self.exception:
            self.layout.add_alert_for_domain_exception(self.exception)
        
        total_amount_input = TextInput(self, self.investment_order.fields.amount, refresh_widget=self)
        allocation_controls.layout.add_input(total_amount_input)

        amount_or_percentage_radio = RadioButtonSelectInput(self, self.investment_order.fields.amount_or_percentage, refresh_widget=self)
        allocation_controls.layout.add_input(amount_or_percentage_radio)