front-end Articles / Blogs / Perficient https://blogs.perficient.com/tag/front-end/ Expert Digital Insights Tue, 25 Mar 2025 16:02:57 +0000 en-US hourly 1 https://blogs.perficient.com/files/favicon-194x194-1-150x150.png front-end Articles / Blogs / Perficient https://blogs.perficient.com/tag/front-end/ 32 32 30508587 Plop.js – A Micro-Generator Framework: Template Creation https://blogs.perficient.com/2025/03/20/plop-js-a-micro-generator-framework-template-creation-part-2/ https://blogs.perficient.com/2025/03/20/plop-js-a-micro-generator-framework-template-creation-part-2/#respond Thu, 20 Mar 2025 11:28:49 +0000 https://blogs.perficient.com/?p=379015

Continuing our Plop.js journey from the last blog. Be sure to go back and read the previous installment in this series.

In our previous discussion, we explored an introduction to Plop.js and its installation in a Next.js project. Additionally, we looked at a basic skeleton of plopfile.js.

Plopfile Js Config

Understanding the Components of plopfile.js

As we saw earlier, the plopfile.js consists of key elements that define the generation. Let’s break them down again for clarity:

  • The “setGenerator” creates a plop generator. Here, plopfile.js has a single generator called “basics.”
  • The “description,” as the name suggests, describes the purpose of the generator.
  • The “prompts” is an array of prompts. This could be added to your created generator.
  • The “actions” take the user’s information to each prompt. It is an array of where each action is an object. This is an important step and requires creating some templates.

Creating Our First Template

Before creating a template, understand the concept of actions inside “setGenerator.” After all, this is where the real magic happens. Let’s write a generator to create a new component.

Plopfile Js Config Create Component

plop.setGenerator("component", {
  description: "Create a new React component",
  prompts: [
    {
      type: "input",
      name: "name",
      message: "What is this component’s name?",
    },
  ],
  actions: [
    {
      type: "add",
      path: "src/components/{{pascalCase name}}/{{pascalCase name}}.tsx",
      templateFile: "plop-template/component.hbs",
    },
  ],
});

Breaking Down the Code

  • In the above example, we use the “add” action type, which creates a file at the specified “path” and fills it with a skeleton defined in “templateFile.”
  • Plop relies on Handlebars (Handlebars.js), a templating engine for generating structured text like HTML or JavaScript files.
  • Notice that the “templateFile” ends with a .hbs extension, which signifies a Handlebars template.

Exploring More Actions

Apart from “add”, there are several other built-in actions like:

  • “addMany”
  • “modify”
  • “append”
  • “custom” (for fully customized actions)

You can explore the complete list here: Plop.js Built-in Actions.

Organizing Templates in a Folder

Now that we understand actions, let’s organize our template files.

  1. First, create a new folder called plop-template at the root of your project.
  2. Inside this folder, create different Handlebar templates for various file types, such as:
    • .tsx for React components
    • .scss for styles
    • .md for documentation
    • .test.tsx for test cases

Handlebars Syntax Example

In Handlebars, variables are enclosed within double curly braces {{}}. Moreover, built-in helpers like “pascalCase” allow the formatting of variables.

Component Handlebar

const {{pascalCase name}} = () => {
  return <div>{{pascalCase name}} Component</div>;
};

export default {{pascalCase name}};

 

In addition to “pascalCase,” you can also use:

  • “camelCase”
  • “snakeCase”
  • “lowerCase”

Check out the complete list here: Plop.js Built-in Helpers.

Running the Generator Using Plop

After setting everything up, we are now ready to run our generator! There are two ways to do this:

1. Using CLI Command

Run Generate ScriptRun Generate Script Running

2. Using VS Code Script Runner

Alternatively, you can open the package.json file, hover over “generate script,” and click “Run Script” in your editor.Generate Plop Script Runner

Generating Our First Component with Plop

Next, let’s create our first real component, “Button,” using the plop command npm run generate (with either of the two options mentioned above). After you run the command, the terminal will show prompts as mentioned in the plopfile.js

This will prompt you with questions as per plopfile.js, such as:

  1. What is this component’s name? → Button
  2. HTML element (default is div)? → button

Run Generate Script First Component

Once you provide the inputs (refer to the above screenshot to understand better), the component gets created at the specified location, and you will see a success message in the terminal.

Final Component Created

Final Thoughts

As you can see, Plop.js simplifies component creation by automating file generation and reducing repetitive tasks. By setting up structured templates, we ensure consistency and boost productivity across the project.

In the upcoming blog, we will explore:

  • Other key Plop.js methods (beyond “setGenerator”)
  • Built-in and custom actions
  • More practical examples

So, stay tuned!

]]>
https://blogs.perficient.com/2025/03/20/plop-js-a-micro-generator-framework-template-creation-part-2/feed/ 0 379015
Lessons from the Front: Mapping Bootstrap Grid Parameters https://blogs.perficient.com/2025/03/06/lessons-from-the-front-mapping-bootstrap-grid-parameters/ https://blogs.perficient.com/2025/03/06/lessons-from-the-front-mapping-bootstrap-grid-parameters/#respond Thu, 06 Mar 2025 17:35:27 +0000 https://blogs.perficient.com/?p=378087

Intro 📖

While working on a recent project to migrate a Sitecore 9.1 solution to Sitecore XM Cloud, an interesting situation arose with Bootstrap grid parameters. More specifically, with grid rendering parameters configured on renderings migrated from the legacy 9.1 content tree into the XM Cloud content tree.

To start, some (brief?) background on SXA and the grid system. SXA includes several grid systems out-of-the-box (reference), including Bootstrap 4 (BS4) and Bootstrap 5 (BS5). These grid systems allow developers to build responsive layouts in a consistent, predictable way. When adding a new headless site in XM Cloud, BS5 is the default grid system (reference). The supporting Sitecore items for the different grid systems can be found in the content tree under /sitecore/system/Settings/Feature/Experience Accelerator (BS4 and BS5 are highlighted in the screenshot below, but there are others):

Bootstrap entries in the Sitecore content tree.

The grid system(s) a site uses can be configured on the site’s Settings item, e.g., /sitecore/content/Test Site Collection/Headless Site A/Settings:

Headless Site Grid Settings

When editing a rendering on a page in the Content Editor, Experience Editor, or XM Cloud Pages (which was recently updated 🎉), content authors can configure the grid and set things like column sizes, offsets, order, etc.:

Grid Rendering Parameters

These grid settings are stored as a single rendering parameter named GridParameters associated to the parent rendering in the page’s Renderings and/or Final Renderings field. For example, assuming a Container rendering has the Size settings depicted in the animation above (meaning: Mobile | Size | 12), the raw value of the rendering in presentation details would look something like this:

<r uid="{FE9A8A21-02C7-4EB0-B1B1-DF67ADD29ADD}"
  ...
  s:par="?Styles=%7b6221E315-B00F-4832-9054-B46F347EC247%7d&amp;GridParameters=%7b7465D855-992E-4DC2-9855-A03250DFA74B%7d&amp;DynamicPlaceholderId=1"
  s:ph="headless-main" />

The s:par attribute stores a URL-encoded query string containing the rendering parameters. The value of GridParameters is “%7b7465D855-992E-4DC2-9855-A03250DFA74B%7d”, or, when URL-decoded, “{7465D855-992E-4DC2-9855-A03250DFA74B}”. That looks a lot like a Sitecore item ID, right 😉?

📝 Note that, if there was more than one grid parameter, GridParameters would be a pipe-delimited (“|”) string of IDs.

The ID points to the grid size definition item located at (in this case, for BS5) /sitecore/system/Settings/Feature/Experience Accelerator/Bootstrap 5/Bootstrap 5 Grid Definition/Extra small/Size/12. Finally, as part of the definition of the size item, the applicable Bootstrap CSS class is specified in the Class field. It is this class (or classes, if more than one grid parameter is specified) that is applied to the markup when the component is rendered in the UI.

Bootstrap 5 Size 12

For a slightly more “real world” example, assume a Container rendering had (BS5) grid settings that looked like this:

Sample Bootstrap 5 Settings

The value of the GridParameters rendering parameter would be:

<r uid="{FE9A8A21-02C7-4EB0-B1B1-DF67ADD29ADD}"
  ...
  s:par="GridParameters=%7B7465D855-992E-4DC2-9855-A03250DFA74B%7D%7C%7B597223E6-B7EE-4D86-BC89-F7DA3DE8A7E5%7D%7C%7B7D865A50-F089-421E-ACDC-620DB03BC49D%7D&amp;FieldNames&amp;BackgroundImage&amp;Styles=%7B6221E315-B00F-4832-9054-B46F347EC247%7D&amp;RenderingIdentifier&amp;CSSStyles&amp;DynamicPlaceholderId=1"
  s:ph="headless-main" />

Which, URL-decoded would be: “{7465D855-992E-4DC2-9855-A03250DFA74B}|{597223E6-B7EE-4D86-BC89-F7DA3DE8A7E5}|{7D865A50-F089-421E-ACDC-620DB03BC49D}”.

The JSON pulled from the layout service for the rendering would look like this:

...
"uid": "fe9a8a21-02c7-4eb0-b1b1-df67add29add",
"componentName": "Container",
"dataSource": "",
"params": {
  "GridParameters": "col-12 col-md-10 col-xl-5", // <=== HERE
  "Styles": "container",
  "DynamicPlaceholderId": "1",
  "FieldNames": "Default"
}
...

And, finally, the resulting markup would look like this:

<div class="col-12 col-md-10 col-xl-5">
...
</div>

For the remainder of this post, assume the following:

  • The legacy 9.1 solution used BS4.
  • The XM Cloud solution used BS5 (but supported BS4).
  • The legacy 9.1 renderings were migrated to XM Cloud (and converted to JSON renderings).
  • The migrated renderings were implemented and appear as expected in the head application UI, complete with the expected/correct CSS grid classes.

The Problem 🙅‍♂️

Okay, so, what’s the issue? We have BS4 and BS5 available in XM Cloud and the migrated renderings are emitting the correct grid styles in the markup (e.g., col-12). What’s the problem?

The problem came when content authors went to view and/or update the grid style parameters, either in the Content Editor, Experience Editor, or XM Cloud Pages. There wasn’t an error or anything, but nothing appeared in the Grid tab or the Advanced tab when editing the rendering parameters for the rendering, as if the parameters weren’t there…🤔.

When viewing raw values of the Renderings and Final Renderings fields on the page, the GridParameters parameter was clearly present–so why wasn’t anything appearing in the editing UI for content authors?

The Cause 🐛

As you may have already guessed (and, yes, it seems obvious now), the reason for this was that the item IDs referenced in the grid rendering parameter are different between BS4 and BS5, even for two grid size definition items with the same CSS class. For example, the col-12 item for BS4 (/sitecore/system/Settings/Feature/Experience Accelerator/Bootstrap 4/Bootstrap 4 Grid Definition/Extra small/Size/12) didn’t have the same ID as the matching col-12 item in BS5 (/sitecore/system/Settings/Feature/Experience Accelerator/Bootstrap 5/Bootstrap 5 Grid Definition/Extra small/Size/12).

Because the new XM Cloud solution had both the supporting BS4 and BS5 items available under /sitecore/system/Settings/Feature/Experience Accelerator, the BS4 grid CSS classes were still coming through in the UI. However, when editing, the rendering parameters modal was expecting the BS5 IDs (since the site was configured to use BS5) to bind the editing controls. Since no BS5 parameter IDs were found, the editing controls in the modal weren’t bound to the correct values and appeared broken for content authors.

The Solution ✅

As usual, Sitecore PowerShell Extensions (SPE) to the rescue. A PowerShell script was written to iterate over page renderings, map the Bootstrap 4 parameter IDs to their equivalent Bootstrap 5 parameter IDs based on a CSS class name match, if possible, and then save the updated rendering parameters back to the page.

⚠ In some cases, it wasn’t possible to map parameters between the two grid systems as there are breaking changes between BS4 and BS5. In BS4, for example, some Order definitions exist that do not exist in BS5 i.e., /sitecore/system/Settings/Feature/Experience Accelerator/Bootstrap 4/Bootstrap 4 Grid Definition/Extra small/Order/6.

Luckily, in the case of this particular project, the vast majority of the parameters did have a BS5 equivalent and were remapped accordingy, allowing the rendering parameters modal to properly reflect the current grid parameters for content authors. ✨

The Script ⚙

Update-GridParameters.ps1

