Reverse a List field?

I have a list of things stored in a field inside another single thing. They are stored, obviously, in the order they were added. I know that if I pull these into a repeating group I can sort alphabetically or randomly etc or in the natural order based on when they were added to the list. What I am wondering is, can I sort starting at the end of the list?

I know that I could work around this by having a separate thing where I can sort by date or an order field, but I don’t need this bad enough to warrant that additional complexity. Just want to know if it is possible to pull a list field and reverse it either natively or with a plugin.

Thanks!
Ian

Looks as if this is not possible. As I understand it. the number of items I can put in a field before compromising performance is higher than the number of items I can put in a thing. This means that having a separate thing is not viable for my case.

It certainly seems to me that it would be just as likely that I would want to list items most recent first as I would in the order they were added. But as far as I can tell, it’s not possible to reverse a list in a field in a thing. I’ll be looking at plugins to see if I can accomplish this.

Or maybe I should try a calculation based on count and index number…

So you just want to reverse the list? (Item n becomes item 1, no sorting?)

Edit: also, where do u want to do this — in the page or on the server?

Right. Here’s more specifics. I have 2 Things: User and Song. User has a field that represents favorite songs that is a list of Songs added one at a time by the user. I’d like to list those recent first or end to beginning or :Count to 1 to put it different ways way.

I was thinking I would use a Repeating Group for this. And I don’t need to change the order server side—this is just how it needs to be shown to the user.

I might be misunderstanding, but doesn’t every “Thing” have a Created Date? Why couldn’t the items be listed in reverse chronology by that field?

Thanks for the reply. What I want to reverse is a list field. The creation date of the thing represented in the list is not the same as the order in which that thing was added to a field of another thing. Hard to describe—I think I am using the correct terminology.

Oh, I see. The song thing isn’t actually created when the user adds it. They are just selecting it.

Correct. Adding the song to their personal favorites.

I’m currently experimenting with a group in each cell that is a number which equals the count of the repeating group minus the index, plus 1. This gives me the index of the inverse. Then I can leverage that in each cell to get the reverse order. I think it’s working. Will test more.
Seems like a lot of work though.

1 Like

So, you could do this with Toolbox, but it is hard-ish to work with arrays in Toolbox’s Run JavaScript and Expression elements, however, as anything coming from Bubble has to be turned into text and then back into the correct data type.

So that’s a bit of a hassle. Easiest thing is just to do it via a rather simple plugin. I’m throwing a free one together as this is actually kind of an interesting problem.

I’m not proficient in javascript, but I figured something could be done with that route. The method I described above seems to work as well, but it does feel like a lot of work for a simple problem.
My original thought was that I was just missing something and that this was just a sort option. I look forward to seeing your plugin.

I thought so too, so I put together a quick-n-dirty client-side solution last night…

  1. First, enable “expose element ID” under the app’s SettingsGeneral tab.
  2. Then, add an ID to the RG in the editor.
  3. Finally, paste the following into an HTML element on the page, and change “my-repeating-group” to whatever you entered for the ID in the editor.

<script>
$( window ).on('load', function(){
    setTimeout(function(){
        var $container = $('#my-repeating-group .rows'),
            $items = $container.children('.GroupItem');
        $( $items.get().reverse() ).appendTo( $container );
    }, 0);
});
</script>

@subtlemonsters, it’s great that you worked out a native Bubble solution! I’d stick with it if performance is acceptable.

My “proof of concept” hasn’t been thoroughly tested (only on a single-column RG) and works only if the entire list is on the page (in the DOM).

EDIT: Forgot the script tags - just added them.

3 Likes

@sudsy, this is a clever solution I’d not thought of. @subtlemonsters, in case you’re wondering: The reason this is interesting is that, in JS, reversing an array is really simple - the reversed version of some array arr is just arr.reverse() … Nifty, right?

BUT, this is a pain to do in Expression or Run JavaScript for the reasons I mentioned before. Additionally, it at first seems very simple to make a plugin for this.

We just have one input field – which should resolve to a list. And then the code in the plugin would be pretty simple: fetch the list in question (so we get it as a JavaScript array) and then hit it with .reverse() and just publish that to an exposed state.

But THEN we start to think: Well, shoot, we want to user to be able to select a list of ANY type. So this requires a little bit of code itself (which I’d worked out in the past, but needs a little tweaking).

