Monday, September 11, 2023

Using the Form Control Creatively: More flexible UI pattern

The first application I wrote for PowerApps utilized the Forms control.  It was...OK.  However, I kept bumping up against it limiting my ability to control the UI in ways that I really found problematic.

The pre-built handling for issues w/ Submissions are nice, but the main reason I was using PowerApps was to have a more flexible user experience.  Since that time I only rarely used Forms except when absolutely required.  

Until now.

As a design pattern, it is worth noting that the starting point for many projects is an Excel list or departmental process that begins from a similar table of data.  So using a pre-built data table to auto-design a UI for you isn't insane.  However, the ability to edit/modify what gets prebuilt suddenly becomes really annoying.

Since my own main issue is building UI's that make sense to end users versus just lobbing a bunch of data at them and asking them to regurgitate it, I stopped using the EditForm, NewForm, SubmitForm, pattern in favor of Collections and the Patch/UpdateIf pattern.  However, while working on my latest project I found a situation where a Form was needed, so dug into a way to do forms "better".  I think this works well.

The final result is the ability for you to build your layout and pages however you'd like, and then at the VERY END, you get to decide how you want to write your data out.  It can be through a Form, JSON->Power Automate, or through the Patch/Update pattern.  You can pivot to any of these approaches at any time with minimal changes to your application.

Hide Your Form

What we're going to do is a hybrid of the Canvas App method, while then eventually shoving all of this into the Form at the end.  

After adding the datasource for my SharePoint list, I moved my Form control (showing all fields) to another page just as a reference page where the user never goes.  You can do this on a non-visible page or on the page you're working on, however, since my own experience regularly pushes users toward multi-page applications, I'd say just drop it on a hidden page.

You don't have to make it pretty at all


However you approach this, just make it so that the bulk of your form is invisible.  You could of course do what I did w/ my first app and just hide only the one field you're dealing with and slowly show more of them, but that is so very tedious and still causes you to get stuck w/ the default layout/design of Forms.

So just dump your form "somewhere" and don't bother making things pretty or useable to an end-user.

Create a base Collection

The key to having this work smoothly is having a Collection that maps to your SharePoint list well.  However, you don't *have* to match this up exactly like you do when using a direct query to SharePoint.  You can create the list manually.  The key is just ensure you have all the required fields included.

This can be done anywhere in your app.  Since Collections are actually Global Variables, if you define this in your hidden page it is fine.  You don't even have to run the code to make this work.  However, to keep your sanity, I'd recommend you do this at the beginning of your application.

ClearCollect(
    itemToSubmitCol,
    {
        name:"",
        jobDescription:"",
        email:"",
        surveyAnswer1:"",
        surveyAnswer2:"",
        ...
    }
)

Now, alternatively and truly for infinite flexibility, I would however recommend that you do a direct map to your SharePoint list so that just in case you decide to pivot to Patch/Update, then it will be quite simple.  To do it that way, you can get the similar results to above through this simple query.

ClearCollect(
    itemToSubmitCol,
    Filter{mySPList, false)
)