A few highlights on the script and how it works:

    1. The script is intended to be executed via the PowerShell ISE interface in Sitecore.
    2. Update the -Path parameter according to where your content pages live in the tree; I’d recommend running the script on a subset of items initially.
    3. The script emits a report using the Show-ListView command that enumerates which renderings and parameters were updated (or that won’t be updated); the report can be exported if you need to, say, attach it to a work item in Azure DevOps or JIRA.
    4. If a grid rendering parameter ID cannot be mapped to a BS5 ID or if the candidate ID isn’t a BS4 ID (including if it’s already a BS5 ID), then it is skipped but still appears in the report.
    5. Keep the -WhatIf switch set until you’re ready to apply the updates.

Here’s the script:

Function Update-GridParameters {
    <#
    .SYNOPSIS
        Maps Bootstrap 4 grid rendering parameters to Bootstrap 5 grid rendering parameters.

    .DESCRIPTION
        This function maps Bootstrap 4 grid rendering parameter IDs to their Bootstrap 5 equivalent rendering parameter IDs, if possible.

    .NOTES
        Nick Sturdivant | Perficient | nick.sturdivant@perficient.com | https://www.linkedin.com/in/nicksturdivant/
    #>

    param
    (
        [Parameter()]
        [String] $Path,

        [Parameter()]
        [Switch] $WhatIf
    )

    BEGIN {
        Write-Host "Beginning $($MyInvocation.MyCommand)"
    }

    PROCESS {
        # get bootstrap 4 grid parameter definitions
        $bs4GridParameters = Get-ChildItem -Path "/sitecore/system/Settings/Feature/Experience Accelerator/Bootstrap 4/Bootstrap 4 Grid Definition" -Recurse `
        | Where-Object {
            $_.TemplateID -eq "{CB3B3906-BE80-4D9B-A2A4-038193DA5422}" # /sitecore/templates/Foundation/Experience Accelerator/Grid/Grid Definition Items/Class
        }

        # get bootstrap 5 grid parameter definitions
        $bs5GridParameters = Get-ChildItem -Path "/sitecore/system/Settings/Feature/Experience Accelerator/Bootstrap 5/Bootstrap 5 Grid Definition" -Recurse `
        | Where-Object {
            $_.TemplateID -eq "{CB3B3906-BE80-4D9B-A2A4-038193DA5422}" # /sitecore/templates/Foundation/Experience Accelerator/Grid/Grid Definition Items/Class
        }

        # build mapping
        $bs4To5Mappings = @()
        foreach ($bs4GridParameter in $bs4GridParameters) {
            # match bootstrap 5 parameter based on class name
            $bs5GridParameter = $bs5GridParameters | Where-Object { $_.Fields["Class"].Value -eq $bs4GridParameter.Fields["Class"].Value }

            if ($null -ne $bs5GridParameter) {
                $bs4To5Mappings += [PSCustomObject]@{
                    BS4Id    = $bs4GridParameter.ID
                    BS4Class = $bs4GridParameter.Fields["Class"].Value
                    BS5Id    = $bs5GridParameter.ID
                    BS5Class = $bs5GridParameter.Fields["Class"].Value
                }
            }   
        }

        $report = @()

        Write-Host "Processing $Path..." -ForegroundColor Cyan
    
        # get children based on path parameter, filtering to only those items with a rendering defined in their final layout
        $items = Get-ChildItem -Path $Path -Recurse | Where-Object { ($null -ne (Get-Rendering -Item $_ -FinalLayout)) }
    
        foreach ($item in $items) {
            $renderings = Get-Rendering -Item $item -FinalLayout

            if ($null -ne $renderings) {
                foreach ($rendering in $renderings) {
                    # get grid rendering parameter
                    $renderingParameter = Get-RenderingParameter -Rendering $rendering -Name "GridParameters"

                    if ($null -ne $renderingParameter) {
                        Write-Host "Processing grid rendering parameters on item $($item.ID), rendering $($rendering.UniqueId)..." -ForegroundColor Green
                        $gridParametersIdString = $renderingParameter["GridParameters"]

                        if (-not [string]::IsNullOrWhiteSpace($gridParametersIdString)) {
                            $gridParameterIds = $gridParametersIdString.Split("|")
                            $newGridParameterIds = @()

                            foreach ($gridParameterId in $gridParameterIds) {
                                # check if the parameter is bootstrap 4
                                $mappedParameter = $bs4To5Mappings | Where-Object { $_.BS4Id.ToString() -eq $gridParameterId } | Select-Object -First 1

                                if ($null -eq $mappedParameter) {   
                                    $message = "Grid parameter ID $gridParameterId is not a Bootstrap 4 parameter that can be mapped to a Bootstrap 5 parameter, retaining original ID."
    
                                    Write-Host $message -ForegroundColor Yellow
    
                                    $report += @{
                                        ItemId                 = $item.ID
                                        ItemPath               = $item.FullPath
                                        RenderingItemId        = $rendering.ItemID
                                        RenderingUniqueId      = $rendering.UniqueId
                                        CurrentGridParameterId = $gridParameterId
                                        MappedGridParameterId  = "N/A"
                                        GridClassName          = "N/A"
                                        Notes                  = $message
                                    }

                                    $newGridParameterIds += $gridParameterId

                                    # process next parameter
                                    continue
                                }
    
                                # map to bootstrap 5 parameter
                                $report += @{
    
                                    ItemId                 = $item.ID
    
                                    ItemPath               = $item.FullPath
    
                                    RenderingItemId        = $rendering.ItemID
    
                                    RenderingUniqueId      = $rendering.UniqueId
    
                                    CurrentGridParameterId = $gridParameterId
    
                                    MappedGridParameterId  = $mappedParameter.BS5Id.ToString()
    
                                    GridClassName          = $mappedParameter.BS5Class
    
                                    Notes                  = "Mapping Bootstrap 4 grid parameter ID $gridParameterId to Bootstrap 5 grid parameter ID $($mappedParameter.BS5Id.ToString())."
    
                                }
    
                                $newGridParameterIds += $mappedParameter.BS5Id.ToString()   
                            }
    
                            $newGridParametersIdString = $newGridParameterIds -join "|"
    
                            Write-Host "Current: $gridParametersIdString"
    
                            Write-Host "Mapped: $newGridParametersIdString"
    
                            if ($gridParametersIdString -eq $newGridParametersIdString) {   
                                Write-Host "Current and mapped grid parameters match, skipping update..." -ForegroundColor Yellow
                                
                                # process next rendering
                                continue
                            }
    
                            # set new rendering parameter   
                            $setMessage = "Setting new grid parameters on item $($item.ID) ($($item.FullPath)), rendering $($rendering.UniqueId)..."
    
                            Write-Host $setMessage -ForegroundColor Yellow
    
                            $newGridParameters = [Ordered]@{"GridParameters" = $newGridParametersIdString }
    
                            $report += @{
                                ItemId                 = $item.ID
                                ItemPath               = $item.FullPath
                                RenderingItemId        = $rendering.ItemID
                                RenderingUniqueId      = $rendering.UniqueId
                                CurrentGridParameterId = $gridParametersIdString
                                MappedGridParameterId  = $newGridParametersIdString
                                GridClassName          = ""
                                Notes                  = "$setMessage ($gridParametersIdString ==> $newGridParametersIdString)"
                            }

                            if (-not $WhatIf) {
                                # update new grid parameters and then update the rendering
                                $rendering | Set-RenderingParameter -Parameter $newGridParameters | Set-Rendering -Item $item -FinalLayout
                            }
                        }
                    }
                }
            }
        }
    
        # display report
        $report |
        Show-ListView -Property @{ Label = "Item ID"; Expression = { $_.ItemId } },
        @{Label = "Item Path"; Expression = { $_.ItemPath } },
        @{Label = "Rendering Item ID"; Expression = { $_.RenderingItemId } },
        @{Label = "Rendering Unique ID"; Expression = { $_.RenderingUniqueId } },
        @{Label = "Current Grid Parameter ID"; Expression = { $_.CurrentGridParameterId } },
        @{Label = "Mapped Grid Parameter ID"; Expression = { $_.MappedGridParameterId } },
        @{Label = "Grid Class Name"; Expression = { $_.GridClassName } },
        @{Label = "Notes"; Expression = { $_.Notes } }
    }

    END {
        Write-Host "Ending $($MyInvocation.MyCommand)"
    }
}

# 1. Change the Path parameter as necessary, depending on where the content pages are in the tree.
# 2. Use the -WhatIf switch to preview the changes; remove the switch to update the renderings and pages.
Update-GridParameters -Path "/sitecore/content/Test Site Collection/Headless Site A" -WhatIf

Bonus Script 🌟

My colleague (and Sitecore MVP) Eric Sanner happened to write a similar script recently. I can neither confirm nor deny that the two of us wrote two scripts to do the same thing at around the same time 😅. His script can be found below and illustrates another approach to solve the same problem. Thanks for sharing, Eric!

BS4toBS5.ps1

<#
    .SYNOPSIS
       Update Final Layouts
        
    .DESCRIPTION
        Find bootstrap4 grid settings in final layout and convert to bootstrap5
        
    .NOTES  
        Eric Sanner | Perficient | eric.sanner@perficient.com | https://www.linkedin.com/in/ericsanner/      
#>

#BEGIN Config
$database = "master"
$allowDelete = $false
#END Config

#BEGIN Helper Functions

function Write-LogExtended {
    param(
        [string]$Message,
        [System.ConsoleColor]$ForegroundColor = $host.UI.RawUI.ForegroundColor,
        [System.ConsoleColor]$BackgroundColor = $host.UI.RawUI.BackgroundColor
    )

    Write-Log -Object $message
    Write-Host -Object $message -ForegroundColor $ForegroundColor -BackgroundColor $backgroundColor
}

function Strip-Html {
    #https://www.regular-expressions.info/lookaround.html#lookahead Replaces multiple spaces with a single space
    param (
        [string]$text
    )
    
    $text = $text -replace '<[^>]+>',' '
    $text = $text -replace " (?= )", "$1"
    $text = $text.Trim()
    
    return $text
}

function Truncate-Output {
    param (
        $obj,
        $maxLeng
    )
    
    $ret = "";
    
    if($obj -ne $null)
    {
        $str = $obj.ToString().Trim()
        $leng = [System.Math]::Min($str.Length, $maxLeng)
        $truncated = ($str.Length -gt $maxLeng)
        
        $ret = $str.Substring(0, $leng)
        if($truncated -eq $true)
        {
            $ret = $ret + "..."
        }
    }

    return $ret
}

#END Helper Functions

#BEGIN Sitecore Functions

function Get-SitecoreItemById {
    param(
        [string]$id
    )

    return Get-Item -Path $database -ID $id -ErrorAction SilentlyContinue
}

function Update-SitecoreItem {
    param(
        [Sitecore.Data.Items.Item]$item,
        [System.Collections.Hashtable]$updates
    )
    
    if($item -eq $null)
    {
        Write-LogExtended "[E] Error updating item $($item) - Item is null" Red
        return
    }
    
    if($updates -eq $null)
    {
        Write-LogExtended "[E] Error updating item $($item) - Update hashtable is null" Red
        return
    }
        
    $changeDetected = $false
    $foregroundColor = "Green"
    
    Write-LogExtended "[I] Updating Item $($item.ID) - $($item.Name)"
    $item.Editing.BeginEdit()
    
    foreach($key in $updates.GetEnumerator())
    {
        if($item.($key.Name) -ne $null)
        {
            $output = "Field Name '$($key.Name)' Current Value: '$(Truncate-Output $item.($key.Name) 40)' New Value: '$(Truncate-Output $key.Value 40)'"            
            
            if($item.($key.Name) -ne $key.Value)
            {
                Write-LogExtended "[U] $($output)"
                $item.($key.Name) = $key.Value
                $changeDetected = $true
            }
            else
            {
                Write-LogExtended "[-] $($output)"
            }
        }
    }
    
    $itemModified = $item.Editing.EndEdit()
    
    if($changeDetected -ne $itemModified)
    {
        $foregroundColor = "Red"
    }
    
    Write-LogExtended "[I] Change Detected: $($changeDetected) Item modified $($itemModified)" $foregroundColor
}
#END Sitecore Functions

#BEGIN Conversion Functions

