Skip to content

Commit

Permalink
Merge pull request #2615 from tvdeyen/node-select-component
Browse files Browse the repository at this point in the history
Add a NodeSelect web component
  • Loading branch information
tvdeyen authored Nov 21, 2023
2 parents ee7ec8b + 2a38786 commit 16e843b
Show file tree
Hide file tree
Showing 10 changed files with 330 additions and 76 deletions.
1 change: 0 additions & 1 deletion app/assets/javascripts/alchemy/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,3 @@
//= require alchemy/alchemy.list_filter
//= require alchemy/alchemy.uploader
//= require alchemy/alchemy.preview_window
//= require alchemy/node_select
39 changes: 0 additions & 39 deletions app/assets/javascripts/alchemy/node_select.js

This file was deleted.

1 change: 0 additions & 1 deletion app/assets/javascripts/alchemy/templates/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
//= require alchemy/templates/page
//= require alchemy/templates/node_folder
//= require alchemy/templates/node
//= require alchemy/templates/page_folder
16 changes: 0 additions & 16 deletions app/assets/javascripts/alchemy/templates/node.hbs

This file was deleted.

38 changes: 38 additions & 0 deletions app/components/alchemy/admin/node_select.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
module Alchemy
module Admin
class NodeSelect < ViewComponent::Base
delegate :alchemy, to: :helpers

def initialize(node = nil, url: nil, placeholder: Alchemy.t(:search_node), query_params: nil)
@node = node
@url = url
@placeholder = placeholder
@query_params = query_params
end

def call
content_tag("alchemy-node-select", content, attributes)
end

private

def attributes
options = {
placeholder: @placeholder,
url: @url || alchemy.api_nodes_path
}

if @query_params
options[:"query-params"] = @query_params.to_json
end

if @node
selection = ActiveModelSerializers::SerializableResource.new(@node, include: :ancestors)
options[:selection] = selection.to_json
end

options
end
end
end
end
1 change: 1 addition & 0 deletions app/javascript/alchemy_admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ $.fx.speeds._default = 400
// Web Components
import "alchemy_admin/components/char_counter"
import "alchemy_admin/components/datepicker"
import "alchemy_admin/components/node_select"
import "alchemy_admin/components/overlay"
import "alchemy_admin/components/page_select"
import "alchemy_admin/components/select"
Expand Down
116 changes: 116 additions & 0 deletions app/javascript/alchemy_admin/components/node_select.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { AlchemyHTMLElement } from "./alchemy_html_element"

class NodeSelect extends AlchemyHTMLElement {
static properties = {
selection: { default: undefined },
placeholder: { default: "" },
queryParams: { default: "{}" },
url: { default: "" }
}

connected() {
this.input.classList.add("alchemy_selectbox")

const dispatchCustomEvent = (name, detail = {}) => {
this.dispatchEvent(new CustomEvent(name, { bubbles: true, detail }))
}

$(this.input)
.select2(this.select2Config)
.on("select2-open", () => {
// add focus to the search input. Select2 is handling the focus on the first opening,
// but it does not work the second time. One process in select2 is "stealing" the focus
// if the command is not delayed. It is an intermediate solution until we are going to
// move away from Select2
setTimeout(() => {
document.querySelector("#select2-drop .select2-input").focus()
}, 100)
})
.on("change", (event) => {
if (event.added) {
dispatchCustomEvent("Alchemy.NodeSelect.NodeAdded", event.added)
} else {
dispatchCustomEvent("Alchemy.NodeSelect.NodeRemoved")
}
})
}

get input() {
return this.getElementsByTagName("input")[0]
}

get select2Config() {
return {
placeholder: this.placeholder,
allowClear: true,
initSelection: (_$el, callback) => {
if (this.selection) {
callback(JSON.parse(this.selection))
}
},
ajax: this.ajaxConfig,
formatSelection: this._renderListEntry,
formatResult: this._renderListEntry
}
}

/**
* Ajax configuration for Select2
* @returns {object}
*/
get ajaxConfig() {
const data = (term, page) => {
return {
q: {
name_or_page_name_cont: term,
...JSON.parse(this.queryParams)
},
page: page
}
}

const results = (response) => {
const meta = response.meta
return {
results: response.data,
more: meta.page * meta.per_page < meta.total_count
}
}

return {
url: this.url,
datatype: "json",
quietMillis: 300,
data,
results
}
}

/**
* html template for each list entry
* @param {object} node
* @returns {string}
* @private
*/
_renderListEntry(node) {
const ancestors = node.ancestors.map((a) => a.name)
return `
<div class="node-select--node">
<i class="icon fas fa-list fa-lg"></i>
<div class="node-select--node-display_name">
<span class="node-select--node-ancestors">
${ancestors.join(" /&nbsp;")}
</span>
<span class="node-select--node-name">
${node.name}
</span>
</div>
<div class="node-select--node-url">
${node.url || ""}
</div>
</div>
`
}
}

