diff --git a/form_props.gemspec b/form_props.gemspec index 416eaaa..33d984c 100644 --- a/form_props.gemspec +++ b/form_props.gemspec @@ -16,5 +16,5 @@ Gem::Specification.new do |s| s.add_dependency "activesupport", ">= 7.0.0" s.add_dependency "actionview", ">= 7.0.0" - s.add_dependency "props_template", ">= 0.23.0" + s.add_dependency "props_template", ">= 0.30.0" end diff --git a/lib/form_props.rb b/lib/form_props.rb index 6d1f158..a518fda 100644 --- a/lib/form_props.rb +++ b/lib/form_props.rb @@ -2,6 +2,7 @@ require "action_view" require "action_pack" +require "form_props/helper" require "form_props/action_view_extensions/form_helper" require "form_props/form_options_helper" require "form_props/inputs/base" diff --git a/lib/form_props/action_view_extensions/form_helper.rb b/lib/form_props/action_view_extensions/form_helper.rb index e275a3e..43487ba 100644 --- a/lib/form_props/action_view_extensions/form_helper.rb +++ b/lib/form_props/action_view_extensions/form_helper.rb @@ -40,12 +40,12 @@ def form_props(model: nil, scope: nil, url: nil, format: nil, **options, &block) end html_options = html_options_for_form_with(url, model, **options) - html_options["acceptCharset"] ||= html_options.delete("accept-charset") json.extras do extra_props_for_form(json, html_options) end - json.props(html_options) + + json.props(FormProps::Helper.format_keys(html_options)) end private @@ -62,8 +62,8 @@ def token_props(json, token = nil, form_options: {}) json.set!("csrf") do json.name request_forgery_protection_token.to_s json.type "hidden" - json.default_value token - json.auto_complete "off" + json.defaultValue token + json.autoComplete "off" end end end @@ -72,8 +72,8 @@ def method_props(json, method) json.set!("method") do json.name "_method" json.type "hidden" - json.default_value method.to_s - json.auto_complete "off" + json.defaultValue method.to_s + json.autoComplete "off" end end @@ -81,8 +81,8 @@ def utf8_enforcer_props(json) json.set!("utf8") do json.name "utf8" json.type "hidden" - json.default_value "✓" - json.auto_complete "off" + json.defaultValue "✓" + json.autoComplete "off" end end diff --git a/lib/form_props/form_builder.rb b/lib/form_props/form_builder.rb index 6a862f9..b8cff1a 100644 --- a/lib/form_props/form_builder.rb +++ b/lib/form_props/form_builder.rb @@ -151,7 +151,7 @@ def fields_for_with_nested_attributes(association_name, association, options, bl if association.respond_to?(:to_ary) explicit_child_index = options[:child_index] - json.set!("#{association_name}_attributes") do + json.set!("#{association_name}Attributes") do json.array! association do |child| if explicit_child_index options[:child_index] = explicit_child_index.call if explicit_child_index.respond_to?(:call) @@ -163,7 +163,7 @@ def fields_for_with_nested_attributes(association_name, association, options, bl end end elsif association - json.set!("#{association_name}_attributes") do + json.set!("#{association_name}Attributes") do fields_for_nested_model(name, association, options, block) end end diff --git a/lib/form_props/helper.rb b/lib/form_props/helper.rb new file mode 100644 index 0000000..5d62e76 --- /dev/null +++ b/lib/form_props/helper.rb @@ -0,0 +1,534 @@ +# frozen_string_literal: true + +# Keep up to date with +# https://github.com/facebook/react/blob/main/packages/react-dom-bindings/src/shared/possibleStandardNames.js +module FormProps + POSSIBLE_STANDARD_NAMES = { + ## HTML + accept: "accept", + acceptcharset: "acceptCharset", + "accept-charset": "acceptCharset", + accesskey: "accessKey", + action: "action", + allowfullscreen: "allowFullScreen", + alt: "alt", + as: "as", + async: "async", + autocapitalize: "autoCapitalize", + autocomplete: "autoComplete", + autocorrect: "autoCorrect", + autofocus: "autoFocus", + autoplay: "autoPlay", + autosave: "autoSave", + capture: "capture", + cellpadding: "cellPadding", + cellspacing: "cellSpacing", + challenge: "challenge", + charset: "charSet", + checked: "checked", + children: "children", + cite: "cite", + class: "className", + classid: "classID", + classname: "className", + cols: "cols", + colspan: "colSpan", + content: "content", + contenteditable: "contentEditable", + contextmenu: "contextMenu", + controls: "controls", + controlslist: "controlsList", + coords: "coords", + crossorigin: "crossOrigin", + dangerouslysetinnerhtml: "dangerouslySetInnerHTML", + data: "data", + datetime: "dateTime", + default: "default", + defaultchecked: "defaultChecked", + defaultvalue: "defaultValue", + defer: "defer", + dir: "dir", + disabled: "disabled", + disablepictureinpicture: "disablePictureInPicture", + disableremoteplayback: "disableRemotePlayback", + download: "download", + draggable: "draggable", + enctype: "encType", + enterkeyhint: "enterKeyHint", + fetchpriority: "fetchPriority", + for: "htmlFor", + form: "form", + formmethod: "formMethod", + formaction: "formAction", + formenctype: "formEncType", + formnovalidate: "formNoValidate", + formtarget: "formTarget", + frameborder: "frameBorder", + headers: "headers", + height: "height", + hidden: "hidden", + high: "high", + href: "href", + hreflang: "hrefLang", + htmlfor: "htmlFor", + httpequiv: "httpEquiv", + "http-equiv": "httpEquiv", + icon: "icon", + id: "id", + imagesizes: "imageSizes", + imagesrcset: "imageSrcSet", + innerhtml: "innerHTML", + inputmode: "inputMode", + integrity: "integrity", + is: "is", + itemid: "itemID", + itemprop: "itemProp", + itemref: "itemRef", + itemscope: "itemScope", + itemtype: "itemType", + keyparams: "keyParams", + keytype: "keyType", + kind: "kind", + label: "label", + lang: "lang", + list: "list", + loop: "loop", + low: "low", + manifest: "manifest", + marginwidth: "marginWidth", + marginheight: "marginHeight", + max: "max", + maxlength: "maxLength", + media: "media", + mediagroup: "mediaGroup", + method: "method", + min: "min", + minlength: "minLength", + multiple: "multiple", + muted: "muted", + name: "name", + nomodule: "noModule", + nonce: "nonce", + novalidate: "noValidate", + open: "open", + optimum: "optimum", + pattern: "pattern", + placeholder: "placeholder", + playsinline: "playsInline", + poster: "poster", + preload: "preload", + profile: "profile", + radiogroup: "radioGroup", + readonly: "readOnly", + referrerpolicy: "referrerPolicy", + rel: "rel", + required: "required", + reversed: "reversed", + role: "role", + rows: "rows", + rowspan: "rowSpan", + sandbox: "sandbox", + scope: "scope", + scoped: "scoped", + scrolling: "scrolling", + seamless: "seamless", + selected: "selected", + shape: "shape", + size: "size", + sizes: "sizes", + span: "span", + spellcheck: "spellCheck", + src: "src", + srcdoc: "srcDoc", + srclang: "srcLang", + srcset: "srcSet", + start: "start", + step: "step", + style: "style", + summary: "summary", + tabindex: "tabIndex", + target: "target", + title: "title", + type: "type", + usemap: "useMap", + value: "value", + width: "width", + wmode: "wmode", + wrap: "wrap", + + # SVG + about: "about", + accentheight: "accentHeight", + "accent-height": "accentHeight", + accumulate: "accumulate", + additive: "additive", + alignmentbaseline: "alignmentBaseline", + "alignment-baseline": "alignmentBaseline", + allowreorder: "allowReorder", + alphabetic: "alphabetic", + amplitude: "amplitude", + arabicform: "arabicForm", + "arabic-form": "arabicForm", + ascent: "ascent", + attributename: "attributeName", + attributetype: "attributeType", + autoreverse: "autoReverse", + azimuth: "azimuth", + basefrequency: "baseFrequency", + baselineshift: "baselineShift", + "baseline-shift": "baselineShift", + baseprofile: "baseProfile", + bbox: "bbox", + begin: "begin", + bias: "bias", + by: "by", + calcmode: "calcMode", + capheight: "capHeight", + "cap-height": "capHeight", + clip: "clip", + clippath: "clipPath", + "clip-path": "clipPath", + clippathunits: "clipPathUnits", + cliprule: "clipRule", + "clip-rule": "clipRule", + color: "color", + colorinterpolation: "colorInterpolation", + "color-interpolation": "colorInterpolation", + colorinterpolationfilters: "colorInterpolationFilters", + "color-interpolation-filters": "colorInterpolationFilters", + colorprofile: "colorProfile", + "color-profile": "colorProfile", + colorrendering: "colorRendering", + "color-rendering": "colorRendering", + contentscripttype: "contentScriptType", + contentstyletype: "contentStyleType", + cursor: "cursor", + cx: "cx", + cy: "cy", + d: "d", + datatype: "datatype", + decelerate: "decelerate", + descent: "descent", + diffuseconstant: "diffuseConstant", + direction: "direction", + display: "display", + divisor: "divisor", + dominantbaseline: "dominantBaseline", + "dominant-baseline": "dominantBaseline", + dur: "dur", + dx: "dx", + dy: "dy", + edgemode: "edgeMode", + elevation: "elevation", + enablebackground: "enableBackground", + "enable-background": "enableBackground", + end: "end", + exponent: "exponent", + externalresourcesrequired: "externalResourcesRequired", + fill: "fill", + fillopacity: "fillOpacity", + "fill-opacity": "fillOpacity", + fillrule: "fillRule", + "fill-rule": "fillRule", + filter: "filter", + filterres: "filterRes", + filterunits: "filterUnits", + floodopacity: "floodOpacity", + "flood-opacity": "floodOpacity", + floodcolor: "floodColor", + "flood-color": "floodColor", + focusable: "focusable", + fontfamily: "fontFamily", + "font-family": "fontFamily", + fontsize: "fontSize", + "font-size": "fontSize", + fontsizeadjust: "fontSizeAdjust", + "font-size-adjust": "fontSizeAdjust", + fontstretch: "fontStretch", + "font-stretch": "fontStretch", + fontstyle: "fontStyle", + "font-style": "fontStyle", + fontvariant: "fontVariant", + "font-variant": "fontVariant", + fontweight: "fontWeight", + "font-weight": "fontWeight", + format: "format", + from: "from", + fx: "fx", + fy: "fy", + g1: "g1", + g2: "g2", + glyphname: "glyphName", + "glyph-name": "glyphName", + glyphorientationhorizontal: "glyphOrientationHorizontal", + "glyph-orientation-horizontal": "glyphOrientationHorizontal", + glyphorientationvertical: "glyphOrientationVertical", + "glyph-orientation-vertical": "glyphOrientationVertical", + glyphref: "glyphRef", + gradienttransform: "gradientTransform", + gradientunits: "gradientUnits", + hanging: "hanging", + horizadvx: "horizAdvX", + "horiz-adv-x": "horizAdvX", + horizoriginx: "horizOriginX", + "horiz-origin-x": "horizOriginX", + ideographic: "ideographic", + imagerendering: "imageRendering", + "image-rendering": "imageRendering", + in2: "in2", + in: "in", + inlist: "inlist", + intercept: "intercept", + k1: "k1", + k2: "k2", + k3: "k3", + k4: "k4", + k: "k", + kernelmatrix: "kernelMatrix", + kernelunitlength: "kernelUnitLength", + kerning: "kerning", + keypoints: "keyPoints", + keysplines: "keySplines", + keytimes: "keyTimes", + lengthadjust: "lengthAdjust", + letterspacing: "letterSpacing", + "letter-spacing": "letterSpacing", + lightingcolor: "lightingColor", + "lighting-color": "lightingColor", + limitingconeangle: "limitingConeAngle", + local: "local", + markerend: "markerEnd", + "marker-end": "markerEnd", + markerheight: "markerHeight", + markermid: "markerMid", + "marker-mid": "markerMid", + markerstart: "markerStart", + "marker-start": "markerStart", + markerunits: "markerUnits", + markerwidth: "markerWidth", + mask: "mask", + maskcontentunits: "maskContentUnits", + maskunits: "maskUnits", + mathematical: "mathematical", + mode: "mode", + numoctaves: "numOctaves", + offset: "offset", + opacity: "opacity", + operator: "operator", + order: "order", + orient: "orient", + orientation: "orientation", + origin: "origin", + overflow: "overflow", + overlineposition: "overlinePosition", + "overline-position": "overlinePosition", + overlinethickness: "overlineThickness", + "overline-thickness": "overlineThickness", + paintorder: "paintOrder", + "paint-order": "paintOrder", + panose1: "panose1", + "panose-1": "panose1", + pathlength: "pathLength", + patterncontentunits: "patternContentUnits", + patterntransform: "patternTransform", + patternunits: "patternUnits", + pointerevents: "pointerEvents", + "pointer-events": "pointerEvents", + points: "points", + pointsatx: "pointsAtX", + pointsaty: "pointsAtY", + pointsatz: "pointsAtZ", + prefix: "prefix", + preservealpha: "preserveAlpha", + preserveaspectratio: "preserveAspectRatio", + primitiveunits: "primitiveUnits", + property: "property", + r: "r", + radius: "radius", + refx: "refX", + refy: "refY", + renderingintent: "renderingIntent", + "rendering-intent": "renderingIntent", + repeatcount: "repeatCount", + repeatdur: "repeatDur", + requiredextensions: "requiredExtensions", + requiredfeatures: "requiredFeatures", + resource: "resource", + restart: "restart", + result: "result", + results: "results", + rotate: "rotate", + rx: "rx", + ry: "ry", + scale: "scale", + security: "security", + seed: "seed", + shaperendering: "shapeRendering", + "shape-rendering": "shapeRendering", + slope: "slope", + spacing: "spacing", + specularconstant: "specularConstant", + specularexponent: "specularExponent", + speed: "speed", + spreadmethod: "spreadMethod", + startoffset: "startOffset", + stddeviation: "stdDeviation", + stemh: "stemh", + stemv: "stemv", + stitchtiles: "stitchTiles", + stopcolor: "stopColor", + "stop-color": "stopColor", + stopopacity: "stopOpacity", + "stop-opacity": "stopOpacity", + strikethroughposition: "strikethroughPosition", + "strikethrough-position": "strikethroughPosition", + strikethroughthickness: "strikethroughThickness", + "strikethrough-thickness": "strikethroughThickness", + string: "string", + stroke: "stroke", + strokedasharray: "strokeDasharray", + "stroke-dasharray": "strokeDasharray", + strokedashoffset: "strokeDashoffset", + "stroke-dashoffset": "strokeDashoffset", + strokelinecap: "strokeLinecap", + "stroke-linecap": "strokeLinecap", + strokelinejoin: "strokeLinejoin", + "stroke-linejoin": "strokeLinejoin", + strokemiterlimit: "strokeMiterlimit", + "stroke-miterlimit": "strokeMiterlimit", + strokewidth: "strokeWidth", + "stroke-width": "strokeWidth", + strokeopacity: "strokeOpacity", + "stroke-opacity": "strokeOpacity", + suppresscontenteditablewarning: "suppressContentEditableWarning", + suppresshydrationwarning: "suppressHydrationWarning", + surfacescale: "surfaceScale", + systemlanguage: "systemLanguage", + tablevalues: "tableValues", + targetx: "targetX", + targety: "targetY", + textanchor: "textAnchor", + "text-anchor": "textAnchor", + textdecoration: "textDecoration", + "text-decoration": "textDecoration", + textlength: "textLength", + textrendering: "textRendering", + "text-rendering": "textRendering", + to: "to", + transform: "transform", + transformorigin: "transformOrigin", + "transform-origin": "transformOrigin", + typeof: "typeof", + u1: "u1", + u2: "u2", + underlineposition: "underlinePosition", + "underline-position": "underlinePosition", + underlinethickness: "underlineThickness", + "underline-thickness": "underlineThickness", + unicode: "unicode", + unicodebidi: "unicodeBidi", + "unicode-bidi": "unicodeBidi", + unicoderange: "unicodeRange", + "unicode-range": "unicodeRange", + unitsperem: "unitsPerEm", + "units-per-em": "unitsPerEm", + unselectable: "unselectable", + valphabetic: "vAlphabetic", + "v-alphabetic": "vAlphabetic", + values: "values", + vectoreffect: "vectorEffect", + "vector-effect": "vectorEffect", + version: "version", + vertadvy: "vertAdvY", + "vert-adv-y": "vertAdvY", + vertoriginx: "vertOriginX", + "vert-origin-x": "vertOriginX", + vertoriginy: "vertOriginY", + "vert-origin-y": "vertOriginY", + vhanging: "vHanging", + "v-hanging": "vHanging", + videographic: "vIdeographic", + "v-ideographic": "vIdeographic", + viewbox: "viewBox", + viewtarget: "viewTarget", + visibility: "visibility", + vmathematical: "vMathematical", + "v-mathematical": "vMathematical", + vocab: "vocab", + widths: "widths", + wordspacing: "wordSpacing", + "word-spacing": "wordSpacing", + writingmode: "writingMode", + "writing-mode": "writingMode", + x1: "x1", + x2: "x2", + x: "x", + xchannelselector: "xChannelSelector", + xheight: "xHeight", + "x-height": "xHeight", + xlinkactuate: "xlinkActuate", + "xlink:actuate": "xlinkActuate", + xlinkarcrole: "xlinkArcrole", + "xlink:arcrole": "xlinkArcrole", + xlinkhref: "xlinkHref", + "xlink:href": "xlinkHref", + xlinkrole: "xlinkRole", + "xlink:role": "xlinkRole", + xlinkshow: "xlinkShow", + "xlink:show": "xlinkShow", + xlinktitle: "xlinkTitle", + "xlink:title": "xlinkTitle", + xlinktype: "xlinkType", + "xlink:type": "xlinkType", + xmlbase: "xmlBase", + "xml:base": "xmlBase", + xmllang: "xmlLang", + "xml:lang": "xmlLang", + xmlns: "xmlns", + "xml:space": "xmlSpace", + xmlnsxlink: "xmlnsXlink", + "xmlns:xlink": "xmlnsXlink", + xmlspace: "xmlSpace", + y1: "y1", + y2: "y2", + y: "y", + ychannelselector: "yChannelSelector", + z: "z", + zoomandpan: "zoomAndPan" + } + + OPTION_STANDARD_NAMES = { + default_value: "defaultValue", + default_checked: "defaultChecked", + checked_value: "checkedValue", + unchecked_value: "uncheckedValue", + include_hidden: "includeHidden", + include_blank: "includeBlank", + max_length: "maxLength", + min_length: "minLength", + class_name: "className", + auto_complete: "autoComplete", + read_only: "readOnly" + } + + STANDARD_NAMES = POSSIBLE_STANDARD_NAMES.merge(OPTION_STANDARD_NAMES) + + module Helper + extend self + + def format_key(key) + if STANDARD_NAMES.has_key?(key.to_sym) + STANDARD_NAMES[key.to_sym] + else + key + end + end + + def format_keys(props) + props.each_with_object({}) do |(key, val), memo| + key = format_key(key) + memo[key] = val + end + end + end +end diff --git a/lib/form_props/inputs/base.rb b/lib/form_props/inputs/base.rb index b92260c..8612b1d 100644 --- a/lib/form_props/inputs/base.rb +++ b/lib/form_props/inputs/base.rb @@ -16,6 +16,10 @@ def initialize(object_name, method_name, template_object, options = {}) private + def sanitized_key + sanitized_method_name.camelize(:lower) + end + def add_options(option_tags, options, value = nil) if options[:include_blank] content = (options[:include_blank] if options[:include_blank].is_a?(String)) @@ -95,6 +99,7 @@ def tag_option(key, value) end end + key = FormProps::Helper.format_key(key) json.set!(key, value) end @@ -150,11 +155,11 @@ def select_content_props(option_tags, options, html_options) end end - json.set!(sanitized_method_name) do + json.set!(sanitized_key) do input_props(html_options) if options.key?(:include_hidden) - json.include_hidden options[:include_hidden] + json.includeHidden options[:include_hidden] end json.options(option_tags) end diff --git a/lib/form_props/inputs/check_box.rb b/lib/form_props/inputs/check_box.rb index 7cc68a2..4a2aac0 100644 --- a/lib/form_props/inputs/check_box.rb +++ b/lib/form_props/inputs/check_box.rb @@ -40,7 +40,7 @@ def render(flatten = false) if flatten body_block.call else - json.set!(sanitized_method_name) do + json.set!(sanitized_key) do body_block.call end end diff --git a/lib/form_props/inputs/collection_check_boxes.rb b/lib/form_props/inputs/collection_check_boxes.rb index 78733c4..e7426c8 100644 --- a/lib/form_props/inputs/collection_check_boxes.rb +++ b/lib/form_props/inputs/collection_check_boxes.rb @@ -21,12 +21,12 @@ def render(extra_html_options = {}) end def render - json.set!(sanitized_method_name) do + json.set!(sanitized_key) do json.collection do render_collection_for(CheckBoxBuilder) end - json.include_hidden(@options.fetch(:include_hidden) { true }) + json.includeHidden(@options.fetch(:include_hidden) { true }) input_props(@html_options) end diff --git a/lib/form_props/inputs/collection_radio_buttons.rb b/lib/form_props/inputs/collection_radio_buttons.rb index 3c05e25..2a07c78 100644 --- a/lib/form_props/inputs/collection_radio_buttons.rb +++ b/lib/form_props/inputs/collection_radio_buttons.rb @@ -20,12 +20,12 @@ def render(extra_html_options = {}) end def render - json.set!(sanitized_method_name) do + json.set!(sanitized_key) do json.collection do render_collection_for(RadioButtonBuilder) end - json.include_hidden(@options.fetch(:include_hidden) { true }) + json.includeHidden(@options.fetch(:include_hidden) { true }) input_props(@html_options) end diff --git a/lib/form_props/inputs/radio_button.rb b/lib/form_props/inputs/radio_button.rb index 58da0c8..3b38ab1 100644 --- a/lib/form_props/inputs/radio_button.rb +++ b/lib/form_props/inputs/radio_button.rb @@ -22,7 +22,7 @@ def render(flatten = false) @options[:value] = @tag_value @options[:checked] = true if input_checked?(@options) - name_for_key = sanitized_method_name + "_#{sanitized_value(@tag_value)}" + name_for_key = (sanitized_method_name + "_#{sanitized_value(@tag_value)}").camelize(:lower) body_block = -> { add_default_name_and_id_for_value(@tag_value, @options) diff --git a/lib/form_props/inputs/text_area.rb b/lib/form_props/inputs/text_area.rb index 956745c..785113d 100644 --- a/lib/form_props/inputs/text_area.rb +++ b/lib/form_props/inputs/text_area.rb @@ -8,7 +8,7 @@ class TextArea < Base include ActionView::Helpers::Tags::Placeholderable def render - json.set!(sanitized_method_name) do + json.set!(sanitized_key) do add_default_name_and_id(@options) @options[:type] ||= field_type @options[:value] = @options.fetch(:value) { value_before_type_cast } diff --git a/lib/form_props/inputs/text_field.rb b/lib/form_props/inputs/text_field.rb index 625ee77..06a1bf3 100644 --- a/lib/form_props/inputs/text_field.rb +++ b/lib/form_props/inputs/text_field.rb @@ -12,7 +12,7 @@ def render @options[:type] ||= field_type @options[:value] = @options.fetch(:value) { value_before_type_cast } unless field_type == "file" - json.set!(sanitized_method_name) do + json.set!(sanitized_key) do add_default_name_and_id(@options) input_props(@options) end diff --git a/test/form_props_test.rb b/test/form_props_test.rb index 8e27fb5..43b4ac6 100644 --- a/test/form_props_test.rb +++ b/test/form_props_test.rb @@ -26,7 +26,7 @@ def test_form_with_multipart } }, props: { - enctype: "multipart/form-data", + encType: "multipart/form-data", action: "http://www.example.com", acceptCharset: "UTF-8", method: "post"