function ProcessItem {
    param(
        [Sitecore.Data.Items.Item]$item     
    )
    
    Write-LogExtended "[I] Processing item $($item.ID): $($item.ItemPath)"

    $gridPattern = "GridParameters=%7B(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})%7D"
    $bs4Pattern = "Bootstrap 4"
    $finalRenderings = $item["__Final Renderings"]
    $finalRenderingsNew = $item["__Final Renderings"]
    $gridRenderings = @{}

    $gridMatches = Select-String -InputObject $finalRenderings -Pattern $gridPattern -AllMatches    
    foreach($gridMatch in $gridMatches.Matches)
    {       
        if(!$gridRenderings.ContainsKey("$($gridMatch.Groups[1].Value)"))
        {
            $gridRenderingItem = Get-SitecoreItemById $gridMatch.Groups[1].Value
            $gridRenderings["$($gridMatch.Groups[1].Value)"] = $gridRenderingItem.ItemPath          
        }
    }

    foreach($rendering in $gridRenderings.GetEnumerator())
    {
        $bs4Matches = Select-String -InputObject $rendering.Value -Pattern $bs4Pattern
        if($bs4Matches)
        {
            Write-LogExtended "[I] Found BS4 GridParams $($rendering.Value)" -ForegroundColor "Red"
            $bs4Items.Add($item.ID, $item.Path)

            if($bs4ToBs5Mapping.ContainsKey($rendering.Name))
            {
                Write-LogExtended "[I] Mapping $($rendering.Name) to $($bs4ToBs5Mapping[$rendering.Name])"  -ForegroundColor "Green"
                
                $finalRenderingsNew = $finalRenderingsNew -replace $rendering.Name, $bs4ToBs5Mapping[$rendering.Name]
            }
            else
            {
                Write-LogExtended "[E] No Mapping Found for $($rendering.Name)"  -ForegroundColor "Red"
            }
        }
    }   

    if($update -and $finalRenderings -ne $finalRenderingsNew)
    {
        $updates = @{}  
        $updates.Add("__Final Renderings", $finalRenderingsNew)

        Update-SitecoreItem $item $updates
    }
}

#END Conversion Functions

#BEGIN Main

    $update = $true
    $bs4Items = @{}

    $bs4ToBs5Mapping = @{}
    #/sitecore/system/Settings/Feature/Experience Accelerator/Bootstrap 4/Bootstrap 4 Grid Definition/Extra small/Size/12 -> /sitecore/system/Settings/Feature/Experience Accelerator/Bootstrap 5/Bootstrap 5 Grid Definition/Extra small/Size/12
    $bs4ToBs5Mapping.Add("908E2BC6-C110-4ED7-AF39-7EEACBB31A34", "7465D855-992E-4DC2-9855-A03250DFA74B") 
    #/sitecore/system/Settings/Feature/Experience Accelerator/Bootstrap 4/Bootstrap 4 Grid Definition/Extra small/Size/8 -> /sitecore/system/Settings/Feature/Experience Accelerator/Bootstrap 5/Bootstrap 5 Grid Definition/Extra small/Size/8
    $bs4ToBs5Mapping.Add("D65D90FB-45BF-4A04-A1EA-6F348E7CCBEA", "F2A11D85-8B09-40AC-B5D8-A9E1025F899D")    

    $parentPath = "master:/sitecore/content/<tenantName>/<siteName>/Home"
    $childItems = Get-ChildItem -Path $parentPath -Recurse

    $childItems | ForEach-Object {
        ProcessItem $_
    }

    Write-LogExtended "[I] Found $($bs4Items.Count) Items that reference BS4"
    
#END Main

Thanks for the read! 🙏

]]>
https://blogs.perficient.com/2025/03/06/lessons-from-the-front-mapping-bootstrap-grid-parameters/feed/ 0 378087
Using TypeScript with React: Best Practices https://blogs.perficient.com/2025/03/05/using-typescript-with-react-best-practices/ https://blogs.perficient.com/2025/03/05/using-typescript-with-react-best-practices/#comments Thu, 06 Mar 2025 04:09:50 +0000 https://blogs.perficient.com/?p=378186

Nowadays, TypeScript has become the first choice for building scalable and maintainable React applications. By combining the approaches for static typing with dynamic capabilities of React, TypeScript enhances productivity, improves the readability of code and reduces the runtime errors. In this blog, we will explore best practices for using TypeScript in React projects, covering type safety, event handling, configuration, and utility types

1. Configuring TypeScript in a React Project
To start with TypeScript in a React project, one need to set up the configurations of TypeScript correctly. Configure tsconfig.json appropriately. The tsconfig.json file is essential for defining TypeScript rules and compiler options. Below is a basic configuration for a React project:

Picture1

2. Strict Type Checking
Enforce strict Mode
Enable strict mode in your tsconfig.json to ensure stricter type checking and improved error detection. It activates several useful checks, including:
noImplicitAny: Prevents TypeScript from inferring any type implicitly, enforcing explicit type annotations.
strictNullChecks: Ensures variables cannot be assigned null or undefined unless explicitly declared.

Picture2

This setting activates a suite of checks like noImplicitAny, strictNullChecks, and more, ensuring your code adheres to TypeScript’s rigorous standards.

Example
Without strict mode

Picture3

With strict mode:

Picture4

3. Typing Props and State
Use interface or type for Props
Define prop types explicitly for better clarity and IDE support. Instead of relying on PropTypes, use TypeScript interfaces or type aliases:

Picture5

For components with dynamic keys:

Picture6

Typing State
Use the useState hook with a type to define state. It ensures the predictable state values:

Picture7

4. Using TypeScript with Events
React events can be strongly typed with TypeScript to ensure correctness and handle incorrect event handling.
Example: Handling Form Events

Picture8

5. Default Props and Optional Props
Setting Default Props
You can provide default values for props:
Providing default values to props ensures that the component functions correctly even if a prop is not provided.

Picture9

Optional Props
Make props optional by adding a ‘?’ , which allows flexibility in component usage:

Picture10

6. Utility Types
TypeScript provides utility types to simplify common tasks.
Examples
Partial
Make all property optional:

Picture11

Pick
Pick specific properties from a type:

Picture12

Conclusion
TypeScript offers an improved type of safety and better developer experience which makes it the valuable addition to React development. We can build more reliable and maintainable React applications by applying their learning and practices.

]]>
https://blogs.perficient.com/2025/03/05/using-typescript-with-react-best-practices/feed/ 1 378186
Adobe Sites: Migrating from Webpack to Vite https://blogs.perficient.com/2024/07/16/adobe-sites-migrating-from-webpack-to-vite/ https://blogs.perficient.com/2024/07/16/adobe-sites-migrating-from-webpack-to-vite/#comments Tue, 16 Jul 2024 20:50:31 +0000 https://blogs.perficient.com/?p=365994

Webpack is an amazing bundler for JavaScript and, with the correct loader, it can also transform CSS, HTML, and other assets.  When a new AEM project is created via the AEM Project Archetype and the front-end module is set to general, Adobe provides a Webpack configuration to generate the project’s client libraries.

Introducing Vite

Vite is a new build tool that has recently come onto the scene.  You can check the NPM trends here.

Compared to Webpack,

  • Vite provides significantly faster build times and hot reloading during development.
  • Vite utilizes Rollup.  Rollup generates small bundles by utilizing optimizations like tree shaking, ES6 modules, scope hoisting, minification, code splitting, and a plugin ecosystem.

Avoid Configuration Challenges With Vite

If you have any experience with Webpack, you know the challenges of configuring different loaders to preprocess your files.  Many of these configurations are unnecessary with Vite.  Vite supports TypeScript out of the box.  Vite provides built-in support for .scss, .sass, .less, .styl, and .stylus files.  There is no need to install Vite-specific plugins for them.  If the project contains a valid PostCSS configuration, it will automatically apply to all imported CSS.  It is truly a game-changer. 

Project “Jete”

“Vite” comes from the French word for “fast”.  In music, the term “Vite” refers to playing at a quickened pace.  For the following tutorial, I have chosen the music term “Jete” for the name of our project.  “Jete” refers to a bowing technique in which the player is instructed to let the bow bounce or jump off the strings.  Let us take a cue from this musical term and “bounce” into our tutorial. 

Migrating From Webpack to Vite Tutorial

Create an AEM Project via the AEM Project Archetype: 

mvn -B archetype:generate -D archetypeGroupId=com.adobe.aem -D archetypeArtifactId=aem-project-archetype -D archetypeVersion=49 -D aemVersion=cloud -D appTitle="Jete" -D appId="jete" -D groupId="com.jete" -D frontendModule=general -D includeExamples=n

Once your project has been created, install your project within your AEM instance:

mvn clean install -PautoInstallSinglePackage

After verifying the Jete site in AEM, we can start migrating our frontend project to Vite. 

Backup the existing ui.frontend directory: 

cd jete/ 

mv ui.frontend ../JeteFrontend 

From within “jete” run: 

npm create vite@latest

Use “aem-maven-archetype” for the project name, select Vanilla for the framework, and “TypeScript” for the variant. 

Rename the directory “aem-maven-archetype” to “ui.frontend”.  We chose that project name to match the name generated by the AEM Archetype. 

mv aem-maven-archetype ui.frontend

Let’s put the pom.xml file back into the frontend directory: 

mv ../JeteFrontend/pom.xml ui.frontend

Since we are updating the POM files, let’s update the Node and NPM versions in the parent.

pom.xml file. 

<configuration>  

  <nodeVersion>v20.14.0</nodeVersion>  

  <npmVersion>10.7.0</npmVersion>  

</configuration>

We will be using various Node utilities within our TypeScript filesLet us install the Node Types package. 

npm install @types/node --save-dev 

Add the following compiler options to our tsconfig.json file: 

"outDir": "dist", 

"baseUrl": ".", 

"paths": { 

  "@/*": [ 

    "src/*" 

  ] 

}, 

"types": [ 

  "node" 

]

These options set the output directory to “dist”, the base url to the current directory: “ui.frontend”, create an alias of “@” to the src directory, and add the Node types to the global scope. 

Let’s move our “public” directory and the index.html file into the “src” directory. 

Create a file named “vite.config.ts” within “ui.frontend” project. 

Add the following vite configurations: 

import path from 'path'; 

import { defineConfig } from 'vite'; 

export default defineConfig({ 

  build: { 

    emptyOutDir: true, 

    outDir: 'dist', 

  }, 

  root: path.join(__dirname, 'src'), 

  plugins: [], 

  server: { 

    port: 3000, 

  }, 

});

Update the index.html file within the “src” directoryChange the reference of the main.ts file from “/src/main.ts” to “./main.ts. 

<script type="module" src="./main.ts"></script>

Run the Vite dev server with the following command: 

npm run dev

You should see the following page: 

AEM Vite + Typescript

We are making progress! 

Let us make some AEM-specific changes to our Vite configuration. 

Change outDir to: 

path.join(__dirname, 'dist/clientlib-site')

Add the following within the build section: 

lib: { 

  entry: path.resolve(__dirname, 'src/main.ts'), 

  formats: ['iife'], 

  name: 'site.bundle', 

}, 

rollupOptions: { 

  output: { 

    assetFileNames: (file) => { 

      if (file.name?.endsWith('.css')) { 

        return 'site.bundle.[ext]'; 

      } 

      return `resources/[name].[ext]`; 

    }, 

    entryFileNames: `site.bundle.js`, 

  }, 

},

These configurations set the entry file, wrap the output within an immediately invoked function expression (to protect against polluting the global namespace), set the JavaScript and CSS bundle names to site.bundle.js and site.bundle.css, and set the output path for assets to a directory named “resources”.  Using the “iife” format requires setting the “process.env.NODE_ENV” variable. 

Add a “define” section at the same level as “build” with the following option: 

define: { 

  'process.env.NODE_ENV': '"production"', 

}, 

Add a “resolve” section at the same level as “define” and “build” to use our “@” alias: 

resolve: { 

  alias: { 

    '@': path.resolve(__dirname, './src'), 

  }, 

}, 

Add the following “proxy” section within the “server” section: 

proxy: { 

  '^/etc.clientlibs/.*': { 

      changeOrigin: true, 

      target: 'http://localhost:4502', 

  }, 

},

These options inform the dev server to proxy all requests starting with /etc.clientlibs to localhost:4502. 

It is time to remove the generated code.  Remove “index.html”, “conter.ts”, “style.css”, “typescript.svg”, “public/vite.svg” from within the “src” directory.  Remove everything from “main.ts”. 

Move the backup of index.html file to the src directory: 

cp ../JeteFrontend/src/main/webpack/static/index.html ui.frontend/src/

Edit the index.html file.  Replace the script including the “clientlib-site.js” with the following: 

