Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Dynamic Form Feature(s) - Custom Formatting and Validation, ControlsTestWebPart updates #1672

Merged
merged 16 commits into from
Dec 2, 2023

Conversation

t0mgerman
Copy link

Q A
Bug fix? [ ]
New feature? [*]
New sample? [ ]
Related issues? #1605

Background

Back in July I had this exchange with @PaoloPia (while also mentioning @AJIXuMuK ) on Twitter
https://twitter.com/T0mGerman/status/1683640358972841985

Since then, I've found some time to implement the feature discussed. It has meant adding quite a bit of code and refactoring, so I understand if this needs more of a thorough review than usual.

This update already contains the fix I proposed in #1605, but I can see other PRs for Dynamic Form - #1662 and #1625 : the result of these PRs if accepted would need to be merged with these changes, and an automatic merge is likely not possible. Happy to do a manual one though if this one is accepted.

What the PR adds

dynamic-form-update

  • Rendering of Custom Formatting in Dynamic Form
  • Evaluation of Formulas/Expressions
  • Using these expressions to show/hide fields in Dynamic Form
  • Using these expressions to validate the form and prevent save if need be
  • Update to the ControlsTestWebPart to persist settings using the Property Pane

dynamic-form-update2

File Changes

DynamicForm.tsx

  • Refactored and reorganised code to support added features
    • getFieldInformations is now getListInformation
    • getListInformation uses a new method in SPService to get list data using RenderListDataAsStream. Passed RenderOptions = 64 this method returns a ClientFormSchema which includes everything we need to render custom formatting and perform client validation. It includes all field information too, in field order. The response from this endpoint does lack certain information for Number columns and server side column validation, which we fetch in a secondary call.
    • Various methods have been added to support the evaluation of expressions. DynamicForm now supports evaluation of two different kinds of expressions:
      • Client Side Column Expressions - these can be set when customising the out-of-the-box form in SharePoint. Using these formulas/expressions, you can make fields in the form show or hide based on the values of other fields.
        • The expression evaluator (for the time being) only supports the functions and field types that the custom formatting JSON schema does. Information about these capabilities and restrictions can be found here
      • Column Validation - usually evaluated server-side, these can be set up when editing Column Settings via the List Settings page (the modern 'Edit Column' panel in List Views doesn't provide a way to add them). Using these formulas/expressions you can prevent a form from saving and ensure that values entered by the user meet a certain criteria.
        • Due to only supporting functions applicable to the aforementioned JSON schema support for this type of validation is not as extensive. ie. not all functions listed here are supported. It should be possible to add additional syntax and functions at a later date. To disable processing this type of validation set useFieldValidation to false
  • Added info / error messages using
    • this allows us to report back to the user if something goes wrong
  • Component reinitializes itself if props change (using componentDidUpdate)

Supporting files for this functionality are in:
src/common/utilities/FormulaEvaluation.ts
src/common/utilities/CustomFormatting.ts

Tests for the formula evaluation util are in:
tests/utils/formulaevaluation.test.ts

IDynamicFieldProps and DynamicField.tsx

  • Refactored IDynamicField props and changed how some are used by DynamicField in order to cleanup legibility, adding JSDoc notation to many of them, which will (hopefully) make the purpose of each easier to understand.
  • We previously had a number of different properties for holding field values: value, newValue, fieldDefaultValue, and changedValue. This appeared to be one too many to me, and in some cases we were mutating prop values and using them in a way that wasn't clear. Therefore, I have reduced the value props to three:
    • value which is used for storing the field value from a loaded ListItem
    • newValue which is used for storing values the user has actually updated since loading
    • defaultValue which is used for storing a reference to the configured default value that was set on the field or column
    • field rendering has subsequently been amended in DynamicField to ensure that if a component has mutually exclusive defaults, selected keys or values, that we are using the latest value entered by the user.
  • DynamicField.tsx has been modified to allow the pass through of error messages from validation carried out on DynamicForm

ControlsTestWebPart

  • I have amended ControlsTestWebPart to source settings and component visibility from configuration in the Property Pane. Previously when testing components using this Web Part, hot reloading would cause users to have to toggle controls again, and edits to ControlTest.tsx were required in order to specify a list or list item for DynamicForm. This has all been added to the Property Pane. Controls can be filtered and toggled using a custom Property Pane control.

.gitignore and VSCode launch config example

  • I have amended gitignore so that I could include .vscode\Example-launch.json and .vscode\Example-tasks.json.
    • This will give users downloading the repository a clean and easy way to add debug settings for Chrome and Edge to their copy of the repo.
    • All the users have to do is change the URLs in those files and then rename them to launch.json and tasks.json respectively, then launch Debugging from within VSCode.

Testing the new functionality

  • Create a new list
  • Create the columns:
    • Person: a User column
    • Lookup: a Lookup column
    • Choice: a Choice column with the options 'Choice 1', 'Choice 2', 'Choice 3'
    • Number: a Number column.
      • Go to List Settings then Number column settings to add validation: '[Number] <> 5' and the message: 'Number cannot be 5' - feel free to add a minimum and maximum value.
  • Go to the list in SharePoint and open the New Form
  • Click the Edit Form button (top right)
  • Click Edit Columns, and amend the Conditional Formula for the Number column. Set it to [$Choice] == 'Choice 2'.
  • Press Save
  • Click the Edit Form button again, then Configure Layout
  • Add the following to the header config:
{
    "elmType": "div",
    "attributes": {
        "class": "ms-borderColor-neutralTertiary"
    },
    "style": {
        "width": "99%",
        "border-top-width": "0px",
        "border-bottom-width": "1px",
        "border-left-width": "0px",
        "border-right-width": "0px",
        "border-style": "solid",
        "margin-bottom": "16px"
    },
    "children": [
        {
            "elmType": "div",
            "style": {
                "display": "flex",
                "box-sizing": "border-box",
                "align-items": "center"
            },
            "children": [
                {
                    "elmType": "div",
                    "attributes": {
                        "iconName": "Group",
                        "class": "ms-fontSize-42 ms-fontWeight-regular ms-fontColor-themePrimary",
                        "title": "Details"
                    },
                    "style": {
                        "flex": "none",
                        "padding": "0px",
                        "padding-left": "0px",
                        "height": "36px"
                    }
                }
            ]
        },
        {
            "elmType": "div",
            "attributes": {
                "class": "ms-fontColor-neutralSecondary ms-fontWeight-bold ms-fontSize-24"
            },
            "style": {
                "box-sizing": "border-box",
                "width": "100%",
                "text-align": "left",
                "padding": "21px 12px",
                "overflow": "hidden"
            },
            "children": [
                {
                    "elmType": "div",
                    "txtContent": "='This is the selected user and lookup - ' + [$Person.title] + ' - ' + substring([$Lookup],indexOf([$Lookup], ';#') + 2,100)"
                }
            ]
        }
    ]
}
  • Add the following to the footer:
{
    "elmType": "div",
    "style": {
        "width": "100%",
        "text-align": "left",
        "overflow": "hidden",
        "border-top-width": "1px"
    },
    "children": [
        {
            "elmType": "div",
            "style": {
                "width": "100%",
                "padding-top": "10px",
                "height": "24px"
            },
            "children": [
                {
                    "elmType": "a",
                    "txtContent": "='Contact Details for ' + [$Title]",
                    "attributes": {
                        "target": "_blank",
                        "href": "='https://aka.ms/contacts?email=' + [$Person.email]",
                        "class": "ms-fontColor-themePrimary ms-borderColor-themePrimary ms-fontWeight-semibold ms-fontSize-m ms-fontColor-neutralSecondary–hover ms-bgColor-themeLight–hover"
                    }
                }
            ]
        }
    ]
}
  • Configure the body config to add custom formatting sections:
{
    "sections": [
        {
            "displayname": "Section 1",
            "fields": [
                "Title"
            ]
        },
        {
            "displayname": "Section 2",
            "fields": [
                "Person",
                "Lookup"
            ]
        },
        {
            "displayname": "Section 3",
            "fields": [
                "Choice",
                "Number",
            ]
        }
    ]
}
  • Save this configuration and test Dynamic Form against this list
    • Observe that the header is updated as you change form values
    • Amend the Choice field to 'Choice 1' and 'Choice 2', observe that the Number field appears when you choose the latter.
    • Set the Number field to 5, observe the error.
    • If you set min and max values, set the Number field above or below those values to observe that error too.
    • If testing in the workbench, turn the various settings on and off to see their effect

If you spot any potential issues or anything - happy to discuss further and make changes.

Tom

@AJIXuMuK AJIXuMuK changed the base branch from master to dev October 8, 2023 15:27
src/common/utilities/FormulaEvaluation.types.ts Outdated Show resolved Hide resolved
src/common/utilities/CustomFormatting.ts Show resolved Hide resolved
src/common/utilities/CustomFormatting.ts Outdated Show resolved Hide resolved
src/common/utilities/CustomFormatting.ts Outdated Show resolved Hide resolved
src/common/utilities/CustomFormatting.ts Outdated Show resolved Hide resolved
src/common/utilities/FormulaEvaluation.ts Outdated Show resolved Hide resolved
src/common/utilities/FormulaEvaluation.ts Outdated Show resolved Hide resolved
src/common/utilities/FormulaEvaluation.ts Outdated Show resolved Hide resolved
src/common/utilities/FormulaEvaluation.ts Outdated Show resolved Hide resolved
src/common/utilities/FormulaEvaluation.ts Outdated Show resolved Hide resolved
@AJIXuMuK
Copy link
Collaborator

AJIXuMuK commented Oct 8, 2023

Thanks @t0mgerman for the great addition!
I have provided some comments and also conflicts should be resolved.

But this is definitely a HUGE improvement for the Dynamic Form!

@t0mgerman
Copy link
Author

Hi @AJIXuMuK - I've acted on all of those points and brought the code up to date with dev - I'm not sure if I responded to the review comments correctly, that is my first time going through that process on here tbh!

Copy link
Collaborator

@AJIXuMuK AJIXuMuK left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @t0mgerman for this iteration.
It looks solid to me.

I left a few comments that are more "nit"/code style ones, not functionality-wise.

I will delay the merge though before we release a new version. To have more "beta" time for this change.

Thanks again!

private convertCustomFormatExpressionNodes = (node: ICustomFormattingExpressionNode | string | number | boolean): ASTNode => {
if (typeof node !== "object") {
switch (typeof node) {
case "string":
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can combine both case in one

stack[parenDepth].push(token);
} else if (token.value === ")") {
// When Right parenthesis is found, items are popped from stack to output until left parenthesis is found
while (stack[parenDepth].length > 0 && stack[parenDepth][stack[parenDepth].length - 1].value !== "(") {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stack[parentDepth].length > 0 => stack[parentDepth].length

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and everywhere below

@AJIXuMuK
Copy link
Collaborator

@t0mgerman - could you please resolve the conflicts and I will merge the PR?

thanks!

@t0mgerman
Copy link
Author

Hi @AJIXuMuK sorry about not getting back sooner, I'll look at getting them resolved today

@t0mgerman
Copy link
Author

t0mgerman commented Dec 1, 2023

Hi @AJIXuMuK - resolved conflicts as of this post 👍

Needed to make a few manual changes to mesh my work together with GuidoZam's file uploading solution (recently merged in to dev), and I added an extra Property Pane control to the testing web part so that feature can be tested too.

edit: oh, also had to update a few references for OUIFR to @fluentui/react etc.

@t0mgerman
Copy link
Author

Just to note, I think my changes possibly also fix the issue resolved in / raised in PR #1662 - I changed the way values were tracked in the form with this PR, defaultValue is now treated more immutably - although not entirely - and is for the default value set on a field (either the field type default, or the default set on the column itself in list settings). The reason I do change it's value in one or two places is to do with spfx-controls-react controls that perhaps don't have another way of setting initial values, or in situations where the value needs to be fully cleared and things like that. I try to avoid modifying it where possible. value is the current value of a field - all fields have this - and newValue is now only populated on a New or Edit form if the user actually fills in a value. Required fields are caught on attempted save, and other validation kicks in onChange. I also changed DynamicField behaviour in some instances so that onBlur is what triggers the onChange and validation cycle...

So I think messages shouldn't be getting displayed inappropriately as described in that PR. If you'd prefer to merge that one first and for me to resolve conflicts again - I'm happy to do so - but equally you might decide the changes in it aren't necessary. Just thought it worth flagging.

@AJIXuMuK AJIXuMuK merged commit 3cd6f18 into pnp:dev Dec 2, 2023
1 check passed
@AJIXuMuK AJIXuMuK added this to the 3.17.0 milestone Dec 2, 2023
@joelfmrodrigues joelfmrodrigues mentioned this pull request Feb 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants