Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add alchemy-button web component #2621

Merged
merged 2 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ class window.Alchemy.ConfirmDialog extends Alchemy.Dialog
@cancel_button.focus()
@cancel_button.click =>
@close()
Alchemy.Buttons.enable()
false
@ok_button.click =>
@close()
Expand Down
3 changes: 0 additions & 3 deletions app/assets/javascripts/alchemy/alchemy.dialog.js.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,6 @@ class window.Alchemy.Dialog
# Watches ajax requests inside of dialog body and replaces the content accordingly
watch_remote_forms: ->
form = $('[data-remote="true"]', @dialog_body)
form.bind "ajax:complete", () =>
Alchemy.Buttons.enable(@dialog_body)
return
form.bind "ajax:success", (event) =>
xhr = event.detail[2]
content_type = xhr.getResponseHeader('Content-Type')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,6 @@ Alchemy.ElementEditors =
# Prevent this event from beeing called twice on the same element
if event.currentTarget == event.target
Alchemy.setElementClean($element)
Alchemy.Buttons.enable($element)
true

# Toggle visibility of the ingredient fields in the group
Expand Down
3 changes: 1 addition & 2 deletions app/javascript/alchemy_admin.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import "@hotwired/turbo-rails"
import Rails from "@rails/ujs"

import Buttons from "alchemy_admin/buttons"
import GUI from "alchemy_admin/gui"
import { translate } from "alchemy_admin/i18n"
import Dirty from "alchemy_admin/dirty"
Expand All @@ -21,6 +20,7 @@ import PagePublicationFields from "alchemy_admin/page_publication_fields"
$.fx.speeds._default = 400

