Grow in Flexbox

Hi,

I am having some issues using flexbox to build a customized UI element. Here’s the background. I would like to create a chat-like interface with an input field at the bottom. This input field would extend if a button is clicked and the list of messages above should shrink.

My idea is to implement this with flexbox much like this CodePen where only the wrapper is assigned a height and the remaining elements set their heights themselves. If the lower element grows (by click of the button), the upper element shrinks. This seems to work fine.

I tried to replicate the same behavior in Anvil using a dummy app. I created a column panel for wrapper, upper and lower and assigned them custom roles with custom CSS:

.anvil-role-wrapper {
  height:600px;
  min-height:600px;
  overflow-y: hidden;
  margin: 100px;
  background-color:green;
  display:flex;
  flex-direction:column;
  border: 5px solid black
}

.anvil-role-upper{
  height:auto;
  background-color:blue;
  flex-grow: 60;
}

.anvil-role-lower{
  height:200px;
  background-color:red;
}

Weirdly, they behave quite differently: Upper doesn’t grow at all (which makes it impossible to test its shrinking behavior). Am I doing something wrong or is there a workaround for this problem?

Here’s a link to my test app: https://anvil.works/ide#clone:XZOTNZATQ364JVQI=IWL2Y7TBOSA4JVGSDRJ74HBW

The structure of the HTML when a component is inside a Content Panel is not simply

<div class="content-panel"><input class="text-box"></input></div>

so using CSS rules that rely on the relative structure of HTML elements won’t always give you the results you want.

The best thing to do if you want to take advantage of this sort of CSS/HTML feature for a very specific component of your app, is to use a Custom HTML Form. I’ve created one for your Chat Box in this example app:

https://anvil.works/ide#clone:VQ7YY4YRFKIMX3PG=4P44SOI55JARL6CY3CQTQDX3

It’s a component that can be dragged-and-dropped into any Form, and you can call its set_text method to set the text in the lower div such that the upper div shrinks to fit.

(The white artefacts between the blue and red regions in that video are thanks to my GIF maker, not present in the real app.)

The HTML is pretty much what you have in your CodePen:

<div class="anvil-role-wrapper">
  <div class="anvil-role-upper">
    Upper
  </div>
  <div class="anvil-role-lower" id="lower"></div>
</div>

<script>
function setText(text) {
  document.getElementById('lower').innerHTML = text;
}
</script>

And the set_text just wraps that JS function:

  def set_text(self, text):
    self.text = text
    self.call_js('setText', self.text)

In the example usage, we add a random word to the text every time a timer ticks:

  def timer_1_tick(self, **event_args):
    """This method is called Every [interval] seconds"""
    new_text = '{} {}'.format(self.chat_box_1.text, choice(WORDS))
    self.chat_box_1.set_text(new_text)

(NB: I removed the height rule from the lower CSS, because its height should not be fixed.)

Shaun, thank you so much for this great explanation!

I haven’t worked with custom HTML elements yet but they seem to be worthwhile to extend Anvil in some edge cases.

I am almost too afraid to ask, but I guess Custom HTML cannot be combined with actual Anvil components? I ask since my “chat input” are actually different forms of varying height which I have already built as Anvil components. I guess I could build the entire input as vanilla HTML/JS, but Anvil is so freaking comfortable!

Yes, you can drag-and-drop Anvil components into your custom HTML - in fact, that simplifies my ChatBox example nicely.

Anvil has some specialised HTML attributes to control where your components go within your custom HTML.

To set up slots that you can put components into, use anvil-slot-repeat="foo". Now you can drag-and-drop components into the Custom HTML Form and set their slot property to foo. Each component you add in this way will appear inside a new copy of the element that uses anvil-slot-repeat="foo".

In the ChatBox example, I’ve created a slot called lower and put a ColumnPanel into it, then a Label inside that. This results in the same thing as I had before, but you can directly interact with chat_box_1.label_1.text rather than calling a JavaScript function.

https://anvil.works/ide#clone:JLRY2YDBGUUYKUD6=2K4DBX5JA7DQDD3JQPJ75PBU

(I set the ColumnPanel’s col_spacing to none and all spacing_above and spacing_below to none in order to get it looking as before.)

The HTML is now:

<div class="anvil-role-wrapper">
  <div class="anvil-role-upper">
    Upper
  </div>
  <div anvil-slot-repeat="lower" class="anvil-role-lower" id="lower"></div>
</div>

And the event code is:

  def timer_1_tick(self, **event_args):
    """This method is called Every [interval] seconds"""
    self.chat_box_1.label_1.text = '{} {}'.format(self.chat_box_1.label_1.text, choice(WORDS))