From 7ce88f37a96d70c59efb1ef7eb81d43f1157427f Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Fri, 15 Dec 2023 18:20:01 -0300 Subject: [PATCH 01/33] WIP --- Dockerfile | 22 +++++++++++++++++----- docker-compose.yml | 13 +++++-------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index 824c5af..67bef9a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -69,6 +69,7 @@ ARG USER_UID=1000 ARG USER_GID=$USER_UID RUN apt-get update && apt-get upgrade -y && \ + <<<<<<< HEAD apt-get install -y --no-install-recommends $PACKAGES_DEV $PACKAGES_RUNTIME && \ gem install $GEMS_DEV && \ addgroup --gid $USER_GID $USERNAME && \ @@ -78,11 +79,22 @@ RUN apt-get update && apt-get upgrade -y && \ mkdir -p $NODE_MODULES_PATH && \ chown -R $USERNAME:$USERNAME $BUNDLE_PATH && \ chown -R $USERNAME:$USERNAME $APP_ROOT + ======= + apt-get install -y --no-install-recommends $PACKAGES_DEV $PACKAGES_RUNTIME && \ + gem install $GEMS_DEV && \ + addgroup --gid $USER_GID $USERNAME && \ + adduser --home /home/$USERNAME --shell /bin/zsh --uid $USER_UID --gid $USER_GID $USERNAME && \ + echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME && \ + mkdir -p $BUNDLE_PATH && \ + mkdir -p $NODE_MODULES_PATH && \ + chown -R $USERNAME:$USERNAME $BUNDLE_PATH && \ + chown -R $USERNAME:$USERNAME $APP_ROOT + >>>>>>> 39d115c (WIP) -USER $USERNAME + USER $USERNAME -RUN bundle install --jobs 4 --retry 3 + RUN bundle install --jobs 4 --retry 3 -WORKDIR /home/$USERNAME -RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v1.1.2/zsh-in-docker.sh)" -WORKDIR $APP_ROOT + WORKDIR /home/$USERNAME + RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v1.1.2/zsh-in-docker.sh)" + WORKDIR $APP_ROOT diff --git a/docker-compose.yml b/docker-compose.yml index 73b5cbe..f7ddea4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,7 @@ x-build-common: &build-common x-app-common: &app-common stdin_open: true tty: true + entrypoint: script/docker-entrypoint.sh volumes: - .:/app - bundler:/bundler @@ -17,12 +18,9 @@ x-app-common: &app-common services: web: <<: [*build-common, *app-common] - command: "rerun --pattern **/*.{rb,ru,yml} -- bundle exec rackup --host=0.0.0.0" - tty: true - stdin_open: true - entrypoint: script/docker-entrypoint.sh - depends_on: - - tailwindcss + command: rerun --pattern **/*.{rb,ru,yml} -- bundle exec rackup --host=0.0.0.0 + ports: + - 9292:9292 environment: ENVIRONMENT_NAME: development HISTFILE: /app/.zsh_history @@ -54,8 +52,7 @@ services: - web command: bundle exec guard ports: - - "35729:35729" - + - 35729:35729 volumes: bundler: From 2b1f6d635b9d7c84af3ac3f2462f02e7ba271d85 Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Sat, 20 Jan 2024 10:44:16 -0300 Subject: [PATCH 02/33] Esbuild --- assets/css/index.css | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/assets/css/index.css b/assets/css/index.css index 3721848..f9cc6e9 100644 --- a/assets/css/index.css +++ b/assets/css/index.css @@ -562,10 +562,22 @@ video { display: flex; } +.hidden { + display: none; +} + .gap-2 { gap: 0.5rem; } +.rounded { + border-radius: 0.25rem; +} + +.border { + border-width: 1px; +} + .border-b { border-bottom-width: 1px; } @@ -575,6 +587,10 @@ video { background-color: rgb(15 23 42 / var(--tw-bg-opacity)); } +.p-1 { + padding: 0.25rem; +} + .p-2 { padding: 0.5rem; } @@ -588,6 +604,11 @@ video { font-weight: 600; } +.text-gray-800 { + --tw-text-opacity: 1; + color: rgb(31 41 55 / var(--tw-text-opacity)); +} + .text-slate-50 { --tw-text-opacity: 1; color: rgb(248 250 252 / var(--tw-text-opacity)); @@ -609,6 +630,16 @@ video { box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } +.placeholder\:text-gray-400::-moz-placeholder { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} + +.placeholder\:text-gray-400::placeholder { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} + .hover\:shadow-sm:hover { --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); From a3d52e2a0218ea97ee02111167f94d2048715d1a Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Sun, 21 Jan 2024 19:35:31 -0300 Subject: [PATCH 03/33] Adding forms --- web/forms/fields/color_field.rb | 15 +++ web/forms/fields/date_field.rb | 15 +++ web/forms/fields/datetime_local_field.rb | 15 +++ web/forms/fields/email_field.rb | 15 +++ web/forms/fields/field.rb | 51 +++++++++ web/forms/fields/hidden_field.rb | 15 +++ web/forms/fields/number_field.rb | 19 ++++ web/forms/fields/password_field.rb | 15 +++ web/forms/fields/search_field.rb | 15 +++ web/forms/fields/select.rb | 41 +++++++ web/forms/fields/text_field.rb | 15 +++ web/forms/form.rb | 137 +++++++++++++++++++++++ 12 files changed, 368 insertions(+) create mode 100644 web/forms/fields/color_field.rb create mode 100644 web/forms/fields/date_field.rb create mode 100644 web/forms/fields/datetime_local_field.rb create mode 100644 web/forms/fields/email_field.rb create mode 100644 web/forms/fields/field.rb create mode 100644 web/forms/fields/hidden_field.rb create mode 100644 web/forms/fields/number_field.rb create mode 100644 web/forms/fields/password_field.rb create mode 100644 web/forms/fields/search_field.rb create mode 100644 web/forms/fields/select.rb create mode 100644 web/forms/fields/text_field.rb create mode 100644 web/forms/form.rb diff --git a/web/forms/fields/color_field.rb b/web/forms/fields/color_field.rb new file mode 100644 index 0000000..5624e39 --- /dev/null +++ b/web/forms/fields/color_field.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Forms + module Fields + class ColorField < Field + include Phlex::HtmlRenderable + + private + + def type + "color" + end + end + end +end diff --git a/web/forms/fields/date_field.rb b/web/forms/fields/date_field.rb new file mode 100644 index 0000000..ca66e99 --- /dev/null +++ b/web/forms/fields/date_field.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Forms + module Fields + class DateField < Field + include Phlex::HtmlRenderable + + private + + def type + "date" + end + end + end +end diff --git a/web/forms/fields/datetime_local_field.rb b/web/forms/fields/datetime_local_field.rb new file mode 100644 index 0000000..1035976 --- /dev/null +++ b/web/forms/fields/datetime_local_field.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Forms + module Fields + class DatetimeLocalField < Field + include Phlex::HtmlRenderable + + private + + def type + "date" + end + end + end +end diff --git a/web/forms/fields/email_field.rb b/web/forms/fields/email_field.rb new file mode 100644 index 0000000..54dbb07 --- /dev/null +++ b/web/forms/fields/email_field.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Forms + module Fields + class EmailField < Field + include Phlex::HtmlRenderable + + private + + def type + "email" + end + end + end +end diff --git a/web/forms/fields/field.rb b/web/forms/fields/field.rb new file mode 100644 index 0000000..5769079 --- /dev/null +++ b/web/forms/fields/field.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Forms + module Fields + class Field + include Phlex::HtmlRenderable + + def initialize(name, **html_options) + @name = name + @css_class = build_class(html_options.delete(:class)) + @value = html_options.delete(:value) + @html_options = default_html_options.merge(html_options) + end + + def template + input( + value: value, + type: type, + name: name, + class: css_class, + **html_options + ) + end + + private + + attr_reader :name + attr_reader :value + attr_reader :html_options + attr_reader :css_class + + def build_class(classes) + default_css_class.concat( + arrayfy_classes(classes) + ) + end + + def default_css_class + %w[p-1 text-gray-800 placeholder:text-gray-400 border rounded] + end + + def arrayfy_classes(classes) + Array(classes).join(" ").split(" ") + end + + def default_html_options + {} + end + end + end +end diff --git a/web/forms/fields/hidden_field.rb b/web/forms/fields/hidden_field.rb new file mode 100644 index 0000000..66251f8 --- /dev/null +++ b/web/forms/fields/hidden_field.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Forms + module Fields + class HiddenField < Field + include Phlex::HtmlRenderable + + private + + def type + "hidden" + end + end + end +end diff --git a/web/forms/fields/number_field.rb b/web/forms/fields/number_field.rb new file mode 100644 index 0000000..f9a9272 --- /dev/null +++ b/web/forms/fields/number_field.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Forms + module Fields + class NumberField < Field + include Phlex::HtmlRenderable + + private + + def type + "number" + end + + def default_options + { inputmode: "numeric" } + end + end + end +end diff --git a/web/forms/fields/password_field.rb b/web/forms/fields/password_field.rb new file mode 100644 index 0000000..83a82c3 --- /dev/null +++ b/web/forms/fields/password_field.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Forms + module Fields + class PasswordField < Field + include Phlex::HtmlRenderable + + private + + def type + "password" + end + end + end +end diff --git a/web/forms/fields/search_field.rb b/web/forms/fields/search_field.rb new file mode 100644 index 0000000..bfe6d94 --- /dev/null +++ b/web/forms/fields/search_field.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Forms + module Fields + class SearchField < Field + include Phlex::HtmlRenderable + + private + + def type + "search" + end + end + end +end diff --git a/web/forms/fields/select.rb b/web/forms/fields/select.rb new file mode 100644 index 0000000..4b1b517 --- /dev/null +++ b/web/forms/fields/select.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Forms + module Fields + class Select < Field + # TODO: I want to use this like: + # select(:field_name) do |select| + # select.option(text: "do", value: "yes", default: true) + # select.option(text: "don't", value: "no") + # end + # For this I need to create Forms::Fields::Option and the Forms::Fields::Select#option method + + include Phlex::HtmlRenderable + + def initialize(name, options: [], **html_options) + @options = Array(options) + super(name, **html_options) + end + + def template + select(value: value, name: name, class: css_class, **html_options) do + options.each do |option_text, option_value| + option(value: option_value) { option_text } + end + end + end + + private + + attr_reader :options + + def type + "number" + end + + def default_options + { inputmode: "numeric" } + end + end + end +end diff --git a/web/forms/fields/text_field.rb b/web/forms/fields/text_field.rb new file mode 100644 index 0000000..cf10ecc --- /dev/null +++ b/web/forms/fields/text_field.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Forms + module Fields + class TextField < Field + include Phlex::HtmlRenderable + + private + + def type + "text" + end + end + end +end diff --git a/web/forms/form.rb b/web/forms/form.rb new file mode 100644 index 0000000..97a1b16 --- /dev/null +++ b/web/forms/form.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +require "dry/inflector" + +# TODO: implement methods and actions + +module Forms + module Form + include Phlex::HtmlRenderable + + def self.included(including_class) + super + including_class.extend(ClassMethods) + end + + def around_template(&) + form( + id: form_id, + class: css_class, + **html_options, & + ) + end + + module ClassMethods + end + + def initialize(object, **html_options) + super(object) + + @css_class = build_class(html_options.delete(:class)) + @html_options = default_html_options.merge(html_options) + end + + private + + attr_reader :html_options + attr_reader :css_class + + def build_class(classes) + default_css_class.concat( + arrayfy_classes(classes) + ) + end + + def default_css_class + %w[] + end + + def arrayfy_classes(classes) + Array(classes).join(" ").split(" ") + end + + def default_html_options + {} + end + + def default_html_options + { + autocomplete: true, + action: "/", + method: "POST", + } + end + + def form_id + inflector = Dry::Inflector.new + + full_class_path = inflector.underscore(self.class.to_s) + form_subpath = full_class_path.gsub(%r{\Aforms/}, "") + + inflector.dasherize(form_subpath + "-#{model_id}") + end + + def model_id + id + end + + def text_field(...) + render Forms::Fields::TextField.new(...) + end + + def hidden_field(...) + render Forms::Fields::HiddenField.new(...) + end + + def password_field(...) + render Forms::Fields::PasswordField.new(...) + end + + def number_field(...) + render Forms::Fields::NumberField.new(...) + end + + def email_field(...) + render Forms::Fields::EmailField.new(...) + end + + def search_field(...) + render Forms::Fields::SearchField.new(...) + end + + def date_field(...) + render Forms::Fields::DateField.new(...) + end + + def datetime_local_field(...) + render Forms::Fields::DatetimeLocalField.new(...) + end + + def color_field(...) + render Forms::Fields::ColorField.new(...) + end + + def select(...) + render Forms::Fields::Select.new(...) + end + + def errors_for(field_name) + raise NotImplementedError, "Implement 'errors_for'" + end + + # TODO: implement types + # + # + # + # + # + # + # + # + # + # + # + # + # + end +end From 3973eacd50348255a72a378efed94cb4e586c1ca Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Tue, 23 Jan 2024 19:17:35 -0300 Subject: [PATCH 04/33] Removed object argument from Forms::Form#initialize --- web/forms/form.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/forms/form.rb b/web/forms/form.rb index 97a1b16..fa5df05 100644 --- a/web/forms/form.rb +++ b/web/forms/form.rb @@ -24,8 +24,8 @@ def around_template(&) module ClassMethods end - def initialize(object, **html_options) - super(object) + def initialize(**html_options) + super @css_class = build_class(html_options.delete(:class)) @html_options = default_html_options.merge(html_options) From e93afd26779c96a0540a966bedbf6b11b52d4912 Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Tue, 23 Jan 2024 19:22:29 -0300 Subject: [PATCH 05/33] Updated Dockerfile and docker-compose.yml from rebase --- Dockerfile | 22 +++++----------------- docker-compose.yml | 16 ++++++---------- 2 files changed, 11 insertions(+), 27 deletions(-) diff --git a/Dockerfile b/Dockerfile index 67bef9a..824c5af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -69,7 +69,6 @@ ARG USER_UID=1000 ARG USER_GID=$USER_UID RUN apt-get update && apt-get upgrade -y && \ - <<<<<<< HEAD apt-get install -y --no-install-recommends $PACKAGES_DEV $PACKAGES_RUNTIME && \ gem install $GEMS_DEV && \ addgroup --gid $USER_GID $USERNAME && \ @@ -79,22 +78,11 @@ RUN apt-get update && apt-get upgrade -y && \ mkdir -p $NODE_MODULES_PATH && \ chown -R $USERNAME:$USERNAME $BUNDLE_PATH && \ chown -R $USERNAME:$USERNAME $APP_ROOT - ======= - apt-get install -y --no-install-recommends $PACKAGES_DEV $PACKAGES_RUNTIME && \ - gem install $GEMS_DEV && \ - addgroup --gid $USER_GID $USERNAME && \ - adduser --home /home/$USERNAME --shell /bin/zsh --uid $USER_UID --gid $USER_GID $USERNAME && \ - echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME && \ - mkdir -p $BUNDLE_PATH && \ - mkdir -p $NODE_MODULES_PATH && \ - chown -R $USERNAME:$USERNAME $BUNDLE_PATH && \ - chown -R $USERNAME:$USERNAME $APP_ROOT - >>>>>>> 39d115c (WIP) - USER $USERNAME +USER $USERNAME - RUN bundle install --jobs 4 --retry 3 +RUN bundle install --jobs 4 --retry 3 - WORKDIR /home/$USERNAME - RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v1.1.2/zsh-in-docker.sh)" - WORKDIR $APP_ROOT +WORKDIR /home/$USERNAME +RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v1.1.2/zsh-in-docker.sh)" +WORKDIR $APP_ROOT diff --git a/docker-compose.yml b/docker-compose.yml index f7ddea4..6002a99 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,6 @@ x-build-common: &build-common x-app-common: &app-common stdin_open: true tty: true - entrypoint: script/docker-entrypoint.sh volumes: - .:/app - bundler:/bundler @@ -18,19 +17,15 @@ x-app-common: &app-common services: web: <<: [*build-common, *app-common] - command: rerun --pattern **/*.{rb,ru,yml} -- bundle exec rackup --host=0.0.0.0 - ports: - - 9292:9292 + command: "rerun --pattern **/*.{rb,ru,yml} -- bundle exec rackup --host=0.0.0.0" + entrypoint: script/docker-entrypoint.sh + depends_on: + - tailwindcss environment: ENVIRONMENT_NAME: development HISTFILE: /app/.zsh_history - expose: - - 9292 ports: - "9292:9292" - volumes: - - .:/app - - bundler:/bundler tailwindcss: <<: [*build-common, *app-common] @@ -52,7 +47,8 @@ services: - web command: bundle exec guard ports: - - 35729:35729 + - "35729:35729" + volumes: bundler: From a9390813cb0f9f012c79489ebcc5e8e14bbd6db3 Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Tue, 23 Jan 2024 19:26:13 -0300 Subject: [PATCH 06/33] Updated Gemfile --- Gemfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 15cfc77..95e494a 100644 --- a/Gemfile +++ b/Gemfile @@ -21,7 +21,10 @@ group :development do end group :test do - gem "rspec" + gem "capybara" + gem "cuprite" + gem "rspec-given" + gem "rspec-html-matchers" end group :development, :test do From 3df090e8297f0f8f3d97bd892c488d30f6b48a29 Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Tue, 23 Jan 2024 19:26:18 -0300 Subject: [PATCH 07/33] Added request specs configuration --- spec/support/request_specs.rb | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 spec/support/request_specs.rb diff --git a/spec/support/request_specs.rb b/spec/support/request_specs.rb new file mode 100644 index 0000000..52b0706 --- /dev/null +++ b/spec/support/request_specs.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.around do |example| + if %i[request web].include?(RSpec.current_example.metadata[:type]) + require "capybara/rspec" + + config.include Capybara::DSL + + Capybara.app = WebApp + Capybara.default_driver = :rack_test + Capybara.server = :puma, { Silent: true } + + if RSpec.current_example.metadata.has_key?(:js) + require "capybara/cuprite" + Capybara.default_driver = :cuprite + Capybara.javascript_driver = :cuprite + Capybara.register_driver(:cuprite) do |app| + Capybara::Cuprite::Driver.new( + app, + # inspector: true, + window_size: [1200, 800], + browser_options: { "no-sandbox": nil }, + timeout: 30, + ) + end + end + + require "rspec-html-matchers" + config.include RSpecHtmlMatchers + end + + example.run + end +end From a87d48c6083d4059b149ca8c4085c53ec974a44f Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Tue, 23 Jan 2024 19:40:56 -0300 Subject: [PATCH 08/33] Added pending specs --- spec/support/pending_specs.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 spec/support/pending_specs.rb diff --git a/spec/support/pending_specs.rb b/spec/support/pending_specs.rb new file mode 100644 index 0000000..2a76e1e --- /dev/null +++ b/spec/support/pending_specs.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +def pending_spec(description) + called_from = caller.first.gsub(/#{ROOT}/, ".") + skip(description + "\n # #{called_from}") +end + +alias pending pending_spec +alias pending_context pending_spec +alias pcontext pending_spec +alias pending_describe pending_spec +alias pdescribe pending_spec From 7960730598c02116746f7164f96bd198f9530c91 Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Tue, 23 Jan 2024 19:41:16 -0300 Subject: [PATCH 09/33] Added rspec-given --- spec/spec_helper.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index bf50042..c65fe9d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,6 +3,7 @@ ENV["ENVIRONMENT_NAME"] = "test" require "./boot" +require "rspec-given" RSpec.configure do |config| config.expect_with :rspec do |expectations| @@ -16,6 +17,6 @@ require "rake" -FileList["./spec/support/**/*.rb"].each do |file| +Dir.glob("./spec/support/**/*.rb").each do |file| require file end From e8c7e1e13e3f091b3777f776eef0df6702220077 Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Tue, 23 Jan 2024 19:44:09 -0300 Subject: [PATCH 10/33] Deleted expect example to have assertions --- .../expect_example_to_have_assertions.rb | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 spec/support/expect_example_to_have_assertions.rb diff --git a/spec/support/expect_example_to_have_assertions.rb b/spec/support/expect_example_to_have_assertions.rb deleted file mode 100644 index ba59b94..0000000 --- a/spec/support/expect_example_to_have_assertions.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -RSpec.configure do |config| - config.include( - Module.new do - attr_accessor :assertions_have_been_run - - def expect(*) - self.assertions_have_been_run = true - super - end - end - ) - - config.after do |example| - unless assertions_have_been_run || example.exception - no_assertions_error = RuntimeError.new("No assertion run in example '#{example.description}'") - no_assertions_error.set_backtrace([example.location]) - raise no_assertions_error - end - end -end From 10847c49020db9167be0ec03b0ca2e8d4d821bc4 Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Tue, 23 Jan 2024 20:45:42 -0300 Subject: [PATCH 11/33] Adding html matchers for tests --- spec/support/html_specs.rb | 55 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 spec/support/html_specs.rb diff --git a/spec/support/html_specs.rb b/spec/support/html_specs.rb new file mode 100644 index 0000000..ad7a137 --- /dev/null +++ b/spec/support/html_specs.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require "nokogiri" +require "rspec-html-matchers" + +def node(html_string) + Nokogiri::HTML(html_string) +end + +module HtmlMatchers + def has_tag?(tagname, text: nil, with: {}) + nodes = css(tagname) + + node_exists = !nodes.empty? + + nodes.any? { |node| + node_has_text?(node, text) && + node_has_attributes?(node, with) + } + end + + def find_tag(tagname) + css(tagname).first + end + + def find_tags(tagname) + css(tagname) + end + + private + + def node_has_text?(node, text) + return true unless text + + node.text == text + end + + def node_has_attributes?(node, attributes) + attributes.all? { |attr, value| + node[attr.to_s] == value + } + end +end + +[ + Nokogiri::XML::Element, + Nokogiri::XML::Document, +].each do |node_type| + node_type.include(HtmlMatchers) +end + +RSpec.configure do |config| + config.before do + end +end From 9e079cfce4b3cb83b585dbdfefcec9405ac51c5e Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Tue, 23 Jan 2024 20:50:32 -0300 Subject: [PATCH 12/33] WIP: Adding html specs --- Gemfile | 1 + spec/framework/web/forms_spec.rb | 39 +++++ spec/support/html_specs.rb | 2 +- spec/support/request_specs.rb | 4 +- web/forms/form.rb | 263 ++++++++++++++++--------------- 5 files changed, 178 insertions(+), 131 deletions(-) create mode 100644 spec/framework/web/forms_spec.rb diff --git a/Gemfile b/Gemfile index 95e494a..4f7ac39 100644 --- a/Gemfile +++ b/Gemfile @@ -23,6 +23,7 @@ end group :test do gem "capybara" gem "cuprite" + gem "nokogiri" gem "rspec-given" gem "rspec-html-matchers" end diff --git a/spec/framework/web/forms_spec.rb b/spec/framework/web/forms_spec.rb new file mode 100644 index 0000000..fbb144b --- /dev/null +++ b/spec/framework/web/forms_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +RSpec.describe Forms::Form, html: true do + def build_form_class(&block) + Class.new do + include Phlex::HtmlRenderable + include Forms::Form + + class_eval(&block) if block_given? + end + end + + Given(:form_class) { + build_form_class do + def template; end + end + } + Given(:form) { form_class.new } + Given(:html_string) { form.call } + + When(:result) { HTML(html_string) } + + Then { + result.has_tag?("form") + } + + pending_context "methods" + pending_context "actions" + pending_context "different field types" + pending_context "form name" + pending_context "form class" + pending_context "form id" + pending_context "html options" + pending_context "" + pending_context "" + pending_context "" + pending_context "" + pending_context "" +end diff --git a/spec/support/html_specs.rb b/spec/support/html_specs.rb index ad7a137..1e9c143 100644 --- a/spec/support/html_specs.rb +++ b/spec/support/html_specs.rb @@ -3,7 +3,7 @@ require "nokogiri" require "rspec-html-matchers" -def node(html_string) +def HTML(html_string) Nokogiri::HTML(html_string) end diff --git a/spec/support/request_specs.rb b/spec/support/request_specs.rb index 52b0706..3464772 100644 --- a/spec/support/request_specs.rb +++ b/spec/support/request_specs.rb @@ -3,6 +3,7 @@ RSpec.configure do |config| config.around do |example| if %i[request web].include?(RSpec.current_example.metadata[:type]) + RSpec.current_example.metadata[:html] = true require "capybara/rspec" config.include Capybara::DSL @@ -25,9 +26,6 @@ ) end end - - require "rspec-html-matchers" - config.include RSpecHtmlMatchers end example.run diff --git a/web/forms/form.rb b/web/forms/form.rb index fa5df05..81f4edb 100644 --- a/web/forms/form.rb +++ b/web/forms/form.rb @@ -1,137 +1,146 @@ # frozen_string_literal: true -require "dry/inflector" +# require "dry/inflector" -# TODO: implement methods and actions +# # TODO: implement methods and actions module Forms module Form - include Phlex::HtmlRenderable - - def self.included(including_class) - super - including_class.extend(ClassMethods) - end - def around_template(&) - form( - id: form_id, - class: css_class, - **html_options, & - ) + form do + div(tete: "toto") { "pepe"} + end end - - module ClassMethods - end - - def initialize(**html_options) - super - - @css_class = build_class(html_options.delete(:class)) - @html_options = default_html_options.merge(html_options) - end - - private - - attr_reader :html_options - attr_reader :css_class - - def build_class(classes) - default_css_class.concat( - arrayfy_classes(classes) - ) - end - - def default_css_class - %w[] - end - - def arrayfy_classes(classes) - Array(classes).join(" ").split(" ") - end - - def default_html_options - {} - end - - def default_html_options - { - autocomplete: true, - action: "/", - method: "POST", - } - end - - def form_id - inflector = Dry::Inflector.new - - full_class_path = inflector.underscore(self.class.to_s) - form_subpath = full_class_path.gsub(%r{\Aforms/}, "") - - inflector.dasherize(form_subpath + "-#{model_id}") - end - - def model_id - id - end - - def text_field(...) - render Forms::Fields::TextField.new(...) - end - - def hidden_field(...) - render Forms::Fields::HiddenField.new(...) - end - - def password_field(...) - render Forms::Fields::PasswordField.new(...) - end - - def number_field(...) - render Forms::Fields::NumberField.new(...) - end - - def email_field(...) - render Forms::Fields::EmailField.new(...) - end - - def search_field(...) - render Forms::Fields::SearchField.new(...) - end - - def date_field(...) - render Forms::Fields::DateField.new(...) - end - - def datetime_local_field(...) - render Forms::Fields::DatetimeLocalField.new(...) - end - - def color_field(...) - render Forms::Fields::ColorField.new(...) - end - - def select(...) - render Forms::Fields::Select.new(...) - end - - def errors_for(field_name) - raise NotImplementedError, "Implement 'errors_for'" - end - - # TODO: implement types - # - # - # - # - # - # - # - # - # - # - # - # - # end end +# module Forms +# module Form +# include Phlex::HtmlRenderable + +# def self.included(including_class) +# super +# including_class.extend(ClassMethods) +# end + +# def around_template(&) +# form( +# id: form_id, +# class: css_class, +# **html_options, & +# ) +# end + +# module ClassMethods +# end + +# def initialize(**html_options) +# super + +# @css_class = build_class(html_options.delete(:class)) +# @html_options = default_html_options.merge(html_options) +# end + +# private + +# attr_reader :html_options +# attr_reader :css_class + +# def build_class(classes) +# default_css_class.concat( +# arrayfy_classes(classes) +# ) +# end + +# def default_css_class +# %w[] +# end + +# def arrayfy_classes(classes) +# Array(classes).join(" ").split(" ") +# end + +# def default_html_options +# {} +# end + +# def default_html_options +# { +# autocomplete: true, +# action: "/", +# method: "POST", +# } +# end + +# def form_id +# inflector = Dry::Inflector.new + +# full_class_path = inflector.underscore(self.class.to_s) +# form_subpath = full_class_path.gsub(%r{\Aforms/}, "") + +# inflector.dasherize(form_subpath + "-#{model_id}") +# end + +# def model_id +# id +# end + +# def text_field(...) +# render Forms::Fields::TextField.new(...) +# end + +# def hidden_field(...) +# render Forms::Fields::HiddenField.new(...) +# end + +# def password_field(...) +# render Forms::Fields::PasswordField.new(...) +# end + +# def number_field(...) +# render Forms::Fields::NumberField.new(...) +# end + +# def email_field(...) +# render Forms::Fields::EmailField.new(...) +# end + +# def search_field(...) +# render Forms::Fields::SearchField.new(...) +# end + +# def date_field(...) +# render Forms::Fields::DateField.new(...) +# end + +# def datetime_local_field(...) +# render Forms::Fields::DatetimeLocalField.new(...) +# end + +# def color_field(...) +# render Forms::Fields::ColorField.new(...) +# end + +# def select(...) +# render Forms::Fields::Select.new(...) +# end + +# def errors_for(field_name) +# raise NotImplementedError, "Implement 'errors_for'" +# end + +# # TODO: implement types +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# end +# end From 39bf5f87b7f8e45bcf5bcb7bba7c65b1c78c3fe8 Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Wed, 24 Jan 2024 19:01:21 -0300 Subject: [PATCH 13/33] Added http method --- assets/css/index.css | 4 ++ spec/framework/web/forms_spec.rb | 87 +++++++++++++++++++++++++++----- web/forms/form.rb | 45 ++++++++++++++++- 3 files changed, 120 insertions(+), 16 deletions(-) diff --git a/assets/css/index.css b/assets/css/index.css index f9cc6e9..1321d7b 100644 --- a/assets/css/index.css +++ b/assets/css/index.css @@ -604,6 +604,10 @@ video { font-weight: 600; } +.lowercase { + text-transform: lowercase; +} + .text-gray-800 { --tw-text-opacity: 1; color: rgb(31 41 55 / var(--tw-text-opacity)); diff --git a/spec/framework/web/forms_spec.rb b/spec/framework/web/forms_spec.rb index fbb144b..b15603b 100644 --- a/spec/framework/web/forms_spec.rb +++ b/spec/framework/web/forms_spec.rb @@ -1,39 +1,98 @@ # frozen_string_literal: true RSpec.describe Forms::Form, html: true do - def build_form_class(&block) + def build_form_class(name: "", action: "", http_method: "get", target: "", &block) Class.new do include Phlex::HtmlRenderable include Forms::Form + http_method(http_method) + class_eval(&block) if block_given? end end Given(:form_class) { - build_form_class do - def template; end - end + build_form_class } Given(:form) { form_class.new } Given(:html_string) { form.call } When(:result) { HTML(html_string) } - Then { - result.has_tag?("form") - } + describe "empty form" do + Then { result.has_tag?("form") } + end + + describe "basic attributes" do + shared_context "method" do |http_method_to_test| + Given(:form_class) { build_form_class(http_method: http_method) } + + context "when the method is #{http_method_to_test.upcase}" do + Given(:expected_method) { http_method_to_test.downcase } + Given(:expected_upcased_method) { expected_method.upcase } + + Given(:http_method) { http_method_to_test.downcase } + Given(:uppercase_http_method) { http_method_to_test.upcase } + + context "rendering" do + Then { + result.has_tag?( + "form", + with: { + method: http_method_to_test.downcase, + } + ) + } + end + + context "in lowercase" do + Then { form.http_method?(expected_method) == true } + And { form.http_method?(expected_upcased_method) == true } + + ( + %w[get post delete patch put head options connect trace] - + [http_method_to_test.downcase] + ).each do |potential_method| + And { form.http_method?(potential_method) == false } + end + end - pending_context "methods" - pending_context "actions" + context "in uppercase" do + Given(:form_class) { build_form_class(http_method: uppercase_http_method) } + + Then { form.http_method?(expected_method) == true } + And { form.http_method?(uppercase_http_method) == true } + end + end + end + + describe "http methods" do + include_context "method", "get" + include_context "method", "post" + include_context "method", "put" + include_context "method", "head" + include_context "method", "delete" + include_context "method", "patch" + include_context "method", "options" + include_context "method", "connect" + include_context "method", "trace" + + context "non-existent method" do + When(:result) { build_form_class(http_method: "other") } + + Then { result == Failure(Forms::Form::InvalidHttpMethod, Forms::Form::INVALID_HTTP_METHOD) } + end + end + end + + describe "action" do + + end pending_context "different field types" pending_context "form name" pending_context "form class" pending_context "form id" pending_context "html options" - pending_context "" - pending_context "" - pending_context "" - pending_context "" - pending_context "" + pending_context "form name" end diff --git a/web/forms/form.rb b/web/forms/form.rb index 81f4edb..a585d35 100644 --- a/web/forms/form.rb +++ b/web/forms/form.rb @@ -7,12 +7,53 @@ module Forms module Form def around_template(&) - form do - div(tete: "toto") { "pepe"} + form(method: http_method) {} + end + + def http_method?(potential_method) + self.class.http_method?(potential_method) + end + + def self.included(including_class) + including_class.extend(ClassMethods) + end + + module ClassMethods + def http_method(an_html_method) + # # TODO: extract HttpMethod hierarchy + @http_method = http_methods.find( + proc { + raise InvalidHttpMethod, INVALID_HTTP_METHOD + } + ) { |http_method| + an_html_method.downcase == http_method + } + end + + def http_methods + %w[get post delete patch put head options connect trace] end + + def http_method?(potential_method) + @http_method == potential_method.downcase + end + + def http_method_option + @http_method + end + end + + private + + def http_method + self.class.http_method_option end + + InvalidHttpMethod = Class.new(RuntimeError) + INVALID_HTTP_METHOD = "Invalid HTTP Method" end end + # module Forms # module Form # include Phlex::HtmlRenderable From 8c8da16cf36e1a902e8fce72a0468cf2be55a858 Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Wed, 24 Jan 2024 19:22:26 -0300 Subject: [PATCH 14/33] Added action to forms --- spec/framework/web/forms_spec.rb | 26 +++++++++++++++++++------- web/forms/form.rb | 17 ++++++++++++++++- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/spec/framework/web/forms_spec.rb b/spec/framework/web/forms_spec.rb index b15603b..61c1d4e 100644 --- a/spec/framework/web/forms_spec.rb +++ b/spec/framework/web/forms_spec.rb @@ -7,6 +7,7 @@ def build_form_class(name: "", action: "", http_method: "get", target: "", &bloc include Forms::Form http_method(http_method) + action(action) class_eval(&block) if block_given? end @@ -39,9 +40,7 @@ def build_form_class(name: "", action: "", http_method: "get", target: "", &bloc Then { result.has_tag?( "form", - with: { - method: http_method_to_test.downcase, - } + with: { method: http_method_to_test.downcase } ) } end @@ -87,12 +86,25 @@ def build_form_class(name: "", action: "", http_method: "get", target: "", &bloc end describe "action" do - + Given(:form_class) { build_form_class(action: action) } + + context "" do + let(:action) { "/my_action" } + + Then { + result.has_tag?( + "form", + with: { action: action } + ) + } + end end - pending_context "different field types" - pending_context "form name" + pending_context "form class" - pending_context "form id" pending_context "html options" + pending_context "form name" + pending_context "different field types" + + pending_context "form id for model-backed forms" end diff --git a/web/forms/form.rb b/web/forms/form.rb index a585d35..b0255e3 100644 --- a/web/forms/form.rb +++ b/web/forms/form.rb @@ -7,7 +7,10 @@ module Forms module Form def around_template(&) - form(method: http_method) {} + form( + method: http_method, + action: action, + ) {} end def http_method?(potential_method) @@ -19,6 +22,10 @@ def self.included(including_class) end module ClassMethods + def action(an_action) + @action = an_action + end + def http_method(an_html_method) # # TODO: extract HttpMethod hierarchy @http_method = http_methods.find( @@ -41,6 +48,10 @@ def http_method?(potential_method) def http_method_option @http_method end + + def action_option + @action + end end private @@ -49,6 +60,10 @@ def http_method self.class.http_method_option end + def action + self.class.action_option + end + InvalidHttpMethod = Class.new(RuntimeError) INVALID_HTTP_METHOD = "Invalid HTTP Method" end From 251b26f57a6b82897ce1bca7c9f061bba7711a8c Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Wed, 24 Jan 2024 19:37:15 -0300 Subject: [PATCH 15/33] Added basic css class functionality --- spec/framework/web/forms_spec.rb | 50 ++++++++++++++++++++++++++++++-- web/forms/form.rb | 13 +++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/spec/framework/web/forms_spec.rb b/spec/framework/web/forms_spec.rb index 61c1d4e..1fbe7d5 100644 --- a/spec/framework/web/forms_spec.rb +++ b/spec/framework/web/forms_spec.rb @@ -1,13 +1,22 @@ # frozen_string_literal: true RSpec.describe Forms::Form, html: true do - def build_form_class(name: "", action: "", http_method: "get", target: "", &block) + pending_context "TODO: eliminate Forms::Form::ClassMethods. It's a buggy implementation because it's saving stuff at the class level" + + def build_form_class( + name: "", + action: "", + http_method: "get", + css_class: "", + &block + ) Class.new do include Phlex::HtmlRenderable include Forms::Form http_method(http_method) action(action) + css_class(css_class) class_eval(&block) if block_given? end @@ -100,8 +109,45 @@ def build_form_class(name: "", action: "", http_method: "get", target: "", &bloc end end - pending_context "form class" + context "form class" do + Given(:form_class) { build_form_class(css_class: css_class) } + + context "when passing a single class" do + let(:css_class) { "a_class" } + + Then { + result.has_tag?( + "form", + with: { class: css_class } + ) + } + end + + context "when passing multiple classes as a string" do + let(:css_class) { "a_class another_class" } + + Then { + result.has_tag?( + "form", + with: { class: css_class } + ) + } + end + + context "when passing multiple classes as an array" do + let(:css_class) { %w[a_class another_class] } + + Then { + result.has_tag?( + "form", + with: { class: css_class.join(" ") } + ) + } + end + end + pending_context "html options" + pending_context "target" pending_context "form name" pending_context "different field types" diff --git a/web/forms/form.rb b/web/forms/form.rb index b0255e3..d35104f 100644 --- a/web/forms/form.rb +++ b/web/forms/form.rb @@ -9,6 +9,7 @@ module Form def around_template(&) form( method: http_method, + class: css_class, action: action, ) {} end @@ -26,6 +27,10 @@ def action(an_action) @action = an_action end + def css_class(a_css_class) + @css_class = a_css_class + end + def http_method(an_html_method) # # TODO: extract HttpMethod hierarchy @http_method = http_methods.find( @@ -52,6 +57,10 @@ def http_method_option def action_option @action end + + def css_class_option + @css_class + end end private @@ -64,6 +73,10 @@ def action self.class.action_option end + def css_class + self.class.css_class_option + end + InvalidHttpMethod = Class.new(RuntimeError) INVALID_HTTP_METHOD = "Invalid HTTP Method" end From 38c09a4ed74f8ba1e1a08d0ec07e91cf958793b2 Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Wed, 24 Jan 2024 19:49:45 -0300 Subject: [PATCH 16/33] Added html_options to Forms::Form --- spec/framework/web/forms_spec.rb | 18 +++++++++++++++++- web/forms/form.rb | 17 +++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/spec/framework/web/forms_spec.rb b/spec/framework/web/forms_spec.rb index 1fbe7d5..035dfbf 100644 --- a/spec/framework/web/forms_spec.rb +++ b/spec/framework/web/forms_spec.rb @@ -8,6 +8,7 @@ def build_form_class( action: "", http_method: "get", css_class: "", + **html_options, &block ) Class.new do @@ -17,6 +18,7 @@ def build_form_class( http_method(http_method) action(action) css_class(css_class) + html_options(**html_options) class_eval(&block) if block_given? end @@ -146,7 +148,21 @@ def build_form_class( end end - pending_context "html options" + context "html options" do + Given(:form_class) { build_form_class(htmloption: an_option) } + + context "when passing a single class" do + let(:an_option) { "my-option" } + + Then { + result.has_tag?( + "form", + with: { htmloption: an_option } + ) + } + end + end + pending_context "target" pending_context "form name" diff --git a/web/forms/form.rb b/web/forms/form.rb index d35104f..b988bb0 100644 --- a/web/forms/form.rb +++ b/web/forms/form.rb @@ -9,8 +9,9 @@ module Form def around_template(&) form( method: http_method, - class: css_class, + class: css_class, action: action, + **html_options, ) {} end @@ -42,6 +43,10 @@ def http_method(an_html_method) } end + def html_options(**some_html_options) + @html_options = some_html_options + end + def http_methods %w[get post delete patch put head options connect trace] end @@ -61,6 +66,10 @@ def action_option def css_class_option @css_class end + + def html_options_option + @html_options + end end private @@ -76,7 +85,11 @@ def action def css_class self.class.css_class_option end - + + def html_options + self.class.html_options_option + end + InvalidHttpMethod = Class.new(RuntimeError) INVALID_HTTP_METHOD = "Invalid HTTP Method" end From 722ce5116350d0f8ece93163fd1bae5ce77e83a6 Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Thu, 25 Jan 2024 06:59:45 -0300 Subject: [PATCH 17/33] Added POST as the default http method --- spec/framework/web/forms_spec.rb | 22 +++++++++++++++++++++- web/forms/form.rb | 7 +++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/spec/framework/web/forms_spec.rb b/spec/framework/web/forms_spec.rb index 035dfbf..d167710 100644 --- a/spec/framework/web/forms_spec.rb +++ b/spec/framework/web/forms_spec.rb @@ -24,6 +24,15 @@ def build_form_class( end end + def build_base_form_class + Class.new do + include Phlex::HtmlRenderable + include Forms::Form + + class_eval(&block) if block_given? + end + end + Given(:form_class) { build_form_class } @@ -88,6 +97,17 @@ def build_form_class( include_context "method", "connect" include_context "method", "trace" + context "default method" do + Given(:form_class) { build_base_form_class } + + Then { + result.has_tag?( + "form", + with: { method: "post" }, + ) + } + end + context "non-existent method" do When(:result) { build_form_class(http_method: "other") } @@ -96,7 +116,7 @@ def build_form_class( end end - describe "action" do + xdescribe "action" do Given(:form_class) { build_form_class(action: action) } context "" do diff --git a/web/forms/form.rb b/web/forms/form.rb index b988bb0..6f45eeb 100644 --- a/web/forms/form.rb +++ b/web/forms/form.rb @@ -6,6 +6,9 @@ module Forms module Form + def initialize + end + def around_template(&) form( method: http_method, @@ -56,7 +59,7 @@ def http_method?(potential_method) end def http_method_option - @http_method + @http_method || "post" end def action_option @@ -68,7 +71,7 @@ def css_class_option end def html_options_option - @html_options + @html_options || {} end end From 23134b5776d91abf0eb2bf2e8408d3ee1c673af3 Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Thu, 25 Jan 2024 08:47:12 -0300 Subject: [PATCH 18/33] Rewriting Forms::Form --- assets/css/index.css | 4 - spec/framework/web/forms_spec.old.rb | 192 ++++++++++++++++++++++ spec/framework/web/forms_spec.rb | 206 ++++++------------------ spec/support/html_specs.rb | 5 - spec/support/request_specs.rb | 1 - web/forms/form.rb | 220 +++---------------------- web/forms/form.txt | 232 +++++++++++++++++++++++++++ 7 files changed, 494 insertions(+), 366 deletions(-) create mode 100644 spec/framework/web/forms_spec.old.rb create mode 100644 web/forms/form.txt diff --git a/assets/css/index.css b/assets/css/index.css index 1321d7b..f9cc6e9 100644 --- a/assets/css/index.css +++ b/assets/css/index.css @@ -604,10 +604,6 @@ video { font-weight: 600; } -.lowercase { - text-transform: lowercase; -} - .text-gray-800 { --tw-text-opacity: 1; color: rgb(31 41 55 / var(--tw-text-opacity)); diff --git a/spec/framework/web/forms_spec.old.rb b/spec/framework/web/forms_spec.old.rb new file mode 100644 index 0000000..d167710 --- /dev/null +++ b/spec/framework/web/forms_spec.old.rb @@ -0,0 +1,192 @@ +# frozen_string_literal: true + +RSpec.describe Forms::Form, html: true do + pending_context "TODO: eliminate Forms::Form::ClassMethods. It's a buggy implementation because it's saving stuff at the class level" + + def build_form_class( + name: "", + action: "", + http_method: "get", + css_class: "", + **html_options, + &block + ) + Class.new do + include Phlex::HtmlRenderable + include Forms::Form + + http_method(http_method) + action(action) + css_class(css_class) + html_options(**html_options) + + class_eval(&block) if block_given? + end + end + + def build_base_form_class + Class.new do + include Phlex::HtmlRenderable + include Forms::Form + + class_eval(&block) if block_given? + end + end + + Given(:form_class) { + build_form_class + } + Given(:form) { form_class.new } + Given(:html_string) { form.call } + + When(:result) { HTML(html_string) } + + describe "empty form" do + Then { result.has_tag?("form") } + end + + describe "basic attributes" do + shared_context "method" do |http_method_to_test| + Given(:form_class) { build_form_class(http_method: http_method) } + + context "when the method is #{http_method_to_test.upcase}" do + Given(:expected_method) { http_method_to_test.downcase } + Given(:expected_upcased_method) { expected_method.upcase } + + Given(:http_method) { http_method_to_test.downcase } + Given(:uppercase_http_method) { http_method_to_test.upcase } + + context "rendering" do + Then { + result.has_tag?( + "form", + with: { method: http_method_to_test.downcase } + ) + } + end + + context "in lowercase" do + Then { form.http_method?(expected_method) == true } + And { form.http_method?(expected_upcased_method) == true } + + ( + %w[get post delete patch put head options connect trace] - + [http_method_to_test.downcase] + ).each do |potential_method| + And { form.http_method?(potential_method) == false } + end + end + + context "in uppercase" do + Given(:form_class) { build_form_class(http_method: uppercase_http_method) } + + Then { form.http_method?(expected_method) == true } + And { form.http_method?(uppercase_http_method) == true } + end + end + end + + describe "http methods" do + include_context "method", "get" + include_context "method", "post" + include_context "method", "put" + include_context "method", "head" + include_context "method", "delete" + include_context "method", "patch" + include_context "method", "options" + include_context "method", "connect" + include_context "method", "trace" + + context "default method" do + Given(:form_class) { build_base_form_class } + + Then { + result.has_tag?( + "form", + with: { method: "post" }, + ) + } + end + + context "non-existent method" do + When(:result) { build_form_class(http_method: "other") } + + Then { result == Failure(Forms::Form::InvalidHttpMethod, Forms::Form::INVALID_HTTP_METHOD) } + end + end + end + + xdescribe "action" do + Given(:form_class) { build_form_class(action: action) } + + context "" do + let(:action) { "/my_action" } + + Then { + result.has_tag?( + "form", + with: { action: action } + ) + } + end + end + + context "form class" do + Given(:form_class) { build_form_class(css_class: css_class) } + + context "when passing a single class" do + let(:css_class) { "a_class" } + + Then { + result.has_tag?( + "form", + with: { class: css_class } + ) + } + end + + context "when passing multiple classes as a string" do + let(:css_class) { "a_class another_class" } + + Then { + result.has_tag?( + "form", + with: { class: css_class } + ) + } + end + + context "when passing multiple classes as an array" do + let(:css_class) { %w[a_class another_class] } + + Then { + result.has_tag?( + "form", + with: { class: css_class.join(" ") } + ) + } + end + end + + context "html options" do + Given(:form_class) { build_form_class(htmloption: an_option) } + + context "when passing a single class" do + let(:an_option) { "my-option" } + + Then { + result.has_tag?( + "form", + with: { htmloption: an_option } + ) + } + end + end + + pending_context "target" + + pending_context "form name" + pending_context "different field types" + + pending_context "form id for model-backed forms" +end diff --git a/spec/framework/web/forms_spec.rb b/spec/framework/web/forms_spec.rb index d167710..4160928 100644 --- a/spec/framework/web/forms_spec.rb +++ b/spec/framework/web/forms_spec.rb @@ -1,92 +1,73 @@ # frozen_string_literal: true -RSpec.describe Forms::Form, html: true do - pending_context "TODO: eliminate Forms::Form::ClassMethods. It's a buggy implementation because it's saving stuff at the class level" - - def build_form_class( - name: "", - action: "", - http_method: "get", - css_class: "", - **html_options, - &block - ) - Class.new do - include Phlex::HtmlRenderable - include Forms::Form - - http_method(http_method) - action(action) - css_class(css_class) - html_options(**html_options) - - class_eval(&block) if block_given? - end - end - - def build_base_form_class - Class.new do - include Phlex::HtmlRenderable - include Forms::Form - - class_eval(&block) if block_given? - end - end - +RSpec.describe Forms::Form do Given(:form_class) { - build_form_class + Class.new { + include Phlex::Renderable + include Forms::Form + } } - Given(:form) { form_class.new } - Given(:html_string) { form.call } - When(:result) { HTML(html_string) } + Given(:result) { + HTML( + form.call + ) + } describe "empty form" do + When(:form) { form_class.new } + Then { result.has_tag?("form") } end describe "basic attributes" do - shared_context "method" do |http_method_to_test| - Given(:form_class) { build_form_class(http_method: http_method) } - - context "when the method is #{http_method_to_test.upcase}" do - Given(:expected_method) { http_method_to_test.downcase } - Given(:expected_upcased_method) { expected_method.upcase } - - Given(:http_method) { http_method_to_test.downcase } - Given(:uppercase_http_method) { http_method_to_test.upcase } - - context "rendering" do - Then { - result.has_tag?( - "form", - with: { method: http_method_to_test.downcase } - ) - } - end + shared_context "method" do |tested_method| + When(:form) { form_class.new(method: method) } - context "in lowercase" do - Then { form.http_method?(expected_method) == true } - And { form.http_method?(expected_upcased_method) == true } + context "when the method is #{tested_method.upcase}" do + Given(:method) { tested_method } - ( - %w[get post delete patch put head options connect trace] - - [http_method_to_test.downcase] - ).each do |potential_method| - And { form.http_method?(potential_method) == false } - end + describe "rendering the form" do + Then { result.has_tag?("form", with: { method: tested_method }) } end - context "in uppercase" do - Given(:form_class) { build_form_class(http_method: uppercase_http_method) } + describe "asserting http methods" do + context "when the method is lowercase <#{tested_method}>" do + Then { form.http_method?(tested_method) == true } + And { form.http_method?(tested_method.upcase) == true } + + ( + Forms::Form.http_methods - + [tested_method] + ).each do |potential_method| + And { form.http_method?(potential_method) == false } + end + end + + context "when the method is uppercase <#{tested_method}>" do + Given(:method) { tested_method.upcase } + + Then { form.http_method?(tested_method) == true } + And { form.http_method?(tested_method.upcase) == true } - Then { form.http_method?(expected_method) == true } - And { form.http_method?(uppercase_http_method) == true } + ( + Forms::Form.http_methods - + [tested_method] + ).each do |potential_method| + And { form.http_method?(potential_method) == false } + end + end end end end describe "http methods" do + context "when the http method is not specified" do + When(:form) { form_class.new } + + Then { form.http_method?("post") == true } + end + include_context "method", "get" include_context "method", "post" include_context "method", "put" @@ -97,96 +78,11 @@ def build_base_form_class include_context "method", "connect" include_context "method", "trace" - context "default method" do - Given(:form_class) { build_base_form_class } - - Then { - result.has_tag?( - "form", - with: { method: "post" }, - ) - } - end - context "non-existent method" do - When(:result) { build_form_class(http_method: "other") } + When(:form) { form_class.new(method: "other") } - Then { result == Failure(Forms::Form::InvalidHttpMethod, Forms::Form::INVALID_HTTP_METHOD) } + Then { form == Failure(Forms::Form::InvalidHttpMethod, Forms::Form::INVALID_HTTP_METHOD) } end end end - - xdescribe "action" do - Given(:form_class) { build_form_class(action: action) } - - context "" do - let(:action) { "/my_action" } - - Then { - result.has_tag?( - "form", - with: { action: action } - ) - } - end - end - - context "form class" do - Given(:form_class) { build_form_class(css_class: css_class) } - - context "when passing a single class" do - let(:css_class) { "a_class" } - - Then { - result.has_tag?( - "form", - with: { class: css_class } - ) - } - end - - context "when passing multiple classes as a string" do - let(:css_class) { "a_class another_class" } - - Then { - result.has_tag?( - "form", - with: { class: css_class } - ) - } - end - - context "when passing multiple classes as an array" do - let(:css_class) { %w[a_class another_class] } - - Then { - result.has_tag?( - "form", - with: { class: css_class.join(" ") } - ) - } - end - end - - context "html options" do - Given(:form_class) { build_form_class(htmloption: an_option) } - - context "when passing a single class" do - let(:an_option) { "my-option" } - - Then { - result.has_tag?( - "form", - with: { htmloption: an_option } - ) - } - end - end - - pending_context "target" - - pending_context "form name" - pending_context "different field types" - - pending_context "form id for model-backed forms" end diff --git a/spec/support/html_specs.rb b/spec/support/html_specs.rb index 1e9c143..515a1e3 100644 --- a/spec/support/html_specs.rb +++ b/spec/support/html_specs.rb @@ -48,8 +48,3 @@ def node_has_attributes?(node, attributes) ].each do |node_type| node_type.include(HtmlMatchers) end - -RSpec.configure do |config| - config.before do - end -end diff --git a/spec/support/request_specs.rb b/spec/support/request_specs.rb index 3464772..11b075e 100644 --- a/spec/support/request_specs.rb +++ b/spec/support/request_specs.rb @@ -3,7 +3,6 @@ RSpec.configure do |config| config.around do |example| if %i[request web].include?(RSpec.current_example.metadata[:type]) - RSpec.current_example.metadata[:html] = true require "capybara/rspec" config.include Capybara::DSL diff --git a/web/forms/form.rb b/web/forms/form.rb index 6f45eeb..e5f3c87 100644 --- a/web/forms/form.rb +++ b/web/forms/form.rb @@ -1,231 +1,49 @@ # frozen_string_literal: true -# require "dry/inflector" - -# # TODO: implement methods and actions - module Forms module Form - def initialize - end + include Phlex::HtmlRenderable - def around_template(&) - form( - method: http_method, - class: css_class, - action: action, - **html_options, - ) {} + def initialize(method: "post") + @http_method = get_http_method(method) end - def http_method?(potential_method) - self.class.http_method?(potential_method) + def around_template + form(method: http_method) end - def self.included(including_class) - including_class.extend(ClassMethods) + def http_method?(potential_http_method) + http_method == potential_http_method.downcase end module ClassMethods - def action(an_action) - @action = an_action - end - - def css_class(a_css_class) - @css_class = a_css_class - end - - def http_method(an_html_method) - # # TODO: extract HttpMethod hierarchy - @http_method = http_methods.find( - proc { - raise InvalidHttpMethod, INVALID_HTTP_METHOD - } - ) { |http_method| - an_html_method.downcase == http_method - } - end - - def html_options(**some_html_options) - @html_options = some_html_options - end - def http_methods %w[get post delete patch put head options connect trace] end + end - def http_method?(potential_method) - @http_method == potential_method.downcase - end - - def http_method_option - @http_method || "post" - end - - def action_option - @action - end - - def css_class_option - @css_class - end - - def html_options_option - @html_options || {} - end + def self.included(including_class) + including_class.extend(ClassMethods) end + extend ClassMethods + private - def http_method - self.class.http_method_option - end + attr_reader :http_method - def action - self.class.action_option - end + def get_http_method(an_http_method) + downcased_http_method = an_http_method.downcase + raise InvalidHttpMethod, INVALID_HTTP_METHOD unless http_methods.include?(downcased_http_method) - def css_class - self.class.css_class_option + downcased_http_method end - def html_options - self.class.html_options_option + def http_methods + self.class.http_methods end InvalidHttpMethod = Class.new(RuntimeError) INVALID_HTTP_METHOD = "Invalid HTTP Method" end end - -# module Forms -# module Form -# include Phlex::HtmlRenderable - -# def self.included(including_class) -# super -# including_class.extend(ClassMethods) -# end - -# def around_template(&) -# form( -# id: form_id, -# class: css_class, -# **html_options, & -# ) -# end - -# module ClassMethods -# end - -# def initialize(**html_options) -# super - -# @css_class = build_class(html_options.delete(:class)) -# @html_options = default_html_options.merge(html_options) -# end - -# private - -# attr_reader :html_options -# attr_reader :css_class - -# def build_class(classes) -# default_css_class.concat( -# arrayfy_classes(classes) -# ) -# end - -# def default_css_class -# %w[] -# end - -# def arrayfy_classes(classes) -# Array(classes).join(" ").split(" ") -# end - -# def default_html_options -# {} -# end - -# def default_html_options -# { -# autocomplete: true, -# action: "/", -# method: "POST", -# } -# end - -# def form_id -# inflector = Dry::Inflector.new - -# full_class_path = inflector.underscore(self.class.to_s) -# form_subpath = full_class_path.gsub(%r{\Aforms/}, "") - -# inflector.dasherize(form_subpath + "-#{model_id}") -# end - -# def model_id -# id -# end - -# def text_field(...) -# render Forms::Fields::TextField.new(...) -# end - -# def hidden_field(...) -# render Forms::Fields::HiddenField.new(...) -# end - -# def password_field(...) -# render Forms::Fields::PasswordField.new(...) -# end - -# def number_field(...) -# render Forms::Fields::NumberField.new(...) -# end - -# def email_field(...) -# render Forms::Fields::EmailField.new(...) -# end - -# def search_field(...) -# render Forms::Fields::SearchField.new(...) -# end - -# def date_field(...) -# render Forms::Fields::DateField.new(...) -# end - -# def datetime_local_field(...) -# render Forms::Fields::DatetimeLocalField.new(...) -# end - -# def color_field(...) -# render Forms::Fields::ColorField.new(...) -# end - -# def select(...) -# render Forms::Fields::Select.new(...) -# end - -# def errors_for(field_name) -# raise NotImplementedError, "Implement 'errors_for'" -# end - -# # TODO: implement types -# # -# # -# # -# # -# # -# # -# # -# # -# # -# # -# # -# # -# # -# end -# end diff --git a/web/forms/form.txt b/web/forms/form.txt new file mode 100644 index 0000000..57e741e --- /dev/null +++ b/web/forms/form.txt @@ -0,0 +1,232 @@ +# frozen_string_literal: true + +# require "dry/inflector" + +# # TODO: implement methods and actions + +module Forms + module Form + def initialize(method: "post") + @http_method = method + end + + def around_template(&) + form( + method: http_method, + class: css_class, + action: action, + **html_options, + ) {} + end + + def http_method?(potential_method) + self.class.http_method?(potential_method) + end + + def self.included(including_class) + including_class.extend(ClassMethods) + end + + module ClassMethods + def action(an_action) + @action = an_action + end + + def css_class(a_css_class) + @css_class = a_css_class + end + + def http_method(an_html_method) + # # TODO: extract HttpMethod hierarchy + @http_method = http_methods.find( + proc { + raise InvalidHttpMethod, INVALID_HTTP_METHOD + } + ) { |http_method| + an_html_method.downcase == http_method + } + end + + def html_options(**some_html_options) + @html_options = some_html_options + end + + def http_methods + %w[get post delete patch put head options connect trace] + end + + def http_method?(potential_method) + @http_method == potential_method.downcase + end + + def http_method_option + @http_method || "post" + end + + def action_option + @action + end + + def css_class_option + @css_class + end + + def html_options_option + @html_options || {} + end + end + + private + + def http_method + @http_method + end + + def action + self.class.action_option + end + + def css_class + self.class.css_class_option + end + + def html_options + self.class.html_options_option + end + + InvalidHttpMethod = Class.new(RuntimeError) + INVALID_HTTP_METHOD = "Invalid HTTP Method" + end +end + +# module Forms +# module Form +# include Phlex::HtmlRenderable + +# def self.included(including_class) +# super +# including_class.extend(ClassMethods) +# end + +# def around_template(&) +# form( +# id: form_id, +# class: css_class, +# **html_options, & +# ) +# end + +# module ClassMethods +# end + +# def initialize(**html_options) +# super + +# @css_class = build_class(html_options.delete(:class)) +# @html_options = default_html_options.merge(html_options) +# end + +# private + +# attr_reader :html_options +# attr_reader :css_class + +# def build_class(classes) +# default_css_class.concat( +# arrayfy_classes(classes) +# ) +# end + +# def default_css_class +# %w[] +# end + +# def arrayfy_classes(classes) +# Array(classes).join(" ").split(" ") +# end + +# def default_html_options +# {} +# end + +# def default_html_options +# { +# autocomplete: true, +# action: "/", +# method: "POST", +# } +# end + +# def form_id +# inflector = Dry::Inflector.new + +# full_class_path = inflector.underscore(self.class.to_s) +# form_subpath = full_class_path.gsub(%r{\Aforms/}, "") + +# inflector.dasherize(form_subpath + "-#{model_id}") +# end + +# def model_id +# id +# end + +# def text_field(...) +# render Forms::Fields::TextField.new(...) +# end + +# def hidden_field(...) +# render Forms::Fields::HiddenField.new(...) +# end + +# def password_field(...) +# render Forms::Fields::PasswordField.new(...) +# end + +# def number_field(...) +# render Forms::Fields::NumberField.new(...) +# end + +# def email_field(...) +# render Forms::Fields::EmailField.new(...) +# end + +# def search_field(...) +# render Forms::Fields::SearchField.new(...) +# end + +# def date_field(...) +# render Forms::Fields::DateField.new(...) +# end + +# def datetime_local_field(...) +# render Forms::Fields::DatetimeLocalField.new(...) +# end + +# def color_field(...) +# render Forms::Fields::ColorField.new(...) +# end + +# def select(...) +# render Forms::Fields::Select.new(...) +# end + +# def errors_for(field_name) +# raise NotImplementedError, "Implement 'errors_for'" +# end + +# # TODO: implement types +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# end +# end From db5a711d16cc7f7315f19176d4accc32a243b45f Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Thu, 25 Jan 2024 10:20:25 -0300 Subject: [PATCH 19/33] Added html_options to Forms::Form --- spec/framework/web/forms_spec.rb | 29 +++++++++++++++++++++++++++++ spec/support/html_specs.rb | 6 +++++- web/forms/form.rb | 15 ++++++++------- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/spec/framework/web/forms_spec.rb b/spec/framework/web/forms_spec.rb index 4160928..516e744 100644 --- a/spec/framework/web/forms_spec.rb +++ b/spec/framework/web/forms_spec.rb @@ -84,5 +84,34 @@ Then { form == Failure(Forms::Form::InvalidHttpMethod, Forms::Form::INVALID_HTTP_METHOD) } end end + + describe "html options" do + context "when passing one html option" do + Given(:action) { "/my_action" } + + When(:form) { form_class.new(action: action) } + + Then { result.has_tag?("form", with: { action: action }) } + end + + context "when passing multiple html options" do + Given(:target) { "_blank" } + Given(:autocomplete) { :on } + + When(:form) { + form_class.new( + target: target, + autocomplete: autocomplete, + ) + } + + Then { + result.has_tag?("form", with: { + target: target, + autocomplete: autocomplete.to_s, + }) + } + end + end end end diff --git a/spec/support/html_specs.rb b/spec/support/html_specs.rb index 515a1e3..e7a3302 100644 --- a/spec/support/html_specs.rb +++ b/spec/support/html_specs.rb @@ -11,7 +11,7 @@ module HtmlMatchers def has_tag?(tagname, text: nil, with: {}) nodes = css(tagname) - node_exists = !nodes.empty? + return false unless node_exists?(nodes) nodes.any? { |node| node_has_text?(node, text) && @@ -40,6 +40,10 @@ def node_has_attributes?(node, attributes) node[attr.to_s] == value } end + + def node_exists?(nodes) + !nodes.empty? + end end [ diff --git a/web/forms/form.rb b/web/forms/form.rb index e5f3c87..15208fc 100644 --- a/web/forms/form.rb +++ b/web/forms/form.rb @@ -4,33 +4,34 @@ module Forms module Form include Phlex::HtmlRenderable - def initialize(method: "post") + def initialize(method: "post", **html_options) @http_method = get_http_method(method) + @html_options = html_options.transform_values(&:to_s) end def around_template - form(method: http_method) + form(method: http_method, **html_options) end def http_method?(potential_http_method) http_method == potential_http_method.downcase end + def self.included(including_class) + including_class.extend(ClassMethods) + end + module ClassMethods def http_methods %w[get post delete patch put head options connect trace] end end - - def self.included(including_class) - including_class.extend(ClassMethods) - end - extend ClassMethods private attr_reader :http_method + attr_reader :html_options def get_http_method(an_http_method) downcased_http_method = an_http_method.downcase From 72b4bad1ece6aad49817cd64dca2c3bf30a89c0e Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Thu, 25 Jan 2024 10:50:43 -0300 Subject: [PATCH 20/33] Added css classes to Forms::Form --- spec/framework/web/forms_spec.rb | 47 ++++++++++++++++++++++++++++++++ web/forms/form.rb | 26 ++++++++++++++---- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/spec/framework/web/forms_spec.rb b/spec/framework/web/forms_spec.rb index 516e744..1846cd4 100644 --- a/spec/framework/web/forms_spec.rb +++ b/spec/framework/web/forms_spec.rb @@ -113,5 +113,52 @@ } end end + + describe "form class" do + When(:form) { form_class.new(class: css_class) } + + context "when giving it a simple html class" do + Given(:css_class) { "a_class" } + + Then { result.has_tag?("form", with: { class: css_class }) } + end + + context "when giving it multiple html classes as a string" do + Given(:css_class) { "a_class other_class" } + + Then { result.has_tag?("form", with: { class: css_class }) } + end + + context "when giving it multiple html classes as an array of strings" do + Given(:css_class) { %w[a_class other_class] } + + Then { result.has_tag?("form", with: { class: css_class.join(" ") }) } + end + + describe "default css class" do + Given(:form_class) { + Class.new { + include Phlex::Renderable + include Forms::Form + + def default_classes + "my-default-class" + end + } + } + + context "when NOT specifying a class in the initializer" do + When(:form) { form_class.new } + + Then { result.has_tag?("form", with: { class: "my-default-class" }) } + end + + context "when specifying a class in the initializer" do + When(:form) { form_class.new(class: "my-class") } + + Then { result.has_tag?("form", with: { class: "my-default-class my-class" }) } + end + end + end end end diff --git a/web/forms/form.rb b/web/forms/form.rb index 15208fc..299d398 100644 --- a/web/forms/form.rb +++ b/web/forms/form.rb @@ -5,12 +5,17 @@ module Form include Phlex::HtmlRenderable def initialize(method: "post", **html_options) - @http_method = get_http_method(method) + @http_method = build_http_method(method) + @css_class = build_css_class(html_options.delete(:class)) @html_options = html_options.transform_values(&:to_s) end def around_template - form(method: http_method, **html_options) + form( + method: http_method, + class: css_class, + **html_options + ) end def http_method?(potential_http_method) @@ -32,18 +37,29 @@ def http_methods attr_reader :http_method attr_reader :html_options + attr_reader :css_class - def get_http_method(an_http_method) + def build_http_method(an_http_method) downcased_http_method = an_http_method.downcase raise InvalidHttpMethod, INVALID_HTTP_METHOD unless http_methods.include?(downcased_http_method) downcased_http_method end - def http_methods - self.class.http_methods + def build_css_class(some_css_classes) + [].concat(arrayify_css_classes(some_css_classes)) end + def arrayify_css_classes(some_css_classes) + ( + Array(default_classes) + + Array(some_css_classes) + ).join(" ").split(" ") + end + + def http_methods = self.class.http_methods + def default_classes = [] + InvalidHttpMethod = Class.new(RuntimeError) INVALID_HTTP_METHOD = "Invalid HTTP Method" end From fcbcbf475b7628b0f5a2993f771570188f6631cc Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Thu, 25 Jan 2024 13:11:03 -0300 Subject: [PATCH 21/33] Added name attribute to Forms::Form --- spec/framework/web/forms_spec.rb | 12 ++++++++++++ web/forms/form.rb | 8 +++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/spec/framework/web/forms_spec.rb b/spec/framework/web/forms_spec.rb index 1846cd4..63990ba 100644 --- a/spec/framework/web/forms_spec.rb +++ b/spec/framework/web/forms_spec.rb @@ -161,4 +161,16 @@ def default_classes end end end + + describe "form name" do + Given(:form) { form_class.new(name: "my-form") } + + Then { form.named?("my-form") == true } + And { form.named?("not-my-form") == false } + Then { result.has_tag?("form", with: { name: "my-form" }) == false } + end + + pending_context "different field types" + + pending_context "form id for model-backed forms" end diff --git a/web/forms/form.rb b/web/forms/form.rb index 299d398..36054b0 100644 --- a/web/forms/form.rb +++ b/web/forms/form.rb @@ -4,9 +4,10 @@ module Forms module Form include Phlex::HtmlRenderable - def initialize(method: "post", **html_options) + def initialize(name: "", method: "post", **html_options) @http_method = build_http_method(method) @css_class = build_css_class(html_options.delete(:class)) + @name = name @html_options = html_options.transform_values(&:to_s) end @@ -26,6 +27,10 @@ def self.included(including_class) including_class.extend(ClassMethods) end + def named?(potential_name) + name == potential_name + end + module ClassMethods def http_methods %w[get post delete patch put head options connect trace] @@ -38,6 +43,7 @@ def http_methods attr_reader :http_method attr_reader :html_options attr_reader :css_class + attr_reader :name def build_http_method(an_http_method) downcased_http_method = an_http_method.downcase From b1e72496ffed1c19f8650e8b9ef8ed9a937c83ee Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Thu, 25 Jan 2024 13:11:24 -0300 Subject: [PATCH 22/33] Removed old spec file --- spec/framework/web/forms_spec.old.rb | 192 --------------------------- 1 file changed, 192 deletions(-) delete mode 100644 spec/framework/web/forms_spec.old.rb diff --git a/spec/framework/web/forms_spec.old.rb b/spec/framework/web/forms_spec.old.rb deleted file mode 100644 index d167710..0000000 --- a/spec/framework/web/forms_spec.old.rb +++ /dev/null @@ -1,192 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Forms::Form, html: true do - pending_context "TODO: eliminate Forms::Form::ClassMethods. It's a buggy implementation because it's saving stuff at the class level" - - def build_form_class( - name: "", - action: "", - http_method: "get", - css_class: "", - **html_options, - &block - ) - Class.new do - include Phlex::HtmlRenderable - include Forms::Form - - http_method(http_method) - action(action) - css_class(css_class) - html_options(**html_options) - - class_eval(&block) if block_given? - end - end - - def build_base_form_class - Class.new do - include Phlex::HtmlRenderable - include Forms::Form - - class_eval(&block) if block_given? - end - end - - Given(:form_class) { - build_form_class - } - Given(:form) { form_class.new } - Given(:html_string) { form.call } - - When(:result) { HTML(html_string) } - - describe "empty form" do - Then { result.has_tag?("form") } - end - - describe "basic attributes" do - shared_context "method" do |http_method_to_test| - Given(:form_class) { build_form_class(http_method: http_method) } - - context "when the method is #{http_method_to_test.upcase}" do - Given(:expected_method) { http_method_to_test.downcase } - Given(:expected_upcased_method) { expected_method.upcase } - - Given(:http_method) { http_method_to_test.downcase } - Given(:uppercase_http_method) { http_method_to_test.upcase } - - context "rendering" do - Then { - result.has_tag?( - "form", - with: { method: http_method_to_test.downcase } - ) - } - end - - context "in lowercase" do - Then { form.http_method?(expected_method) == true } - And { form.http_method?(expected_upcased_method) == true } - - ( - %w[get post delete patch put head options connect trace] - - [http_method_to_test.downcase] - ).each do |potential_method| - And { form.http_method?(potential_method) == false } - end - end - - context "in uppercase" do - Given(:form_class) { build_form_class(http_method: uppercase_http_method) } - - Then { form.http_method?(expected_method) == true } - And { form.http_method?(uppercase_http_method) == true } - end - end - end - - describe "http methods" do - include_context "method", "get" - include_context "method", "post" - include_context "method", "put" - include_context "method", "head" - include_context "method", "delete" - include_context "method", "patch" - include_context "method", "options" - include_context "method", "connect" - include_context "method", "trace" - - context "default method" do - Given(:form_class) { build_base_form_class } - - Then { - result.has_tag?( - "form", - with: { method: "post" }, - ) - } - end - - context "non-existent method" do - When(:result) { build_form_class(http_method: "other") } - - Then { result == Failure(Forms::Form::InvalidHttpMethod, Forms::Form::INVALID_HTTP_METHOD) } - end - end - end - - xdescribe "action" do - Given(:form_class) { build_form_class(action: action) } - - context "" do - let(:action) { "/my_action" } - - Then { - result.has_tag?( - "form", - with: { action: action } - ) - } - end - end - - context "form class" do - Given(:form_class) { build_form_class(css_class: css_class) } - - context "when passing a single class" do - let(:css_class) { "a_class" } - - Then { - result.has_tag?( - "form", - with: { class: css_class } - ) - } - end - - context "when passing multiple classes as a string" do - let(:css_class) { "a_class another_class" } - - Then { - result.has_tag?( - "form", - with: { class: css_class } - ) - } - end - - context "when passing multiple classes as an array" do - let(:css_class) { %w[a_class another_class] } - - Then { - result.has_tag?( - "form", - with: { class: css_class.join(" ") } - ) - } - end - end - - context "html options" do - Given(:form_class) { build_form_class(htmloption: an_option) } - - context "when passing a single class" do - let(:an_option) { "my-option" } - - Then { - result.has_tag?( - "form", - with: { htmloption: an_option } - ) - } - end - end - - pending_context "target" - - pending_context "form name" - pending_context "different field types" - - pending_context "form id for model-backed forms" -end From 5bc6326c7fcbdd27f2f2301c510f2f9669a8c377 Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Thu, 25 Jan 2024 19:17:43 -0300 Subject: [PATCH 23/33] Added classes to fields --- spec/framework/web/form_fields_spec.rb | 107 +++++++++++++++++++++++++ web/forms/fields/field.rb | 59 +++++++++----- web/forms/form.rb | 4 +- 3 files changed, 147 insertions(+), 23 deletions(-) create mode 100644 spec/framework/web/form_fields_spec.rb diff --git a/spec/framework/web/form_fields_spec.rb b/spec/framework/web/form_fields_spec.rb new file mode 100644 index 0000000..d31acd1 --- /dev/null +++ b/spec/framework/web/form_fields_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +RSpec.describe Forms::Fields do + Given(:html) { field.call } + Given(:result) { HTML(html) } + + describe Forms::Fields::Field do + describe "field with name" do + When(:field) { described_class.new(:field_name) } + + Then { result.has_tag?("input", with: { name: "field_name" }) } + end + + describe "value attribute" do + When(:field) { described_class.new(:field_name, value: "field value") } + + Then { + result.has_tag?("input", + with: { name: "field_name", value: "field value" }) + } + end + + describe "css class attribute" do + Given(:default_css_classes) { + %w[p-1 text-gray-800 placeholder:text-gray-400 border rounded] + } + + When(:field) { described_class.new(:field_name, class: css_class) } + + context "when giving it a simple html class" do + Given(:css_class) { "a_class" } + + Then { + result.has_tag?( + "input", + with: { + class: (default_css_classes + [css_class]).join(" "), + } + ) + } + end + + context "when giving it multiple html classes as a string" do + Given(:css_class) { "a_class another_class" } + + Then { + result.has_tag?( + "input", + with: { + class: (default_css_classes + [css_class]).join(" "), + } + ) + } + end + + context "when giving it multiple html classes as an array of strings" do + Given(:css_class) { %w[a_class another_class] } + + Then { + result.has_tag?( + "input", + with: { + class: (default_css_classes + css_class).join(" "), + } + ) + } + end + + describe "default css class" do + When(:field) { described_class.new(:field_name) } + + Then { + result.has_tag?( + "input", + with: { + class: default_css_classes.join(" "), + } + ) + } + end + end + end + + pending_describe "text_field" + pending_describe "hidden_field" + pending_describe "password_field" + pending_describe "number_field" + pending_describe "email_field" + pending_describe "search_field" + pending_describe "date_field" + pending_describe "datetime_local_field" + pending_describe "color_field" + pending_describe "select" + pending_describe "button" + pending_describe "checkbox" + pending_describe "file" + pending_describe "image" + pending_describe "month" + pending_describe "radio" + pending_describe "range" + pending_describe "reset" + pending_describe "submit" + pending_describe "tel" + pending_describe "time" + pending_describe "url" + pending_describe "week" +end diff --git a/web/forms/fields/field.rb b/web/forms/fields/field.rb index 5769079..2018a64 100644 --- a/web/forms/fields/field.rb +++ b/web/forms/fields/field.rb @@ -5,47 +5,64 @@ module Fields class Field include Phlex::HtmlRenderable - def initialize(name, **html_options) + def initialize(name, value: nil, **html_options) @name = name - @css_class = build_class(html_options.delete(:class)) - @value = html_options.delete(:value) - @html_options = default_html_options.merge(html_options) + @value = value + @css_classes = Array(html_options.delete(:class)) end def template input( - value: value, - type: type, name: name, - class: css_class, - **html_options + value: value, + class: css_classes, ) end + # def initialize(name, **html_options) + # @name = name + # @css_class = build_class(html_options.delete(:class)) + # @value = html_options.delete(:value) + # @html_options = default_html_options.merge(html_options) + # end + + # def template + # input( + # value: value, + # type: type, + # name: name, + # class: css_class, + # **html_options + # ) + # end private attr_reader :name attr_reader :value - attr_reader :html_options - attr_reader :css_class - def build_class(classes) - default_css_class.concat( - arrayfy_classes(classes) - ) + # attr_reader :html_options + + def css_classes + default_css_classes + @css_classes end - def default_css_class + # def build_class(classes) + # default_css_class.concat( + # arrayfy_classes(classes) + # ) + # end + + def default_css_classes %w[p-1 text-gray-800 placeholder:text-gray-400 border rounded] end - def arrayfy_classes(classes) - Array(classes).join(" ").split(" ") - end + # def arrayfy_classes(classes) + # Array(classes).join(" ").split(" ") + # end - def default_html_options - {} - end + # def default_html_options + # {} + # end end end end diff --git a/web/forms/form.rb b/web/forms/form.rb index 36054b0..26538ac 100644 --- a/web/forms/form.rb +++ b/web/forms/form.rb @@ -30,7 +30,7 @@ def self.included(including_class) def named?(potential_name) name == potential_name end - + module ClassMethods def http_methods %w[get post delete patch put head options connect trace] @@ -60,7 +60,7 @@ def arrayify_css_classes(some_css_classes) ( Array(default_classes) + Array(some_css_classes) - ).join(" ").split(" ") + ).join(" ").split end def http_methods = self.class.http_methods From f01ec38236ec5a33de1c575b46316392986797d4 Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Thu, 25 Jan 2024 19:37:04 -0300 Subject: [PATCH 24/33] Added html_options to fields --- spec/framework/web/form_fields_spec.rb | 25 +++++++++++++++++++++++++ web/forms/fields/field.rb | 5 +++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/spec/framework/web/form_fields_spec.rb b/spec/framework/web/form_fields_spec.rb index d31acd1..14ccb88 100644 --- a/spec/framework/web/form_fields_spec.rb +++ b/spec/framework/web/form_fields_spec.rb @@ -79,8 +79,33 @@ } end end + + describe "html options" do + context "adding one html option" do + When(:field) { described_class.new(:field_name, placeholder: "a placeholder") } + + Then { result.has_tag?("input", with: { placeholder: "a placeholder" }) } + end + + context "adding multiple html options" do + When(:field) { described_class.new(:field_name, placeholder: "a placeholder", size: "10") } + + Then { result.has_tag?("input", with: { placeholder: "a placeholder", size: "10" }) } + end + + context "numeric html option" do + When(:field) { described_class.new(:field_name, size: 20) } + + Then { result.has_tag?("input", with: { size: "20" }) } + end + end end + # describe "text-ish fields" do + # describe Forms::Fields::TextField do + # end + # end + pending_describe "text_field" pending_describe "hidden_field" pending_describe "password_field" diff --git a/web/forms/fields/field.rb b/web/forms/fields/field.rb index 2018a64..5134a82 100644 --- a/web/forms/fields/field.rb +++ b/web/forms/fields/field.rb @@ -9,6 +9,7 @@ def initialize(name, value: nil, **html_options) @name = name @value = value @css_classes = Array(html_options.delete(:class)) + @html_options = html_options end def template @@ -16,6 +17,7 @@ def template name: name, value: value, class: css_classes, + **html_options ) end # def initialize(name, **html_options) @@ -39,8 +41,7 @@ def template attr_reader :name attr_reader :value - - # attr_reader :html_options + attr_reader :html_options def css_classes default_css_classes + @css_classes From 6f706de02385e9319cf9fe05f3f50b756d501b71 Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Thu, 25 Jan 2024 19:39:13 -0300 Subject: [PATCH 25/33] Refactored field --- web/forms/fields/field.rb | 34 ++++------------------------------ 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/web/forms/fields/field.rb b/web/forms/fields/field.rb index 5134a82..c51746b 100644 --- a/web/forms/fields/field.rb +++ b/web/forms/fields/field.rb @@ -8,7 +8,7 @@ class Field def initialize(name, value: nil, **html_options) @name = name @value = value - @css_classes = Array(html_options.delete(:class)) + @css_classes = build_css_classes(html_options.delete(:class)) @html_options = html_options end @@ -20,22 +20,6 @@ def template **html_options ) end - # def initialize(name, **html_options) - # @name = name - # @css_class = build_class(html_options.delete(:class)) - # @value = html_options.delete(:value) - # @html_options = default_html_options.merge(html_options) - # end - - # def template - # input( - # value: value, - # type: type, - # name: name, - # class: css_class, - # **html_options - # ) - # end private @@ -47,23 +31,13 @@ def css_classes default_css_classes + @css_classes end - # def build_class(classes) - # default_css_class.concat( - # arrayfy_classes(classes) - # ) - # end + def build_css_classes(classes) + Array(classes) + end def default_css_classes %w[p-1 text-gray-800 placeholder:text-gray-400 border rounded] end - - # def arrayfy_classes(classes) - # Array(classes).join(" ").split(" ") - # end - - # def default_html_options - # {} - # end end end end From 26398dcc393d940c6d0dd4fde755086bee171f86 Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Thu, 25 Jan 2024 19:55:34 -0300 Subject: [PATCH 26/33] Added some field types --- spec/framework/web/form_fields_spec.rb | 35 ++++++++++++++---------- web/forms/fields/datetime_local_field.rb | 2 +- web/forms/fields/field.rb | 5 +++- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/spec/framework/web/form_fields_spec.rb b/spec/framework/web/form_fields_spec.rb index 14ccb88..1abbd27 100644 --- a/spec/framework/web/form_fields_spec.rb +++ b/spec/framework/web/form_fields_spec.rb @@ -101,20 +101,27 @@ end end - # describe "text-ish fields" do - # describe Forms::Fields::TextField do - # end - # end - - pending_describe "text_field" - pending_describe "hidden_field" - pending_describe "password_field" - pending_describe "number_field" - pending_describe "email_field" - pending_describe "search_field" - pending_describe "date_field" - pending_describe "datetime_local_field" - pending_describe "color_field" + describe "field types" do + shared_context "field type" do |type, type_text| + describe type do + When(:field) { described_class.new(:my_field_name) } + + Then { result.has_tag?("input", with: { type: type_text }) } + end + end + + include_context "field type", Forms::Fields::TextField, "text" + include_context "field type", Forms::Fields::HiddenField, "hidden" + include_context "field type", Forms::Fields::HiddenField, "hidden" + include_context "field type", Forms::Fields::PasswordField, "password" + include_context "field type", Forms::Fields::NumberField, "number" + include_context "field type", Forms::Fields::EmailField, "email" + include_context "field type", Forms::Fields::SearchField, "search" + include_context "field type", Forms::Fields::DateField, "date" + include_context "field type", Forms::Fields::DatetimeLocalField, "datetime-local" + include_context "field type", Forms::Fields::ColorField, "color" + end + pending_describe "select" pending_describe "button" pending_describe "checkbox" diff --git a/web/forms/fields/datetime_local_field.rb b/web/forms/fields/datetime_local_field.rb index 1035976..f038a3f 100644 --- a/web/forms/fields/datetime_local_field.rb +++ b/web/forms/fields/datetime_local_field.rb @@ -8,7 +8,7 @@ class DatetimeLocalField < Field private def type - "date" + "datetime-local" end end end diff --git a/web/forms/fields/field.rb b/web/forms/fields/field.rb index c51746b..1faee71 100644 --- a/web/forms/fields/field.rb +++ b/web/forms/fields/field.rb @@ -5,9 +5,10 @@ module Fields class Field include Phlex::HtmlRenderable - def initialize(name, value: nil, **html_options) + def initialize(name, value: nil, type: nil, **html_options) @name = name @value = value + @type = type @css_classes = build_css_classes(html_options.delete(:class)) @html_options = html_options end @@ -16,6 +17,7 @@ def template input( name: name, value: value, + type: type, class: css_classes, **html_options ) @@ -26,6 +28,7 @@ def template attr_reader :name attr_reader :value attr_reader :html_options + attr_reader :type def css_classes default_css_classes + @css_classes From f4b6069289e050c1a0adcc6ec566f5b6433803c4 Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Fri, 26 Jan 2024 19:44:55 -0300 Subject: [PATCH 27/33] Forms can have content --- spec/framework/web/forms_spec.rb | 16 ++++++++++++++++ web/forms/form.rb | 6 ++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/spec/framework/web/forms_spec.rb b/spec/framework/web/forms_spec.rb index 63990ba..c1b0457 100644 --- a/spec/framework/web/forms_spec.rb +++ b/spec/framework/web/forms_spec.rb @@ -20,6 +20,22 @@ Then { result.has_tag?("form") } end + describe "form with content" do + When(:form_class) { + Class.new { + include Phlex::Renderable + include Forms::Form + + def template + h1 { "form content" } + end + } + } + When(:form) { form_class.new } + + Then { result.has_tag?("form > h1", text: "form content") } + end + describe "basic attributes" do shared_context "method" do |tested_method| When(:form) { form_class.new(method: method) } diff --git a/web/forms/form.rb b/web/forms/form.rb index 26538ac..c2a58db 100644 --- a/web/forms/form.rb +++ b/web/forms/form.rb @@ -11,14 +11,16 @@ def initialize(name: "", method: "post", **html_options) @html_options = html_options.transform_values(&:to_s) end - def around_template + def around_template(&) form( method: http_method, class: css_class, - **html_options + **html_options, & ) end + def template; end + def http_method?(potential_http_method) http_method == potential_http_method.downcase end From aab5cd1732c8e751ce4b80a63f94ad828d9718d9 Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Fri, 26 Jan 2024 20:03:31 -0300 Subject: [PATCH 28/33] Added Forms::Fields::Label --- spec/framework/web/form_fields_spec.rb | 108 +++++++++++++++++++++++++ web/forms/fields/label.rb | 42 ++++++++++ web/forms/form.rb | 2 +- 3 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 web/forms/fields/label.rb diff --git a/spec/framework/web/form_fields_spec.rb b/spec/framework/web/form_fields_spec.rb index 1abbd27..1c2b327 100644 --- a/spec/framework/web/form_fields_spec.rb +++ b/spec/framework/web/form_fields_spec.rb @@ -136,4 +136,112 @@ pending_describe "time" pending_describe "url" pending_describe "week" + + describe Forms::Fields::Label do + describe "empty label" do + When(:field) { described_class.new(:my_field_is_for) } + + Then { + result.has_tag?( + "label", + with: { + for: "my_field_is_for", + }, + text: "my_field_is_for", + ) + } + end + + describe "value" do + When(:field) { described_class.new(:my_field_is_for, text: "Label text") } + + Then { + result.has_tag?( + "label", + with: { + for: "my_field_is_for", + }, + text: "Label text", + ) + } + end + + describe "css class attribute" do + Given(:default_css_classes) { [] } + + When(:field) { described_class.new(:field_name, class: css_class) } + + context "when giving it a simple html class" do + Given(:css_class) { "a_class" } + + Then { + result.has_tag?( + "label", + with: { + class: (default_css_classes + [css_class]).join(" "), + } + ) + } + end + + context "when giving it multiple html classes as a string" do + Given(:css_class) { "a_class another_class" } + + Then { + result.has_tag?( + "label", + with: { + class: (default_css_classes + [css_class]).join(" "), + } + ) + } + end + + context "when giving it multiple html classes as an array of strings" do + Given(:css_class) { %w[a_class another_class] } + + Then { + result.has_tag?( + "label", + with: { + class: (default_css_classes + css_class).join(" "), + } + ) + } + end + + describe "default css class" do + When(:field) { described_class.new(:field_name) } + + Then { + result.has_tag?( + "label", + with: { + class: default_css_classes.join(" "), + } + ) + } + end + end + + describe "html options" do + context "adding one html option" do + When(:field) { described_class.new(:field_name, placeholder: "a placeholder") } + + Then { result.has_tag?("label", with: { placeholder: "a placeholder" }) } + end + + context "adding multiple html options" do + When(:field) { described_class.new(:field_name, placeholder: "a placeholder", size: "10") } + + Then { result.has_tag?("label", with: { placeholder: "a placeholder", size: "10" }) } + end + + context "numeric html option" do + When(:field) { described_class.new(:field_name, size: 20) } + + Then { result.has_tag?("label", with: { size: "20" }) } + end + end + end end diff --git a/web/forms/fields/label.rb b/web/forms/fields/label.rb new file mode 100644 index 0000000..552c5b1 --- /dev/null +++ b/web/forms/fields/label.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Forms + module Fields + class Label + include Phlex::HtmlRenderable + + def initialize(for_field, text: for_field, **html_options) + @for_field = for_field + @text = text + @css_classes = build_css_classes(html_options.delete(:class)) + @html_options = html_options + end + + def template + label( + for: for_field, + class: css_classes, + **html_options + ) { text } + end + + private + + attr_reader :for_field + attr_reader :text + attr_reader :html_options + + def css_classes + default_css_classes + @css_classes + end + + def build_css_classes(classes) + Array(classes) + end + + def default_css_classes + %w[] + end + end + end +end diff --git a/web/forms/form.rb b/web/forms/form.rb index c2a58db..b509a4f 100644 --- a/web/forms/form.rb +++ b/web/forms/form.rb @@ -19,7 +19,7 @@ def around_template(&) ) end - def template; end + def template(*); end def http_method?(potential_http_method) http_method == potential_http_method.downcase From b0698737e93f4ad524cdf779597eedb51dc08ae8 Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Fri, 26 Jan 2024 20:50:15 -0300 Subject: [PATCH 29/33] Added methods for adding fields to a form --- web/forms/form.rb | 52 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/web/forms/form.rb b/web/forms/form.rb index b509a4f..5cdb74c 100644 --- a/web/forms/form.rb +++ b/web/forms/form.rb @@ -68,6 +68,58 @@ def arrayify_css_classes(some_css_classes) def http_methods = self.class.http_methods def default_classes = [] + def color_field(...) + render Forms::Fields::ColorField.new(...) + end + + def date_field(...) + render Forms::Fields::DateField.new(...) + end + + def datetime_local_field(...) + render Forms::Fields::DatetimeLocalField.new(...) + end + + def email_field(...) + render Forms::Fields::EmailField.new(...) + end + + def field(...) + render Forms::Fields.field.new(...) + end + + def hidden_field(...) + render Forms::Fields::HiddenField.new(...) + end + + def label(...) + render Forms::Fields::Label.new(...) + end + + def number_field(...) + render Forms::Fields::NumberField.new(...) + end + + def password_field(...) + render Forms::Fields::PasswordField.new(...) + end + + def search_field(...) + render Forms::Fields::SearchField.new(...) + end + + def select(...) + render Forms::Fields::Select.new(...) + end + + def text_field(...) + render Forms::Fields::TextField.new(...) + end + + def field_wrapper(&) + div(&) + end + InvalidHttpMethod = Class.new(RuntimeError) INVALID_HTTP_METHOD = "Invalid HTTP Method" end From 260e63526680bf398d73ea73a96bfe7e94777574 Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Fri, 26 Jan 2024 21:03:08 -0300 Subject: [PATCH 30/33] Rendering ExampleForm on HomePage --- assets/css/index.css | 73 ++++++++++++++++++++++++++++++++ web/forms/example_form.rb | 86 ++++++++++++++++++++++++++++++++++++++ web/forms/fields/select.rb | 2 +- web/views/home_page.rb | 2 + 4 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 web/forms/example_form.rb diff --git a/assets/css/index.css b/assets/css/index.css index f9cc6e9..7814ddc 100644 --- a/assets/css/index.css +++ b/assets/css/index.css @@ -544,6 +544,10 @@ video { --tw-backdrop-sepia: ; } +.m-2 { + margin: 0.5rem; +} + .mx-3 { margin-left: 0.75rem; margin-right: 0.75rem; @@ -562,31 +566,81 @@ video { display: flex; } +.grid { + display: grid; +} + .hidden { display: none; } +.items-center { + align-items: center; +} + .gap-2 { gap: 0.5rem; } +.gap-1 { + gap: 0.25rem; +} + .rounded { border-radius: 0.25rem; } +.rounded-lg { + border-radius: 0.5rem; +} + .border { border-width: 1px; } +.border-2 { + border-width: 2px; +} + .border-b { border-bottom-width: 1px; } +.border-slate-300 { + --tw-border-opacity: 1; + border-color: rgb(203 213 225 / var(--tw-border-opacity)); +} + +.border-slate-800 { + --tw-border-opacity: 1; + border-color: rgb(30 41 59 / var(--tw-border-opacity)); +} + .bg-slate-900 { --tw-bg-opacity: 1; background-color: rgb(15 23 42 / var(--tw-bg-opacity)); } +.bg-slate-50 { + --tw-bg-opacity: 1; + background-color: rgb(248 250 252 / var(--tw-bg-opacity)); +} + +.bg-blue-400 { + --tw-bg-opacity: 1; + background-color: rgb(96 165 250 / var(--tw-bg-opacity)); +} + +.bg-blue-300 { + --tw-bg-opacity: 1; + background-color: rgb(147 197 253 / var(--tw-bg-opacity)); +} + +.bg-slate-800 { + --tw-bg-opacity: 1; + background-color: rgb(30 41 59 / var(--tw-bg-opacity)); +} + .p-1 { padding: 0.25rem; } @@ -595,6 +649,20 @@ video { padding: 0.5rem; } +.p-4 { + padding: 1rem; +} + +.px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +.py-1 { + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + .text-xl { font-size: 1.25rem; line-height: 1.75rem; @@ -640,6 +708,11 @@ video { color: rgb(156 163 175 / var(--tw-text-opacity)); } +.hover\:bg-slate-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(51 65 85 / var(--tw-bg-opacity)); +} + .hover\:shadow-sm:hover { --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); diff --git a/web/forms/example_form.rb b/web/forms/example_form.rb new file mode 100644 index 0000000..5b27ab1 --- /dev/null +++ b/web/forms/example_form.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +module Forms + class ExampleForm + include Phlex::HtmlRenderable + include Forms::Form + + def template + h1(class: "text-slate-800 font-semibold text-xl") { "HTML Form Example" } + + field_wrapper do + label(:username) + text_field(:username, value: "snth") + end + + field_wrapper do + label(:color_field) + color_field(:color_field) + end + + field_wrapper do + label(:date_field) + date_field(:date_field) + end + + field_wrapper do + label(:datetime_local_field) + datetime_local_field(:datetime_local_field) + end + + field_wrapper do + label(:email_field) + email_field(:email_field) + end + + field_wrapper do + label(:hidden_field) + hidden_field(:hidden_field) + end + + field_wrapper do + label(:label) + label(:label) + end + + field_wrapper do + label(:number_field) + number_field(:number_field) + end + + field_wrapper do + label(:password_field) + password_field(:password_field) + end + + field_wrapper do + label(:search_field) + search_field(:search_field) + end + + field_wrapper do + label(:select) + select(:select, options: [ + %w[text value] + ]) + end + + field_wrapper do + label(:text_field) + text_field(:text_field) + end + + input(type: "submit", class: "py-1 px-2 text-slate-50 bg-slate-800 border border-1 border-slate-800 hover:bg-slate-700 rounded") { "send" } + end + + private + + def field_wrapper(&) + div(class: "flex items-center gap-1", &) + end + + def default_classes + %w[p-4 m-2 bg-slate-50 border border-2 border-slate-300 rounded-lg] + end + end +end diff --git a/web/forms/fields/select.rb b/web/forms/fields/select.rb index 4b1b517..5cd5013 100644 --- a/web/forms/fields/select.rb +++ b/web/forms/fields/select.rb @@ -18,7 +18,7 @@ def initialize(name, options: [], **html_options) end def template - select(value: value, name: name, class: css_class, **html_options) do + select(value: value, name: name, class: css_classes, **html_options) do options.each do |option_text, option_value| option(value: option_value) { option_text } end diff --git a/web/views/home_page.rb b/web/views/home_page.rb index 7b344eb..fe1d9ef 100644 --- a/web/views/home_page.rb +++ b/web/views/home_page.rb @@ -11,6 +11,8 @@ def template p(class: "mx-3 text-slate-600") { "On file #{__FILE__}" } p(class: "mx-3 text-slate-600") { "Current time #{Time.now}" } + + render Forms::ExampleForm.new end end end From 137799faa13410dca9436f90425bd42e68045499 Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Sat, 3 Feb 2024 13:56:38 -0300 Subject: [PATCH 31/33] Added pending spec --- spec/framework/web/forms_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/framework/web/forms_spec.rb b/spec/framework/web/forms_spec.rb index c1b0457..a2ad6e3 100644 --- a/spec/framework/web/forms_spec.rb +++ b/spec/framework/web/forms_spec.rb @@ -189,4 +189,6 @@ def default_classes pending_context "different field types" pending_context "form id for model-backed forms" + + pending_context "default action for form should be \"?\"" end From e26094ad00a354168cf38a1f01b805bb74aec07a Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Sat, 10 Feb 2024 11:03:07 -0300 Subject: [PATCH 32/33] Odded errors for form fields --- assets/css/index.css | 30 +++------ spec/framework/web/form_errors_spec.rb | 89 ++++++++++++++++++++++++++ spec/framework/web/form_fields_spec.rb | 6 +- spec/support/html_specs.rb | 4 ++ web/forms/errors.rb | 44 +++++++++++++ web/forms/form.rb | 21 +++++- 6 files changed, 168 insertions(+), 26 deletions(-) create mode 100644 spec/framework/web/form_errors_spec.rb create mode 100644 web/forms/errors.rb diff --git a/assets/css/index.css b/assets/css/index.css index 7814ddc..6c90be6 100644 --- a/assets/css/index.css +++ b/assets/css/index.css @@ -566,10 +566,6 @@ video { display: flex; } -.grid { - display: grid; -} - .hidden { display: none; } @@ -578,14 +574,14 @@ video { align-items: center; } -.gap-2 { - gap: 0.5rem; -} - .gap-1 { gap: 0.25rem; } +.gap-2 { + gap: 0.5rem; +} + .rounded { border-radius: 0.25rem; } @@ -616,29 +612,19 @@ video { border-color: rgb(30 41 59 / var(--tw-border-opacity)); } -.bg-slate-900 { - --tw-bg-opacity: 1; - background-color: rgb(15 23 42 / var(--tw-bg-opacity)); -} - .bg-slate-50 { --tw-bg-opacity: 1; background-color: rgb(248 250 252 / var(--tw-bg-opacity)); } -.bg-blue-400 { - --tw-bg-opacity: 1; - background-color: rgb(96 165 250 / var(--tw-bg-opacity)); -} - -.bg-blue-300 { +.bg-slate-800 { --tw-bg-opacity: 1; - background-color: rgb(147 197 253 / var(--tw-bg-opacity)); + background-color: rgb(30 41 59 / var(--tw-bg-opacity)); } -.bg-slate-800 { +.bg-slate-900 { --tw-bg-opacity: 1; - background-color: rgb(30 41 59 / var(--tw-bg-opacity)); + background-color: rgb(15 23 42 / var(--tw-bg-opacity)); } .p-1 { diff --git a/spec/framework/web/form_errors_spec.rb b/spec/framework/web/form_errors_spec.rb new file mode 100644 index 0000000..9c8e7bc --- /dev/null +++ b/spec/framework/web/form_errors_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +RSpec.describe Forms::Errors do + Given(:html) { form.call } + Given(:result) { HTML(html) } + + Given(:form_class) { + Class.new { + include Phlex::Renderable + include Forms::Form + + def template + errors_for(:my_field) + end + } + } + + When(:form) { form_class.new(errors: { my_field: field_errors }) } + When(:form_tag) { result.find_tag(:form) } + + context " when there are NO errors for field" do + context "when errors for the field is empty" do + Given(:field_errors) { [] } + + Then { form_tag.empty_element? } + end + + context "when errors for the field is nil" do + Given(:field_errors) { nil } + + Then { form_tag.empty_element? } + end + + context "when there are no errors" do + Given(:form) { form_class.new(errors: {}) } + + Then { form_tag.empty_element? } + end + + context "when there are no errors passed in" do + Given(:form) { form_class.new } + + Then { form_tag.empty_element? } + end + end + + context "when the field has errors" do + context "when there is one error for the field" do + Given(:field_errors) { "an error" } + + Then { form_tag.has_tag?(:span, text: "an error") } + end + + context "when there are multiple errors for the field" do |_v| + Given(:field_errors) { %w[error-one error-two] } + + Then { form_tag.has_tag?(:span, text: "error-one") } + And { form_tag.has_tag?(:span, text: "error-two") } + end + end + + describe "css classes" do + Given(:form_class) { + Class.new { + include Phlex::Renderable + include Forms::Form + + def template + errors_for(:my_field) + end + + private + + def errors_css_classes + %w[a-class] + end + + def error_css_classes + %w[another-class] + end + } + } + + When(:field_errors) { "an-error" } + + Then { form_tag.has_tag?(:div, with: {class: "a-class"}) } + Then { form_tag.has_tag?(:span, with: {class: "another-class"}) } + end +end diff --git a/spec/framework/web/form_fields_spec.rb b/spec/framework/web/form_fields_spec.rb index 1c2b327..9a432b6 100644 --- a/spec/framework/web/form_fields_spec.rb +++ b/spec/framework/web/form_fields_spec.rb @@ -172,7 +172,7 @@ When(:field) { described_class.new(:field_name, class: css_class) } context "when giving it a simple html class" do - Given(:css_class) { "a_class" } + When(:css_class) { "a_class" } Then { result.has_tag?( @@ -185,7 +185,7 @@ end context "when giving it multiple html classes as a string" do - Given(:css_class) { "a_class another_class" } + When(:css_class) { "a_class another_class" } Then { result.has_tag?( @@ -198,7 +198,7 @@ end context "when giving it multiple html classes as an array of strings" do - Given(:css_class) { %w[a_class another_class] } + When(:css_class) { %w[a_class another_class] } Then { result.has_tag?( diff --git a/spec/support/html_specs.rb b/spec/support/html_specs.rb index e7a3302..a606bec 100644 --- a/spec/support/html_specs.rb +++ b/spec/support/html_specs.rb @@ -19,6 +19,10 @@ def has_tag?(tagname, text: nil, with: {}) } end + def empty_element? + children.empty? + end + def find_tag(tagname) css(tagname).first end diff --git a/web/forms/errors.rb b/web/forms/errors.rb new file mode 100644 index 0000000..5a9987a --- /dev/null +++ b/web/forms/errors.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Forms + class Errors + include Phlex::HtmlRenderable + + def self.for(field_name, + errors: [], + errors_css_classes: [], + error_css_classes: []) + new( + field_name: field_name, errors: errors, + errors_css_classes: errors_css_classes, + error_css_classes: error_css_classes, + ) + end + + def template + return if errors.empty? + + div(class: errors_css_classes) { + errors.each do |error| + span(class: error_css_classes) { error } + end + } + end + + private + + attr_reader :errors + attr_reader :errors_css_classes + attr_reader :error_css_classes + + def initialize( + field_name:, errors:, + errors_css_classes:, + error_css_classes: + ) + @errors = Array(errors) + @errors_css_classes = Array(errors_css_classes) + @error_css_classes = Array(error_css_classes) + end + end +end diff --git a/web/forms/form.rb b/web/forms/form.rb index 5cdb74c..2a0cd61 100644 --- a/web/forms/form.rb +++ b/web/forms/form.rb @@ -4,10 +4,11 @@ module Forms module Form include Phlex::HtmlRenderable - def initialize(name: "", method: "post", **html_options) + def initialize(name: "", method: "post", errors: {}, **html_options) @http_method = build_http_method(method) @css_class = build_css_class(html_options.delete(:class)) @name = name + @errors = errors @html_options = html_options.transform_values(&:to_s) end @@ -46,6 +47,7 @@ def http_methods attr_reader :html_options attr_reader :css_class attr_reader :name + attr_reader :errors def build_http_method(an_http_method) downcased_http_method = an_http_method.downcase @@ -116,6 +118,23 @@ def text_field(...) render Forms::Fields::TextField.new(...) end + def errors_for(field_name) + render Forms::Errors.for( + field_name, + errors: errors[field_name], + errors_css_classes: errors_css_classes, + error_css_classes: error_css_classes, + ) + end + + def errors_css_classes + [] + end + + def error_css_classes + [] + end + def field_wrapper(&) div(&) end From 482f1d16961d5bf5dd57450073623da51180d54d Mon Sep 17 00:00:00 2001 From: Federico Iachetti Date: Sat, 10 Feb 2024 11:11:05 -0300 Subject: [PATCH 33/33] Refactored form errors --- web/forms/errors.rb | 14 +++----------- web/forms/errors/empty.rb | 13 +++++++++++++ web/forms/errors/populated.rb | 19 +++++++++++++++++++ 3 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 web/forms/errors/empty.rb create mode 100644 web/forms/errors/populated.rb diff --git a/web/forms/errors.rb b/web/forms/errors.rb index 5a9987a..5a459b6 100644 --- a/web/forms/errors.rb +++ b/web/forms/errors.rb @@ -8,23 +8,15 @@ def self.for(field_name, errors: [], errors_css_classes: [], error_css_classes: []) - new( + subclasses.find { |subclass| + subclass.for?(errors) + }.new( field_name: field_name, errors: errors, errors_css_classes: errors_css_classes, error_css_classes: error_css_classes, ) end - def template - return if errors.empty? - - div(class: errors_css_classes) { - errors.each do |error| - span(class: error_css_classes) { error } - end - } - end - private attr_reader :errors diff --git a/web/forms/errors/empty.rb b/web/forms/errors/empty.rb new file mode 100644 index 0000000..81f75fc --- /dev/null +++ b/web/forms/errors/empty.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Forms + class Errors + class Empty < Forms::Errors + def self.for?(errors) + Array(errors).empty? + end + + def template; end + end + end +end diff --git a/web/forms/errors/populated.rb b/web/forms/errors/populated.rb new file mode 100644 index 0000000..1372df9 --- /dev/null +++ b/web/forms/errors/populated.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Forms + class Errors + class Populated < Forms::Errors + def self.for?(errors) + Array(errors).any? + end + + def template + div(class: errors_css_classes) { + errors.each do |error| + span(class: error_css_classes) { error } + end + } + end + end + end +end