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 )
- 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:
1. Creating custom states
We will need two custom states (CS further in the text):
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.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):
- 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:
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:
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):
Parameters:
- row_number (type = number)
- task name (type = text)
- task description (type = text)
In the custom event add action set state
:
Expression breakdown
rg_row_addition's rows_content:filtered:count > 0
→ in this part we are checking if there is already an entry (text) in CSrows_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:
: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
:
- 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.
- formatting for no: if there is no item for row number → add it to the list.
:split by(,)
: applying:formatted as text
evaluates to a text. But our CSrows_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.
- Trigger: when input’s value is changed
- Action:
trigger custom event
“collect values”
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:
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.
will update this section tomorrow
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
- From
rows_content
list we extract the value that matches the number defined inindex
:
- 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
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):