How to make an 'add to favourites' function

newbie-question
repeater-widget
mobile-prototyping

#1

I want to make a function in my app that allows you to like/save items (each item can only be saved once, so its not a shopping cart idea). I want the saved items to appear on a different page once they are saved, in a list layout. I also want the user to be able to easily delete items out of this list, so the layout does not get impacted. The items (shown in a swipe carousel) and their like-buttons are already there.


#2

You will need to use global variables to store and access data across pages. You don’t mention repeaters, but this post is tagged with “repeater-widget”. Sharing data between repeaters on different pages can be more complex, because you need to build or update the repeater automatically on page load. The easiest way to do this is to have the same basic repeaters on both pages. They don’t have to be identical in terms of layout or repeater widgets, but should have the same list of items (number of rows) and a common way to refer to each item in both repeaters, like an index, product number, or unique name.

For example, let’s say on Page 1 you have a carousel of 5 items, represented as a horizontal repeater of 5 rows. When a user likes an item, a corresponding global variable can be set. For the sake of simplicity, you could have 5 global variables, Fav1, Fav2, Fav3, Fav4, Fav5. When the user clicks the Like button in row 2, it can set Fav2 to “true”.

On Page 2 let’s say you want to show a vertical list of all liked items from the carousel on Page 1.
You can have a repeater on Page 2 with the same 5 rows (but styled to show the list vertically, maybe less or more info per item, etc. but the same basic items in the same order.) Then, you can have a series of conditional cases that tests the value of Fav1, Fav2, etc. and shows the corresponding row if the value is “true” by applying a filter. Do this in the Page Loaded event to make it automatic–something like,

Page Loaded
Case 1
If value of Fav1 equals “true”
Add Filter
MyRepeater add [[Item.index == 1]]
Case 2
If value of Fav2 equals “true”
Add Filter
MyRepeater add [[Item.index == 2]]
… etc. for all rows

I’m not sure what this means… If you delete a list item, that means it gets removed, which typically changes the layout of the list, especially in a repeater. So, could you please clarify what you mean by “delete items” and maybe which list… the original list of items that can be liked, or the subsequent list of liked items? …Or maybe “deleting” really means just clicking the Like button again to deselect it? Either way, how would the layout not get impacted? Maybe you could share some examples or sketches of layouts for various states?

If you could reply here and upload a sample .rp file it will be much easier for forum users to provide solutions tailored for your needs.


Here is a demo I made for you, based off one I made a few years ago.
repeater favorites across pages.rp (158.5 KB)

  • On Page 1, there is a repeater with a few items. Clicking an item “likes” it, showing a “favorite heart” icon. Clicking again “unlikes” it. This prototype uses one global variable, VarFavorites, to store which items are liked. As each item is liked (selected) it adds itself to VarFavorites, thus creating an array (a “list” as the variable value.) When unselected it removes its value from VarFavorites. (This is done with built-in string functions that you can inspect in the interaction code.) This is a more extensible way to represent liked items; you can add or remove items from the repeater and the code will still work (just be sure to add the same rows to the repeater on Page 2.)

  • On Page 2, there is a corresponding repeater, prebuilt with the same items as the repeater on Page 1. It has a Loaded event (triggered after all its items are loaded) that adds a filter to itself, hiding all the rows by default.)

    • The Page Loaded event copies the value of VarFavorites to OnLoadVariable (the built-in global variable). Then it triggers a recursive subroutine to filter the repeater based on which items have been liked–the values in OnLoadVariable. The subroutine is represented as a dynamic panel, “ShowFavs”, with two empty states. Each time ShowFavs changes state it looks at the first value in OnLoadVariable, and updates the corresponding repeater row, setting the “Fav” column cell to “true”, then pops that value off of OnLoadVariable, then changes its state to Next, wrap (making it a recursive function/subroutine.) In this way, it walks through each liked item until there are no more items to process–thus the value of OnLoadVariable is blank. When this happens, it adds a filter to the repeater with the rule, [[Item.Fav == 'true']] remove other filters which results in showing only the liked/favorite items. Note that the original set of liked items is still stored in VarFavorites (which comes in handy if user needs to navigate back to Page 1.)
    • There are a set of test buttons which can help demonstrate how things work. They are for learning purposes only and can be deleted.
  • Back on Page 1, the liked items are restored in the repeater (if a user navigates back to this page from Page 2 or elsewhere) via the Page Loaded event.

    • OnLoadVariable is set to the value of VarFavorites.
    • VarFavorites is then set to blank. It will be rebuilt as items are set to “Like”.
    • Because this repeater is structured a little different from the Page 2 repeater, it doesn’t need the recursive subroutine. Rather, a “listener event” is set up in the “rowGroup” widget group in the repeater. Its Moved event “listens” to OnLoadVariable, and if it “hears” its number, it sets its selection state to “true”, marking it as liked (just the same as if a user clicked it …which they actually already did the last time they were on Page 1).

