How to use layouts?

Layouts sound like a really powerful feature. However, I have not gotten over the initial “how do I use this?” learning curve.

What I’m trying to do:
I’m mainly hung up on how to share state between the Layout Form, the Form using the Layout (we’ll call it the “Main Form”), and the “SubForms” using the slots.

My general use-case is going to be to load a Form with an entity (maybe an anvil row, or a portable class), and each of these SubForms will be responsible for showing the user some aspect of that.

It feels like each SubForm should take the entity as an argument, but, it’s not clear to me when/how the Form should propagate that through. We could instantiate them in code and insert them into slots in the Main Form __init__ but that doesn’t feel very Anvil-y (not at all WYSIWYG).

Alternatively, should my SubForms somehow retrieve the entity from the Main Form? Sub Form get data from the Main Form (or is that discouraged)?

What if we wanted to add logic to the Layout Form? I can imagine there being common methods we want to add there, to be used by different Main Forms sharing the Layout Form. How do I get the Layout Form instance form my Main Form? From my SubForms? I see that MainForm has a layout attribute - is that available from SubForm? Am I right in assuming that’s what layout is for?

Edited to add: The tutorial talks about using open_form, but we are using routing, so I’m not sure if that’s relevant or not.

What I’ve tried and what’s not working:
I see that Forms dropped into a Slot have a item binding, so I tried binding to that, but, that is not available when the SubForms are actually instantiated.

See this clone link (that is not at all working, but is meant to show how I thought it might be supposed to work):

This includes a Layout that is meant to show a User details, so it includes layout that includes a User Header (to show the user details) and a User Comments (which accesses a user’s comments, through a multiple row relationship).

1 Like

Hi @danbolinson,

What you’re doing there is pretty much the right thing!

  • You’re letting the layout handle the high-level page structure (the two cards, each with a slot), in a way that can be reused

  • Your MainForm is putting UI into those slots.
    (In your example, the only UI in MainForm is a pair of custom components, which makes it quite “empty”; I assume that in the real app you’re building there would be more logic and/or UI in MainForm. Your choice to put a custom component into each slot is fine if that’s how you want to organise your UI and code – but you don’t have to organise things this way if you’re using Layouts, even though you did kinda have to do that if you were creating this sort of page in the “before times”.)

  • You’re using ordinary Anvil machinery to communicate between the MainForm and the custom components you’re using to encapsulate UI. At this point it’s no different from how you’d create any form that uses custom components: If you’re going to split this page across multiple forms, those instances are going to have to communicate. In this case you’ve chosen to use Data Bindings to set the item property, which is a perfectly reasonable way of doing things.

In the end, the only things that are causing errors are:

  1. The random j in your MainForm code (probably a typo), which is causing errors in the designer – check the “Designer Output” tab at the bottom!

  2. Although you were correctly passing the result of anvil.users.get_user() into the form, the data bindings in your subforms couldn’t cope with that value being None (which it was by default). I added an anvil.users.login_with_form() to your startup module and reset the password of the user in your database, and your app just worked!

1 Like

Well, I feel silly. Good to know I was going in the right direction.