And then further, we start to think: Well, it’s not just arrays that can be reversed! For example, if we have a single string (what Bubble calls a text) we can reverse it almost as easily as we can an array (strings in JavaScript are NOT arrays, but they are very array-like!). For example:

var k = 'keith' // <-- variable k contains a string that is my name
k.split("").reverse().join("") // <-- is my name, reversed 'htiek'

So now we might desire for this plugin to not just allow any type of list, but indeed to accept any type of thing at all and to (1) reverse the order of items if the thing is a list or (2) if the thing is singular to convert it to some string representation and reverse that.

And, in fact, if the thing doesn’t have reasonable string representation (for example, let’s say you send it something like a User), that needn’t throw an error, we could just echo the thing to the exposed state, unaltered (possibly with another boolean state that indicates whether the thing was successfully reversed or not).

So, kind of a fun plugin when you think about it that way!

(The above is a long way of saying: I thought about just slamming out a really simple plugin, but I decided to make it more advanced and so it’s taking me a bit longer to complete. :wink: )

1 Like

Thanks for the detailed description. I feel like this is loosely related to another question I keep running into while developing and that is how to implement simple key/value pairs.

There is certainly an argument to be made that the proper way to handle what I want to do in Bubble is to add another thing with 2 fields - song and order. Then I could establish and arbitrary order for songs related to a user and not to title, creation date or any other song data. But I see 2 issues with this approach.

  1. I mentioned above, that based on some of the performance threads I have read, large numbers of items in a field performs better than large numbers of items in a thing. 2. How do I associate a list of songs with the user? I am now connecting 2 existing things with a third thing (order). I could put a user field in each thing but now I have to parse all items in that thing for only ones associated with that user and now that thing not only contains a long list of key/values for that user but for all users.

Every time I need a simple set of key/value pairs stored, I am faced with this conundrum. Obviously, I may not know what I’m talking about and this may not be as complicated as it seems. But so far, this is one aspect of Bubble—awesome as it is—that I have wrestled with.

So, just to be clear: You really do have Song created as its own type, yes?

(At it’s simplest level, a Song would be a thing with just 1 field “Title” (which would be of type text). But of course a Song could have many more fields on it. But do not make the mistake of thinking that a Song is just a text string. This type of error is made all the time by those new to Bubble / new to apps and so it must be said.)

Now, in your app, there is some relationship between Users and Songs. I don’t exactly know what it is in your app, but let us assume for a moment that a User has Favorite Songs (or it could anything – Songs to Play at the Wedding, Songs for My Next Bangin’ Set List for example). Whatever it represents, this is a List of Songs. I’m just going to continue to refer to this as Favorite Songs.

We have many options for how to store this and for how to implicitly or explicitly retrieve Favorite Songs that are associated with a given User.

Option 1: The most obvious one is to create a field on the User object called “Favorite Songs” that is of type Song and it is a list. This might be what you do today – you push a new song onto this list.

Option 2: If Songs do not exist in the abstract – that is, if the only Songs associated with a User are Songs that User has created (e.g., The User enters the title of a song and this causes a Song to be created… they do not select a Song from some list we have provided or that is fetched from an API or whatnot) – we don’t need to create a list at all. The list of Songs created by the User is just: Do a Search for… Songs (constraint: Created by that User)

This search returns the list of those Songs.

Option 3: We might create a data type for holding generic lists of Songs. We could call that data type Song List and it would have at least 1 field: Field Songs of type Song and it is a list.

In this way, a Song List might represent many different kinds of lists of Songs: Favorite songs, songs in a bangin’ setlist for my big show saturday night, songs to play at my wedding, songs that I despise, songs I never hope to hear again, songs that have been stuck in my head.