That’s pretty much how this works. Reply with any further questions about details, or if anything should behave differently and you can’t figure out how to do it.


If your Liked/Saved list on Page 2 is not a repeater, then you can create a set of items to match those on Page 1, hide them all by default, then use Page Loaded with conditional cases to show certain items, based on global variable values.

If your lists are dynamic repeaters, meaning the user (or your interaction code) needs to edit, add, or remove rows on Page 1, you can use similar logic to add rows to the repeater on Page 2. If it is possible to add new items (rows) or edit data, e.g., maybe a name or comment field, and you want to transfer this to the list on Page 2, then you’ll need to store all that data in the global variable(s) on Page 1, then add rows to a blank repeater on Page 2 using the data in the global variables.

Here is a post with examples of how to do this more advanced and more extensible repeater copying across pages:


#3

Hi,

Thanks a lot for the reply!!!, i will try it later today!!
Because you asked for a file, here it is:
Testcardcarousel_addtofavourites.rp (142.0 KB)

I’m making an app with conversation cards, so on each card is a question. I want the user to be able to save any card by clicking on the heart icon. I have yet to make an interaction where it is filled with pink when clicked. When liked, the cards appear on the ‘liked cards’ page. What i mean by that i dont want the layout to be impacted when deleting a card, is that when you delete the middle card, there doesn’t appear a gap in the list layout. I hope this makes it clearer

I also stumbled upon something else. I noticed that the counter i have made above the conversation cards carousel doesnt work properly. It should always show ‘1/20’ when the card with question 1 is shown. Do you know how to fix this issue?

Thanks a lot in advance!


#4

Thanks for posting your file. Things make more sense to me now… I see you’re not using repeaters for your lists, which is fine–just a bit more work to setup and maintain, and more work if you need to go from say, 5 cards to 20 and don’t have repeaters. On the other hand, repeaters are fairly complicated and take a good while to get used to using them.

Your counter dynamic panel (dp) has 6 states but there are only 5 states/cards in the carousel dp. That is why they match for the first 5 “next” state changes, then don’t match thereafter–because you are showing card State1 but the counter is at State6. The easiest fix is to just delete State6 from “kaarenset1_nummer” dp, but there are more reliable methods to ensure two dp’s have matching states. Also, if you only need to change the text value (e.g., from “1/20” to “2/20”) then you can just use Set Text and have one counter widget instead of a different one (as a dp state) for every card. I demonstrate methods for each below.


Here is an update to your file with examples of how I would approach this, first without, then with repeaters.
Testcardcarousel_addtofavourites.rp (586.6 KB)

I duplicated your two pages into a folder named, “Version 2” and applied much of what I recommended in my previous demo file, but kept your basic structure. Some key details…

  • (I moved the hidden widgets to the back so it was easier to edit things on the canvas. I added the “bring to front” option to the Show actions for these “more info” card widgets.)
  • For the sake of demonstration, I made five global variables, one for each card (Fav1, Fav2, etc.) that keep track of favorites (liked cards.)
  • I made a selected interaction style for the heart icon-button, along with click and selection state events to like and unlike cards. Basically all it does is set the corresponding global variable–so liking Card 1 sets Fav1 to “true”.
  • It is more efficient and reliable to put the Swiped actions on the parent dynamic panel (named "cardset_panel") instead of duplicating for each card within this panel. Less editing when you make changes and less chance of bugs and inconsistencies. Doing this for your 5-state carousel removed 8 events and 16 actions, simplifying your overhead.
  • To ensure the counter dp state matches the card list state, I made some conditional cases in the cardsset_panel dp’s Panel State Changed event. Pretty straightforward–it tests the state of “This” (cardsset_panel) and sets the state of kaartenset1_nummer to the same state. So, regardless of how the card state changes (swipe, arrow-button click, or anything else) the counter state will match.
  • On the “Liked cards (2)” page, I made a “favorite card” for all 5 possible cards. I kept them shown for easier editing, positioning the layout, etc.
  • The Page Loaded event tests the value of each of the “Fav#” global variables. If one is not “true” (not a favorited card) it hides the corresponding card.
  • To preserve the layout positioning of the card list, I use the “pull widgets below” option in the Hide actions. For example, when “Q1” (card 1) is hidden, all the widgets below it–the rest of the cards–are pulled up (by the height of the Q1 widget.) To make this work with the 15px gutter spacing you have between the cards, I included a 15px tall hotspot widget (just something transparent so user doesn’t see it) below the widgets in each card dp (Q1, Q2, etc.) In this way, the spacing is built in to each card widget …if it wasn’t you get all sorts of potential problems with the “push/pull” option.
  • (I made a set of test buttons to the right which helped me debug this stuff. You can show/hide each of the cards.)
  • I added a Home button in the top nav to show what happens if you delete a saved card. That card is no longer liked on the first page. See the Page Loaded event to inspect how this works.