<script type="module" src="./main.ts"></script>

Save the following image to “src/public/resources/images/”: 

https://raw.githubusercontent.com/PRFTAdobe/jete/main/ui.frontend/src/public/resources/images/favicon.ico 

Add the following element within the head section of the index.html file: 

<link rel="icon" href="./resources/images/favicon.ico" type="image/x-icon" />

While we are updating favicons, edit the

ui.apps/src/main/content/jcr_root/apps/jete/components/page/customheaderlibs.html file.

Add the following to the end of the file: 

<link rel="icon" href="/etc.clientlibs/jete/clientlibs/clientlib-site/resources/images/favicon.ico" type="image/x-icon" />

Run the Vite dev server once more … 

npm run dev

You should see the following: 

Project Jete With AEM Vite

It is not very attractiveLet us add some stylingRun the following command to install “sass”. 

npm i -D sass

Create a “main.scss” file under the “src” directory. 

touch main.scss

Edit the main.ts file and add the following line to the top of the file: 

import '@/main.scss'

Copy the variables stylesheet from the frontend backup to the “src” directory: 

cp ../JeteFrontend/src/main/webpack/site/_variables.scss ./ui.frontend/src/

Edit the _variables.scss file and add the following: 

$color-foreground-rgb: rgb(32 32 32);

Copy the base stylesheet from the frontend backup to the “src” directory: 

cp ../JeteFrontend/src/main/webpack/site/_base.scss ./ui.frontend/src/

Include references to these files within main.scss: 

@import 'variables'; 

@import 'base';

Run the Vite dev server once more … 

npm run dev

You should see the following: 

Project Jete With AEM Vite Version 2

Things are getting better, but there is still more work to do! 

Copy the component and site stylesheets from the frontend backup to the “src” directory: 

cp -R ../JeteFrontend/src/main/webpack/components ./ui.frontend/src/ 

 

cp -R ../JeteFrontend/src/main/webpack/site/styles ./ui.frontend/src/

Add the following to the main.scss file: 

@import './components/**/*.scss'; 

@import './styles/**/*.scss';

Run the Vite dev server … 

npm run dev

No luck this timeYou will probably see this error: 

Project Jete With AEM Vite Error

Vite doesn’t understand “splat imports”, “wildcard imports”, or “glob imports”.  We can fix this by installing a package and updating the Vite configuration file. 

Install the following package: 

npm i -D vite-plugin-sass-glob-import

Update the vite.config.ts fileAdd the following to the import statements: 

import sassGlobImports from 'vite-plugin-sass-glob-import';

Add “sassGlobImports” to the plugins section: 

plugins: [sassGlobImports()],

Now, let’s run the Vite dev server again. 

npm run dev

You should see the following: 

Project Jete With Aem Vite Version 3

Much better.  The front end is looking great!  Time to work on the JavaScript imports! 

TypeScript has been working well for us so far, so there’s no need to switch back to JavaScript. 

Remove the “helloworld” JavaScript file: 

rm -rf src/components/_helloworld.js

Grab the TypeScript from this URL and save it as src/components/_helloworld.ts: https://raw.githubusercontent.com/PRFTAdobe/jete/main/ui.frontend/src/components/_helloworld.ts 

To see the results of this script within our browser, we have to include this file within main.ts.  Importing splats won’t work on a TypeScript file.  So we can’t write: “import ‘@/components/**/*.ts’”.  Instead, we will write:

import.meta.glob('@/components/**/*.ts', { eager: true });

Now, let’s run the Vite dev server. 

npm run dev

You should see the following in Chrome DevTools: 

Aem Vite Javascript Example

Very good!  The JavaScript is working as well! 

The following section is optional, but it is good practice to add some linting rules. 

Install the following: 

npm i -D @typescript-eslint/eslint-plugin @typescript-eslint/parser autoprefixer eslint eslint-config-airbnb-base eslint-config-airbnb-typescript eslint-config-prettier eslint-import-resolver-typescript eslint-plugin-import eslint-plugin-prettier eslint-plugin-sort-keys eslint-plugin-typescript-sort-keys postcss postcss-dir-pseudo-class postcss-html postcss-logical prettier stylelint stylelint-config-recommended stylelint-config-standard stylelint-config-standard-scss stylelint-order stylelint-use-logical tsx

Save the following URLs to ui.frontend:

https://raw.githubusercontent.com/PRFTAdobe/jete/main/ui.frontend/.eslintrc.json

https://raw.githubusercontent.com/PRFTAdobe/jete/main/ui.frontend/.postcssrc.json 

https://raw.githubusercontent.com/PRFTAdobe/jete/main/ui.frontend/.prettierrc.json 

https://raw.githubusercontent.com/PRFTAdobe/jete/main/ui.frontend/.stylelintrc.json 

Add the following to the “script” section of package.json: 

"lint": "stylelint src/**/*.scss --fix && eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"

Let’s try out our new script by running: 

npm run lint

You should see a fair amount of sass linting errors.  You can fix the errors manually or overwrite your local versions with the ones from the git repo: https://github.com/PRFTAdobe/jete/tree/main/ui.frontend/src 

We are ready to move on from linting.  Let’s work on the AEM build. 

Install the following: 

npm i -D aem-clientlib-generator aemsync

Save the following URLs to ui.frontend: 

https://github.com/PRFTAdobe/jete/blob/main/ui.frontend/aem-sync-push.ts 

https://github.com/PRFTAdobe/jete/blob/main/ui.frontend/clientlib.config.ts 

https://github.com/PRFTAdobe/jete/blob/main/ui.frontend/aem-clientlib-generator.d.ts 

https://github.com/PRFTAdobe/jete/blob/main/ui.frontend/aemsync.d.ts 

The files with the “d.ts” extensions are used to provide typescript type information about the referenced packages. 

The “clientlib.config.ts” script, creates a client library based on the JS and CSS artifacts created during the build process.  It also copies the artifacts to the “clientlib” directory within “ui.apps”. 

The “aem-sync-push.ts” script takes the clientlib created above and pushes it to a running AEM instance. 

It is time to update the “script” section of package.json. 

Remove the existing “build” and “preview” commands.  Add the following commands: 

"build": "tsc && npm run lint && vite build && tsx ./clientlib.config.ts && tsx ./aem-sync-push.ts", 

"prod": "tsc && npm run lint && vite build && tsx ./clientlib.config.ts",

Let’s try out the build command first: 

npm run build

If the command has been completed successfully, you will see messages indicating that the “generator has finished” and the “aem sync has finished”.  You will also notice the creation of a “dist” directory under “ui.frontend”. 

Our last step is to copy over the “assembly.xml” file from the backup we made earlier. 

cp ../JeteFrontend/assembly.xml ui.frontend/

With that file in place, we are ready to rerun the AEM build: 

mvn clean install -PautoInstallSinglePackage

Congratulations!

The build should be complete without errors.  You have successfully migrated from Webpack to Vite! 

Make sure to follow our Adobe blog for more Adobe solution tips and tricks!  

]]>
https://blogs.perficient.com/2024/07/16/adobe-sites-migrating-from-webpack-to-vite/feed/ 1 365994
Angular 17: Maximize Your Development Efficiency and Performance https://blogs.perficient.com/2024/06/13/elevate-your-development-with-angular-17/ https://blogs.perficient.com/2024/06/13/elevate-your-development-with-angular-17/#respond Thu, 13 Jun 2024 14:32:59 +0000 https://blogs.perficient.com/?p=364242

Angular has long been a leading web development framework, powering countless dynamic websites with its robust features and scalable architecture. However, recent advances in technology have seen other frameworks offering faster, more efficient solutions. With the release of Angular 17, the framework responds with a transformative update, enhancing performance and flexibility. This post will examine those features and their impact on web development.

Angular 17 

This latest version was released in November 2023, and marked a significant evolution from its predecessors, introducing substantial changes and new features. 

These are its major features: 

  • Block Template Syntax: changes the structure of Angular templates for more intuitive coding to the developer. 
  • Defer: Optimizing the loading strategy to improve application performance. 
  • SSR (Server-Side Rendering) Configuration: Enhancing SEO (Search Engine Optimization) and load times with advanced server-side rendering options. 
  • Vite: Integrating a faster, more efficient interpreter tool to speed up development. 
  • Signals: Introducing a new way to manage state changes in applications, for more responsive interfaces. 
  • Reactivity and Zoneless: Offering improved state management and performance by reducing reliance on Angular’s zone mechanism. 

These updates collectively aim to refine the development experience and elevate the capabilities of Angular applications, let’s look into it!

Block Template Syntax 

Angular completely changed the way we can use template directives, now we have a syntax that is more intuitive and JavaScript-like to improve the developer experience and code readability.

@if block: Conditional block.

//--file.component.html
public showContent = signal(false);

public toggleContent() {
  this.showContent.update(value => !value)
}

//--file.component.html
<button (click)="toggleContent()">
  Click Me!
</button>

@if(showContent()) {
  <p>Hello World!</p>
} @else {
  <p>*******</p>
}

@Switch block: Multi-conditional block.

//--file.component.ts
type Grade = 'A'|'B'|'F';
public grade= signal<Grade>('A');

//--file.component.html
@switch (grade()) {
  @case ('A'){
    <p>Up to 90, Excellent! 😍</p>
  }
  @case ('B') {
    <p>Up to 80, Great! 😁</p>
  }
  @default {
    <p>Sorry, Try again! 😞</p>
  }
}

@for and @empty block: Iterative block and default empty option block.

public frameworks2 = signal([]);
//-------------------------------------
<ul>
  @For (framework of frameworks2(); track $index) {
    <li>{{framework}}</li>
  }
  @empty {
    <li>There's no added values!</li>
  }
</ul>

Other performance improvements are included during its construction. Although we could perceive the difference in performance with Angular 17, we tried to confirm these changes by analyzing the creation times of scripts and DOM(Document Object Model) loading, taking as a basis a code that generates some tags iteratively 3000 times validating if they are displayed based on their index is even, finding the following results:

CommonModule

<ng-container
  *ngFor="let item of items; let i = index">
  <p *ngIf="i % 2 === 0">
    {{item}}
  </p>
</ng-container>

New Angular 17 Directives

@for (item of items; track $index) {
  @if ( $index %2 === 0) {
    <p>{{item}}</p>
  }
}

Scripting Average: 180-220ms
Dom Loading Average: 400ms

Angular 17 - 1

Angular 17 - 2

Angular 17 - 3

Image (1)

Average Scripting: 105-110ms
Average Dom Loading: 295ms

Angular 17 - 4

Angular 17 - 5

Angular 17 - 6

Image (9)

Defer 

Angular now offers a new and advanced way to handle Lazy Loading with the template block @defer. This block allows developers to specify components that should load lazily, directly within the template. 

To be able to use this feature, the component must be standalone, which means that the component is not going to be in a module. 

This approach not only simplifies the structure of Angular applications but also enhances performance by reducing the initial load time. Components marked with @defer are loaded only when needed, thereby improving the application’s efficiency and user experience. This selective lazy loading can significantly optimize resource utilization and accelerate rendering speeds, especially in complex applications with numerous components. 

This lazy loading mechanism is further refined with additional directives like @placeholder, @error, and @loading, which manage the component’s loading states. 

@placeholder: provides a temporary display element until the component loads 

@error: handles any loading errors 

@loading: indicates the loading process. 

An example of a defer block in code can be the following: 

<section class="col-start-1">
  @defer (on interaction) { 
    <app-heavy-loaders-slow cssClass="bg-blue-500 h-20"> 
    </app-heavy-loaders-slow>

  } @loading { 
    <div class="w-full h-20 bg-yellow-100"> 
      Loading component (loading) 
    </div> 

  } @placeholder { 
    <div class="w-full h-20 bg-purple-100"> 
      Click the div (placeholder) 
    </div> 

  } @error { 
    <div class="w-full h-20 bg-red-100"> 
      An error happened 
    </div> 
  }
</section>

Doing a comparative performance on loading a Heavy Component, a person can see a clear difference on Core Web Vitals when interacting with a page that lazy loads these kinds of components vs loading components on page load.

Performance without Defer:

Performancewithoutdefer

Performance with Defer:

Performancewithdefer

 

SSR

Angular 17 allows you to create a new application with Server-Side Rendering (SSR) from the start, eliminating the need to separately install the Angular Universal package. Previously, Angular Universal required a double bundle to render the HTML, leading to issues in SEO, performance, and user experience. Angular 17 streamlines this by implementing SSR by default, creating server and client bundles simultaneously, and avoiding the double-bundling issue.

