Skip to content

Commit 41aa851

Browse files
committed
Convert ElementEditor to ViewComponent
Replace the ElementEditor decorator and _element.html.erb partial with a single Alchemy::Admin::ElementEditor ViewComponent. This consolidates view-specific logic and template rendering into one component, following the pattern established by ingredient editor components.
1 parent dabc262 commit 41aa851

File tree

7 files changed

+242
-248
lines changed

7 files changed

+242
-248
lines changed

app/views/alchemy/admin/elements/_element.html.erb renamed to app/components/alchemy/admin/element_editor.html.erb

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,48 @@
11
<alchemy-element-editor
2-
id="element_<%= element.id %>"
3-
data-element-id="<%= element.id %>"
4-
data-element-name="<%= element.name %>"
5-
class="<%= element.css_classes.join(" ") %>"
6-
<%= element.compact? ? "compact" : nil %>
7-
<%= local_assigns[:created] ? "created" : nil %>
8-
<%= element.fixed? ? "fixed" : nil %>
2+
id="element_<%= id %>"
3+
data-element-id="<%= id %>"
4+
data-element-name="<%= name %>"
5+
class="<%= css_classes.join(" ") %>"
6+
<%= compact? ? "compact" : nil %>
7+
<%= created ? "created" : nil %>
8+
<%= fixed? ? "fixed" : nil %>
99
>
10-
<% unless element.fixed? %>
10+
<% unless fixed? %>
1111
<%= render 'alchemy/admin/elements/header', element: element %>
1212
<% end %>
1313

1414
<%= render 'alchemy/admin/elements/toolbar', element: element %>
1515

16-
<% element.definition.message.tap do |message| %>
16+
<% definition.message.tap do |message| %>
1717
<%= render_message(:info, sanitize(message)) if message %>
1818
<% end %>
1919

20-
<% element.definition.warning.tap do |warning| %>
20+
<% definition.warning.tap do |warning| %>
2121
<%= render_message(:warning, sanitize(warning)) if warning %>
2222
<% end %>
2323

24-
<% if element.editable? %>
24+
<% if editable? %>
2525
<%= form_for [alchemy, :admin, element], remote: true,
26-
html: {id: "element_#{element.id}_form".html_safe, class: 'element-body'} do |f| %>
26+
html: {id: "element_#{id}_form".html_safe, class: 'element-body'} do |f| %>
2727

28-
<div id="element_<%= element.id %>_errors" class="element_errors hidden">
28+
<div id="element_<%= id %>_errors" class="element_errors hidden">
2929
<alchemy-icon name="alert"></alchemy-icon>
3030
<p><%= Alchemy.t(:ingredient_validations_headline) %></p>
3131
</div>
3232

3333
<!-- Ingredients -->
34-
<% if element.has_ingredients_defined? %>
34+
<% if has_ingredients_defined? %>
3535
<div class="element-ingredient-editors">
3636
<%= render Alchemy::Admin::IngredientEditor.with_collection(
37-
element.ungrouped_ingredients,
37+
ungrouped_ingredients,
3838
element_form: f
3939
) %>
4040

