Dynamic row addition in Repeating Group and bulk CRUD of Things using vanilla Bubble

0. Intro

In this tip I’ll show you how to dynamically add new rows to a form (Repeating Group), fill inputs in them and bulk create multiple things with a single button click avoiding:

  • any plug-in or JS (while using JS will be much easier - I try to avoid it until I have no choice :grinning:)
  • creating empty things and using auto-binding

As an example we will be creating new Tasks. Data type Task has 2 fields - task name and task description (both are “text” fields).

Here is what we will get in the end:
Screen Recording 2024-03-10 at 01.05.43

1. Creating custom states

We will need two custom states (CS further in the text):

  1. row_numbers [state type = number, list = yes]. This CS will be used to collect numbers (integers) and add new rows in the Repeating Group. Also it will serve as a reference during bulk things creation.
  2. rows_content [state type = text, list = yes]. This CS will be used to accumulate values from each individual row.

My custom states are defined at a page level (page name = rg_row_addition), so you’ll see expressions like rg_row_addition's row_numbers and rg_row_addition's rows_content below.

2. Creating a form with dynamic rows addition/deletion

2.1 Setting up RG

Add a Repeating Group (RG further in the text):
Screenshot 2024-03-09 at 23.32.42

  • type of content = number
  • data source = CS row_numbers

Add required input elements to our RG. In my example I need 2 inputs to enter task name and task description:

2.2 Setting up the logic to add and delete rows

2.2.1 Adding rows

Add a button/icon that will be used to add new rows to the RG. In the WF we will use set state action with the following expression:
Screenshot 2024-03-09 at 23.37.19

Expression breakdown

row_numbers CS is a list of numbers (empty on page load). When button/icon is clicked we are using :plus item to add a new number to the list. The number to be added is determined as “find the highest number in the list and +1 to it”. So when our row_numbers is empty - we will add number 1 to the list and the list will look like [1]. When row_numbers value is [0,1,2,3,4] - we will add number 5, and so on.

2.2.2 Deleting rows

To delete a row add an icon in the RG cell. When clicked → set state action and remove current cell’s number from row_numbers CS:

2.2.3 Testing results

After completing steps 2.2.1 and 2.2.2 our logic is ready:
:warning: Pay attention to the fact that row number isn’t always equal to RG’s cell index. If we have 3 rows and delete the Row #2 → Row #3 will have cell index #2. This will add us some issues like reseting inputs values while deleteing any row except the last one. Check 3.4 Fixing issues with updating indexes after row deletion for more info.

3. Storing content from each row

Before diving into details I’ll describe the general idea of the approach. We will use CS rows_content to accumulate content from each individual row of the RG:

  • Row content storing format: each item (text) of rows_content stores values according to the following template: number/input’s A value/input’s B value
  • Row content update: we are going to update CS rows_content after any input’s element value change.
⚠️ important note about text separator choosing

for simplicity in my example scenario I chose / separator, but keep in mind that if users will use / in input field - everything will break. So it’s better to use something that users will not type, like ///// or whatever else.

3.1 Creating a custom event to add/modify row content to CS

As I mentioned above - we need to trigger WF each time any input element’s value is changed. We will follow DRY principle and use Custom Event.
Create a custom event (”collect values” in my example):


  • row_number (type = number)
  • task name (type = text)
  • task description (type = text)

In the custom event add action set state:

Expression breakdown
  1. rg_row_addition's rows_content:filtered:count > 0 → in this part we are checking if there is already an entry (text) in CS rows_content for our row number.
    • :filtered: we are splitting our item (text) using / separator. :first item will be the row number (as a text), that’s why we need to convert it to number, then compare with row number:
      Screenshot 2024-03-10 at 00.23.06
  2. :formatted as text: the result of previous part is a boolean (yes/no). This is where the power of :formatted as text operator arise:
    • formatting for no: if there is no item for row number → add it to the list. Arbitrary text:
      Screenshot 2024-03-10 at 00.28.55
    • formatting for yes: if the item for row number already exists in the list → we need to update it. This can be achieved by deleting old value from the list (:minus item) and adding new version (:plus item). Arbitrary text is the same as in formatting for no.
  3. :split by(,): applying :formatted as text evaluates to a text. But our CS rows_content is a list of texts. So we need to convert the result of previous expression part into a list. Unfortunately, Bubble doesn’t provide us with :coverted to list operator for text type. As a workaround we can use :split by operator. Text separator should be ,

3.2 Setting up WFs to trigger on inputs values changes

Now we need to create a workflow for each input element in our RG.

I have 2 input fields (task name and task description) - so I have 2 workflows. The only difference between them is the trigger (input “task name” or input ‘task description” values changed) while the action (trigger custom event) is the same.

3.3 Testing results

Check filling and editing your inputs:

3.4 Fixing issues with updating indexes after row deletion

