Skip to content

Commit

Permalink
Add vanilla form components
Browse files Browse the repository at this point in the history
This adds a jsconfig for easier reference to the javascript folder and
a set of vanilla form components copied from [candy_wrapper](https://github.com/thoughtbot/candy_wrapper)
that works with form_props
  • Loading branch information
jho406 committed Nov 30, 2024
1 parent e88728c commit 3efca67
Show file tree
Hide file tree
Showing 7 changed files with 347 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ def create_files
say "Copying application_visit.js file to #{app_js_path}"
copy_file "#{__dir__}/templates/js/application_visit.js", "#{app_js_path}/application_visit.js"

say "Copying components.js file to #{app_js_path}"
copy_file "#{__dir__}/templates/js/components.js", "#{app_js_path}/components.js"

say "Copying jsconfig.json file to #{app_js_path}"
copy_file "#{__dir__}/templates/js/jsconfig.json", "jsconfig.json"

say "Copying Superglue initializer"
copy_file "#{__dir__}/templates/initializer.rb", "config/initializers/superglue.rb"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
/**
* Vanilla is a minimum set of
* [candy_wrappers](https://github.com/thoughtbot/candy_wrapper) around react
* HTML tags. It works with the output from
* [FormProps](https://github.com/thoughtbot/form_props).
*
* There is no style and structured with bare necessities. You should modify
* these components to fit your design needs.
*/
import React, { useContext, createContext, useMemo } from 'react';
export const ValidationContext = createContext({});
export const useErrorKeyValidation = ({ errorKey, }) => {
const errors = useContext(ValidationContext);
return useMemo(() => {
return errors[errorKey];
}, [errors, errorKey]);
};
/**
* Extras renders the hidden inputs generated by form_props.
*
* Its meant to be used with a form component and renders hidden values for
* utf8, crsf_token, _method
*/
export const Extras = (hiddenInputAttributes) => {
const hiddenProps = Object.values(hiddenInputAttributes);
const hiddenInputs = hiddenProps.map((props) => (<input {...props} type="hidden" key={props.name}/>));
return <>{hiddenInputs}</>;
};
/**
* A basic form component that supports inline errors.
*
* It's meant to be used with FormProps and mimics the ways that
* Rails forms are generated.
*/
export const Form = ({ extras, validationErrors, children, ...props }) => {
return (<form {...props}>
<ValidationContext.Provider value={validationErrors}>
<Extras {...extras}></Extras>
{children}
</ValidationContext.Provider>
</form>);
};
/**
* An inline error component.
*
* When a Field has an error, this will show below the label and input.
* Please modify this to your liking.
*/
export const FieldError = ({ errorKey }) => {
const errors = useContext(ValidationContext);
if (!errorKey || !errors) {
return null;
}
const validationError = errors[errorKey];
const hasErrors = errorKey && validationError;
if (!hasErrors) {
return null;
}
const errorMessages = Array.isArray(validationError)
? validationError
: [validationError];
return <span>{errorMessages.join(' ')}</span>;
};
/**
* A Field component.
*
* Combines a label, input and a FieldError. Please modify this to your liking.
*/
export const FieldBase = ({ label, errorKey, children, ...props }) => {
return (<>
<label htmlFor={props.id}>{label}</label>
{children || <input {...props}/>}
<FieldError errorKey={errorKey}/>
</>);
};
export const Checkbox = ({ type: _type, includeHidden, uncheckedValue, errorKey, ...rest }) => {
const { name } = rest;
return (<FieldBase {...rest} errorKey={errorKey}>
{includeHidden && (<input type="hidden" name={name} defaultValue={uncheckedValue} autoComplete="off"/>)}
<input type="checkbox" {...rest}></input>
</FieldBase>);
};
/**
* A collection checkbox component.
*
* Designed to work with a payload form_props's [collection_check_boxes helper](https://github.com/thoughtbot/form_props?tab=readme-ov-file#collection-select).
* Mimics the rails equivalent. Please modify to your liking.
*/
export const CollectionCheckboxes = ({ includeHidden, collection, label, errorKey, }) => {
if (collection.length == 0) {
return null;
}
const checkboxes = collection.map((options) => {
return <Checkbox {...options} key={options.id}/>;
});
const { name } = collection[0];
return (<>
{includeHidden && (<input type="hidden" name={name} defaultValue={''} autoComplete="off"/>)}
<label>{label}</label>
{checkboxes}
<FieldError errorKey={errorKey}/>
</>);
};
/**
* A collection radio button component.
*
* Designed to work with a payload form_props's [collection_radio_buttons helper](https://github.com/thoughtbot/form_props?tab=readme-ov-file#collection-select).
* Mimics the rails equivalent. Please modify to your liking.
*/
export const CollectionRadioButtons = ({ includeHidden, collection, label, errorKey, }) => {
if (collection.length == 0) {
return null;
}
const radioButtons = collection.map((options) => {
return (<div key={options.value}>
<input {...options} type="radio"/>
<label htmlFor={options.id}>{options.label}</label>
</div>);
});
const { name } = collection[0];
return (<>
{includeHidden && (<input type="hidden" name={name} defaultValue={''} autoComplete="off"/>)}
<label>{label}</label>
{radioButtons}
<FieldError errorKey={errorKey}/>
</>);
};
/**
* A text field component.
*
* Designed to work with a payload form_props's [text_field helper](https://github.com/thoughtbot/form_props?tab=readme-ov-file#text-helpers).
* Mimics the rails equivalent. Please modify to your liking.
*/
export const TextField = ({ type: _type, ...rest }) => {
return <FieldBase {...rest} type="text"/>;
};
/**
* A email field component.
*
* Designed to work with a payload form_props's [email_field helper](https://github.com/thoughtbot/form_props?tab=readme-ov-file#text-helpers).
* Mimics the rails equivalent. Please modify to your liking.
*/
export const EmailField = ({ type: _type, ...rest }) => {
return <FieldBase {...rest} type="email"/>;
};
/**
* A color field component.
*
* Designed to work with a payload form_props's [color_field helper](https://github.com/thoughtbot/form_props?tab=readme-ov-file#text-helpers).
* Mimics the rails equivalent. Please modify to your liking.
*/
export const ColorField = ({ type: _type, ...rest }) => {
return <FieldBase {...rest} type="color"/>;
};
/**
* A date field component.
*
* Designed to work with a payload form_props's [date_field helper](https://github.com/thoughtbot/form_props?tab=readme-ov-file#date-helpers).
* Mimics the rails equivalent. Please modify to your liking.
*/
export const DateField = ({ type: _type, ...rest }) => {
return <FieldBase {...rest} type="date"/>;
};
/**
* A date field component.
*
* Designed to work with a payload form_props's [date_field helper](https://github.com/thoughtbot/form_props?tab=readme-ov-file#date-helpers).
* Mimics the rails equivalent. Please modify to your liking.
*/
export const DateTimeLocalField = ({ type: _type, ...rest }) => {
return <FieldBase {...rest} type="datetime-local"/>;
};
/**
* A search field component.
*
* Designed to work with a payload form_props's [search_field helper](https://github.com/thoughtbot/form_props?tab=readme-ov-file#text-helpers).
* Mimics the rails equivalent. Please modify to your liking.
*/
export const SearchField = ({ type: _type, ...rest }) => {
return <FieldBase {...rest} type="search"/>;
};
/**
* A tel field component.
*
* Designed to work with a payload form_props's [tel_field helper](https://github.com/thoughtbot/form_props?tab=readme-ov-file#text-helpers).
* Mimics the rails equivalent. Please modify to your liking.
*/
export const TelField = ({ type: _type, ...rest }) => {
return <FieldBase {...rest} type="tel"/>;
};
/**
* A url field component.
*
* Designed to work with a payload form_props's [tel_field helper](https://github.com/thoughtbot/form_props?tab=readme-ov-file#text-helpers).
* Mimics the rails equivalent. Please modify to your liking.
*/
export const UrlField = ({ type: _type, ...rest }) => {
return <FieldBase {...rest} type="url"/>;
};
/**
* A month field component.
*
* Designed to work with a payload form_props's [month_field helper](https://github.com/thoughtbot/form_props?tab=readme-ov-file#date-helpers).
* Mimics the rails equivalent. Please modify to your liking.
*/
export const MonthField = ({ type: _type, ...rest }) => {
return <FieldBase {...rest} type="month"/>;
};
/**
* A month field component.
*
* Designed to work with a payload form_props's [month_field helper](https://github.com/thoughtbot/form_props?tab=readme-ov-file#date-helpers).
* Mimics the rails equivalent. Please modify to your liking.
*/
export const TimeField = ({ type: _type, ...rest }) => {
return <FieldBase {...rest} type="time"/>;
};
/**
* A number field component.
*
* Designed to work with a payload form_props's [month_field helper](https://github.com/thoughtbot/form_props?tab=readme-ov-file#number-helpers).
* Mimics the rails equivalent. Please modify to your liking.
*/
export const NumberField = ({ type: _type, ...rest }) => {
return <FieldBase {...rest} type="number"/>;
};
/**
* A range field component.
*
* Designed to work with a payload form_props's [range_field helper](https://github.com/thoughtbot/form_props?tab=readme-ov-file#number-helpers).
* Mimics the rails equivalent. Please modify to your liking.
*/
export const RangeField = ({ type: _type, ...rest }) => {
return <FieldBase {...rest} type="range"/>;
};
/**
* A password field component.
*
* Designed to work with a payload form_props's [password_field helper](https://github.com/thoughtbot/form_props?tab=readme-ov-file#text-helpers).
* Mimics the rails equivalent. Please modify to your liking.
*/
export const PasswordField = ({ type: _type, ...rest }) => {
return <FieldBase {...rest} type="password"/>;
};
/**
* A select component.
*
* Designed to work with a payload form_props's [select helpers](https://github.com/thoughtbot/form_props?tab=readme-ov-file#select-helpers),
* [collection_select helper](https://github.com/thoughtbot/form_props?tab=readme-ov-file#collection-select), and [grouped_collection_select helper](https://github.com/thoughtbot/form_props?tab=readme-ov-file#group-collection-select).
*
* Please modify to your liking.
*/
export const Select = ({ includeHidden, name, id, children, options, multiple, type: _type, ...rest }) => {
const addHidden = includeHidden && multiple;
const optionElements = options.map((item) => {
if ('options' in item) {
return (<optgroup label={item.label} key={item.label}>
{item.options.map((opt) => (<option key={opt.label} {...opt}/>))}
</optgroup>);
}
else {
return <option key={item.label} {...item}/>;
}
});
return (<>
{addHidden && (<input type="hidden" name={name} value={''} autoComplete="off"/>)}
<select name={name} id={id} multiple={multiple} {...rest}>
{children}
{optionElements}
</select>
</>);
};
/**
* A text area component.
*
* Designed to work with a payload form_props's text_area helper.
* Mimics the rails equivalent. Please modify to your liking.
*/
export const TextArea = ({ type: _type, errorKey, ...rest }) => {
const { label } = rest;
return <FieldBase label={label} errorKey={errorKey} id={rest.id}>
<textarea {...rest}/>
</FieldBase>;
};
/**
* A file field component.
*
* Designed to work with a payload form_props's [file_field helper](https://github.com/thoughtbot/form_props?tab=readme-ov-file#text-helpers).
* Mimics the rails equivalent. Please modify to your liking.
*/
export const FileField = ({ type: _type, ...rest }) => {
return <FieldBase {...rest} type="file"/>;
};

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"baseUrl": ".", // Usually the root of your project
"paths": {
"@javascript/*": ["app/javascript/*"],
"@views/*": ["app/views/*"]
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React from 'react'
import { Form, TextField } from '@javascript/components'

// import { useSelector } from 'react-redux'

export default function <%= js_plural_table_name(:upper) %>Edit ({
Expand All @@ -9,24 +11,15 @@ export default function <%= js_plural_table_name(:upper) %>Edit ({
<%= js_singular_table_name %>Path,
<%= js_plural_table_name %>Path,
}) {
const messagesEl = errors && (
<div id="error_explanation">
<h2>{ errors.explanation }</h2>
<ul>{ errors.messages.map(({body})=> <li key={body}>{body}</li>) }</ul>
</div>
)

const { inputs, props, extras } = form
return (
<div>
{messagesEl}
<form {...form.props} data-sg-visit>
{Object.values(form.extras).map((hiddenProps) => (<input {...hiddenProps} key={hiddenProps.id} type="hidden"/>))}
<Form {...props} extras={extras} data-sg-visit>
<%- attributes.each do |attr| -%>
<input {...form.inputs.<%= attr.column_name %>} type="text"/>
<label htmlFor={form.inputs.<%= attr.column_name %>.id}><%= attr.column_name %></label>
<<%= js_component(attr)%> {...inputs.<%= attr.column_name.camelize(:lower)%>} label="<%= attr.column_name.humanize %>" errorKey="<%= attr.column_name %>" />
<%- end -%>
<button {...form.inputs.submit} type="submit"> {...form.inputs.submit.text} </button>
</form>
<button {...inputs.submit} type="submit"> {...inputs.submit.text} </button>
</Form>

<a href={<%= js_singular_table_name %>Path} data-sg-visit>Show</a>
<a href={<%= js_plural_table_name %>Path} data-sg-visit>Back</a>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react'
import { Form, TextField } from '@javascript/components'
import { useSelector } from 'react-redux'

export default function <%= js_plural_table_name(:upper) %>Index({
Expand All @@ -10,7 +11,8 @@ export default function <%= js_plural_table_name(:upper) %>Index({
const flash = useSelector((state) => state.flash)

const <%= js_singular_table_name %>Items = <%= js_plural_table_name %>.map((<%= js_singular_table_name %>, key) => {
const deleteForm = <%=js_singular_table_name%>.deleteForm;
const { deleteForm } = <%=js_singular_table_name%>;
const { inputs, props, extras } = deleteForm;

return (
<tr key={<%= js_singular_table_name %>.id}>
Expand All @@ -20,10 +22,9 @@ export default function <%= js_plural_table_name(:upper) %>Index({
<td><a href={ <%=js_singular_table_name%>.<%=js_singular_table_name%>Path } data-sg-visit>Show</a></td>
<td><a href={ <%=js_singular_table_name%>.edit<%=js_singular_table_name(:upper)%>Path } data-sg-visit>Edit</a></td>
<td>
<form {...deleteForm.props} data-sg-visit>
{Object.values(deleteForm.extras).map((hiddenProps) => (<input {...hiddenProps} key={hiddenProps.id} type="hidden"/>))}
<Form {...props} extras={extras} data-sg-visit>
<button type="submit">Delete</button>
</form>
</Form>
</td>
</tr>
)
Expand Down
Loading

0 comments on commit 3efca67

Please sign in to comment.