This enhancement boosts SEO and improves Core Web Vitals metrics such as First Contentful Paint (FCP) and Largest Contentful Paint (LCP). It also enhances the overall user experience by delivering content more quickly and consistently from server to client, especially beneficial for users with slower internet connections. By including the HTML template directly in the initial server response while waiting for the JavaScript to load, Angular 17 ensures that users see meaningful content faster. This approach simplifies the development process and streamlines rendering, allowing for more efficient management of dynamic content. As a result, websites become more interactive and responsive from the initial load, increasing user engagement and satisfaction. 

Vite

The usage of Vite in the Angular CLI (Command Line Interface) is currently only within a development server capacity only.

The current development server process uses the new build system to generate a development build of the application in memory and passes the results to Vite to serve the application.  

The usage of Vite, much like the Webpack-based development server, is encapsulated within the Angular CLI dev-server builder and currently cannot be directly configured. 

Signals  

Signals in Angular 17 bring a major improvement to how state and reactivity are managed. Developers can now more precisely monitor and react to state changes, making UI (User Interface) updates faster and more streamlined. ‘Signals’ leverage a reactive programming approach to simplify code structure and cut down on the redundant code typical in complex state handling. This efficiency boost means the system only updates when it is absolutely needed, lowering the complexity, and enhancing performance. Consequently, ‘Signals’ in Angular 17 steers the framework towards more agile, powerful, and easy-to-maintain web applications, improving the development process and user engagement. 

With Signals a person can control when a single part of an application needs to be updated, not triggering updates on the whole tree of components, but into a single component instead. This has the advantage of allowing component-based reactivity on any Angular-based application.  

There are important takeaways to consider. The first is that angular signals can work with RXJS, and they do not have to exclude each other, as RXJS offers a rich environment for advanced reactivity. The second is that angular signals are not supported out of the box yet and that a person has to be pretty careful in the implementation to check that angular ZoneJs is not being used without notice.  

Reactivity and Zoneless 

In the Change Detection processes, Angular has sought an approach to the Concept of Fine Grain Reactivity that achieves that each time the performance in this process decreases in time.  

Zoneless is a feature of previous versions of Angular 17 that has applied the detection process considering the entire tree of components, but even the most recent versions have used the On Push method which tries to keep in mind only the components that change for Inputs and Outputs. 

Now with the use of signals in Angular 17, it seeks to reduce much more the Change Detection process approximating a more minimal expression within the components and corresponding only to the change of the signal. 

Conclusion 

With the new Angular 17 features, it provides the capabilities to keep up with other front-end development frameworks such as Vue or React, as it has managed to reduce performance costs and complexity in development.

Written in collaboration with Carlos Borrero and Sebastian Echeverry.

]]>
https://blogs.perficient.com/2024/06/13/elevate-your-development-with-angular-17/feed/ 0 364242
useOptimistic: Powering Next.js https://blogs.perficient.com/2024/05/02/useoptimistic-powering-next-js/ https://blogs.perficient.com/2024/05/02/useoptimistic-powering-next-js/#respond Thu, 02 May 2024 14:25:21 +0000 https://blogs.perficient.com/?p=360284

In today’s blog, we will examine an experimental hook that helps us display optimized results when we display data we fetch from the server. What can we do to optimize such a use case? Is there a way to show updated data as it is being fetched from the server? We will explore precisely that with the new useOptimistic hook.

This hook from React gives us a copy of our data, which we pass to it, and a function like how useState works. This function is then utilized to manipulate the copied data we show in our application until the new data is fetched from the server or API call.

Implementing the “useOptimistic” Hook in Your Project

Like useFormStatus and useFormState in earlier blogs, this is an experimental hook by react-dom; therefore, it will not be available with the usual npm install package command. To accomplish this, run the following command in your terminal to install the experimental version of react and react-dom:

npm install react@experimental react-dom@experimental

After installing the experimental packages, your package.json should contain the dependencies listed below:

useOptimistic

After doing this, if you want to use typescript in your project, you should additionally create a file informing your project that you will be utilizing the experimental utilities:
useOptimistic

Following that, you should be able to import useFormState into your files and use it appropriately. If you use TypeScript, you may need to add “//@ts-ignore” above the import. TypeScript will not recognize this experimental import but will continue functioning as intended.

//@ts-ignore
import { useFormState } from "react-dom";

Developing a Simple User Interface

Let’s now create a simple react component showing an input and a button. You can use a form here as well; I have simply used the onClick property of the button to recreate the API service call scenario.

return (
    <div className="bg-dark text-white py-5">
      <div className="row mx-0">
        <h2 className="text-center">useOptimistic</h2>
        <div className="col-md-6 p-5">
          <input
            ref={inputRef}
            type="text"
            className="form-control my-3"
            placeholder="Enter item name"
          />
          <button
            disabled={isLoading}
            onClick={() => startTransition(() => onClick())}
            className="btn btn-warning form-control"
          >
            Add Item
          </button>
        </div>
      </div>
    </div>
  );

We now have a simple input and button component. The input component is passed with an inputRef created by using the useRef hook of react, and the button’s onClick function is wrapped in a startTransition function to simulate loading.

useOptimistic

The Utilities Listed in Form

Next, we look at the useful hooks we need to import into our app and some other useful utilities we will use to demonstrate our new hook.

import { useState, useOptimistic, useTransition, useRef } from "react";
import "bootstrap/dist/css/bootstrap.css";

function App() {
  const createItem = (item) => {
    return new Promise((resolve) => setTimeout(() => resolve(item), 2000));
  };
  const inputRef = useRef();
  const [isLoading, startTransition] = useTransition();

  const [itemList, setItemList] = useState([]);
  const [optimisticItems, setOptimisticItems] = useOptimistic(itemList);

As shown in the code above, we import all our hooks from React. As I mentioned earlier, we are using the “startTransition” function to simulate loading here. We are taking this function as the second value provided to us from the useTransition hook in React, the first being the loading state for the function we use startTransition with.

I have created a function here to replicate the API service call behavior of fetching data. It is the createItem function. It basically returns me a value with a 2-second delay. Other than this, we just have the inputRef, which we will use to get the data from input and useState to put the data in an array.

The last line of the code above is what we are looking for. We use the useOptimistic hook to duplicate our itemList, storing it in the variable optimisticItems. The second item we get from this hook is a function that updates the same copied state. We will use this method to initially update our copied state until the service call is complete and we have the correct data in our itemList variable. So, let’s examine the function where we will make this call.

const onClick = async () => {
   const inputValue = inputRef.current.value;
   if (inputRef.current == null || !inputValue) return;
   setOptimisticItems((list) => [
     ...list,
     {
       itemName: inputValue,
       key: crypto.randomUUID(),
       loading: true,
     },
   ]);
   const newItem = await createItem(inputValue);
   setItemList((list) => [
     ...list,
     {
       key: crypto.randomUUID(),
       itemName: newItem,
     },
   ]);
 };

setOptimisticItems Function

Let’s go through this code one by one. First, we take the value of input from inputRef and check to see if it is empty. If it is empty, we will simply return from the function. Next, we have the setOptimisticItems function, which is basically the main purpose of this blog.

Here, we are keeping it as simple as it could get. We get the previous state as the first parameter of this function, which would be the existing list of items, so we will spread that array and add our new value to its end. This now creates our data array and puts the latest data with it before the service call starts in the following line. We are also giving it a loading value which we will use to indicate this is not the complete data, more on that later.

In the following line, we are making the service call (or mocking a service call in this case), getting actual data from the service, and updating our state with that exact data. Therefore, until we perform the await operation, we have already displayed the desired data using the optimistic value. Let’s see how we can implement using the loading indicator I mentioned earlier.

<div className="col-md-6 p-5 ms-4">
  <div>Item's List:</div>
  <ul className="my-3">
    {optimisticItems.map(({ itemName, loading, key }) => (
      <li key={key} className={loading ? "opacity-50" : "opacity-100"}>
        {itemName}
        {loading ? "..." : null}
      </li>
    ))}
  </ul>
</div>

In the above code, we show that the value that is not loaded yet will be displayed with half opacity and will have an ellipsis at the end to indicate it is loading or waiting for the service call to be completed.

Output

Initial form state:

useOptimistic

The user enters a value in the input field:

Picture5

 

The Add button can be clicked to start a service call:
Picture9

The value appears less visible than standard text and has an ellipsis at the end to indicate the loading state. When the loading is complete or the API call successfully completes, it will appear in its normal state, as shown in the image below.

Picture7

This is basically how we can show the data before it is ultimately part of the database and indicate that proper data is loading using this hook. Another screenshot to show the same.

Picture10

One simple yet powerful way to optimize user interactions in Next.js apps is to include the ‘useOptimistic’ hook. Developers may guarantee improved user pleasure and better performance by implementing optimistic updates. ‘useOptimistic’ is a helpful utility for Next.js developers, offering better user experiences with less work thanks to its effect and simplicity.

]]>
https://blogs.perficient.com/2024/05/02/useoptimistic-powering-next-js/feed/ 0 360284
Advanced Array Methods in JavaScript: Part 3 https://blogs.perficient.com/2024/04/08/advanced-array-methods-in-javascript-part-3/ https://blogs.perficient.com/2024/04/08/advanced-array-methods-in-javascript-part-3/#respond Mon, 08 Apr 2024 10:36:53 +0000 https://blogs.perficient.com/?p=359525

Welcome back to the third part of our series on elevating your JavaScript skills through array methods. Having established a solid foundation with simple array methods, we’re now poised to tackle more advanced methods. In this blog, we will discover sophisticated array methods that offer greater flexibility and power in manipulating data. Prepare to unlock new ranges of programming prowess as we continue our deep dive into JavaScript’s array methods. If you haven’t yet, make sure to explore essential array methods in Part 1 and Part 2 of this series.

Advanced array methods encompass a diverse range of categories, each serving specific purposes in data manipulation. These include:

  • Array Find and Search Methods
  • Array Sort Methods and Tricks
  • Array Iteration Methods

These categories provide developers with powerful tools for locating elements, organizing data, and iterating through arrays efficiently, enhancing the capabilities of JavaScript programming.

Array Find and Search Methods

indexOf() and lastIndexOf()

These advanced array methods are like searchlights in the dark, helping you pinpoint the exact location of a specific element within an array. If the element is found, it reveals its index. indexOf() uncovers the first occurrence, while lastIndexOf() reveals the last. However, if the element is nowhere to be found, they report back -1, indicating that the search was unsuccessful.

Example:

const animals = ["Cheetah", "Lion", "Zebra", "Horse", "Cheetah", "Deer"];
const firstIndex = animals.indexOf("Cheetah");
console.log("firstIndex", firstIndex);
const lastIndex = animals.lastIndexOf("Cheetah");
console.log("lastIndex", lastIndex);

Output:
Advanced Array 1

includes()

It is used to determine whether a value is included in the system entry and returns true or false as appropriate.

Example:

const colors = ['red', 'green', 'blue'];
console.log("includes method:")
console.log(colors.includes('green'));
console.log(colors.includes('yellow'));

Output:
Advanced Array 2

find() and findIndex()

find()

This function helps find the first array element that meets a condition. If found, it returns the element; otherwise, it is undefined.

Example:

const movies = ["The Lion King", "Aladdin", "The Jungle Book", "Moana"];
const foundMovie = movies.find((movie) => movie === "Aladdin");
console.log("find method:\n", foundMovie);

Output:
Advanced Array 3

findIndex()

It returns the index of the first element in the array that satisfies the given testing function. If the function does not satisfy any element, it will return -1.

Example:

const movies = ["The Lion King", "Aladdin", "The Jungle Book", "Moana"];
const index = movies.findIndex((movie) => movie === "Moana");
console.log("findIndex method:\n", index);

Output:
Advanced Array 4

findLast()

This method fetches the last array element that meets the condition set by the provided testing function.

Example:

const numbers = [10, 20, 30, 40, 50];
const lastNumber = numbers.findLast(num => num > 20);
console.log("Output:",lastNumber);

Output:
Advanced Array 5

findLastIndex()

It retrieves the index of the last array element that fulfills the conditions set by the testing function.

Example:

const numbers = [10, 20, 30, 40, 50];
const lastIndex = numbers.findLastIndex(num => num > 20);
console.log("Last index of matched condition:",lastIndex); // Output: 4 (index of 50)

Output:
Advanced Array 6

Array Iteration Methods

forEach()

