Skip to content

Commit ffe13b8

Browse files
committed
In which I reinstate turbo stream replace for polls
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.
1 parent 80fe072 commit ffe13b8

File tree

9 files changed

+100
-65
lines changed

9 files changed

+100
-65
lines changed

app/controllers/share/polls/votes_controller.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@ def create
1010
@vote = @question.votes.find_by(device_uuid: ensure_device_uuid)
1111
@vote ||= record_vote(@answer)
1212

13+
@question.broadcast_replace_later \
14+
target: [@question, :results],
15+
html: Share::Polls::Results.new(@poll, @question).call(view_context:),
16+
attributes: {method: :morph}
17+
1318
if @vote.valid?
1419
respond_to do |format|
15-
format.html { redirect_to [:share, @poll] }
20+
format.html { redirect_to [:share, @poll], notice: "Thank you for voting!".emojoy }
1621
format.turbo_stream do
17-
flash.now[:notice] = "Thank you for voting!"
22+
flash.now[:notice] = "Thank you for voting!".emojoy
1823
render turbo_stream: [
1924
turbo_stream.prepend("flash", partial: "application/flash"),
2025
turbo_stream.replace(@poll, renderable: Share::Polls::PollComponent.new(@poll, device_uuid: ensure_device_uuid))

app/models/polls/question.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ class Polls::Question < ApplicationRecord
2626

2727
scope :ordered, -> { order(position: :asc, id: :asc) }
2828

29-
broadcasts_refreshes
30-
3129
def votes_count
3230
answers.sum(&:votes_count)
3331
end

app/views/share/polls/ballot.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
module Share
2+
module Polls
3+
class Ballot < ApplicationComponent
4+
include Phlex::Rails::Helpers::ButtonTo
5+
include Phlex::Rails::Helpers::DOMID
6+
include Phlex::Rails::Helpers::Pluralize
7+
8+
attr_reader :poll, :question
9+
10+
def initialize(poll, question, voted: false)
11+
@poll = poll
12+
@question = question
13+
@voted = voted
14+
end
15+
16+
def view_template
17+
div id: dom_id(question, :ballot), class: "question flex flex-col gap-2" do
18+
p { question.body }
19+
20+
question.answers.ordered.each do |answer|
21+
div id: dom_id(answer) do
22+
button_to answer.body,
23+
share_poll_votes_path(poll, answer_id: answer.id),
24+
class: "button transparent slim"
25+
end
26+
end
27+
end
28+
end
29+
end
30+
end
31+
end

app/views/share/polls/lazy_page_poll.rb

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module Share
22
module Polls
33
class LazyPagePoll < ApplicationComponent
44
include Phlex::Rails::Helpers::Provide
5-
include Phlex::Rails::Helpers::Request
5+
include Phlex::Rails::Helpers::TurboFrameTag
66
include Phlex::Rails::Helpers::TurboRefreshesWith
77

88
attr_reader :page, :title, :question_data
@@ -14,15 +14,7 @@ def initialize(page, title, question_data = {})
1414

1515
def view_template
1616
poll = Poll.generate_for(page, title, question_data) or return
17-
18-
provide :head, turbo_refreshes_with(method: :morph, scroll: :preserve)
19-
render Share::Polls::PollComponent.new(poll, device_uuid: device_uuid)
20-
end
21-
22-
def device_uuid
23-
return "" unless helpers.request.key_generator
24-
25-
helpers.cookies.signed[:device_uuid]
17+
turbo_frame_tag poll, src: share_poll_path(poll), class: "poll"
2618
end
2719
end
2820
end

app/views/share/polls/poll_component.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ def view_template
2222
.includes(:answers)
2323
.ordered
2424
.each do |question|
25-
render Share::Polls::Question.new(
26-
poll:,
27-
question:,
25+
render Share::Polls::QuestionComponent.new(
26+
poll,
27+
question,
2828
voted: question.voted?(device_uuid: device_uuid)
2929
)
3030
end
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
module Share
2+
module Polls
3+
class QuestionComponent < ApplicationComponent
4+
include Phlex::Rails::Helpers::TurboFrameTag
5+
include Phlex::Rails::Helpers::TurboStreamFrom
6+
7+
attr_reader :poll, :question
8+
9+
def initialize(poll, question, voted: false)
10+
@poll = poll
11+
@question = question
12+
@voted = voted
13+
end
14+
15+
def view_template
16+
turbo_frame_tag question, class: "question flex flex-col gap-2" do
17+
if voted?
18+
render Share::Polls::Results.new(poll, question)
19+
else
20+
render Share::Polls::Ballot.new(poll, question)
21+
end
22+
end
23+
turbo_stream_from question
24+
end
25+
26+
private
27+
28+
def voted? = !!@voted
29+
end
30+
end
31+
end

app/views/share/polls/question.rb renamed to app/views/share/polls/results.rb

Lines changed: 10 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,32 @@
11
module Share
22
module Polls
3-
class Question < ApplicationComponent
4-
include Phlex::Rails::Helpers::ButtonTo
5-
include Phlex::Rails::Helpers::DOMID
3+
class Results < ApplicationComponent
64
include Phlex::Rails::Helpers::Pluralize
7-
include Phlex::Rails::Helpers::TurboFrameTag
8-
include Phlex::Rails::Helpers::TurboStreamFrom
5+
include Phlex::Rails::Helpers::DOMID
96

107
attr_reader :poll, :question
118

12-
def initialize(poll:, question:, voted: false)
9+
def initialize(poll, question)
1310
@poll = poll
1411
@question = question
15-
@voted = voted
1612
end
1713

1814
def view_template
19-
turbo_frame_tag question, class: "question flex flex-col gap-2" do
20-
if voted?
21-
results
22-
else
23-
ballot
24-
end
25-
end
26-
turbo_stream_from question
27-
end
28-
29-
def ballot
30-
p { question.body }
15+
div id: dom_id(question, :results), class: "question flex flex-col gap-2" do
16+
p { question.body }
3117

32-
question.answers.ordered.each do |answer|
33-
div id: dom_id(answer) do
34-
button_to answer.body,
35-
share_poll_votes_path(poll, answer_id: answer.id),
36-
class: "button transparent slim"
18+
question.answers.ordered.each do |answer|
19+
render AnswerBar.new(answer, question)
3720
end
38-
end
39-
end
40-
41-
def results
42-
p { question.body }
43-
44-
question.answers.ordered.each do |answer|
45-
render AnswerBar.new(answer, question)
46-
end
4721

48-
div(class: "p-2") do
49-
p(class: "text-small font-extrabold") { pluralize question.votes_count, "vote" }
22+
div(class: "p-2") do
23+
p(class: "text-small font-extrabold") { pluralize question.votes_count, "vote" }
24+
end
5025
end
5126
end
5227

5328
private
5429

55-
def voted? = !!@voted
56-
5730
class AnswerBar < ApplicationComponent
5831
include Phlex::Rails::Helpers::DOMID
5932
include Phlex::Rails::Helpers::NumberToPercentage

spec/system/share/polls_spec.rb

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,15 @@
4242
expect(page).to have_content("2 votes")
4343
end
4444

45-
# We could theoretically test that the original user sees the refreshed
46-
# results but I could only get this to work in system tests when the
47-
# broadcast was emitted in process while "refresh later" via background job,
48-
# as I would prefer it work, doesn’t seem to work with test adapters for
49-
# solid cable / solid queue.
45+
# Assert the vote results are broadcasted to the other guest session
46+
perform_enqueued_jobs
47+
48+
within("#polls_answer_#{answer1.id}") do
49+
expect(page).to have_content("50.0")
50+
end
51+
within("#polls_answer_#{answer2.id}") do
52+
expect(page).to have_content("50.0%")
53+
end
54+
expect(page).to have_content("2 votes")
5055
end
5156
end

spec/views/share/polls/question_spec.rb renamed to spec/views/share/polls/question_component_spec.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
require "rails_helper"
22

3-
RSpec.describe Share::Polls::Question, type: :view do
3+
RSpec.describe Share::Polls::QuestionComponent, type: :view do
44
context "when not voted on" do
55
it "renders with no answers" do
66
poll = FactoryBot.create(:poll)
77
question = FactoryBot.create(:polls_question, poll:)
88

9-
render described_class.new(poll:, question:, voted: false)
9+
render described_class.new(poll, question, voted: false)
1010

1111
expect(rendered).to have_css("p")
1212
end
@@ -16,7 +16,7 @@
1616
question = FactoryBot.create(:polls_question, poll:)
1717
FactoryBot.create(:polls_answer, question:)
1818

19-
render described_class.new(poll:, question:, voted: false)
19+
render described_class.new(poll, question, voted: false)
2020

2121
expect(rendered).to have_css("button", count: 1)
2222
end
@@ -27,7 +27,7 @@
2727
FactoryBot.create(:polls_answer, question:)
2828
FactoryBot.create(:polls_answer, question:)
2929

30-
render described_class.new(poll:, question:, voted: false)
30+
render described_class.new(poll, question, voted: false)
3131

3232
expect(rendered).to have_css("button", count: 2)
3333
end
@@ -39,7 +39,7 @@
3939
question = FactoryBot.create(:polls_question, poll:)
4040
FactoryBot.create(:polls_answer, question:)
4141

42-
render described_class.new(poll:, question:, voted: true)
42+
render described_class.new(poll, question, voted: true)
4343

4444
expect(rendered).to have_css(".question")
4545
expect(rendered).to have_css(".answer")
@@ -53,7 +53,7 @@
5353
answer = FactoryBot.create(:polls_answer, question:)
5454
FactoryBot.create(:polls_vote, answer:)
5555

56-
render described_class.new(poll:, question:, voted: true)
56+
render described_class.new(poll, question, voted: true)
5757

5858
expect(rendered).to have_css(".question")
5959
expect(rendered).to have_css(".answer")

0 commit comments

Comments
 (0)