Skip to content

Commit

Permalink
Add textbox react component
Browse files Browse the repository at this point in the history
  • Loading branch information
hungoptimizely committed Oct 9, 2023
1 parent 8bca1d7 commit fae77c3
Show file tree
Hide file tree
Showing 23 changed files with 323 additions and 64 deletions.
2 changes: 1 addition & 1 deletion samples/ManagementSite/Alloy.ManagementSite.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<PackageReference Include="EPiServer.CMS.UI.VisitorGroups" Version="12.22.7" />
<PackageReference Include="EPiServer.CMS.UI.AspNetIdentity" Version="12.22.7" />
<PackageReference Include="EPiServer.ImageLibrary.ImageSharp" Version="2.0.1" />
<PackageReference Include="Optimizely.Headless.Form.Service" Version="0.1.0--inte-110" />
<PackageReference Include="Optimizely.Headless.Form.Service" Version="0.1.0--inte-123" />
<PackageReference Include="Optimizely.Cms.Content.EPiServer" Version="0.3.1" />
<PackageReference Include="EPiServer.Forms" Version="5.6.1-feature-AFORM-3540-021035" />
</ItemGroup>
Expand Down
115 changes: 115 additions & 0 deletions samples/ManagementSite/StaticAutofillSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using EPiServer.Forms.Core.Internal.Autofill;
using EPiServer.Forms.Core.Internal.ExternalSystem;
using EPiServer.Forms.Core;
using Microsoft.AspNetCore.Http;
using System.Collections.Generic;
using System.Linq;

namespace AlloyMvcTemplates.CustomCode
{
/// <summary>
/// Always autofill FormElement with static suggestion list
/// </summary>
public class StaticAutofillSystem : IExternalSystem, IAutofillProvider
{
public virtual string Id
{
get { return "StaticAutofillSystem"; }
}

/// <summary>
/// This system does not have datasource, because it is static
/// </summary>
public virtual IEnumerable<IDatasource> Datasources
{
get
{
var ds1 = new Datasource()
{
Id = "StaticAutofillDatasource1",
Name = "Static Autofill Datasource 1",
OwnerSystem = this,
Columns = new Dictionary<string, string> {
{ "staticds1email", "static ds1 email" },
{ "staticds1firstname", "static ds1 first name" }
}
};

var ds2 = new Datasource()
{
Id = "StaticAutofillDatasource2",
Name = "Static Autofill Datasource 2",
OwnerSystem = this,
Columns = new Dictionary<string, string> {
{ "staticds2avatar", "static ds2 avatar" },
{ "staticds2name", "static ds2 name" },
{ "staticds2bio", "static ds2 Bio" }
}
};

return new[] { ds1, ds2 };
}
}

/// <summary>
/// Returns a list of suggested values by field mapping key.
/// </summary>
/// <param name="fieldMappingKeys">List of field mapping keys</param>
/// <returns>Collection of suggested value</returns>
public virtual IEnumerable<string> GetSuggestedValues(IDatasource selectedDatasource, IEnumerable<RemoteFieldInfo> remoteFieldInfos, ElementBlockBase content, IFormContainerBlock formContainerBlock, HttpContext context)
{
if (selectedDatasource == null || remoteFieldInfos == null)
{
return Enumerable.Empty<string>();
}

if (!Datasources.Any(ds => ds.Id == selectedDatasource.Id) // datasource belong to this system
|| !remoteFieldInfos.Any(mi => mi.DatasourceId == selectedDatasource.Id)) // and remoteFieldInfos is for our system datasource
{
return Enumerable.Empty<string>();
}

var activeRemoteFieldInfo = remoteFieldInfos.FirstOrDefault(mi => mi.DatasourceId == selectedDatasource.Id);
switch (activeRemoteFieldInfo.ColumnId)
{
case "staticds1email":
return new List<string> {
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]"
};

case "staticds1firstname":
return new List<string> {
"Hung",
"Phu",
"Tuan",
"Thach"
};

case "staticds2avatar":
return new List<string> {
"tuan.png",
"thach.jpg"
};

case "staticds2name":
return new List<string>{
"Tuan Do",
"Thach Nguyen",
"Hung Phan",
"Phu Nguyen"
};

case "staticds2bio":
return new List<string> {
"I am from Vietnam",
"I am from Sweden"
};
default:
return Enumerable.Empty<string>();
}
}
}
}
20 changes: 20 additions & 0 deletions samples/sample-react-app/public/css.min.css

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions samples/sample-react-app/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="stylesheet" href="css.min.css" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Expand Down
6 changes: 2 additions & 4 deletions samples/sample-react-app/src/components/Form.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { FormContainer, FormLoader } from "@optimizely/forms-sdk";
import React, { useEffect, useState } from "react";
import { FormContainerBlock } from "@optimizely/forms-react"
import { UseFormLoaderProps, useFormLoader } from "../hooks/useFormLoader";