It’s like having a guide show you around a museum, stopping at each exhibit along the way. method executes a function for every element within an array.

Example:

const numbers = [1, 2, 3, 4, 5];
console.log("forEach method:")
numbers.forEach((num) => console.log(num * 2));

Output:
Advanced Array 7

flat() and flatMap()

flat()

Imagine you have a stack of nested trays, each containing some items. flat() is like taking out all the items from those trays and putting them into a single tray, simplifying the organization.

Example:

const nestedArray = [["Peter Pan", "Aladdin"], ["Mulan", "Maleficent"], ["Moana", "Tangled"]];
const flattenedArray = nestedArray.flat();
console.log("flat method:\n",flattenedArray);

Output:
Advanced Array 8

flatMap()

It’s like having a stack of notebooks, and you need to examine each page, write something on it, and then gather all those pages into a new notebook. flatMap() first maps over every element inside the array using a function you offer and then flattens the result into a new array, making it easier to deal with.

Example:

const numbers = [1, 2, 3];
const mappedAndFlattened = numbers.flatMap(num => [num * 2, num * 3]);
console.log("flatMap:\n",mappedAndFlattened);

Output:
Advanced Array 9

filter()

Think of it as a filter on a coffee machine that separates ground coffee from brewed coffee, ensuring that only pure water flows in. Filter() in JavaScript searches each element of an array to establish a condition specifically, storing only those elements that satisfy the condition and using those elements to create a new array.

Example:

const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter((num) => num % 2 === 0);
console.log("filter method:\n", evenNumbers);

Output:
Advanced Array 10

every() and some()

These methods are like gatekeepers, checking each element against a condition.

every():

The condition is checked if all elements are met.

Example:

const numbers = [2, 4, 6, 7, 8];
const allEven = numbers.every((num) => num % 2 === 0);
console.log("every method:\n", allEven);

output:
Advanced Array 11

some():

Checks if at least one element meets a condition.

Example:

const numbers = [2, 4, 6, 7, 8];
const anyEven = numbers.some((num) => num % 2 === 0);
console.log("some method:\n", anyEven);

Output:
Advanced Array 12

reduce():

It’s like having a calculator that provides all the numbers in a list for you. You provide a function that tells the calculator a way to combine every range with the running total.

Syntax:

array.reduce(function(total, currentValue, currentIndex, arr), initialValue)

Example:

const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((total, current) => total + current, 0);
console.log("reduce method:\n",sum);

Output:
Advanced Array 13

Example 2:

const items = [
  { name: "Shirt", price: 20 },
  { name: "Pants", price: 30 },
  { name: "Shoes", price: 50 },
];

const totalPrice = items.reduce((acc, item) => acc + item.price, 0);
console.log("reduce method:");
console.log("Total Price:", totalPrice);

Output:
Advanced Array 14

reduceRight()

The function reduces each value of the array (from right to left) against an accumulator to produce a single value.

Example:

const arr = [1, 2, 3, 4];
const sum = arr.reduceRight((accumulator, currentValue) => accumulator + currentValue);
console.log("Sum of all numbers:",sum); // Output: 10 (4 + 3 + 2 + 1)

Output:
reduceRight

Array Sort Methods

1. Array Alphabetic Sort

sort():

The elements of an array are sorted in place by the sort() method, and the sorted array is returned. It rearranges elements either in place or by creating a new sorted array.

Example:

const numbers = [36, 17, 84, 01, 65, 19, 22, 16];
const sortedNumbers = numbers.sort();
console.log("sort method:\n",sortedNumbers)

output:
sort

reverse()

reverse() The order of the elements in the array is reversed. It’s like looking at a mirror image of your array. Moves an array to its location and returns a reference to the same array; the first array element is now the last, and the last array element is the first.

Example:

const animals = ["Cheetah", "Lion", "Zebra", "Horse", "Deer"];
animals.reverse();
console.log("reverse method:");
console.log("Array in reverse order:", animals);

Output:
reverse

toSorted()

The array is returned with elements sorted in ascending order.

Example:

const numbers = [5, 2, 8, 1, 4];
const sortedNumbers = numbers.toSorted();
console.log("Sorted in ascending order",sortedNumbers);

Output:
toSorted

toReversed()

Returns the array with elements in reverse order.

Example:

const numbers = [1, 2, 3, 4, 5];
const reversedNumbers = numbers.toReversed();
console.log("Elements in reverse order:",reversedNumbers);

Output:
toReversed

2. Array Numeric Sort

Math.min()

The smallest number among the provided arguments is returned.

Example:

const minNumber = Math.min(22,15,34); 
console.log("Smallest number in the series:",minNumber);

Output:
Math.min

Math.max()

The function determines the largest number among the arguments provided.

Example:

const maxNumber = Math.max(10, 45, 20); 
console.log("Largest number in the series:",maxNumber);

Output:
Math.max

Conclusion

Our exploration of JavaScript array methods has led us from fundamental operations to more advanced techniques. These tools empower developers to manipulate data efficiently and think critically about problem-solving in JavaScript. By mastering these methods, you’ll enhance your coding skills and uncover deeper layers of JavaScript’s potential. Keep practicing and experimenting to unlock even greater possibilities in your coding journey. For those who started here, consider revisiting Essential Methods Part 1 and Part 2 to ensure a comprehensive understanding of array basics.

]]>
https://blogs.perficient.com/2024/04/08/advanced-array-methods-in-javascript-part-3/feed/ 0 359525
Exploring AngularJS Routing: A Practical Guide https://blogs.perficient.com/2024/04/05/exploring-angularjs-routing-a-practical-guide/ https://blogs.perficient.com/2024/04/05/exploring-angularjs-routing-a-practical-guide/#respond Fri, 05 Apr 2024 06:56:35 +0000 https://blogs.perficient.com/?p=360619

AngularJS is a widely adopted JavaScript framework, arming developers with a rich arsenal of tools to engineer dynamic and captivating web applications. Notably, its robust routing capabilities emerge as a key pillar for constructing Single Page Applications (SPAs). Effectively orchestrating navigation and dynamically presenting diverse content sans the need for complete page refreshes, AngularJS routing emerges as a cornerstone in contemporary web development.

In this comprehensive guide, we will embark on a detailed exploration of AngularJS routing, meticulously dissecting its foundational principles. Moreover, we will equip you with tangible instances to foster a more profound comprehension and proficiency in navigating this vital element. Nevertheless, before delving into the complexities of AngularJS routing, it’s crucial to take a moment to explore the benefits inherent in Single Page Applications (SPAs). Then, we’ll explore how to implement both an about route as dynamic content and a product route as a dynamic route with practical code examples.

Benefits of Building SPAs with AngularJS Routing

Building SPAs with AngularJS brings a myriad of benefits to both developers and end-users. Some of these include:

Enhanced User Experience

By requesting only necessary information and resources from the server, Single-Page Applications (SPAs) maximize performance by reducing latency and enhancing overall speed and efficiency. Our optimized Method guarantees quick loading times and improves user experience by prioritizing relevant content delivery.

Improved Performance

SPAs fetch just the vital records and assets from the server, reducing latency and enhancing performance.

Simplified Development

AngularJS streamlines development by supplying a modular, element-based design that makes large-scale applications easier to manipulate and preserve.

Cross-Platform Compatibility

SPAs built with AngularJS are inherently cell-pleasant and can be easily adapted for diverse devices and display sizes.

Now that we’ve covered the blessings of SPAs, let’s investigate AngularJS routing and how it improves dynamic and attractive internet apps.

Understanding AngularJS Routing

AngularJS routing is based on the concept of mapping URLs to different views or templates within the application. It enables developers to define routes and associate each route with a specific template and controller. When a user navigates to a particular URL, AngularJS loads the associated template and controller, updating the view dynamically without requiring a full page reload.

Understanding Dynamic Content

Dynamic content refers to website elements or data that can change dynamically without requiring a full page reload. This dynamism enables developers to create more engaging and personalized user experiences by updating content in real-time based on user interactions or other factors. In AngularJS, dynamic content is typically achieved through data binding and the manipulation of model data.

Understanding Dynamic Routes

However, dynamic routes allow developers to outline routes with dynamic parameters that could be exchanged primarily based on person entry or other situations. These dynamic parameters act as placeholders in the route path, capturing values from the URL and allowing for the dynamic rendering of content. Dynamic routes provide a flexible and powerful mechanism for handling different types of content within an application.

Key Concepts

1. ngRoute

ngRoute is a module provided by AngularJS that enables routing capabilities in an application. Including this module as a dependency when defining an AngularJS application that utilizes routing is essential.

2. $routeProvider

The $routeProvider service is a crucial component of critical routing. It allows developers to configure routes within an application by defining URL paths and associating them with specific templates and controllers.

3. ng-view Directive

The ng-view directive plays a crucial role in AngularJS, indicating precisely where within an HTML template AngularJS should inject the contents of the current route’s template. It is a placeholder for rendering dynamic perspectives based on the cutting-edge route.

4. Controllers

Controllers in AngularJS are JavaScript functions that are responsible for defining the behavior and data associated with a particular view.It plays a crucial role in separating concerns within an application by handling view logic and interactions.

5. Template

In AngularJS routing, a template denotes an HTML file representing a distinct view within the application. These templates are linked with routes and dynamically loaded and rendered based on the current accessed route.

6. otherwise Method

The otherwise() Method specifies a default route to navigate if the requested route doesn’t match any of the defined routes. It ensures users are redirected to a designated route when accessing unrecognized URLs.

Example AngularJS Routing Implementation

Let’s dive into a practical example to demonstrate AngularJS routing in action.

Setting Up AngularJS Routing

To use routing in an AngularJS application, you first need to include the AngularJS and ngRoute libraries for your undertaking (index.html). You can download them from the official website or include them via a CDN link in your HTML file.

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular-route.js"></script>

You can also leverage package managers like NPM to manage your project dependencies more efficiently.

npm install angular angular-route

By going for walks with the above command, you may set up AngularJS and the angular-path module, which offers routing competencies, as dependencies in your project.

Once you’ve included AngularJS, you can define your application module and configure routing using the $routeProvider service provided by AngularJS.

routeProvider.js

var app = angular.module("myApp", ["ngRoute"]);
app.config(function ($routeProvider, $locationProvider) {
  $locationProvider.hashPrefix("");
  $routeProvider
    .when("/", {
      templateUrl: "views/home.html",
      controller: "HomeController",
    })
    .when("/about", {
      templateUrl: "views/about.html",
      controller: "AboutController",
    })
    .when("/product/:id", {
      templateUrl: "views/product.html",
      controller: "ProductController",
    })
    .otherwise({ redirectTo: "/" });
});

app.controller("MainController", function ($scope) {
  // MainController logic here
});

