From ffe13b89189b3bd3bbbcd22fd03413e34865954d Mon Sep 17 00:00:00 2001 From: Ross Kaffenberger Date: Sat, 7 Dec 2024 16:57:20 -0500 Subject: [PATCH] In which I reinstate turbo stream replace for polls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I’m having trouble getting refreshes to work well for polls embedded in lazy loaded turbo frames within other pages like articles. I originally implemented polls this way and it appears to work well for what I want to accomplish even though it’s a bit more surgical. --- .../share/polls/votes_controller.rb | 9 +++- app/models/polls/question.rb | 2 - app/views/share/polls/ballot.rb | 31 ++++++++++++ app/views/share/polls/lazy_page_poll.rb | 12 +---- app/views/share/polls/poll_component.rb | 6 +-- app/views/share/polls/question_component.rb | 31 ++++++++++++ .../share/polls/{question.rb => results.rb} | 47 ++++--------------- spec/system/share/polls_spec.rb | 15 ++++-- ...ion_spec.rb => question_component_spec.rb} | 12 ++--- 9 files changed, 100 insertions(+), 65 deletions(-) create mode 100644 app/views/share/polls/ballot.rb create mode 100644 app/views/share/polls/question_component.rb rename app/views/share/polls/{question.rb => results.rb} (58%) rename spec/views/share/polls/{question_spec.rb => question_component_spec.rb} (82%) diff --git a/app/controllers/share/polls/votes_controller.rb b/app/controllers/share/polls/votes_controller.rb index 5667e1cc..f72cad1b 100644 --- a/app/controllers/share/polls/votes_controller.rb +++ b/app/controllers/share/polls/votes_controller.rb @@ -10,11 +10,16 @@ def create @vote = @question.votes.find_by(device_uuid: ensure_device_uuid) @vote ||= record_vote(@answer) + @question.broadcast_replace_later \ + target: [@question, :results], + html: Share::Polls::Results.new(@poll, @question).call(view_context:), + attributes: {method: :morph} + if @vote.valid? respond_to do |format| - format.html { redirect_to [:share, @poll] } + format.html { redirect_to [:share, @poll], notice: "Thank you for voting!".emojoy } format.turbo_stream do - flash.now[:notice] = "Thank you for voting!" + flash.now[:notice] = "Thank you for voting!".emojoy render turbo_stream: [ turbo_stream.prepend("flash", partial: "application/flash"), turbo_stream.replace(@poll, renderable: Share::Polls::PollComponent.new(@poll, device_uuid: ensure_device_uuid)) diff --git a/app/models/polls/question.rb b/app/models/polls/question.rb index 7b6285cf..28ba6300 100644 --- a/app/models/polls/question.rb +++ b/app/models/polls/question.rb @@ -26,8 +26,6 @@ class Polls::Question < ApplicationRecord scope :ordered, -> { order(position: :asc, id: :asc) } - broadcasts_refreshes - def votes_count answers.sum(&:votes_count) end diff --git a/app/views/share/polls/ballot.rb b/app/views/share/polls/ballot.rb new file mode 100644 index 00000000..ad7801e6 --- /dev/null +++ b/app/views/share/polls/ballot.rb @@ -0,0 +1,31 @@ +module Share + module Polls + class Ballot < ApplicationComponent + include Phlex::Rails::Helpers::ButtonTo + include Phlex::Rails::Helpers::DOMID + include Phlex::Rails::Helpers::Pluralize + + attr_reader :poll, :question + + def initialize(poll, question, voted: false) + @poll = poll + @question = question + @voted = voted + end + + def view_template + div id: dom_id(question, :ballot), class: "question flex flex-col gap-2" do + p { question.body } + + question.answers.ordered.each do |answer| + div id: dom_id(answer) do + button_to answer.body, + share_poll_votes_path(poll, answer_id: answer.id), + class: "button transparent slim" + end + end + end + end + end + end +end diff --git a/app/views/share/polls/lazy_page_poll.rb b/app/views/share/polls/lazy_page_poll.rb index c68c4b5c..0eb3b583 100644 --- a/app/views/share/polls/lazy_page_poll.rb +++ b/app/views/share/polls/lazy_page_poll.rb @@ -2,7 +2,7 @@ module Share module Polls class LazyPagePoll < ApplicationComponent include Phlex::Rails::Helpers::Provide - include Phlex::Rails::Helpers::Request + include Phlex::Rails::Helpers::TurboFrameTag include Phlex::Rails::Helpers::TurboRefreshesWith attr_reader :page, :title, :question_data @@ -14,15 +14,7 @@ def initialize(page, title, question_data = {}) def view_template poll = Poll.generate_for(page, title, question_data) or return - - provide :head, turbo_refreshes_with(method: :morph, scroll: :preserve) - render Share::Polls::PollComponent.new(poll, device_uuid: device_uuid) - end - - def device_uuid - return "" unless helpers.request.key_generator - - helpers.cookies.signed[:device_uuid] + turbo_frame_tag poll, src: share_poll_path(poll), class: "poll" end end end diff --git a/app/views/share/polls/poll_component.rb b/app/views/share/polls/poll_component.rb index 8f5edd0d..a40841a6 100644 --- a/app/views/share/polls/poll_component.rb +++ b/app/views/share/polls/poll_component.rb @@ -22,9 +22,9 @@ def view_template .includes(:answers) .ordered .each do |question| - render Share::Polls::Question.new( - poll:, - question:, + render Share::Polls::QuestionComponent.new( + poll, + question, voted: question.voted?(device_uuid: device_uuid) ) end diff --git a/app/views/share/polls/question_component.rb b/app/views/share/polls/question_component.rb new file mode 100644 index 00000000..141bbae0 --- /dev/null +++ b/app/views/share/polls/question_component.rb @@ -0,0 +1,31 @@ +module Share + module Polls + class QuestionComponent < ApplicationComponent + include Phlex::Rails::Helpers::TurboFrameTag + include Phlex::Rails::Helpers::TurboStreamFrom + + attr_reader :poll, :question + + def initialize(poll, question, voted: false) + @poll = poll + @question = question + @voted = voted + end + + def view_template + turbo_frame_tag question, class: "question flex flex-col gap-2" do + if voted? + render Share::Polls::Results.new(poll, question) + else + render Share::Polls::Ballot.new(poll, question) + end + end + turbo_stream_from question + end + + private + + def voted? = !!@voted + end + end +end diff --git a/app/views/share/polls/question.rb b/app/views/share/polls/results.rb similarity index 58% rename from app/views/share/polls/question.rb rename to app/views/share/polls/results.rb index ff9df83f..35ca104f 100644 --- a/app/views/share/polls/question.rb +++ b/app/views/share/polls/results.rb @@ -1,59 +1,32 @@ module Share module Polls - class Question < ApplicationComponent - include Phlex::Rails::Helpers::ButtonTo - include Phlex::Rails::Helpers::DOMID + class Results < ApplicationComponent include Phlex::Rails::Helpers::Pluralize - include Phlex::Rails::Helpers::TurboFrameTag - include Phlex::Rails::Helpers::TurboStreamFrom + include Phlex::Rails::Helpers::DOMID attr_reader :poll, :question - def initialize(poll:, question:, voted: false) + def initialize(poll, question) @poll = poll @question = question - @voted = voted end def view_template - turbo_frame_tag question, class: "question flex flex-col gap-2" do - if voted? - results - else - ballot - end - end - turbo_stream_from question - end - - def ballot - p { question.body } + div id: dom_id(question, :results), class: "question flex flex-col gap-2" do + p { question.body } - question.answers.ordered.each do |answer| - div id: dom_id(answer) do - button_to answer.body, - share_poll_votes_path(poll, answer_id: answer.id), - class: "button transparent slim" + question.answers.ordered.each do |answer| + render AnswerBar.new(answer, question) end - end - end - - def results - p { question.body } - - question.answers.ordered.each do |answer| - render AnswerBar.new(answer, question) - end - div(class: "p-2") do - p(class: "text-small font-extrabold") { pluralize question.votes_count, "vote" } + div(class: "p-2") do + p(class: "text-small font-extrabold") { pluralize question.votes_count, "vote" } + end end end private - def voted? = !!@voted - class AnswerBar < ApplicationComponent include Phlex::Rails::Helpers::DOMID include Phlex::Rails::Helpers::NumberToPercentage diff --git a/spec/system/share/polls_spec.rb b/spec/system/share/polls_spec.rb index 20ab32b5..0ab92273 100644 --- a/spec/system/share/polls_spec.rb +++ b/spec/system/share/polls_spec.rb @@ -42,10 +42,15 @@ expect(page).to have_content("2 votes") end - # We could theoretically test that the original user sees the refreshed - # results but I could only get this to work in system tests when the - # broadcast was emitted in process while "refresh later" via background job, - # as I would prefer it work, doesn’t seem to work with test adapters for - # solid cable / solid queue. + # Assert the vote results are broadcasted to the other guest session + perform_enqueued_jobs + + within("#polls_answer_#{answer1.id}") do + expect(page).to have_content("50.0") + end + within("#polls_answer_#{answer2.id}") do + expect(page).to have_content("50.0%") + end + expect(page).to have_content("2 votes") end end diff --git a/spec/views/share/polls/question_spec.rb b/spec/views/share/polls/question_component_spec.rb similarity index 82% rename from spec/views/share/polls/question_spec.rb rename to spec/views/share/polls/question_component_spec.rb index 44d740c2..4046cd9a 100644 --- a/spec/views/share/polls/question_spec.rb +++ b/spec/views/share/polls/question_component_spec.rb @@ -1,12 +1,12 @@ require "rails_helper" -RSpec.describe Share::Polls::Question, type: :view do +RSpec.describe Share::Polls::QuestionComponent, type: :view do context "when not voted on" do it "renders with no answers" do poll = FactoryBot.create(:poll) question = FactoryBot.create(:polls_question, poll:) - render described_class.new(poll:, question:, voted: false) + render described_class.new(poll, question, voted: false) expect(rendered).to have_css("p") end @@ -16,7 +16,7 @@ question = FactoryBot.create(:polls_question, poll:) FactoryBot.create(:polls_answer, question:) - render described_class.new(poll:, question:, voted: false) + render described_class.new(poll, question, voted: false) expect(rendered).to have_css("button", count: 1) end @@ -27,7 +27,7 @@ FactoryBot.create(:polls_answer, question:) FactoryBot.create(:polls_answer, question:) - render described_class.new(poll:, question:, voted: false) + render described_class.new(poll, question, voted: false) expect(rendered).to have_css("button", count: 2) end @@ -39,7 +39,7 @@ question = FactoryBot.create(:polls_question, poll:) FactoryBot.create(:polls_answer, question:) - render described_class.new(poll:, question:, voted: true) + render described_class.new(poll, question, voted: true) expect(rendered).to have_css(".question") expect(rendered).to have_css(".answer") @@ -53,7 +53,7 @@ answer = FactoryBot.create(:polls_answer, question:) FactoryBot.create(:polls_vote, answer:) - render described_class.new(poll:, question:, voted: true) + render described_class.new(poll, question, voted: true) expect(rendered).to have_css(".question") expect(rendered).to have_css(".answer")