That builds out a list w/ identical column names to your SharePoint list (and probably lots of them you won't use).  It's slightly more cluttered doing it this way (especially if somebody's been renaming fields in your SharePoint list), but it means you won't fight w/ naming issues if you ever decide to do a Patch instead of a SubmitForm.

NOTE: The second method will show a warning but will work fine.

Once either versions of the above code are typed anywhere in your application, that then becomes the map for the Collection throughout your app.  So you can now reference items within it like:

First(itemToSubmitCol).surveyAnswer1

This code will not throw any errors once you have the Collection defined.

Note: You might have to save/exit and come back into your app to get the code wizard to properly prompt you for fields when referencing this new Collection.

Link Your Form to your Collection

Go back to your hidden Form page and unlock the first field.

The simple way to do this is simply to unlock each field/card, and add the following to the Default value for the datacard:

First(itemToSubmitCol).email

Just change the blue text to the corresponding field that links up.

What will happen here is that anytime we clear/reset our form to default, it will update the values in this form to whatever our Collection contains.

Build Your Canvas UI 

Now you can go back and be as creative as you'd like around data entry, layout, etc. without needing to be restricted to the default Forms layout.

As you go from screen to screen, you can update your Collection incrementally, or just do it all at once before you submit.

If you want to do this incrementally, then as you navigate forward (e.g. Next icon in my image above), you can add code similar to the following on each page:

UpdateIf(
    itemToSubmitCol, 
    true, 
    {
        roomNumber:roomNumberInput.Text
    }
)

The blue text should represent the value you defined in your original Collection, the green text should reference the field name the user entered the data into.

You can of course just do this all at once at the end.  It is just your preference of keeping code near where you're updating it or if you just want all the code done in one block.

UpdateIf(

    itemToSubmitCol, 
    true, 
    {
        name:nameInput.Text,
        jobDescription:jobDescriptionInput.Text,
        email:emailInput.Text,
        surveyAnswer1:survey1.Text,
        surveyAnswer2:survey2.Text,
        ...
    }
)

What about Photo Attachments for SharePoint?

One of the reasons we often use Form controls is to allow for each inclusion of Attachments, Photos or documents.  There are even more creative methods to do this, but for simple methods this will get it done in a matter of minutes and limited complexity.

You can of course simply expose your Form control at this point and only show the Attachments field.  This honestly works fine if you're uploading documents.

However, if you're doing Photos, then you can sneak past this and allow the Camera control to actually build an Attachments Collection manually.

Alter your Camera Control's OnSelect property to the following:

//generate a random unique GUID for this item
UpdateContext({imageGUID: GUID()});
Collect(
    submissionImages,
    {
        DisplayName: imageGUID & ".jpg",
        Id: imageGUID & ".jpg",
        Value: Camera1_1.Photo
    }
);

What we're doing here is creating a unique ID that we're going to use to auto-generate a filename for (you could also of course create this using other data from your app).  Once we have that, we're appending to a Collection the precise values that a SharePoint Attachment field is expecting.  Do not rename any of the black bold values above.  However, the blue text is your new Collection's name.

As you take photos (and you can even get slick and add controls to display/delete them then), new items are appended to the Collection.  

To Link this to your Form, just do a similar step as above but with your new Collection for the Attachments field in your form's Default property.  However, as the Attachments control expects a Collection, we will put all of it in there.  Therefore, set the Default value for the Attachment datacard to be the same as the name as your Collection:

submissionImages

That's it.  Stupid simple.

Submitting Your Data

Finally, you can easily submit your Form using just a few lines of code.  However, remember that your Collection above must be populated before you run any of this.  If you change it afterward then it might not update properly.

UpdateIf(
    itemToSubmitCol, 
    true, 
    {
        lastAnswer:lastAnswer.Text
    }
);
ResetForm(myForm);
SubmitForm(myForm);

ResetForm causes the form to reset all of the defaults.  If you submitted your form previously, then this reloads everything from your Collection again w/ fresh values.  Then SubmitForm is the same as if you'd done it from the same page as the Form as usual.

All of the normal error-handling can take place and you can throw warning/issues to the end user or handle errors in a more standardized manner.

When To Use This

For simple security model applications, I think the Forms approach is still viable.  As well, I think this approach is more universal and lets you pivot should you need to adjust to a more adaptable security approach.  You always have a Collection that includes the record you need to write, so you could definitely send it via an array of other methods.  It doesn't force you to commit to the Form and you can change at the last minute.

You can change security post-submission


I do believe that this does require you to understand a bit more about the Forms error-handling vs. doing it on your own.  There are pluses/minuses to this, but if you're trying to have things spun up quickly, then this is a reasonable approach to start with.  If you need more, then you have very little you have to change.

No comments:

Post a Comment

Because some d-bag is throwing 'bot posts at my blog I've turned on full Moderation. If your comment doesn't show up immediately then that's why.