From 7d66b7521656fee3abb268311009ee6aab587fb1 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Mon, 21 Feb 2022 11:03:43 -0600 Subject: [PATCH] Improve notifications, fix styles not being added Based on https://petr.codes/blog/rails/modern-rails-flash-messages/part-3/ --- .../stylesheets/application.tailwind.css | 1 + .../stylesheets/components/notifications.css | 39 +++++++ .../notification_component.html.erb | 78 +++++++------ app/components/notification_component.rb | 9 ++ app/controllers/dashboard_controller.rb | 5 - app/javascript/application.js | 2 +- .../controllers/hello_controller.js | 7 -- .../controllers/notification_controller.js | 110 ++++++++++++++++++ app/views/layouts/admin.html.erb | 16 +-- app/views/layouts/application.html.erb | 4 +- tailwind.config.js | 1 + 11 files changed, 214 insertions(+), 58 deletions(-) create mode 100644 app/assets/stylesheets/components/notifications.css delete mode 100644 app/javascript/controllers/hello_controller.js create mode 100644 app/javascript/controllers/notification_controller.js diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index f7380bf..7cdbcd5 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -6,3 +6,4 @@ @import "components/buttons"; @import "components/forms"; @import "components/links"; +@import "components/notifications"; diff --git a/app/assets/stylesheets/components/notifications.css b/app/assets/stylesheets/components/notifications.css new file mode 100644 index 0000000..8af9c8f --- /dev/null +++ b/app/assets/stylesheets/components/notifications.css @@ -0,0 +1,39 @@ +@layer components { + @keyframes notification-countdown { + from { width: 100%; } + to { width: 0; } + } + + .notification-enter { + @apply transform ease-out duration-300 transition; + } + + .notification-enter-from { + @apply translate-y-2 opacity-0; + } + + .notification-enter-to { + @apply translate-y-0 opacity-100; + } + + @screen sm { + .notification-enter-from { + @apply translate-y-0 translate-x-2; + } + .notification-enter-to { + @apply translate-x-0; + } + } + + .notification-leave { + @apply transition ease-in duration-100; + } + + .notification-leave-from { + @apply opacity-100; + } + + .notification-leave-to { + @apply opacity-0; + } +} diff --git a/app/components/notification_component.html.erb b/app/components/notification_component.html.erb index 0319056..6f960b6 100644 --- a/app/components/notification_component.html.erb +++ b/app/components/notification_component.html.erb @@ -1,39 +1,49 @@ - -
-
- -
-
-
-
- - <%= render "icons/#{@icon_name}" %> - -
-
-

<%= @data[:title] %>

-

<%= @data[:body] %>