In the Version 3 folder, I made some more simplifications, primarily to show how you can use one simple widget for the counter.

  • I simplified the card list movement so everything is controlled in one place: the arrow buttons. When the cardsset_panel dp is swiped, it triggers the corresponding arrow-button click event (by using the Fire Event action to target that button’s Click or Tap event.)
  • I could have kept the same Panel State Changed method with a case for each state to set the counter text. However, if you need to add more cards, you’d have to add more cases, and that can lead to unintended and hard-to-find bugs later on. So, the counter text is incremented in one place–the right-arrow button–using some string and number functions to format it. Likewise, the counter is decremented in the left-arrow button.
    • Because your counter string is in the format, “i/n” and you only need to change the first “i” number, that number needs to be extracted from the counter string, incremented or decremented, then inserted in the string again.
    • Additionally, you need upper and lower limits, so it doesn’t go higher than the number of cards (5) nor lower than 1. For this kind of “looped counting”, programmers use a modulus operator (represented with the percent-sign, as in “1%5”, read as “one modulus 5” which equates to 1. 6%5 = 1, and 7%5 = 2, etc. so the counting loops–or is constrained–between 1 and 5.)
    • The magic expression in Axure for all this is:

[[ i.substring(0, i.indexOf(’/’)) % i.substring(i.indexOf(’/’)+1) + 1 ]][[ i.substring(i.indexOf(’/’)) ]]

…where i is a local variable pointing to “text on widget” Target, meaning the text of the target widget of the Set Text action, which is the “kaartenset1_nummer” widget.

…Also, note that the modulus operator doesn’t work with negative numbers, so I added a case for the left-arrow to adjust things when it is clicked at the first card–meaning it should show the last card in the list, card 5. So instead of the resulting math going to 0%5 then -1%5, it is set to 5%5 (which equates to 5.)

  • Another method that could work just as well without all the string manipulation would be to use three widgets for your counter string, so instead of one widget with “i/n” you could have:
    1. “current_card” (just the ‘i’) with right-aligned text
    2. a widget with only the slash character, “/” and center-aligned text)
    3. “total_cards” (just the ‘n’) with left-aligned text
    • Then you could use something like,

Set Text
current_card to [[ (Target.text + 1) % N ]]

…where N is a local variable pointing to the text on total_cards.

In the Version 4 folder, I use a repeater for the card list.

  • So, there is only one “card element” that gets repeated N times–5 in this example. If you click into the repeater, named “Questions List”, and look at the Style pane, you’ll see the datasheet used to control the content and state of the repeater cells–each card in the list. I pasted in some questions from a 20-questions game list. I also made a “More” column for content to be displayed in the More Info card.
    • To add a sixth–or any number of more–card, just add a row to the datasheet and type or paste info.
    • The repeater automatically sets the “n” value in the counter, by testing for the last row in the datasheet, and updating the counter total based on how many card items (rows) there are.
  • The card changing logic is a different with the repeater, because the repeater becomes an actual list of cards, instead of dp states, it needs to be moved left-right so the current card is shown in the middle of the screen (so actually more like a true carousel.) Axure doesn’t allow repeaters to be moved, so I created a dynamic panel from the repeater, named “Questions Panel” and move that instead.
  • When the right-arrow button is clicked, the counter is incremented and the next card is shown by moving Questions Panel to the left by the width of one card plus the margin between cards.
    • I use some hotspots the same size as the left and right margins to help with calculating exactly how much to move Questions Panel.
    • I also set a right-most boundary on this movement, so the list doesn’t keep moving to the left way off screen.
    • I include a conditional case to test when the last card is shown (the Questions Panel dp has moved as far left as it can go) and if so, “wrap” the list by positioning it at the starting point (as far right as the list can go.)
  • Because the repeater can contain any number of cards, I use a single global variable, named “varFavorites” to store an array of liked cards, rather than a bunch of separate global variables, one per card.
    • Just as in my previous demo file, selecting the heart on a card adds its card number to the global variable, and unselecting it removes its number from the variable
  • On the “Liked cards (4)” page, there is a repeater for the saved cards. It is styled to show the list vertically, and the visuals for the cards are different, but the repeater’s datasheet is largely the same. I just copy-pasted the first two columns from the repeater on the first page.
  • As in my previous demo, a “ShowFavs” dp is used as a recursive subroutine to walk through the array of liked cards and show the correct rows by updating their “Fav” column values if true, then applying a filter to show only the rows where “Fav” equals true.
  • When a saved card is deleted (user clicks the trashcan icon) the varFavorites array is updated to remove its card number, then its own “Fav” column value is set to “false”. Because the repeater is already filtered to only show rows where “Fav” is true, this row is thus automatically hidden. The repeater also automatically updates is layout so there is no extra gap in the cards. Nice!