This gets us closer. However, the use of item is problematic for us, for a few reasons, and that becomes a real barrier here:

  • We use models, not dictionaries, and so the suppression of key errors before item is fully set doesn’t work when we’re using attributes.
  • item is really non-descript; we use the name of the entity for clarity (usually before init_components, we do something like self.user = properties['item'] if we know the form is being used in a repeating panel or something.
  • Similarly we usually have our forms take (often optional) arguments for the entity they act on. I.e.:
    •  class MyForm(MyFormTemplate):
           def __init__(self, user=None, **properties):
               self.user = user # None, or, we should assume `user` is bound
               self.init_components()
               # self.user should never be None at this point
      

To avoid use of item, I tried making all my SubForms components with a user property, and binding to that. That fully enables the pattern I describe above, so I can use forms in MainForm and just bind all my SubForms user to the self.user in MainForm - awesome. And, I can call alert(SubForm(user=self.user)) and that works fine - also awesome, I can use that form as usual anywhere else I need to.

My problem arises in the init. It seems like the SubForm __init__, and the included init_components inside, are called BEFORE the self.user binding is resolved in MainForm. So, self.user is None and my form fails to load, even though it is bound to a (jnon-null) user in MainForm.

I can fix this by handling the condition where self.user is None, and things will work because that gets us through the in initial SubForm.__init__), and by the time the form shows, the binding has resolved. Handling the case where self.user is Nonein every single binding is prohibitive though. The current working version of my clone link shows me doing this inuser header, but not in user comments`:

So:

  • How do I make sure that my binding of MainForm.user to SubForm.user is resolved BEFORE the SubForm.init_components() is called?
  • Is there something different I should be doing? (I’m trying to avoid dynamically instantiating my forms in code - that feels like throwing out Layouts altogether)

Usually, when __init__ is too early to recognize a Data Binding, I do that work in the form’s/component’s show event, instead.

Edit: That way, init_components() has completed for all the components on screen.

1 Like

Look, another stray j!!
Maybe your problem is the keyboard!!

There’s an interesting thought… @meredydd What does init_components actually do? If we simple called refresh_data_bindings in the form_show event (and never called init_components could that be a way around this?

An issue is that your SubFormUserComments isn’t following the pattern in the docs:

If you want to set up any values that your Data Bindings rely on, do it before the call to self.init_components(**properties) .
Anvil Docs | Forms as Components

SubFormUserComments has no reference to self.user before its init_components is called
So if the SubFormUserComments is constructed without a user property it blows up. Its own init_components is called during its construction and assigning the user as a data binding in the parent form is too late (It has already been constructed before the parent form’s init_components is called).

The order of creation looks something like:

- start constructing MainForm
  - start constructing components for MainForm
    - start constructing SubFormUserComments
      - start constructing components for SubFormUserComments
      - call init_components on SubFormUserComments
    - finished constructing  SubFormUserComments 
  - finished constructing components for MainForm
  - call init_components on MainForm
- finished constructing MainForm

All components for MainForm exist before init_components is called. In fact they are constructed in the __new__.
init_components is mostly responsible for setting up the databindings.
And if you’re a custom component, it’ll assign the correct properties to the instance of the form.

Another note is that self.item is special because when you set it, the refresh_data_bindings method is called.

So if you want the refresh_data_bindings method to be called when assigning a specific attribute you might need to create a property that manually calls refresh_data_bindings.

class SubFormUserComments(SubFormUserCommentsTemplate):
  def __init__(self, **properties):
    self._init = False
    self._user = None
    self.init_components(**properties)
    self._init = True

  @property
  def user(self):
    return self._user

  @user.setter
  def user(self, val):
    self._user = val
    if self._init:
        self.refresh_data_bindings()

In this case you will still have to account for not having a user.
You could do the lazy thing and make _user an empty dictionary if it’s None since data bindings ignore key errors are ignored by data bindings.

  @property
  def user(self):
    return self._user or {}

Alternatively change the data binding to be something like

self.user and self.user["comment_links"]

I don’t think this is really a discussion about layouts per-se. This setup could be done without layouts being a thing, just adding a custom component to a BlankPanel would result in the same discussion.


1 Like

The correct approach to data binding in relation to the chronological order of what’s executed when has already been discussed in other answers. I don’t understand what you mean by “models”, but if it helps, you can have a look at this post to see how I use my own classes rather than dictionaries, and I get them to play nice with databinding by deriving them from the class AttributeToKey.

1 Like

Thanks both. @stucork that order of operations is helpful, and @stefano.menci a class to support this “in-between” is an interesting idea.

I understand that self.user is not set which is why I’m failing, but adjusting every binding to something like self.user and self.user["comment_links"] is pretty painful, and I think still fails when None is not a valid result.

The lazy option of using a dict is not available to me because we make such heavy use of models (which are more likely to raise AttributeErrors, not KeyErrors). Swallowing those as Stefano suggests is interesting but a little smelly.

Can I simply defer the init_components call to the form_show event? Or, is there another way to ensure that my init_components call on SubFormUserComments is deferred until after my precondition is satisfied (i.e. self.user has been assigned? This feels like the root of the problem - the SubForm depends on the MainForm and should therefore be init after the MainForm.

I agree that this is not layouts-specific per-se, but this is putting a thumb right into this topic, which is why I’m trying to get a “generalizable” answer…

I probably wouldn’t move init_components, but you could extend the refresh_data_bindings method.

example

class Form1(Form1Template):
    def __init__(self, **properties):
        self._shown = False
        self.init_components(**properties)

    def refresh_data_bindings(self):
        if self._shown:
            super().refresh_data_bindings()

    def form_show(self, **event_args):
        self._shown = True
        self.refresh_data_bindings()

Adjust the logic as you need.

2 Likes

This leaves us with the issue where self.user is undefined when init_components is called though, does it not?

Yes but ultimately refresh_databindings is the method that sets the databindings during the call to init_components. So by adding a flag we delay setting any data bindings until we’re ready while letting init_components do anything else it wants. The code assumes that you will set the user attribute by the time the show event fires.

1 Like

Okay great! I did not realize you could overload refresh_data_bindings that simply.

So, I can implement a pattern like this:

class MyUserForm(MyUserFormTemplate):
    def __init__(self, user=None, **properties):
        self._user = user
        self.init_components(**properties)
        
    @property
    def user(self):
        return self._user

    @user.setter
    def user(self, set_user):
        self._user = set_user
        self.refresh_data_bindings()
    
    def refresh_data_bindings(self):
        if self.user:
            super().refresh_data_bindings()

This satisfies all my requirements:

  • I can optionally pass in an object (user in this case) as a form argument, OR, I can bind to a property (if a layout or a custom component)
  • I don’t have to worry about dealing with bindings when self.user is None since the bindings won’t refresh
  • All my bindings work very intuitively since the refresh_data_bindings is called as soon as the user is set
  • I’m not counting on suppressed errors (which create no end of other headaches)
  • This same pattern will work for layouts, custom components, and simple Forms.

Thank you!

3 Likes