context testing rails-ujs functionality with Rails 7 turbo streams as a full replacement.
Class nesting is structured as follows:
class Cartitem < ApplicationRecord
belongs_to :article
belongs_to :cart
validates :quantity_ordered, numericality: { greater_than: 0 }, on: :create
validates :quantity_ordered, numericality: { greater_than_or_equal_to: 0 }, on: :update
end
A page of articles can potentially create cartitems, one per article.
Each article needs to be creatable, updatable, deletable (so it may be recreated for users who change their minds incessantly).
Accordingly, that page has a block for each article
<% articles.each do |article| %>
<%= render 'article', article: article %>
<% end %>
# article partial
<div>
<%= article.description_short %>
</div>
<div>
<%= turbo_frame_tag dom_id(article) do %>
<%= render 'cartitem_block', article: article %>
<% end %>
</div>
the partial then invokes a turbo frame:
<%= turbo_frame_tag dom_id(article) do %>
<%= form_with(model: @cartitem, local: false) do |f| %>
<%= f.number_field :quantity_ordered %>
<%= f.submit %>
<% end %>
<% end %>
The rendered HTML does call the dom_ids and renders as expected:
<turbo-frame id="article_1">
<form action="/cartitems" accept-charset="UTF-8" data-remote="true" method="post"><input type="hidden" name="authenticity_token" value="sgABxVmZX0TDditdtwuGrgG4t9fkdfFMg02tpDnfgX3ch_IqD_YGoFJroE4u3y9t-bdLfGAyXZBUyJe04RBqtQ" autocomplete="off">
<input min="1" type="number" name="cartitem[quantity_ordered]" id="cartitem_quantity_ordered">
<button name="button" type="submit" ></button>
</form>
</turbo-frame>
[...]
<turbo-frame id="article_5">
<form action="/cartitems" accept-charset="UTF-8" data-remote="true" method="post"><input type="hidden" name="authenticity_token" value="..." autocomplete="off">
<input min="1" type="number" name="cartitem[quantity_ordered]" id="cartitem_quantity_ordered">
<button name="button" type="submit" ></button>
</form>
</turbo-frame>
The controller:
def create
@article = Article.find(params[:cartitem][:article_id])
price_ordered = @article.sell_price * params[:cartitem][:quantity_ordered].to_d
@cartitem = Cartitem.create!(article_id: @article.id, quantity_ordered: params[:cartitem][:quantity_ordered].to_d, price_ordered: price_ordered, cart_id: params[:cartitem][:cart_id].to_i)
respond_to do |format|
if @cartitem.save
format.turbo_stream
format.html { redirect_to cartitem_url(@cartitem), notice: "Cartitem was successfully created." }
else
format.turbo_stream
format.html { render :new, status: :unprocessable_entity }
end
end
end
If @cartitem is valid, the process runs and renders as expected:
Rendered cartitems/_cartitem.html.erb (Duration: 1.0ms | Allocations: 953)
Rendered cartitems/create.turbo_stream.erb (Duration: 1.3ms | Allocations: 1082)
However upon submitting with an empty value, to test the validation, an unexpected result occurs:
Started POST "/cartitems" for 127.0.0.1 at 2022-01-22 08:24:38 +0100
Processing by CartitemsController#create as TURBO_STREAM
Parameters: {"authenticity_token"=>"[FILTERED]", "cartitem"=>{"price_ordered"=>"3.41", "cart_id"=>"1", "article_id"=>"1", "fulfilled"=>"false", "quantity_ordered"=>""}}
[...]
TRANSACTION (0.2ms) ROLLBACK
TRANSACTION (0.2ms) ROLLBACK
↳ app/controllers/cartitems_controller.rb:28:in `create'
Completed 422 Unprocessable Entity in 11ms (ActiveRecord: 1.1ms | Allocations: 4874)
ActiveRecord::RecordInvalid (Validation failed: Quantity ordered must be greater than 0):
As expected, the transaction rolls back. However, the response is an Unprocessable Entity.
and the return is not what the create.turbo_stream.erb
is stated to do, i.e. if the cartitem is nil re-render the partial.
*note: the else
portion of the respond_to block was tried both with and without a reference to turbo_stream with identical result.
<% if @cartitem %>
<%= turbo_stream.replace "article_#{article.id}", @cartitem %>
<% else %>
<%= render 'cartitem_block', article: @article %>
<% end %>
# _cartitem partial:
<%= turbo_frame_tag dom_id(@cartitem.article), class: 'fade-text' do %>
<%= form_with(url: update_q_cartitem_path(@cartitem), local: false, method: :patch) do |form| %>
<%= form.number_field :quantity_ordered, value: number_with_precision(@cartitem.quantity_ordered, precision: 0), min: 0, style: 'width: 69px; display: inline-block;' %>
<%= button_tag(style: 'display: inline-block;') do %>
<p class='button warning' style='margin-top: 10px;'><%= fa_icon('recycle', class: 'fa 3x') %></p>
<% end %>
<% end %>
<% end %>
Instead, the xhr
response returned is the HTML page of an error returned when in development mode.
The browser console in addition complains with
Response has no matching <turbo-frame id="article_1"> element
which is untrue as the HTML still has a lingering:
<turbo-frame id="article_1"></turbo-frame>
What needs to be done to properly error handle the case, render the form partial in its rightfully recognized place?