Version 5 is the same as Version 4. I just pasted more card rows into the repeater to show how easy and quick you can go from a few cards to many cards, once the repeater “infrastructure” is set up.


#5

Thank you so much! I do have another small question. I have replaced the menu on top with a menubar below (in your version 3). How can i make sure that the menu bar’s position is always fixed on the liked cards page? Now it moves along with the adding and deleting of cards.


#6

I would recommend putting the list of cards in a dynamic panel (and/or pulling the menu bar out of the dynamic panel, depending on which version of mine you’re using, and decrease the height of the dynamic panel accordingly), and placing the menu below and in front of the dynamic panel. The “push/pull” actions will be limited to widgets inside the same dynamic panel, so won’t affect widgets outside of the panel.


#7

Thank you so much. I have one final question about the add to favourites function. I now have two card sets in my app and it looks like the add to favourites only works when going from the first set directly to the favourites page or from the home page to favourites, but when i first go to the home page with the back button in the first card set page, it doesn’t seem to work anymore. Also when i have already liked cards from the first set, the cards i like from the second set don’t appear in the favourites page. It seems as though pressing the back button completely removes liked cards. I will send you a link so you can see that it doesn’t work properly. I can’t figure out why. I hope you can help me.
addtofavourites_testfinalquestion.rp (592.8 KB)


#8

Hmm… I see what you mean. What you coded should work, but doesn’t. This looks like a bug in Axure, in which navigating back to the previous page resets all the global variables to their default values. I’m not sure why, but the same thing happens if you click the browser’s Back button. It looks like the browser and/or Axure treats the two pages as separate webspaces or something…

This gets more strange though… In testing your prototype, I noticed that if you never use the Back button, and only use the Home and Heart buttons in the bottom nav–which link directly to named pages in the prototype–then global variables don’t get reset, and everything performs as expected. If you then use the Back button after having navigated to Home by pressing the Home button in the bottom menu, the global variables don’t get reset! …Until you go to the “Set2” page, like a card, and click the Back button. Then, any global variables set from that “Set2” page get reset, but not global variables set from the “Set1” page. So, this results in a somewhat predictable but mostly illogical behavior. We’re going to need help from Axure Support. I want to test this a bit further and then submit a bug to support@axure.com and see what they say.

In the meantime, here is a somewhat convoluted workaround. It shouldn’t be necessary, but I can’t think of a better way to do this, given Axure’s strange global variable behavior. In this updated .rp file, I changed the behavior of the Back buttons on your pages so they only use direct links to pages in the prototype. To do this, I used the (otherwise unused) OnLoadVariable to keep track of the “previous” page.
addtofavourites_testfinalquestion_solved.rp (597.9 KB)

  • On the “Home” page there is no Back button, so its Page Loaded event just sets OnLoadVariable to its name, using the built-in variable, [[PageName]]. Thus, OnLoadVariable = “Home”.

  • On each of the other pages–all pages in the “Convo cards” folder–I only set OnLoadVariable in events that link to another page, just prior to the Open Link action, in order to keep track of the page it came from. For example, the Heart button has this:
    image

  • On each of the other pages, the Click or Tap for the Back button tests the value of OnLoadVariable. If it equals “Home” it opens the Home page.

    • I added cases to test for the name of all the other pages. For example, the Back button (hotspot) on “Set1” page has this:

This solution works–at least for me–but is pretty tedious, and if you later add more pages then you need to edit the code for all the Back buttons all over again. You could help this a bit by making a Component from the Back button so the same widget is reused on each page. Then you’d have only one set of interaction code to edit.

