Skip to content

Commit

Permalink
Customize styling of the porting embed (#5)
Browse files Browse the repository at this point in the history
- Adds the ability to pass in custom class names based on a class name
function
- This supports all kinds of custom stylings: CSS Modules, css-in-js,
Tailwind, plain css
- Still ships with some unique class names (`GigsEmbeds-XYZ`) so we can
ship a default CSS file
- Adds an option to use a custom `formId`, which is used together with
the submit button
- Adds an `stepChange` event which is fired when the wizard step changes
  • Loading branch information
timomeh authored Mar 1, 2024
1 parent 96befd7 commit 9407d13
Show file tree
Hide file tree
Showing 24 changed files with 1,631 additions and 117 deletions.
21 changes: 18 additions & 3 deletions lib/PortingEmbed/EmbedField.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import { FieldStore } from '@modular-forms/preact'

import { useEmbedOptions } from './Options'

type Props = {
children: React.ReactNode
of: FieldStore<any, string> // eslint-disable-line @typescript-eslint/no-explicit-any
}

export function EmbedField({ children }: Props) {
// TODO: customizable classNames
export function EmbedField({ children, of: field }: Props) {
const options = useEmbedOptions()
const customClassName =
options.className?.field?.({
name: field.name,
touched: field.touched.value,
dirty: field.dirty.value,
valid: !field.error.value,
}) || ''

return (
<div className="GigsEmbeds GigsPortingEmbed GigsEmbeds-field">
<div
className={`GigsEmbeds GigsPortingEmbed GigsEmbeds-field ${customClassName}`}
>
{children}
</div>
)
Expand Down
23 changes: 19 additions & 4 deletions lib/PortingEmbed/EmbedFieldError.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
import { FieldStore } from '@modular-forms/preact'

import { useEmbedOptions } from './Options'

type Props = {
error: string
of: FieldStore<any, string> // eslint-disable-line @typescript-eslint/no-explicit-any
}

export function EmbedFieldError({ error }: Props) {
export function EmbedFieldError({ of: field }: Props) {
const options = useEmbedOptions()
const customClassName =
options.className?.error?.({
name: field.name,
touched: field.touched.value,
dirty: field.dirty.value,
}) || ''

const error = field.error.value

if (!error) {
return null
}

// TODO: customizable classNames
return (
<div className="GigsEmbeds GigsPortingEmbed GigsEmbeds-fieldError">
<div
className={`GigsEmbeds GigsPortingEmbed GigsEmbeds-error ${customClassName}`}
>
{error}
</div>
)
Expand Down
26 changes: 21 additions & 5 deletions lib/PortingEmbed/EmbedFieldInput.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
type Props = React.HTMLAttributes<HTMLInputElement>
import { FieldStore } from '@modular-forms/preact'

import { useEmbedOptions } from './Options'

type Props = {
of: FieldStore<any, string> // eslint-disable-line @typescript-eslint/no-explicit-any
} & React.HTMLAttributes<HTMLInputElement>

export function EmbedFieldInput({ of: field, ...rest }: Props) {
const options = useEmbedOptions()
const customClassName =
options.className?.input?.({
name: field.name,
touched: field.touched.value,
dirty: field.dirty.value,
valid: !field.error.value,
}) || ''
const id = `__ge_${field.name}`

export function EmbedFieldInput(props: Props) {
// TODO: customizable classNames
return (
<input
className="GigsEmbeds GigsPortingEmbed GigsEmbeds-fieldInput"
{...props}
className={`GigsEmbeds GigsPortingEmbed GigsEmbeds-input ${customClassName}`}
id={id}
{...rest}
/>
)
}
26 changes: 21 additions & 5 deletions lib/PortingEmbed/EmbedFieldLabel.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
type Props = React.HTMLAttributes<HTMLLabelElement>
import { FieldStore } from '@modular-forms/preact'

import { useEmbedOptions } from './Options'

type Props = {
of: FieldStore<any, string> // eslint-disable-line @typescript-eslint/no-explicit-any
} & React.HTMLAttributes<HTMLLabelElement>

export function EmbedFieldLabel({ of: field, ...rest }: Props) {
const options = useEmbedOptions()
const customClassName =
options.className?.label?.({
name: field.name,
touched: field.touched.value,
dirty: field.dirty.value,
valid: !field.error.value,
}) || ''
const id = `__ge_${field.name}`

export function EmbedFieldLabel(props: Props) {
// TODO: customizable classNames
return (
<label
className="GigsEmbeds GigsPortingEmbed GigsEmbeds-fieldLabel"
{...props}
htmlFor={id}
className={`GigsEmbeds GigsPortingEmbed GigsEmbeds-label ${customClassName}`}
{...rest}
/>
)
}
36 changes: 36 additions & 0 deletions lib/PortingEmbed/Options.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { createContext } from 'preact'
import { useContext } from 'preact/hooks'

type FieldState = {
name: string
dirty: boolean
touched: boolean
valid: boolean
}

type FormState = {
name: string
dirty: boolean
valid: boolean
submitting: boolean
touched: boolean
}

export const defaultFormId = 'gigsPortingEmbedForm'

export type EmbedOptions = {
formId?: string
className?: {
form?: (state: FormState) => string
field?: (state: FieldState) => string
input?: (state: FieldState) => string
label?: (state: FieldState) => string
error?: (state: Omit<FieldState, 'valid'>) => string
}
}

export const OptionsContext = createContext<EmbedOptions>({})

export function useEmbedOptions() {
return useContext(OptionsContext)
}
31 changes: 15 additions & 16 deletions lib/PortingEmbed/PortingEmbed.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { Porting, UpdatePortingBody } from '../types'
import { EmbedOptions, OptionsContext } from './Options'
import { PortingForm } from './PortingForm'

export type CustomizableEmbedProps = {
// TODO: add styling options
styleConfig?: {
foo?: string
}
}
export type CustomizableEmbedProps = EmbedOptions

export type ValidationChangeEvent = {
isValid: boolean
Expand All @@ -18,22 +14,25 @@ type CoreEmbedProps = {
onPortingUpdate?: (updatedFields: UpdatePortingBody) => unknown
}

type PortingEmbedProps = CoreEmbedProps & CustomizableEmbedProps
type PortingEmbedProps = CoreEmbedProps & { options?: CustomizableEmbedProps }

export function PortingEmbed({
porting,
onPortingUpdate,
onValidationChange,
options,
}: PortingEmbedProps) {
return (
<div className="__ge_portingRoot">
<PortingForm
porting={porting}
onValidationChange={onValidationChange}
onSubmit={async (updatedFields) => {
await onPortingUpdate?.(updatedFields)
}}
/>
</div>
<OptionsContext.Provider value={options || {}}>
<div className="__ge_portingRoot">
<PortingForm
porting={porting}
onValidationChange={onValidationChange}
onSubmit={async (updatedFields) => {
await onPortingUpdate?.(updatedFields)
}}
/>
</div>
</OptionsContext.Provider>
)
}
2 changes: 1 addition & 1 deletion lib/PortingEmbed/PortingForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function PortingForm({ porting, onValidationChange, onSubmit }: Props) {
)
}

if (step === 'donorApproval') {
if (step === 'donorProviderApproval') {
return (
<StepDonorProviderApprovalForm
porting={porting}
Expand Down
Loading

0 comments on commit 9407d13

Please sign in to comment.