If you have multiple rows with filled input elements and delete any row (except the last one) - all inputs will be reset:
Screen Recording 2024-03-10 at 02.22.07
I suppose the problem root cause is tied to recalculating cells indexes. The good news is that we still have inputs values in our rows_content CS. So all we need is just to use conditional initial content for each input element that will reference values from the CS.
:hourglass_flowing_sand: will update this section tomorrow:hourglass_flowing_sand:

4. Bulk creating Things

To bulk create things (tasks in my example) we will use a recursive API WF.

Params row_numbers and rows_content store values from our custom states. Param index will be used to iterate through row_numbers and to stop recursive WF rescheduling after last number from row_numbers is processed.

In Create a new Thing action I’m defining tasks name and description:

Expression breakdown
  1. From rows_content list we extract the value that matches the number defined in index:
  2. Split the text we extracted in the previous step by / and get a list of 3 values (number, task name, task value). For task name we will use :item #2 and for task name :item #3.

The last thing to do is to reschedule our API WF:

5. Testing the final result

Screen Recording 2024-03-10 at 01.05.43

6. Bonus: bulk updating multiple Things values in RG with a single button click

You can use the idea from this tip to bulk update existing Things (just skip step #2 and start with step #3 Storing content from each row. For example, you can solve this task (you’ll need to adapt #3.1 Creating a custom event to add/modify row content to CS in the part where :split by operator is used to catch issues with , in prices):


This is incredible, super detailed, and very practical! I’ve been needing to figure out a way to do this for a bit, I really appreciate you taking the time to break it down.

I have 2 use cases, the first of which would fit perfectly in your example: in my app, a user can clone a recipe, make changes to it (ingredient quantities, adding / deleting ingredients, etc). I found auto-binding very WU intensive so I opted to use custom events each time a value changed. The problem is if a user continuosuly changes a value, adds or deletes a lot of lines (or adds/deletes continuously) I rack up a bunch of WU. Your method would solve this first use case by ensuring the WU is only counted at the end, upon Saving.

My second use case, although similar: a usee can edit their own recipe. So instead of making a copy , they’re pulling up an already created recipe under their account, making tweaks (again, quanity, add or remove line), and saving it - but it must remain the same recipe / UID. Is there a way to use what you’ve laid out above, and instead of creating mew things, make changes to the items that changed, delete the items that were remkved, and only create the items that weren’t there before?


@artemzheg - awesome overview! The only thing I would add is having a custom and very unique delimiter that isn’t ’ ,’ or ‘/’. That way, when your users inevitably use these characters in their inputs, it won’t break the ‘split by’ logic :slight_smile:


While I’ve initially made a note about being careful with choosing a text separator for template number/input’s A value/input’s B value in 3. Storing content from each row, there is another place where thing will go wrong . If user will use , in inputs, :split by(,) will give us erroneous data in step 3.1 Creating a custom event to add/modify row content to CS after applying it to :formatted as text(which is a text with all our list items being separated by ,).
The problem should be solved by adding some special characters in the very beginning of our mask for rows_content items. Like ******number/////input's A value/////input's B value. So instead of :split by(,) we will use :split by(, ******) after :formatted as text.

Thanks for pointing this out, I’ll update the tip later today :slight_smile:

Is Ingredient a separate data type or users just input anything they want to?

It is a separate datatype; actually for my database structure, it’s a 3rd datatype called recipe-ingredient, which is a combination of an ingredient and a measurement value (i.e. Sugar / 2oz) and the subsequent calculated nurtition values based on that quantity (i.e. if sugar is 111 calories per oz, that recipe ingredient of qty:2oz will have 222 under its calories value - I save this in the Db to make filtering and searching easier.

So for editing or deleting Thing’s values the approach I’ve described above should work.
On page load or other trigger (button click, for example) you can accumulate rows values in a custom state and use unique ID instead of number. So you format will look like unique ID/input's A value/input's B value (with wisely chosen separators instead of / :slight_smile:)
When values are changed in a row - update appropriate item in rows_content CS.
When a row is removed (or marked as to-be-deleted) visually but the thing from the row still exists - you remove appropriate item from rows_content CS and can add it to new CS rows_to_delete. Later CS will be used to delete things on button “save” click and also make it possible to restore a row with all values in case if user changes his mind and do not want to delete this row.

The problem is adding new row to the RG of Things type. For now, it seems to me there is no option except creating an empty thing and modifying it later. But I’ll try to experiment later :slight_smile:

update: I have an idea of a workaround for adding new rows to the RG with existing things without a need for creating an empty thing. Will check later today and share results.

1 Like

I have a similar method of storing data in states for my own editable tables using RGs but i store both the row number and the data together. It’s usually row number and data (which can be a Thing’s UID) joined with a seperator (i usually use |).

The row number starts with 01 so it’s always sortable and I can add additional paramters to fine tune the sorting like 01a, 01b.

What makes this useful is when i need to remove a row i just use a regex to match the row parameter to remove the exact text.

When I add a new value i the list will always be sorted since a text will always sort nicely with 01|data, 02|data, 03|data.


@msgiblin after playing around with different approaches for a couple of days I can confirm that it is possible :slight_smile:
Besides, in fact, a separate custom state for a list of numbers (row_numbers in my initial example from above post) that is used in new row addition is not needed.

So, first of all, let me show my example:
bulk CRUD

There are 3 tasks in my DB. I mark one of them to-be-deleted (row turns red), modify content of another task (row turns orange) and add a new row (blue one). When “save” button is clicked - CRUD actions are triggered (backend WF to create and update tasks, delete a list of things for tasks to-be-deleted).

I use a bunch of custom states (lists of text):

  • to get Tasks from the DB and on page load, accumulate updated inputs values and values from new rows
  • to accumulate Things that are marked to be deleted
  • to find which Things should be updated

In my example I use mask id ||| Task name ||| Task description. For existing Tasks id = Task's unique id. For quasi-Things (new rows) id = quasi-Thing #X (where X is calculated by checking the latest quasi-Thing’s X in the list and +1 to it).

In the end you can see the bug where quasi-Thing #1 stays for some time in the RG while real Thing (Task #4) that used quasi-Things #1 values has been already created. The root cause sits in the fact that I use custom state to load Tasks from the DB (and convert them into texts). And while all backend WFs are still scheduling - custom state loads data before it has been actually created/modified. Wrapping actions in custom events doesn’t help here. So for demo purposes I’ve just added a 5 second interval before re-fetching existing Tasks from the DB.

1 Like

This is absolutely incredible!! Thank you for taking the time to figure this out and break it all down so well! I will certainly work on putting this into practice - I’m very impressed by what you’ve done here.

1 Like

Wondering how do you assign numbers in the RG to existing Things?
For example, we have 3 Things in the DB. How do you assign 01, 02 and 03 to Things text representation in a Repeating Group?

Thanks for this great tip! I’m trying to implement it but I got stucked at the beginning.

Not sure why but, in step “2.2.1 Adding rows”, I’mt not being able to add the “+1” when I set the state of my element (see image 1). It just doesn’t appear the “+” icon.

Thanks in advance!

1 Like

Turn ON new Expression Composer:

This will activate parentheses feature and you’ll be able to apply + right after :max operator:
Screenshot 2024-03-14 at 22.39.43

When expression is done - deactivate new expression composer if you don’t want to suffer :slight_smile:

BTW, I already have an updated draft of the tip I’ve posted in this topic, polishing some issues. In fact you don’t need a separate custom state for numbers, everything can be done without it. I hope I’ll post updated version on the weekend or in the beginning of the next week.

1 Like

Thanks! I’ll think I’ll wait for the update then :smile:.
Question: is it possible to work with more than just one type of form-input-element?
For example: I want the user to be able to add:

  • Long text inputs
  • Short text inputs
  • URL inputs

Like in this image :point_down:t3:

Do you want to dynamically add some new input element to the form you posted?
Or you have some other task?

Exactly, I want the user to be able to create dynamically new inputs but in the RG example I only saw 1 type. I want to have more than 1 type of inputs: normal input, multiline inputs and 1 input specific for URLs.

Very short video here to showcase the concept.

And user should be able to add only 1 input field for each field?
I mean: 1 short description input element, 1 long description input element, 1 checkbox and so on.

If yes - at first glance it is possible.

Noo haha more than 1 ideally. He/she should be able to build something like:

  • short
  • long
  • long
  • url
  • short

Just an example ofc.

I’m thinking a solution could be to use only “Multiline input” and add coditionals connected to the custom state. For example I could put:

  • If the user clicked in “+ short-input” then “min heigth 40px” and “max height 40px”

I’ll try it now then.

Something like this? :slight_smile:
Screen Recording 2024-03-15 at 00.52.42

1 Like

I need that! :pray:t3: :sweat_smile:

Is that a repeating group @artemzheg ?

Yep, this is a repeating group of content type “text”.
In the RG there are 3 elements that are not visible on page load.
When some of these buttons are clicked - I make a :plus item to a custom state (type “list of text”) and the text instance that is added to the custom state is using the following mask id|||type, where:

  • id is a row number (current cell’s index in my quick example. Not a good solution cause when you will delete some row - indexes will be recalculated and inputs will be reset. It’s better to calculate a number and make +1 for each new row).
  • type is one of the following input forms:
    • input
    • multiline
    • checkbox

So, for example, when “add input” is clicked and RG has now rows - text instance will look like 0|||input.
Each input form has a conditional expression. For example, for input element: “when Current cell's text :split by(|||) :item #2 is "input" - make it visible”.

1 Like