A drawback to this approach is you only get one level of “Back”, so if you navigate from Home to Set1, then to Favorites, then click Back, you’d go to Set1, but if you click Back on that page, you’d return to Favorites, not Home. Keeping a running page list as a breadcrumb element is even more convoluted to keep track of in Axure. Conceivably, it should be possible to keep a running list of visited pages in one global variable and remove one page name at a time as you back up through the page list.


#9

Here is a simplified example to demonstrate the “Back” issue with global variables. There is only one global variable in this prototype, “OnLoadVariable”, to eliminate any possible issues with having too many global variables, or anything else going on with some other issue in the prototype @nohorse first posted.

Global Variables and Back Link.rp (68.6 KB)

  • Page 1 has a text field and a “Go To Page 2” button.
    • The text field, “input” has Hint text (“Enter anything here”) and a Loaded event that sets its own text to the value of OnLoadVariable.
    • The “go button” sets OnLoadVarible to the text value of “input”, then opens Page 2.
    • It also has some test buttons to set and get the value of OnLoadVariable.
  • Page 2 has a rectangle widget, “copied text”, a “Back” button, and a “go button”.
    • The “copied text” widget has a Loaded event that sets its own text to the value of OnLoadVariable.
    • The “Back” button opens a link with the “Back to previous page” option.
    • The “Go To Page 1” button opens a link to “Page 1” directly as a page in the prototype.

Try this procedure when previewing the prototype:

  • On Page 1, enter some text in the text field.
  • Click the “Go To Page 2” button.
  • View the result on Page 2. The “copied text” rectangle contains the text entered on Page 1 as expected.
  • Click the “Back” button.
  • View the result on Page 1. The text field has a blank text value, and its hint has changed to the (previous) value of OnLoadVariable, which is definitely not expected!
  • View the console. The value shown for OnLoadVariable is blank. However, when this page loaded, the previous value of OnLoadVariable must have been accessible because the hint text matches it.
  • Click the “Get OnLoadVariable” button. The “output” widget’s text value is blank, confirming OnLoadVariable is blank.
  • Click the “Go To Page 2” button.
  • View the result on Page 2. The “copied text” rectangle text value is blank, confirming OnLoadVariable is blank.

Now reload the prototype and try this procedure when previewing the prototype:

  • On Page 1, enter this text: “one”, in the text field.
  • Click the “Go To Page 2” button.
  • View the result on Page 2. The “copied text” rectangle contains “one”, as expected.
  • Click the “Go To Page 1” button.
  • View the result on Page 1. The text field contains “one”, as expected.
  • Enter “two” in the text field.
  • Click the “Go To Page 2” button.
  • View the result on Page 2. The “copied text” rectangle contains “two” as expected.
  • Click the “Back” button.
  • View the result on Page 1. The text field contains “two”, as it should.
    • So why does this work only when the “Back” button is clicked after previously clicking the “Go to Page 1” button?
  • Now view the console to inspect the value of OnLoadVariable. It is “one”, which is not expected!
    • This is even more problematic as the resulting behavior after clicking the “Back” button changes, and furthermore, the value of OnLoadVariable has changed from the time the page loaded to the time it can be accessed programmatically.

Now reload the prototype and try this procedure when previewing the prototype. Keep the console open and observe the value of OnLoadVariable as you proceed.

  1. On Page 1, enter “one” in the text field.
  2. Click the “Go To Page 2” button and view the result.
  3. Click the “Go To Page 1” button and view the result.
  4. On Page 1, enter “two” in the text field.
  5. Click the “Go To Page 2” button and view the result.
  6. Click the “Back” button and view the result.
  7. Repeat steps 4–6 several times, entering “three”, then “four” then “five”.
    It appears to work correctly each time, but note that OnLoadVariable is set to “one” every time Page 1 loads, but only after the Loaded event is processed. Of course this is very confusing and problematic. By the way, I also tested this using the Page Loaded event to set the text field’s value, and got the same results.

I published this prototype to Axure Cloud here: https://74pc1w.axshare.com

  • Same crazy results. You can verify the value of OnLoadVariable by clicking “Get OnLoadVariable” on Page 1 at any time.

I also tested this out on RP 9 and got the same results. Could not find any previous posts with this exact problem, but it looks like it may have been around since at least RP 8.


#10

We were able to do some testing with the “Open Link > Back” interaction and found that this is indeed unexpected that the global variables get reset. I’ve gone ahead and filed this as a bug for our engineers to investigate.

Thanks for discovering this bug for us! Hopefully, using @mbc66’s solution to use direct links and different cases to specify which page to go to works in the meantime.