So now if you wish to have a field “Favorite Songs” on the User object, this field would be of type Song List… and it would NOT be a list! (But of course, you could have a field that is a list of Song Lists. In fact, just like in Option 2, you could always retrieve the Song Lists created by any given user, right? (Do a Search for… Song Lists… created by this User.)

A Song List would have at least 1 field on it – again, Songs, a list of type Song. But it might also have a Title, right? (We might think of this object as being a “Playlist”.)

Now, with this sort of structure, you create Song Lists and then you might attach them to something like the User. Or they might be attached to any other sort of thing. Or you might just get them again simply by searching by their creator.

Option 4: Well, I could go on and on and on, but the main decision is around if a list of songs is just a one-time thing or if there might be many different types of lists of songs in your app.

NOTE: In NONE of these scenarios do we HAVE to add an order field. I didn’t forget to mention order. Lists that are stored in the database as lists HAVE AN ORDER and KEEP IT.

(Note: I’m not 100% sure that dynamically retrieved lists – such as Search for SomeThing created by SomeUser – are always returned in the same order… but I suspect, if no sorting - is defined, the list comes to us sorted by Creation Date or some such. But this is also no matter as we cannot take such a list and change its ordering – we must save to the database to do that – and so it would be a different list.)

Anyway, lists stored in the database have an order… and we can reorder them. Look here:

(Editor here: Lets-do-stuff-with-lists-and-js | Bubble Editor everyone can view. BTW, any errors you see thrown in this sample app are coming from the REVERSE plugin for which this app is the test app – just ignore them – that plugin element is not doing anything yet.)

There are a couple of things demonstrated by this example:

  1. The list of texts retrieved here (it was easiest to build this example with texts, but the list could be anything) maintains its order on repeated page loads. There is no sorting applied to the list. It is simply displayed in the RG as it currently sits in the database. (This list of texts I called a “Bar” and it resides on an object called a Foo. So the RG displays a certain Foo’s Bar.)

  2. We CAN do fairly complex array manipulations using the built in list operators, but often these expressions are not just hard to get your head around, but also physically difficult to build.

  3. After we perform those gymnastics, we can push the reordered list back into the database in its new order. We do not need to add an “order” field.

What I’ve implemented here is swapping of two list elements’ positions in the list. We cannot, in vanilla Bubble, directly address the individual elements of the list. We can GET them by index number, but we cannot SET them.

What we must do instead is construct a list-wise expression that gives us the intended result (this may not always be possible in a one-liner, nor would we want to construct such one liners on a regular basis, if we value our wrists) and then take that result and write it back to the database, replacing the “original” list.

Now, I did not “know” how to do this particular operation list-wise (swap two elements’ positions), but I did have an idea about how it might be done. The algorithm works like this:

a. We have a list of things. We desire to swap the places of item x and item y (x and y represent the indexes of those items in the original list). For example, swap item 2 with item 4. In JS, we would do this directly – we would store item 2 temporarily in a variable, set the value of item 2 to item 4 and then set item 4 to the stored value of item 2. We don’t have this luxury in Bubble.

b. We can however get a subsets of the original list. Of x and y, one index will be smaller than the other. Let’s say that’s x (and y is the larger value – it is later/lower/further into the list than item x). We can isolate x and y by subsetting the original list – we can get the list through item x (“List 1”) and then get the list through item y (“List 2”).

Now x and y are at the endpoints of two lists and these are the items we need to swap. So we can take List 1 and pop item x off of the list (using :minus item). Then we can pop item y on to that list (using :plus item, putting it into the last position in the list – the space previously occupied by item x).

We have swapped the first element. Now, the second list we pop item y off of (again, using :minus item) we then push item x on to that list (again, using :plus item).

Now List 2 has junk at the front of it (junk we must replace with the contents of List 1), followed by some number of items and ending in the repositioned item x. We get rid of the junk by doing a :filter on List 2, using the advanced criteria – which can be used to iterate over a list. The criteria we use is List 1 does not contain “this item” (this loops through list one and, if an item in List 1 matches an item in List 2, that item is removed).

Now List 1 and List 2 represent the start of the list: Put List 1 in front of List 2 and however many items are in there – that is the correct re-arranged list up to item y. We can put them in this order by doing List 1:merged with List 2. (This preserves their order!)

After this there may still be items in the list that were after item y. But these items do not need rearranging – they are just “everything else that is not in List 1 or List 2”. Handily, Bubble does not retain duplicate items. So we can get our final list by merging “List1:merged with List 2” :merged with the original list.

Of course, this sounds and looks absolutely ludicrous. But that’s how you do it. Our expressions for List 1 and List 2 wind up looking like this in my example:

And, of course, List 2 has that gnarly filter condition on it – the long-ass expression there is just “List 1” written again… adding “doesn’t contain this text”.

Now, this is simply not how your mind thinks about these things – but this algorithm is exactly right. And though it is mildly inefficient, it’s just fine in an environment like Bubble where we cannot really iterate anyway.

The text element in the page I point to helps illustrate how this works.

As you can see from the screencap, to write the :merged expressions would become absolutely insane (and, in fact, due to order of evaluation, you cannot actually write this as a one-liner, even if your crazy enough to try it). So we do the merges in a workflow in steps.

We eventually wind up with the final result, which we save in a custom state. (As I’m always saying, custom states are just local variables where we can hold things.)

We can now, whenever we like, write that value back into the database, overwriting the original list using “set list”.

BUT, as you can see, this is a beautifully brutal proof that you do not need to store an “order” field with a list. You can reorder the list and you can replace it – even using vanilla Bubble.

NOW, it would be easier if you use JavaScript to manipulate the list and then rewrite the list to the database (more performant in most cases, as well).

So, all you need is a way to reorder your list (whatever method you use) and then write it back to the db.

A few other notes:

Now, because you can’t write the above as a one-liner, it is in fact impossible to do in a server-side workflow in vanilla Bubble. The proof of this is left as an exercise for the reader. :wink:

We do wonder: Why is Bubble “missing” so many useful array manipulation operators that would make this easier? (ESPECIALLY since, while we have a plugin interface for adding in-page elements (which can be used for visual elements or for computation), an interface for adding in-page actions (which are really only intended for manipulating the DOM, as far as I can tell), and an interface for adding server-side computations, we DO NOT have an interface for adding new operators.

(Like, we would like to add :reverse now, wouldn’t we? Or :indexOf. Or any number of other things. It’s funky, right?)

2 Likes

Wow, I must admit that I got a little lost in the middle of that, but I find these sort of discussions very helpful! Thanks!

This may be rehashing, but this is the mental process I have gone thru.

  • I have 2 types of users, let’s call them Owners and Listeners.
  • I have 2 things, Songs and Users (obviously).
  • Owners can add Songs
  • Listeners can choose Songs for a list of Favorites

First I thought these Favorites would be a list-of-things field. When a Listener selects a song, the workflow adds the song to the Favorites field which puts it at the end. Thus the default order is arbitrary according to the order in which the Listener selected them—the first on added is first and the most recently added is last. Now, there is no thoughtfulness behind this order on the Listener’s part; it’s just the way it plays out.

When I display this list, what makes the most sense to me is that a Listener would see the most recent adds at the top and the oldest at the bottom. However, none of the sorting options allow for this. Rather, they all relate to fields associated with the Song and have nothing to do with the Listener’s interaction. Of course, this makes plenty of sense as to why it is this way. It also makes sense that if I need to track when a Song is added to a list, that I should have another field along side the Favorite.

This leads to having Favorites as its own thing. When a Listener selects a song I could create a new Favorite and it automatically has a creation date and a user associated. Now I can do a search for a users Favorites ordered by reverse creation date.

The problem with this approach is scale. Let’s say an average Listener has 50 Favorites and I have 1000 users. Now I have 50,000 Favorites in my Favorite thing instead of 50 things in a Favorite field. Based on what I’ve read on performance, this is pushing it and 1000 users is a starting number for my app.

So it seems to me that my options are finding a way to reverse a list field, or list a Listener’s favorites by another available sorting method—alphabetical perhaps.

The method I listed above does seem to work, though I have not had a chance to implement it widely. To restate my current solution, in each cell of my repeating group I have a group called inverseIndex with type of content: number and the data source is this expression: thisRepeatingGroup’s listOfSongs:count-currentCellIndex+1. Then I can refer to this number with a text field in the same cell like this: thisRepeatingGroup’s listOfSongs:item# inverseIndex

You are able to do a lot more with the methods you are exploring, @keith. One of them may be better for sure. I don’t really have a need of changing the order on the database, personally. And I’m not sure that would be a plus, because though it would avoid me having to revers a list when displaying, I would have to re-order every time a new Favorite is added.

Thanks again for the effort and the discussion.

Ian

Hey @subtlemonsters: I made a thing for you – https://bubble.io/plugin/list-shifter-karma-ware-1557241599197x311227033793855500

Videos to come. However, if you’re just needing to reverse a list, the editor options should be obvious and helpful to you. Thanks for your patience.

Also, this solution is karma-ware (“free”… but not free)… see plugin page for info. Happy Bubblin’!

Cheers,
Keith

3 Likes