In the above code:

  • We define the application module `myApp` and specify the dependency on the `ngRoute` module, which provides routing capabilities.
  • We configure routes using the `$routeProvider.when()` Method. Each route definition consists of a URL path, a template URL, and, optionally, a controller.
  • We’ve added the $locationProvider.hashPrefix(”) configuration to remove the default hashbang (#!) from the URL.
  • The `otherwise()` Method specifies the default route to navigate if the requested route doesn’t match any defined routes.

Creating Views and Controllers

Now that we’ve configured routing let’s create the views and controllers for our routes.

Home View and Controller

Create a file named home.html inside a directory called views. This will be the template for the home route.

home.html

<div ng-controller="HomeController">
  <h1>Welcome to the Home Page</h1>
  <p>This is the home page of our AngularJS application.</p>
</div>

Next, create a controller named HomeController in the routeProvider.js file.

routeProvider.js

app.controller("HomeController", function ($scope) {
  //Home Controller logic here
});

Implementing Dynamic Content and Routes

Let’s now see how we can implement both an about route as dynamic content and a product route as a dynamic route in an AngularJS application.

About View and Controller as Dynamic Content

Let’s observe how we can enforce dynamic content in AngularJS. Similarly, create a report named about.html in the perspectives listing.

about.html

<div ng-controller="AboutController">
  <h1>{{ pageTitle }}</h1>
  <p>{{ pageContent }}</p>
</div>

In this about.html template:

  • We use AngularJS expressions ({{ }}) to dynamically render the page title and content, which are bound to the corresponding scope variables (pageTitle and pageContent) set in the AboutController.
  • When navigating the About page, users will see the dynamically populated title and content.

In the AboutController, we can dynamically set the page title and content based on route parameters or any other logic specific to the about page. Then, define the <strong>AboutController</strong> by adding the mentioned code in the routeProvider.js file.

routeProvider.js

app.controller("AboutController", function ($scope) {
  $scope.pageTitle = "About Us";
  $scope.pageContent = "Learn more about our company and mission here.";
});

Product View and Controller as Dynamic Route

Create the product view template (product.html) and define the corresponding controller (ProductController) to handle the dynamic product data.

product.html

<div ng-controller="ProductController">
  <h1>Product Details</h1>
  <p>ID: {{ productId }}</p>
</div>

In the above product.html template, we display the product details, including the product ID obtained from the $routeParams service.

Then, define the ‘ProductController’ by adding the mentioned code in the routeProvider.js file.

routeProvider.js

app.controller('ProductController', function($scope, $routeParams) {
  $scope.productId = $routeParams.id;
  // Fetch product data based on the ID and update the view
});

The ProductController extracts the product ID from the route parameters and can then use it to fetch the corresponding product data from the server or another data source.

Linking Routes to Navigation

For seamless navigation between various routes, leverage the ng-href directive within your HTML to establish links to different paths specified in your route definitions.

index.html

<div ng-controller="MainController">
  <ul>
    <li><a ng-href="#/">Home</a></li>
    <li><a ng-href="#/about">About</a></li>
    <li><a ng-href="#/product/1">Product 1</a></li>
    <li><a ng-href="#/product/2">Product 2</a></li>
  </ul>
  <div ng-view></div>
</div>

Note: When implementing AngularJS routing, remember to add the ng-app= “myApp” attribute to the opening <html> tag in your index.html file. Additionally, link the routeProvider.js file in your index.html to enable routing functionality.

The ng-view directive is a placeholder where AngularJS will inject the templates associated with the current route.

Output

Output

Conclusion

AngularJS routing provides a powerful mechanism for building SPAs by enabling navigation between different views without page reloads. Developers can create dynamic and interactive web applications by configuring routes, defining templates, and linking them to controllers. With the examples and explanations provided in this guide, you should now understand AngularJS routing and be well-equipped to leverage it in your projects.

Remember to explore further documentation and best practices to enhance your routing implementation and create seamless user experiences in your AngularJS applications. Happy coding!

]]>
https://blogs.perficient.com/2024/04/05/exploring-angularjs-routing-a-practical-guide/feed/ 0 360619
Exploring Basics of React’s useReducer and useRef Hooks https://blogs.perficient.com/2024/04/04/exploring-basics-of-reacts-usereducer-and-useref-hooks/ https://blogs.perficient.com/2024/04/04/exploring-basics-of-reacts-usereducer-and-useref-hooks/#respond Thu, 04 Apr 2024 10:00:56 +0000 https://blogs.perficient.com/?p=360923

In the vast landscape of React development, developers are armed with powerful tools to navigate challenges effortlessly: the useReducer and useRef hooks. This guide offers a deep dive into these hooks, unveiling their functionalities, adaptable use cases, and advanced methodologies.

What is `useReducer`?

The <strong>useReducer</strong> hook in React is a cornerstone for managing state, offering a more nuanced approach than its counterpart, <strong>useState.</strong> Its value shines brightest when grappling with intricate state logic, especially in scenarios where the state comprises multiple sub-values or the next state hinges on its predecessor.

Basic Usage

Let’s start with a basic example. Suppose we have a counter component. Instead of resorting to <strong>useState,</strong> developers can employ <strong>useReducer</strong> to orchestrate state management within their React applications.

Counter.js

import React, { useReducer } from "react";

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </div>
  );
}

export default Counter;

Within this demonstration, the reducer function operates as a pivotal entity responsible for processing both the current state and an action, ultimately yielding a fresh state contingent upon the nature of the action. Through the utilization of the useReducer hook, we seamlessly integrate this reducer function alongside the initial state. This amalgamation furnishes us with access to the current state and a potent dispatch function, empowering us to propagate actions directly to the reducer, thereby orchestrating state transformations with precision and efficiency.

Output

Counter

Advanced Usage

useReducer can handle more complex state objects. You can use it with objects, arrays, or any other data structure. Additionally, you can combine it with useContext for global state management or optimize performance with useMemo and useCallback.

Now, let’s explore an advanced usage scenario of useReducer with a more complex state object:

AdvancedCounter.js

// AdvancedCounter.js
import React, { useReducer } from 'react';

const initialState = {
  count: 0,
  showText: false
};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + 1 };
    case 'toggleText':
      return { ...state, showText: !state.showText };
    default:
      throw new Error();
  }
}

function AdvancedCounter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <div>
        Count: {state.count}
        <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      </div>
      <div>
        <button onClick={() => dispatch({ type: 'toggleText' })}>
          {state.showText ? 'Hide Text' : 'Show Text'}
        </button>
        {state.showText && <p>This is a dynamic text!</p>}
      </div>
    </div>
  );
}

export default AdvancedCounter;

In this example, our state object comprises both a counter and a Boolean value to toggle text visibility. The reducer function now handles both incrementing the count and toggling the text display.

Output

Advancecounter

What is `useRef`?

The useRef hook is another essential hook in React. It is primarily used for accessing and manipulating DOM elements directly. Unlike useState or useReducer, changes to a ref don’t cause a re-render. Such versatility makes it apt for various tasks, ranging from overseeing focus management and instigating imperative animations to seamlessly integrating with third-party DOM libraries.

Basic Usage

Let’s create a simple instance to apprehend how useRef works. Suppose we have a form component that requires focusing on an input field when it mounts.

Form.js

import React, { useRef, useEffect } from 'react';

function Form() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button>Submit</button>
    </div>
  );
}

export default Form;

In the illustration provided, useRef emerges as the linchpin, facilitating the establishment of a reference to the input element within the component’s scope. Subsequently, leveraging the effect hook, we orchestrate the focus onto the input element upon the component’s initial rendering, meticulously ensuring that this operation occurs precisely once through the judicious utilization of an empty dependency array [].

Output

Form

Advanced Usage

useRef is not limited to DOM elements, it can also persist mutable values across renders without causing re-renders. This property makes it useful for storing previous values, caching values between renders, or interacting with imperative APIs.

Now, let’s discover a complicated utilization scenario where useRef is hired to persist mutable values:

AdvancedForm.js

// AdvancedForm.js
import React, { useRef, useEffect } from "react";

function AdvancedForm() {
  const renderCount = useRef(0);

  useEffect(() => {
    renderCount.current++;
  });

  return (
    <div>
      <p>This component has rendered {renderCount.current} times.</p>
    </div>
  );
}

export default AdvancedForm;

Within this instance, useRef is ingeniously harnessed to maintain the tally of component renderings throughout successive re-renders, all the while circumventing the unnecessary triggering of additional renders. By employing this tactic, we ensure the seamless preservation of the render counts across iterations, thus exemplifying the versatile capabilities of the useRef hook in React development.

Output

Advancedform

Conclusion

In this guide, we’ve explored two effective hooks supplied by React: useReducer and useRef. useReducer is a versatile tool for dealing with complicated state logic, while useRef gives direct get entry to DOM elements and permits for staying power of mutable values. Understanding and mastering those hooks will notably enhance your React development skills, allowing you to construct greater green and robust packages. Start integrating them into your projects and explore their full potential!

]]>
https://blogs.perficient.com/2024/04/04/exploring-basics-of-reacts-usereducer-and-useref-hooks/feed/ 0 360923
Essential Array Methods Guide: Part 2 https://blogs.perficient.com/2024/03/28/essential-array-methods-guide-part-2/ https://blogs.perficient.com/2024/03/28/essential-array-methods-guide-part-2/#respond Thu, 28 Mar 2024 13:07:31 +0000 https://blogs.perficient.com/?p=359523

In continuation of Part 1, we will now delve into essential array methods that enhance your JavaScript toolkit. These methods allow you to effortlessly handle complex data manipulation tasks, opening new possibilities for your projects.

isArray()

isArray() is your trusty detector for identifying arrays. Just pass any value to Array.isArray(), and it quickly tells you whether it’s an array or not. Simple, yet incredibly useful for handling different types of data.

Example:

const animals = ["Cheetah", "Lion", "Zebra", "Horse", "Deer"];
const isArray = Array.isArray(animals);
console.log("Is passed value an array: ", isArray);

Output:
Essential Array 1

keys(), values(), and entries()

These methods give you iterators for keys, values, and key-value pairs within an array. Essentially, they provide different ways for you to move through the elements of an array.

Key

const fruits = ["apple", "banana", "orange"];
for (const key of fruits.keys()) {
  console.log("Keys of an array: ", key);
}

Output:
Essential Array 2

value

const fruits = ["apple", "banana", "orange"];
for (const value of fruits.values()) {
  console.log("Values of an array: ", value);
}

Output:

entries

const fruits = ["apple", "banana", "orange"];
for (const [index, fruit] of fruits.entries()) {
  console.log("Values of array with its index: ", index, fruit);
}

Output:
Array 4

from()

Array.from(): It’s like a magic wand that turns almost anything into an array. Whether you have scattered pieces of data or an iterable object, Array.from() neatly organizes them into a new array, ready for action.

Example:

const arrayLike = { length: 3, 0: "a", 1: "b", 2: "c" };
const newArray = Array.from(arrayLike);
console.log(newArray);

Output:
from()

fill()

The fill() method is used to change all elements in an array to a static value.

Example:

const numbers = [1, 2, 3, 4, 5];
numbers.fill(0, 2, 4); // [1, 2, 0, 0, 5]
console.log(numbers)

Output:
fill()

flat()

Imagine you have a stack of nested trays, each containing some items. flat() is like taking out all the items from those trays and putting them into a single tray, simplifying the organization.

Example:

const nestedArray = [["Peter Pan", "Aladdin"], ["Mulan", "Maleficent"], ["Moana", "Tangled"]];
const flattenedArray = nestedArray.flat();
console.log("flat method:\n",flattenedArray);

Output:
flat()

with()

The with() method is a tool for arrays that lets you change one specific item without touching the original array. It creates a new array reflecting the update.

Syntax

newArray = array.with(index, newValue);

Parameters

  • index: This represents the position of the item that needs to be updated.
  • newValue: The new value for the specified item.

Example:

const originalArray = ['apple', 'banana', 'cherry'];
const newArray = originalArray.with(1, 'grape');

console.log("New Array:",newArray); 
console.log("Orignal Array:",originalArray);

Output:
with()

length

Length tells you how many items are in an array or characters in a string.

Example:

const languages = ["Urdu", "English", "Hindi", "Spanish", "Italian"];

console.log("Length of array:",languages.length);

Output:
length

at()

Get the item located at a particular position within an array or a string, considering positive and negative indexes.

Example:

const languages = ["Urdu", "English", "Hindi", "Spanish", "Italian"];

console.log("Element at end of the array:",languages.at(-1));

Output:
Essential Array 10

join()

The function combines all elements of an array into a single string, separating them with a specified delimiter.

Example:

const series = ["Array", "Methods", "In", "JavaScript"];

console.log("Series:",series.join(' '));

Output:
join()

delete()

Removes a property from an object; it is not typically used for arrays as it leaves undefined holes.

Example:

const languages = ["Urdu", "English", "Hindi", "Spanish", "Italian"];

delete languages[1];

console.log("After deleting element at 1",languages);

Output:
Essential Array 12

spread (…)

This method enables an array or other iterable object to be spread out in locations where functions or arrays anticipate zero or more arguments or elements.

Example:

const languagesOne = ["Urdu", "English"];
const languagesTwo = ["Spanish", "Italian"];
const allLanguages = [...languagesOne, ...languagesTwo]; // Combines languagesOne and languagesTwo into allLanguages
console.log("Combined array:",allLanguages);

Output:
Essential Array 13

toString()

converts all elements of an array into a single string, separating them with commas.

Example:

const languages = ["Urdu", "English", "Hindi", "Spanish", "Italian"];

let languagesString = languages.toString();
console.log("Array to String:",languagesString);

Output:
Essential Array 14

Conclusion:

As we wrap up the essential and primary part of our exploration into JavaScript array methods, remember that these foundational techniques serve as the building blocks for more complex operations. Mastery of adding, removing, and manipulating elements within arrays opens a myriad of possibilities for data handling in your JavaScript projects. Stay tuned for the next part of our series, where we’ll delve into advanced array methods and further expand our JavaScript toolkit.

]]>
https://blogs.perficient.com/2024/03/28/essential-array-methods-guide-part-2/feed/ 0 359523
Essential Array Methods Guide: Part 1 https://blogs.perficient.com/2024/03/27/essential-array-methods-guide-part-1/ https://blogs.perficient.com/2024/03/27/essential-array-methods-guide-part-1/#respond Wed, 27 Mar 2024 16:26:05 +0000 https://blogs.perficient.com/?p=359501