4141
<!-- Each ingredient group -->
42-
<% element.grouped_ingredients.each do |group, ingredients| %>
43-
<%= content_tag :details, class: "ingredient-group", id: "element_#{element.id}_ingredient_group_#{group.parameterize.underscore}", is: "alchemy-ingredient-group" do %>
42+
<% grouped_ingredients.each do |group, ingredients| %>
43+
<%= content_tag :details, class: "ingredient-group", id: "element_#{id}_ingredient_group_#{group.parameterize.underscore}", is: "alchemy-ingredient-group" do %>
4444
<summary>
45-
<%= element.translated_group group %>
45+
<%= translated_group group %>
4646
<%= render_icon "arrow-left-s" %>
4747
</summary>
4848
<%= render Alchemy::Admin::IngredientEditor.with_collection(
@@ -54,7 +54,7 @@
5454
</div>
5555
<% end %>
5656

57-
<% if element.taggable? %>
57+
<% if taggable? %>
5858
<%= render Alchemy::Admin::TagsAutocomplete.new do %>
5959
<%= f.label :tag_list %>
6060
<%= f.text_field :tag_list, value: f.object.tag_list.join(",") %>
@@ -68,17 +68,15 @@
6868
<%# We need to render nested elements even if the element is folded,
6969
because we need the element present in the DOM for the feature
7070
"click element in the preview => load and expand element editor". %>
71-
<% if element.nestable_elements.any? %>
71+
<% if nestable_elements.any? %>
7272
<div class="nestable-elements">
7373
<%= content_tag :div,
74-
id: "element_#{element.id}_nested_elements",
74+
id: "element_#{id}_nested_elements",
7575
class: "nested-elements", data: {
76-
'droppable-elements' => element.nestable_elements.join(' '),
77-
'element-name' => element.name
76+
'droppable-elements' => nestable_elements.join(' '),
77+
'element-name' => name
7878
} do %>
79-
<%= render element.all_nested_elements.map { |element|
80-
Alchemy::ElementEditor.new(element)
81-
} %>
79+
<%= render Alchemy::Admin::ElementEditor.with_collection(all_nested_elements) %>
8280
<% end %>
8381

8482
<%= render "alchemy/admin/elements/add_nested_element_form", element: element %>
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# frozen_string_literal: true
2+
3+
module Alchemy
4+
module Admin
5+
class ElementEditor < ViewComponent::Base
6+
with_collection_parameter :element
7+
8+
attr_reader :element, :created
9+
10+
delegate :compact?, :definition, :fixed?, :folded?, :id, :ingredient_definitions,
11+
:name, :nestable_elements, :all_nested_elements, :taggable?, :public?, :deprecated?,
12+
to: :element
13+
14+
delegate :alchemy, :render_icon, :render_message, to: :helpers
15+
16+
def initialize(element:, created: false)
17+
@element = element
18+
@created = created
19+
end
20+
21+
# CSS classes for the element editor.
22+
def css_classes
23+
[
24+
"element-editor",
25+
ingredient_definitions.present? ? "with-ingredients" : "without-ingredients",
26+
nestable_elements.any? ? "nestable" : "not-nestable",
27+
taggable? ? "taggable" : "not-taggable",
28+
folded? ? "folded" : "expanded",
29+
compact? ? "compact" : nil,
30+
deprecated? ? "deprecated" : nil,
31+
fixed? ? "is-fixed" : "not-fixed",
32+
public? ? nil : "element-hidden"
33+
]
34+
end
35+
36+
# Tells us, if we should show the element footer and form inputs.
37+
def editable?
38+
ingredient_definitions.any? || taggable?
39+
end
40+
41+
# Are any ingredients defined?
42+
# @return [Boolean]
43+
def has_ingredients_defined?
44+
ingredient_definitions.any?
45+
end
46+
47+
# Returns ingredient instances for defined ingredients
48+
#
49+
# Creates ingredient on demand if the ingredient is not yet present on the element
50+
#
51+
# @return Array<Alchemy::Ingredient>
52+
def ingredients
53+
ingredient_definitions.map do |ingredient|
54+
find_or_create_ingredient(ingredient)
55+
end
56+
end
57+
58+
# Returns ingredients that are not part of any group
59+
def ungrouped_ingredients
60+
ingredients.reject { _1.definition.group }
61+
end
62+
63+
# Returns ingredients grouped by their group name
64+
#
65+
# @return [Hash<String, Array<Alchemy::Ingredient>>]
66+
def grouped_ingredients
67+
ingredients.select { _1.definition.group }.group_by { _1.definition.group }
68+
end
69+
70+
# Returns the translated ingredient group for displaying in admin editor group headings
71+
#
72+
# Translate it in your locale yml file:
73+
#
74+
# alchemy:
75+
# element_groups:
76+
# foo: Bar
77+
#
78+
# Optionally you can scope your ingredient role to an element:
79+
#
80+
# alchemy:
81+
# element_groups:
82+
# article:
83+
# foo: Baz
84+
#
85+
def translated_group(group)
86+
Alchemy.t(
87+
group,
88+
scope: "element_groups.#{element.name}",
89+
default: Alchemy.t("element_groups.#{group}", default: group.humanize)
90+
)
91+
end
92+
93+
private
94+
95+
def find_or_create_ingredient(definition)
96+
element.ingredients.detect { _1.role == definition.role } ||
97+
element.ingredients.create!(
98+
role: definition.role,
99+
type: Alchemy::Ingredient.normalize_type(definition.type)
100+
)
101+
end
102+
end
103+
end
104+
end

app/decorators/alchemy/element_editor.rb

Lines changed: 0 additions & 102 deletions
This file was deleted.

app/views/alchemy/admin/elements/create.turbo_stream.erb

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
1-
<% opts = {
2-
partial: "alchemy/admin/elements/element",
3-
locals: {
4-
element: Alchemy::ElementEditor.new(@element),
5-
created: true
6-
}
7-
} %>
8-
91
<% if @element.fixed? %>
102
<% target = "fixed_element_#{@element.id}" %>
113
<% elsif @element.parent_element %>
@@ -19,9 +11,13 @@
1911
<% end %>
2012

2113
<% if @insert_at_top %>
22-
<%= turbo_stream.prepend target, **opts %>
14+
<%= turbo_stream.prepend target do %>
15+
<%= render Alchemy::Admin::ElementEditor.new(element: @element, created: true) %>
16+
<% end %>
2317
<% else %>
24-
<%= turbo_stream.append target, **opts %>
18+
<%= turbo_stream.append target do %>
19+
<%= render Alchemy::Admin::ElementEditor.new(element: @element, created: true) %>
20+
<% end %>
2521
<% end %>
2622

2723
<%= turbo_stream.replace "clipboard_button",

app/views/alchemy/admin/elements/index.html.erb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@
3838
id="main-content-elements"
3939
style="--padding: 0"
4040
>
41-
<%= render @elements.map { |element| Alchemy::ElementEditor.new(element) } %>
41+
<%= render Alchemy::Admin::ElementEditor.with_collection(@elements) %>
4242
</sl-tab-panel>
4343
<% @fixed_elements.each do |element| %>
4444
<sl-tab-panel id="fixed_element_<%= element.id %>" name="fixed-element-<%= element.id %>" style="--padding: 0" class="scrollable-elements">
45-
<%= render Alchemy::ElementEditor.new(element) %>
45+
<%= render Alchemy::Admin::ElementEditor.new(element: element) %>
4646
</sl-tab-panel>
4747
<% end %>
4848
</sl-tab-group>
@@ -53,7 +53,7 @@
5353
data-droppable-elements="<%= @page.element_definition_names.join(' ') %>"
5454
data-element-name="main-content-elements"
5555
>
56-
<%= render @elements.map { |element| Alchemy::ElementEditor.new(element) } %>
56+
<%= render Alchemy::Admin::ElementEditor.with_collection(@elements) %>
5757
</div>
5858
<% end %>
5959
</alchemy-elements-window>

0 commit comments

Comments
 (0)