customElements.define("alchemy-node-select", NodeSelect)
25 changes: 6 additions & 19 deletions app/views/alchemy/ingredients/_node_editor.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,11 @@
data: node_editor.data_attributes do %>
<%= element_form.fields_for(:ingredients, node_editor.ingredient) do |f| %>
<%= ingredient_label(node_editor, :node_id) %>
<%= f.text_field :node_id,
value: node_editor.node&.id,
id: node_editor.form_field_id(:node_id),
class: 'alchemy_selectbox full_width' %>
<%= render Alchemy::Admin::NodeSelect.new(node_editor.node, url: alchemy.api_nodes_path(language_id: node_editor.page&.language_id, include: :ancestors), query_params: node_editor.settings.fetch(:query_params, {})) do %>
<%= f.text_field :node_id,
value: node_editor.node&.id,
id: node_editor.form_field_id(:node_id),
class: 'alchemy_selectbox full_width' %>
<% end %>
<% end %>
<% end %>

<script>
<% query_params = node_editor.settings.fetch(:query_params, {}).merge({
include: :ancestors
}) %>
$('#<%= node_editor.form_field_id(:node_id) %>').alchemyNodeSelect({
placeholder: "<%= Alchemy.t(:search_node) %>",
url: "<%= alchemy.api_nodes_path(language_id: node_editor.page&.language_id) %>",
query_params: <%== query_params.to_json %>,
<% if node_editor.node %>
<% serialized_node = ActiveModelSerializers::SerializableResource.new(node_editor.node, include: :ancestors) %>
initialSelection: <%== serialized_node.to_json %>
<% end %>
})
</script>
72 changes: 72 additions & 0 deletions spec/components/alchemy/admin/node_select_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
require "rails_helper"

RSpec.describe Alchemy::Admin::NodeSelect, type: :component do
before do
render
end

context "without parameters" do
subject(:render) do
render_inline(described_class.new) { "Node Select Content" }
end

it "should render the component and render given block content" do
expect(page).to have_selector("alchemy-node-select")
expect(page).to have_text("Node Select Content")
end

it "should have the default placeholder" do
expect(page).to have_selector("alchemy-node-select[placeholder='Search node']")
end

it "should have the default node api - url" do
expect(page).to have_selector("alchemy-node-select[url='/api/nodes']")
end

it "should not have a selection" do
expect(page).to_not have_selector("alchemy-node-select[selection]")
end
end

context "with page" do
let(:node) { create(:alchemy_node, id: 123, name: "Test Node") }

subject(:render) do
render_inline(described_class.new(node))
end

it "should have a serialized page information" do
expect(page).to have_selector('alchemy-node-select[selection="{\"id\":123,\"name\":\"Test Node\",\"lft\":1,\"rgt\":2,\"url\":null,\"parent_id\":null,\"ancestors\":[]}"]')
end
end

context "with url" do
subject(:render) do
render_inline(described_class.new(nil, url: "/foo-bar"))
end

it "should have an url parameter" do
expect(page).to have_selector('alchemy-node-select[url="/foo-bar"]')
end
end

context "with custom placeholder" do
subject(:render) do
render_inline(described_class.new(nil, placeholder: "Custom Placeholder"))
end

it "should have a custom placeholder" do
expect(page).to have_selector("alchemy-node-select[placeholder='Custom Placeholder']")
end
end

context "with query parameter" do
subject(:render) do
render_inline(described_class.new(nil, query_params: {foo: :bar}))
end

it "should have serialized custom parameter" do
expect(page).to have_selector('alchemy-node-select[query-params="{\"foo\":\"bar\"}"]')
end
end
end
Loading

0 comments on commit 16e843b

Please sign in to comment.