Arrays are a cornerstone of JavaScript programming, offering a versatile way to keep and manage data. In this initial segment, we`ll explore the essential array methods that every JavaScript developer should know. If you’re eager to advance further, keep an eye out for advanced array methods in JavaScript.

Introduction to Essential Array Methods

Arrays are fundamental data structures in JavaScript, allowing collections of objects to be stored and manipulated efficiently. This blog builds the foundation for array conversions in JavaScript and explores important array methods.

push() and pop()

push()

The function adds elements to the end of an array.

Example:

const fruits = ["apple", "banana"];
fruits.push("orange"); 
console.log("Push method\n", fruits);

Output:
Essential Array Methods 1

pop()

Only the last person in the queue leaves when someone leaves from the end. Similarly, the last element is removed from an array and returned by pop().

Example:

const fruits = ["apple", "banana", "orange"];
const removedFruit = fruits.pop();
console.log("Pop method", fruits);

Output:
Essential Array Methods 2

shift() and unshift()

Let’s switch gears and talk about the beginning of the array, where folks arrive and leave first.

shift()

This method is like the first person in line leaving. The function removes the first element from an array and returns it.

Example:

const Movies = ['The Lion King', 'Aladdin', 'The Jungle Book', 'Moana']
const firstMovie = Movies.shift();
console.log('Shift method:\n',Movies)

Output:
Essential Array Methods 3

unshift():

It’s like making room for a brand-new character at the front of a line. Unshift() provides new factors at the beginning of an array.

Example:

const Movies = ['The Lion King', 'Aladdin', 'The Jungle Book', 'Moana']
Movies.unshift('maleficent');
console.log('unshift method:\n',Movies)

Output:
Essential Array Methods 4

concat

This combines arrays, creating a fresh array without altering the originals.

Example:

const Movies1 = ["The Lion King", "Aladdin"];
const Movies2 = ["The Jungle Book", "Moana"];

const combinedMovies = Movies1.concat(Movies2);
console.log("concat method:\n", combinedMovies);

Output:
Essential Array Methods 5

slice()

Picture cutting a slice of pizza without disturbing the rest of it. The slice() method takes out a piece of an array and gives you a new array without affecting the original.

Example:

const vegetables = ["cucumbers", "carrots", "potatoes", "Onions"];
const sliceOfVegetable = vegetables.slice(1, 3);
console.log("slice method:\n", sliceOfVegetable);

Output:
Essential Array Methods 6

splice()

Consider getting a necklace with flowers that you can rearrange or add. splice() allows you to make changes to an array by removing or replacing elements and inserting new ones at specific locations.

Example:

const games = ["Archery", "Base ball", "Cricket"];
games.splice(2, 2, "Dodgeball", "Football");
console.log("splice mathods:\n", games);

Output:
Essential Array Methods 7

toSpliced()

The Array.toSpliced() method creates a new array by removing or replacing elements in an existing array without modifying the original array. Since it’s an immutable operation, the source array doesn’t change. When you wish to work with a changed version of the original array but need to preserve its state, this approach comes in handy.

Syntax

array.toSpliced(start, deleteCount, item1, item2,... itemN)

Parameters

start: The index at which to initiate changing the array. A negative index can be used, counting back from the last item.

deleteCount: items will be deleted from the array, starting from the given start.

item1, item2,…, itemN: Items to add to the array starting from the start position.

Original Tasks List

const tasks = ['Email team', 'Meeting at 2 PM', 'Write report'];

Adding Elements Without Removing

Suppose you want to add two new tasks at the end without removing any existing tasks.

const addedTasks = tasks.toSpliced(3, 0, 'Update project plan', 'Review budgets');

console.log(addedTasks);

Output:
Picture9

Removing Elements Without Adding

Let’s say the meeting has been canceled, and you want to remove it from the list without adding anything new.

const removedTasks = tasks.toSpliced(1, 1);

console.log(removedTasks);

Output:
Picture10

Replacing an Element

If you need to replace ‘Write report’ with ‘Prepare presentation’, here’s how you could do it:

const replacedTask = tasks.toSpliced(2, 1, 'Prepare presentation');

console.log(replacedTask);

Output:
Picture11

Using Negative Indexes

Suppose you want to add a task at the end, but use a negative index to specify the position.

const negativeIndexAdd = tasks.toSpliced(-1, 0, 'Check emails');

console.log(negativeIndexAdd);

Output:
Picture12

Removing and Adding with a Negative Index

Lastly, if you want to replace the last task with two new ones using a negative index,

const negativeIndexReplace = tasks.toSpliced(-1, 1, 'Prepare invoices', 'Send updates');

console.log(negativeIndexReplace);

Output:
Picture13

copyWithin()

method copies a sequence of elements within the same array, effectively overwriting existing elements.

Syntax

array.copyWithin(target, start, end)

Parameters

  • target: the index to copy elements to.
  • start: The index to commence copying elements from.
  • end: (optional) index to stop copying, but not including. If omitted, copyUntil() will copy until the end of the array.

Example:

const array = ['apple', 'banana', 'cherry', 'date', 'elderberry'];

// Copy the elements from index 0 to index 3, overwriting elements starting from index 2
array.copyWithin(2, 0, 3);

console.log(array);

Output:
Picture14

Conclusion

As we conclude the first part of our study of JavaScript essential array methods, keep in mind that these fundamental techniques serve as the foundation for more complicated operations. Stay tuned for the second chapter, where we’ll review more important array functions and broaden our JavaScript toolkit.

]]>
https://blogs.perficient.com/2024/03/27/essential-array-methods-guide-part-1/feed/ 0 359501
useFormState in Next.js: Form Management https://blogs.perficient.com/2024/03/26/useformstate-in-next-js-form-management/ https://blogs.perficient.com/2024/03/26/useformstate-in-next-js-form-management/#respond Tue, 26 Mar 2024 16:09:21 +0000 https://blogs.perficient.com/?p=359720

In my previous blog, we had discussed using server actions with our forms and managing the loading state with the new useFormStatus hook from react-dom. In this one, we are going to explore another experimental hook from react-dom: useFormState.

Concisely, useFormState is a hook to which we provide a function to manipulate form data. The function provides us with two values, the first being the form data value or the manipulated data we get from our provided function. As we know, when developing NextJS apps, we prefer to have our code on the server side. This hook is beneficial since the function we provide to it will be a server function. More on that later.

Using useFormState in your Code

As I have mentioned earlier, this is an experimental hook by react-dom, meaning it will not be available with our usual npm install package installing command. For this, we will have to run the command instruction in our terminal meant to install the experimental version of react and react-dom:

npm install react@experimental react-dom@experimental

After installation of experimental packages, your package.json should have the dependencies as follows:

Useformstate6

Once this has been completed, a file should also be created to inform your project that the experimental utilities will be used if typescript is being used in your project:

Useformstate7

After this, you should be able to import useFormState in your files and use them accordingly. //@ts-ignore” may need to be added right above the import as TypeScript will not recognize this experimental import, but it will still function as intended.

//@ts-ignore
import { useFormState } from "react-dom";

Creating a Simple Form

Let’s create a simple form that accepts a name and an age.

<form action={onSubmit} className="form-control py-3 border-primary border-3">
<Input inputName="username" placeholder="Enter Name" />
    <Input inputName="age" placeholder="Enter Age" />
<SaveButton />
</form>

We will be using the “name” property of HTML’s input tag, which is given value here in our Input component by the “inputName” property, for picking data from the form inputs.

const Input = ({
  inputName,
  placeholder
}: {
  inputName: string;
  placeholder: string;
}) => {
  return (
    <div className="my-2">
      <input
        className="form-control"
        name={inputName}
        type="text"
        placeholder={placeholder}
      />
    </div>
  );
};

This is the output of our basic form so far:

Useformstate1

As for the submit button, this is where we will be using an aspect of useFormStatus, another experimental hook I mentioned earlier, that is the loading state. It is a Boolean value by the name “pending,” which we get from this hook. We can use this for disabling the submit button to avoid multiple form submit calls or to change the text on the button like we have done here:

//@ts-ignore
import { useFormStatus } from "react-dom";

const SaveButton = () => {
  const { pending: isLoading } = useFormStatus();
  return (
    <div className="my-3">
      <button
        disabled={isLoading}
        className="btn btn-warning form-control"
        type="submit"
      >
        {isLoading ? "Saving..." : "Save"}
      </button>
    </div>
  );
};

More on this in my previous blog post on this hook.

As for our form, we will use the form’s action attribute for our submit action. But first, let’s take a look at the action we create for the purpose of form submission.

const [{ username, age, error }, updateProfile] = useFormState(
    saveProfile,
    {
      username: "",
      age: "",
      error: {},
    }
  );

As seen in the above code snippet, useFormState takes two values. The first is the action we want to perform when submitting the form, and the second is the initial state values of the form. It then returns two values in the form of an array, as we see with the useState hook from React. When we execute our submit action, we receive the updated value as the first value, and the second value is a function that mirrors our created function, the “saveProfile” function. We will utilize this new function, “updateProfile,” for our form submission. When the form is submitted, we will get the updated values again in the first value of the returned array, which I have deconstructed here for convenience.

Form Submit Action

Now let’s take a look at the server action that we passed to our experimental new hook:

"use server";

import { Profile} from "./types";

export const saveProfile: (_prevState: Profile, profile: Profile) => void = (
  _prevSate,
  profile
) => profile;

As we can observe from the top of this file, the file is a server file, and therefore, it will execute its action on the server side rather than the client side. This is basically the main reason for the use of this process. Now if we look at our function, it is accepting two parameters, but we only should be passing one parameter value, which would be our form data. So where did this extra parameter come from? It is actually due to the useFormState hook.

Earlier, I stated that we pass a function to the useFormState hook and, in return, receive a value and a function that resembles a copy of the function passed to the hook. So the extra parameter is actually from the new “updateProfile” function we have taken from the hook. And as seen from the first parameter’s name, this is the extra parameter which has the previous state data of our form, since we do not have a need for this, I have appended an underscore to its name to denote it as an unused variable.

Now Let’s Use This With our Form

To do so, we just need to pass the new function we got from the useFormState hook to the form’s action property. By passing FormData of the form with the details of the form to our function, we can retrieve the data using the get function of the FormData.

const username = (formData.get("username") as string) || "";
  const age = (formData.get("age") as string) || "";

Let’s add some validation to our form submit function:

export const saveProfile: (_prevState: Profile, formData: FormData) => Profile = (
  _prevSate,
  formData
) => {
  const username = (formData.get("username") as string) || "";
  const age = (formData.get("age") as string) || "";
  const invalidName = username.length && username.length < 4;
  const invalidAge = age.length && parseInt(age) < 18;
  let error: ProfileError = {};
  if (!username) error.username = "Name cannot be empty";
  if (!age) error.age = "Age cannot be empty";
  if (invalidName) error.username = "Username must be at least 4 characters";
  if (invalidAge) error.age = "Age must be above 18 years";
  if (username && !invalidName) error.username = "";
  if (age && !invalidAge) error.age = "";

  return ({ username, age, error });
};

Typically, when a user enters values into form input fields and clicks on the submit button, we expect the submit action to be on the client side as these actions are performed on the client device. But with the useFormState we have managed to move this logic to the server side as a server action. We can add more complex validations than what we see above using regex or service API for more checks if required.

Adding these error messages to our form along with their respective input fields:

<form action={updateProfile} className="form-control py-3 border-primary border-3">
        <Input inputName="username" placeholder="Enter Name" />
        <div className="text-danger fs-6">{error.username}</div>
        <Input inputName="age" placeholder="Enter Age" />
        <div className="text-danger fs-6">{error.age}</div>
        <SaveButton />
      </form>

Let’s see the result of our form with the error validation:

Empty form submission:

Useformstate2

Invalid Username input:

Useformstate3

Invalid Age input:

Useformstate4

Successful form submission:
Useformstate5

Conclusion

In conclusion, mastering the useFormState hook in React enables efficient form management, offering easy access to form data and dynamic state tracking. Experiment with its features to enhance your form-building skills and deliver seamless user experiences.

 

]]>
https://blogs.perficient.com/2024/03/26/useformstate-in-next-js-form-management/feed/ 0 359720