-
-
- -
+ diff --git a/app/components/notification_component.rb b/app/components/notification_component.rb index a10404d..7b3bc84 100644 --- a/app/components/notification_component.rb +++ b/app/components/notification_component.rb @@ -1,13 +1,22 @@ # frozen_string_literal: true +# @param type [String] Classic notification type `error`, `alert` and `info` + custom `success` +# @param data [String, Hash] `String` for backward compatibility, +# `Hash` for the new functionality `{title: '', body: '', timeout: 5, countdown: false, action: { url: '', method: '', name: ''}}`. +# The `title` attribute for `Hash` is mandatory. class NotificationComponent < ViewComponent::Base def initialize(type:, data:) @type = type @data = prepare_data(data) @icon_name = icon_name @icon_color_class = icon_color_class + + @data[:timeout] ||= 5 + @data[:action][:method] ||= "get" if @data[:action] end + private + def prepare_data(data) case data when Hash diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 81a080c..49c0764 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -3,10 +3,5 @@ class DashboardController < ApplicationController def index @current_section = :dashboard - # flash[:error] = 'Notification shown successfully!' - flash[:success] = { - title: 'Discussion moved', - body: 'Lorem ipsum dolor sit amet consectetur adipisicing elit oluptatum tenetur.' - } end end diff --git a/app/javascript/application.js b/app/javascript/application.js index 1621348..0d7b494 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -1,3 +1,3 @@ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails import "@hotwired/turbo-rails" -// import "controllers" +import "controllers" diff --git a/app/javascript/controllers/hello_controller.js b/app/javascript/controllers/hello_controller.js deleted file mode 100644 index 5975c07..0000000 --- a/app/javascript/controllers/hello_controller.js +++ /dev/null @@ -1,7 +0,0 @@ -import { Controller } from "@hotwired/stimulus" - -export default class extends Controller { - connect() { - this.element.textContent = "Hello World!" - } -} diff --git a/app/javascript/controllers/notification_controller.js b/app/javascript/controllers/notification_controller.js new file mode 100644 index 0000000..e0291ce --- /dev/null +++ b/app/javascript/controllers/notification_controller.js @@ -0,0 +1,110 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["buttons", "countdown"] + + connect() { + const timeoutSeconds = parseInt(this.data.get("timeout")); + + setTimeout(() => { + this.element.classList.remove('hidden'); + this.element.classList.add('notification-enter', 'notification-enter-from'); + + // Trigger transition + setTimeout(() => { + this.element.classList.add('notification-enter-to'); + }, 100); + + // Trigger countdown + if (this.hasCountdownTarget) { + this.countdownTarget.style.animation = 'notification-countdown linear ' + timeoutSeconds + 's'; + } + + }, 500); + this.timeoutId = setTimeout(() => { + this.close(); + }, timeoutSeconds * 1000 + 500); + } + + run(e) { + e.preventDefault(); + this.stop(); + let _this = this; + this.buttonsTarget.innerHTML = 'Processing...'; + + // Call the action + fetch(this.data.get("action-url"), { + method: this.data.get("action-method").toUpperCase(), + dataType: 'script', + credentials: "include", + headers: { + "X-CSRF-Token": this.csrfToken + }, + }) + .then(response => { + if (!response.ok) { + throw Error(response.statusText); + } + return response; + }) + .then(response => response.json()) + .then(data => { + // Set new content + _this.buttonsTarget.innerHTML = '' + data.message + ''; + + // Remove hidden class and display the record + if (data.inline) { + document.getElementById(data.dom_id).classList.toggle('hidden'); + } + + // Close + setTimeout(() => { + if (data.inline) { + // Just close the notification + _this.close(); + } else { + // Reload the page using Turbo + window.Turbo.visit(window.location.toString(), {action: 'replace'}) + } + }, 1000); + }) + .catch(error => { + console.log(error); + _this.buttonsTarget.innerHTML = 'Error!'; + setTimeout(() => { + _this.close(); + }, 1000); + }); + } + + stop() { + clearTimeout(this.timeoutId) + this.timeoutId = null + } + + continue() { + this.timeoutId = setTimeout(() => { + this.close(); + }, parseInt(this.data.get("timeout"))); + } + + close() { + this.element.classList.remove('notification-enter', 'notification-enter-from', 'notification-enter-to'); + this.element.classList.add('notification-leave', 'notification-leave-from') + + // Trigger transition + setTimeout(() => { + this.element.classList.add('notification-leave-to'); + }, 100); + + // Remove element after transition + setTimeout(() => { + this.element.remove(); + }, 300); + } + + get csrfToken() { + const element = document.head.querySelector('meta[name="csrf-token"]') + return element.getAttribute("content") + } +} diff --git a/app/views/layouts/admin.html.erb b/app/views/layouts/admin.html.erb index 51ad0f5..9d886dc 100644 --- a/app/views/layouts/admin.html.erb +++ b/app/views/layouts/admin.html.erb @@ -66,15 +66,15 @@
- <%= yield %> -
- -
- <% flash.each do |type, msg| %> -
-

<%= msg %>

+
+
+ <% flash.each do |type, data| %> + <%= render NotificationComponent.new(type: type, data: data) %> + <% end %>
- <% end %> +
+ + <%= yield %>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index fb0f75e..b55c13a 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -57,16 +57,14 @@
-
+
<% flash.each do |type, data| %> <%= render NotificationComponent.new(type: type, data: data) %> diff --git a/tailwind.config.js b/tailwind.config.js index 7ccaee6..20a464f 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,5 +1,6 @@ module.exports = { content: [ + './app/assets/stylesheets/components/**/*.css', './app/components/**/*.html.erb', './app/components/**/*.rb', './app/views/**/*.html.erb',