Expand All @@ -9,11 +7,11 @@ interface FormProps {
}

export default function Form ({formKey, language}: FormProps) {
const {data: formData, error, loading } = useFormLoader({ formKey, language, baseUrl: process.env.REACT_APP_HEADLESS_FORM_BASE_URL } as UseFormLoaderProps)
const {data: formData } = useFormLoader({ formKey, language, baseUrl: process.env.REACT_APP_HEADLESS_FORM_BASE_URL } as UseFormLoaderProps)

return (
<>
{formData && <FormContainerBlock form={formData} />}
{formData && <FormContainerBlock form={formData} key={formData.key} />}
</>
);
}
46 changes: 24 additions & 22 deletions src/@optimizely/forms-react/src/FormContainerBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, { useRef } from "react";
import { FormContainer } from "@optimizely/forms-sdk";
import { buildFormStep } from "./helpers/stepHelper";
import { FormContainer, StepBuilder } from "@optimizely/forms-sdk";
import { RenderElementInStep } from "./components/RenderElementInStep";
import { SubmitButtonType } from "./models/SubmitButtonType";
import "./FormStyle.scss";
Expand All @@ -10,7 +9,8 @@ export interface FormContainerProps {
}

export function FormContainerBlock(props: FormContainerProps){
const form = buildFormStep(props.form);
const stepBuilder = new StepBuilder(props.form);
const form = stepBuilder.buildForm();
const formTitleId = `${form.key}_label`;
const statusDisplay = useRef<string>("hide");
const stepCount = form.steps.length;
Expand Down Expand Up @@ -50,19 +50,23 @@ export function FormContainerBlock(props: FormContainerProps){
const FormBody = () => {
return (
<>
{form.properties.title && <h2 className="Form__Title" id={formTitleId}>
{form.properties.title}
</h2>}
{form.properties.description && <aside className="Form__Description">
{form.properties.description}
</aside>}
{isReadOnlyMode && readOnlyModeMessage && (
{form.properties.title &&
<h2 className="Form__Title" id={formTitleId}>
{form.properties.title}
</h2>
}
{form.properties.description &&
<aside className="Form__Description">
{form.properties.description}
</aside>
}
{isReadOnlyMode && readOnlyModeMessage &&
<div className="Form__Status">
<span className="Form__Readonly__Message" role="alert">
{readOnlyModeMessage}
</span>
</div>
)}
}
{/* area for showing Form's status or validation */}
<div className="Form__Status">
<div role="status" className={`Form__Status__Message ${statusDisplay}`}>
Expand All @@ -77,37 +81,35 @@ export function FormContainerBlock(props: FormContainerProps){
{/* render element */}
{form.steps.map((e, i)=>{
let stepDisplaying = (currentStepIndex === i && !formFinalized && isStepValidToDisplay) ? "" : "hide";
return <>
<section id={e.formStep.key} className={`Form__Element__Step ${stepDisplaying}`}>
return (
<section key={e.formStep.key} id={e.formStep.key} className={`Form__Element__Step ${stepDisplaying}`}>
<RenderElementInStep elements={e.elements} stepIndex={i} />
</section>
</>
);
})}

{/* render step navigation */}
{isShowStepNavigation && (
<>
<nav role="navigation" className="Form__NavigationBar">
{isShowStepNavigation &&
<nav role="navigation" className="Form__NavigationBar">
<button type="submit" name="submit" value={SubmitButtonType.PreviousStep} className="Form__NavigationBar__Action FormExcludeDataRebind btnPrev"
disabled={prevButtonDisableState}>
Previous
{form.localizations["previousButtonLabel"]}
</button>

<div className="Form__NavigationBar__ProgressBar">
<div className="Form__NavigationBar__ProgressBar--Progress" style={{width: progressWidth}}></div>
<div className="Form__NavigationBar__ProgressBar--Text">
<span className="Form__NavigationBar__ProgressBar__ProgressLabel">Page</span>
<span className="Form__NavigationBar__ProgressBar__ProgressLabel">{form.localizations["pageButtonLabel"]}</span>
<span className="Form__NavigationBar__ProgressBar__CurrentStep">{currentDisplayStepIndex}</span>/
<span className="Form__NavigationBar__ProgressBar__StepsCount">{stepCount}</span>
</div>
</div>
<button type="submit" name="submit" value={SubmitButtonType.NextStep} className="Form__NavigationBar__Action FormExcludeDataRebind btnNext"
disabled={nextButtonDisableState}>
Next
{form.localizations["nextButtonLabel"]}
</button>
</nav>
</>
)}
}
</div>
</>
)
Expand Down
19 changes: 19 additions & 0 deletions src/@optimizely/forms-react/src/components/ElementWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React, { ReactNode } from "react";

export interface ElementWrapperProps{
className: string
isVisible: boolean,
children: ReactNode
}

export default function ElementWrapper(props: ElementWrapperProps){
return (
<>
{props.isVisible && (
<>
{props.children}
</>
)}
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ export function RenderElement(props: ElementProps) {
return (<p>{`Cannot render ${props.name} component`}</p>)
}

return React.createElement(FoundElement, { element });
return <FoundElement element={element} />
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function RenderElementInStep(props: RenderElementInStepProps){
const {elements} = props;
return (<>
{elements.map(e => (
<RenderElement name={e.contentType} element={e} />
<RenderElement name={e.contentType} element={e} key={e.key} />
))}
</>);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,64 @@
import { Textbox } from "@optimizely/forms-sdk";
import React, { FunctionComponent} from "react";
import React, { useRef} from "react";
import { ValidatorType } from "../../models";
import { FormValidationModel } from "../../models/FormValidationModel";
import ElementWrapper from "../ElementWrapper";

export interface TextboxElementBlockProps {
element: Textbox
}

export const TextboxElementBlock = (props: TextboxElementBlockProps) => {
//TODO: update code later
return (<>Textbox</>);
const {element} = props;
const formContext = {} as Record<string, object>;
const formValidationContext = {} as Record<string, FormValidationModel[]>;
const handleChange = () => {}; //TODO: update data to context
const handleBlur = () => {}; //TODO: validation, dependency

const isRequire = element.properties.validators?.some(v => v.type === ValidatorType.RequiredValidator);
const validatorClasses = element.properties.validators?.reduce((acc, obj) => `${acc} ${obj.model.validationCssClass}`, "");

const extraAttr = useRef<any>({});
if(isRequire){
extraAttr.current = {...extraAttr.current, required: isRequire, "aria-required": isRequire };
}
if(element.properties.descripton && element.properties.descripton.trim() !== ""){
extraAttr.current = {...extraAttr.current, title: element.properties.descripton }
}
if(element.properties.forms_ExternalSystemsFieldMappings?.length > 0){
extraAttr.current = {...extraAttr.current, list: `${element.key}_datalist` }
}

return (
<ElementWrapper className={`FormTextbox ${validatorClasses ?? ""}`} isVisible={true}>
<label htmlFor={element.key} className="Form__Element__Caption">
{element.properties.label}
</label>
<input name={element.key} id={element.key} type="text" className="FormTextbox__Input"
aria-describedby={`${element.key}_desc`}
placeholder={element.properties.placeHolder}
value={formContext[element.key]}
autoComplete={element.properties.autoComplete}
{...extraAttr.current}
onChange={handleChange}
onBlur={handleBlur}/>
{element.properties.validators?.map((v)=> {
let validationResult = formValidationContext[element.key]?.filter(r => r.type == v.type);
let valid = !validationResult || validationResult?.length == 0 || validationResult[0].valid;
return (
<span key={v.type} className="Form__Element__ValidationError" id={`${element.key}_desc`} role="alert"
style={{display: valid ? "none" : ""}}>
{v.model.message}
</span>
);
})}
{element.properties.forms_ExternalSystemsFieldMappings?.length > 0 &&
<datalist id={`${element.key}_datalist`}>
{element.properties.forms_ExternalSystemsFieldMappings?.map(i =>
<option value={i} key={i}></option>
)}
</datalist>
}
</ElementWrapper>
);
}
26 changes: 0 additions & 26 deletions src/@optimizely/forms-react/src/helpers/stepHelper.ts

This file was deleted.

5 changes: 5 additions & 0 deletions src/@optimizely/forms-react/src/models/FormValidationModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface FormValidationModel
{
type: string
valid: boolean
}
Loading

0 comments on commit fae77c3

Please sign in to comment.