// Web Components
import "alchemy_admin/components/button"
import "alchemy_admin/components/char_counter"
import "alchemy_admin/components/datepicker"
import "alchemy_admin/components/node_select"
Expand All @@ -41,7 +41,6 @@ if (typeof window.Alchemy === "undefined") {

// Enhance the global Alchemy object with imported features
Object.assign(Alchemy, {
Buttons,
...Dirty,
GUI,
t: translate, // Global utility method for translating a given string
Expand Down
62 changes: 0 additions & 62 deletions app/javascript/alchemy_admin/buttons.js

This file was deleted.

52 changes: 52 additions & 0 deletions app/javascript/alchemy_admin/components/button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Spinner from "../spinner"

class Button extends HTMLButtonElement {
connectedCallback() {
if (this.form) {
this.form.addEventListener("submit", (event) => {
const isDisabled = this.getAttribute("disabled") === "disabled"

if (isDisabled) {
event.preventDefault()
event.stopPropagation()
} else {
this.disable()
}
})

if (this.form.dataset.remote == "true") {
this.form.addEventListener("ajax:complete", () => {
this.enable()
})
}
} else {
console.warn("No form for button found!", this)
}
}

disable() {
const spinner = new Spinner("small")
const rect = this.getBoundingClientRect()

this.dataset.initialButtonText = this.innerHTML
this.setAttribute("disabled", "disabled")
this.setAttribute("tabindex", "-1")
this.classList.add("disabled")
this.style.width = `${rect.width}px`
this.style.height = `${rect.height}px`
this.innerHTML = " "

spinner.spin(this)
}

enable() {
this.classList.remove("disabled")
this.removeAttribute("disabled")
this.removeAttribute("tabindex")
this.style.width = null
this.style.height = null
this.innerHTML = this.dataset.initialButtonText
}
}

customElements.define("alchemy-button", Button, { extends: "button" })
1 change: 0 additions & 1 deletion app/javascript/alchemy_admin/gui.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import TagsAutocomplete from "alchemy_admin/tags_autocomplete"

function init(scope) {
Alchemy.Buttons.observe(scope)
if (!scope) {
Alchemy.watchForDialogs()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<%= content_tag :div, class: 'add-nested-element', data: { element_id: element.id } do %>
<% if element.expanded? || element.fixed? %>
<% if element.nestable_elements.length == 1 &&
(nestable_element = element.nestable_elements.first) &&
<% if element.nestable_elements.length == 1 &&
(nestable_element = element.nestable_elements.first) &&
Alchemy::Element.all_from_clipboard_for_parent_element(get_clipboard("elements"), element).none?
%>
<%= form_for [:admin, Alchemy::Element.new(name: nestable_element)],
remote: true, html: { class: 'add-nested-element-form', id: nil } do |f| %>
<%= f.hidden_field :name %>
<%= f.hidden_field :page_version_id, value: element.page_version_id %>
<%= f.hidden_field :parent_element_id, value: element.id %>
<button class="button add-nestable-element-button" data-alchemy-button>
<button class="add-nestable-element-button" is="alchemy-button">
<%= Alchemy.t(:add_nested_element, name: Alchemy.t(nestable_element, scope: 'element_names')) %>
</button>
<% end %>
Expand All @@ -24,4 +24,4 @@
}, class: "button add-nestable-element-button" %>
<% end %>
<% end %>
<% end %>
<% end %>
2 changes: 1 addition & 1 deletion app/views/alchemy/admin/elements/_footer.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</p>
<% end %>

<button type="submit" form="element_<%= element.id %>_form" class="button" data-alchemy-button>
<button type="submit" form="element_<%= element.id %>_form" is="alchemy-button">
<%= Alchemy.t(:save) %>
</button>
</div>
1 change: 0 additions & 1 deletion app/views/alchemy/admin/elements/create.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
$element_area = $('[name="fixed-element-<%= @element.id %>"]');
<% elsif @element.parent_element %>
$element_area = $('#element_<%= @element.parent_element_id %> > .nestable-elements > .nested-elements');
Alchemy.Buttons.enable('.nestable-elements');
<% else %>
$element_area = $('#main-content-elements');
<% end %>
Expand Down
1 change: 0 additions & 1 deletion app/views/alchemy/admin/elements/update.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
$errors.html('<%= j @error_message %><ul><li><%== j @error_messages.join("</li><li>") %></li></ul>');
$errors.show();
$('<%== @element.ingredients_with_errors.map { |ingredient| "[data-ingredient-id=\"#{ingredient.id}\"]" }.join(", ") %>').addClass('validation_failed');
Alchemy.Buttons.enable($el);

<%- end -%>
})();
2 changes: 1 addition & 1 deletion app/views/alchemy/admin/styleguide/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@

<div class="submit">
<button class="secondary">Secondary</button>
<button data-alchemy-button>Primary Submit</button>
<button type="submit" is="alchemy-button">Primary Submit</button>
</div>
</form>
</div>
Expand Down
1 change: 0 additions & 1 deletion app/views/alchemy/base/error_notice.js.erb
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
Alchemy.growl('<%= j @notice %>', 'error');
Alchemy.Buttons.enable();
5 changes: 3 additions & 2 deletions lib/alchemy/forms/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ def datepicker(attribute_name, options = {})
#
def submit(label, options = {})
options = {
wrapper_html: {class: "submit"}
wrapper_html: {class: "submit"},
input_html: {is: "alchemy-button"}
}.update(options)
template.content_tag("div", options.delete(:wrapper_html)) do
template.content_tag("button", label, options.delete(:input_html))
template.button_tag(label, options.delete(:input_html))
end
end
end
Expand Down
64 changes: 64 additions & 0 deletions spec/javascript/alchemy_admin/components/button.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import "alchemy_admin/components/button"
import { renderComponent } from "./component.helper"

describe("alchemy-button", () => {
it("disables button on form submit", () => {
const html = `
<form>
<button type="submit" is="alchemy-button">Save</button>
</form>
`
const button = renderComponent("alchemy-button", html)
const submit = new Event("submit", { bubbles: true })

button.form.dispatchEvent(submit)

expect(button.getAttribute("disabled")).toEqual("disabled")
expect(button.getAttribute("tabindex")).toEqual("-1")
expect(button.classList.contains("disabled")).toBeTruthy()
expect(button.innerHTML).toEqual(
'&nbsp;<alchemy-spinner size="small" color="currentColor"></alchemy-spinner>'
)
})

it("logs warning if no form found", () => {
global.console = {
...console,
warn: jest.fn()
}

const html = `
<button is="alchemy-button">Save</button>
`
const button = renderComponent("alchemy-button", html)

expect(console.warn).toHaveBeenCalledWith(
"No form for button found!",
button
)
})

describe("on remote forms", () => {
it("re-enables button on ajax complete", () => {
const html = `
<form data-remote="true">
<button type="submit" is="alchemy-button">Save</button>
</form>
`
const button = renderComponent("alchemy-button", html)

const submit = new Event("submit", { bubbles: true })
button.form.dispatchEvent(submit)

expect(button.getAttribute("disabled")).toEqual("disabled")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could split that into different describe and it - functions. Jest is pretty fast it would desribe, what are you going to test.


const ajaxComplete = new CustomEvent("ajax:complete", { bubbles: true })
button.form.dispatchEvent(ajaxComplete)

expect(button.getAttribute("disabled")).toBeNull()
expect(button.getAttribute("tabindex")).toBeNull()
expect(button.classList.contains("disabled")).toBeFalsy()
expect(button.innerHTML).toEqual("Save")
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
export const renderComponent = (name, html) => {
document.body.innerHTML = html
return document.querySelector(name)
return document.querySelector(`${name}, [is="${name}"]`)
}

export const setupLanguage = () => {
Expand Down
4 changes: 4 additions & 0 deletions spec/javascript/alchemy_admin/utils/ajax.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ describe("get", () => {
})

it("network errors get rejected", async () => {
global.console = {
...console,
error: jest.fn()
}
xhrMock.get("http://localhost/users", () => {
return Promise.reject(new Error())
})
Expand Down
Loading