DEV Community

Cover image for Power Apps- How to do Loops in Power FX
david wyatt
david wyatt

Posted on

Power Apps- How to do Loops in Power FX

Power FX is the LowCode language of the Power Platform, originally only in Canvas Apps it is not extending to Dataverse and Power Virtual Agents. Yet because it's based on expressions (like Excel and Power Automate) it is not directly comparable to something like VBA or Python. Like Excel, Power FX is at a base level entwined with components (Excel formulas/functions are entwined with cells/worksheets etc), this means core functionality can be outsourced to components (like timers/delays).

One of the most interesting aspects of Power FX is how it handles loops. Out of the box other expression-based languages don't have loops (Excel none and Power Automate requires actions). Power FX does have a Loop (ForAll), but it is not quite the same as other languages loops. To cover for some of its omissions is the timer component, which due to its repeatability can also be treated as a loop.

ForAll

The ForAll function is most comparable to a ForEach loop, as it requires an array/collection to increment over. There are a couple of key things to be aware of:

  • You can't set variables within it ForAll set error
  • It Can't update itself (you can't Patch the collection you are looping over) ForAll self update error
  • The Loop has to complete (no break / stop)
  • Always steps one (e.g. can't increment in 2s)
  • There is no built-in index
  • All internal items are referenced as ThisRecord (making loops within loops hard to reference the correct loop)

If you wish to skip standard uses for the more interesting ones click here

ForAll - Standard Use

So a basic ForAll would look like this:

ClearCollect(array,[1,2,3,5,8,13,21]);
ForAll(array,
    Notify(ThisRecord.Value)
)
Enter fullscreen mode Exit fullscreen mode

But as you can see its pretty pointless to use it like this (as there is no delay so you will only see the last notify).

A more useful use would be to create a new collection:

ClearCollect(array,[1,2,3,5,8,13,21]);
ClearCollect(array2,
    ForAll(array,
        {Value:Value+1}
    )
);
Enter fullscreen mode Exit fullscreen mode

In above we create a collection of 2,3,4,6,9,14,22.

We can also do this on Object collections:

ClearCollect(array,[
    {fib:1,word:"one",id:1},
    {fib:2,word:"two",id:2},
    {fib:3,word:"three",id:3},
    {fib:5,word:"five",id:4},
    {fib:8,word:"eight",id:5},
    {fib:13,word:"thirteen",id:6},
    {fib:21,word:"twenty on",id:7}
]);
ClearCollect(array2,
    ForAll(array,
        {Value:Left(word,2)}
    )
);
Enter fullscreen mode Exit fullscreen mode

Result
array2 collection results

Though quick tip you can use certain functions on collections too, removing the need for a ForAll.

ClearCollect(array2,Left(array.word,2));
Enter fullscreen mode Exit fullscreen mode

The above will give the same results as the ForAll above.

The other standard use it to update another collection with a value from the first collection:

ClearCollect(Objects,
    {a:1,b:"A",c:true,ID:1},
    {a:11,b:"B",c:false,ID:2},
    {a:-11,b:"C",c:false,ID:3},
    {a:7,b:"D",c:true,ID:4}
);
ForAll( Objects, 
    Patch(variables,{ID:ThisRecord.ID},
        {LetterNo:ThisRecord.b&ThisRecord.ID}
    );
);
Enter fullscreen mode Exit fullscreen mode

Although another tip is to use Drop/Add/RenameColumns to do the same:

ClearCollect(varaibles2,
    DropColumns(
        AddColumns(Objects,"Letter",b&ID)
    ,
        "a","b","c"
    )
)
Enter fullscreen mode Exit fullscreen mode

ForAll - Non-Standard Use

What do I mean by non-standard use, well it's a way to do bypass some of the limitations we mentioned.

Can't Set variables
You may not be able to set variables, but you can update arrays/collections. And a single row collection is pretty much an object, and an object is a group of variables (can you can see where I'm going). So all we need to do is create a collection with a single row with fields you want as variables. Then in the ForAll use an If to Patch the collection values.

ClearCollect(Objects,
    {a:1,b:"A",c:true,ID:1},
    {a:11,b:"B",c:false,ID:2},
    {a:-11,b:"C",c:false,ID:3},
    {a:7,b:"D",c:true,ID:4}
);
ClearCollect(variables3,
    {ID:1,Field_b:"",
    Field_c:false,
    Field_a:0}
);
ForAll(Objects,
    If(ThisRecord.b="B",
        Patch(variables3,{ID:1},
            {Field_b:ThisRecord.b}
        )
    );
    If(ThisRecord.c=true,
        Patch(variables3,{ID:1},
            {Field_c:ThisRecord.c}
        )
    );
      If(ThisRecord.a=-11,
        Patch(variables3,{ID:1},
            {Field_a:ThisRecord.a}
        )
    )
)
Enter fullscreen mode Exit fullscreen mode

Image description

It Can't update itself
Building on setting variables, all we need to do is use the Select() function, as here we can add the Patch to another button, which when OnSelected uses the variables/collection we have created.

ClearCollect(Objects,
    {a:1,b:"A",c:true,ID:1},
    {a:11,b:"B",c:false,ID:2},
    {a:-11,b:"C",c:false,ID:3},
    {a:7,b:"D",c:true,ID:4}
);
ClearCollect(variables4,
    {ID:1,Set:0}
);
ForAll(Objects,
    If(ThisRecord.b="B",
        Patch(variables4,{ID:1},
            {Set:ThisRecord.a}
        );
        Select(buUpdateSelf);
    );
)
Enter fullscreen mode Exit fullscreen mode

buUpdateSelf

Patch(Objects,{ID:Index(variables4,1).ID},{
    a: Index(variables4,1).Set
    }
)
Enter fullscreen mode Exit fullscreen mode

If you are wondering about infinite
loops, sadly you can not create them. I tried changing the button to adding a new row, but looks like the ForAll caches the collection, so any changes happen after it is complete).

buUpdateSelf

Patch(Objects,Defaults(Objects),{
    a: Index(variables4,1).Set,
    b:"B",
    ID:Max(Objects,ID+1),
    c:false
    }
)
Enter fullscreen mode Exit fullscreen mode

The Loop has to complete & Always Steps one
With the help of an If this is quite easy to overcome:

Stop at 2

ClearCollect(Objects,
    {a:1,b:"A",c:true,ID:1},
    {a:11,b:"B",c:false,ID:2},
    {a:-11,b:"C",c:false,ID:3},
    {a:7,b:"D",c:true,ID:4}
);

ForAll( Objects, 
    If(ThisRecord.ID<3,
        Collect(variable,ThisRecord);
    );
);
Enter fullscreen mode Exit fullscreen mode

Step 2

ForAll( Objects, 
    If(Mod(ThisRecord.ID,2)=0,
        Collect(variable,ThisRecord);
    );
);
Enter fullscreen mode Exit fullscreen mode

One last cool tip is the Index() function, as this allows us to reference other records in the collection. The below gets one from previous, one from current, and one from next:

ClearCollect(Objects,
    {a:1,b:"A",c:true,ID:1},
    {a:11,b:"B",c:false,ID:2},
    {a:-11,b:"C",c:false,ID:3},
    {a:7,b:"D",c:true,ID:4}
);
ClearCollect(Objects2,
    ForAll(Objects,
        Switch(ID,
        1,{a:a,b:b,c:Index(Objects,2).c,ID:ID},
        Max(Objects,ID),{a:Index(Objects,ID-1).a,b:b,c:c,ID:ID},
        {a: Index(Objects,ID-1).a,b:b,c:Index(Objects,ID+1).c,ID:ID}
        )
    )
)
Enter fullscreen mode Exit fullscreen mode

Timers

The first thought of a timer is it's a delayed action. But because it has the option to repeat it can also be used as a loop. What value does it add over a ForAll, well:

  • Its async (non-blocking, so the user can continue interacting with the app)
  • It can include a delay
  • It does not require an array
  • You can step any amount
  • You can stop/restart the loop
  • Any function can be called in the loop

And what about the negatives:

  • Its slow (max speed is 1 millisecond)
  • You have to write more code

As you can see, as long as speed isn't critical then a timer is generally the best loop to use.

To turn a timer into a loop you need to set following parameters (vbTimer is just a Boolean variable).

Repeat true
Reset vbTimer
Start vbTimer
Duration 1 (or higher if you want a longer delay)
OnTimerStart or OnTimerEnd YourCode

The following code is an example of a simple ForLoop
StartButton

Set(viCounter,0);
Set(viLimit,100);
Set(vbTimer,true);
Enter fullscreen mode Exit fullscreen mode

OnTimerEnd

If(viCounter<viLimit,
    Notify(viCounter);
    viCounter=viCounter+1;
,
    Set(vbTimer,false);
)
Enter fullscreen mode Exit fullscreen mode

If you want to update items of a collection, you can use the Patch & Index

Set(viCounter,1);
Set(viLimit,CountRows(Objects)+1);
Set(vbTimer,true);
Enter fullscreen mode Exit fullscreen mode

OnTimerEnd

If(viCounter<viLimit,
    viCounter=viCounter+1;
    Patch(Objects,{ID:viCounter},
        {a: Index(Objects,viCounter).a+viCounter}
    )
,
    Set(vbTimer,false)
)
Enter fullscreen mode Exit fullscreen mode

You can also use it to find a value and then break:

If(viCounter>viLimit,Set(viCounter,1),Set(viCounter,viCounter+1));
If(Index(Objects,viCounter).b="D",
    Set(viFind,Index(Objects,viCounter).ID);
    Set(vbTimer,false);
)
Enter fullscreen mode Exit fullscreen mode

What's useful is the collection isn't being cached, so you can update the collection and it will be shown in the loop. In the above example the loop will check every value, if it can't find it, then the loop resets and tries every item again. As soon as the collection is updated then it will be read and stop the loop if it matches the condition.

Additionally it can be a straight DoWhile loop:

If(Not(vbTimer),Set(viEndValue,viCounter));
Set(viCounter,viCounter+1);
Enter fullscreen mode Exit fullscreen mode

Although loops can be unintuitive in PowerFX, the full range of functionality is there, it just takes a little out of the box thinking.


Further Reading

Top comments (2)

Collapse
 
jottie profile image
Jottie

Great collection of tips and tricks!!!
Btw, you can do Forall(collection as myCol... to avoid the confusion with ThisRecord in nested loops.

Collapse
 
wyattdave profile image
david wyatt

Great call out, something I only just discovered and a total life saver