diff --git a/Sources/App/Views/Employees/EmployeeForm.swift b/Sources/App/Views/Employees/EmployeeForm.swift index 0f8072b..a6640eb 100644 --- a/Sources/App/Views/Employees/EmployeeForm.swift +++ b/Sources/App/Views/Employees/EmployeeForm.swift @@ -17,7 +17,7 @@ struct EmployeeForm: HTML { } var content: some HTML { - Float(shouldDisplay: shouldShow, resetURL: "/employees") { + Float(shouldDisplay: shouldShow, resetURL: .employee(.index)) { form( employee == nil ? .hx.post(route: targetURL) : .hx.put(route: targetURL), .hx.target(target), @@ -64,9 +64,9 @@ struct EmployeeForm: HTML { private var target: HXTarget { guard let employee else { - return .employee(.table) + return .id(.employee(.table)) } - return .employee(.row(id: employee.id)) + return .id(.employee(.row(id: employee.id))) } private var buttonLabel: String { diff --git a/Sources/App/Views/Employees/EmployeeTable.swift b/Sources/App/Views/Employees/EmployeeTable.swift index 2066452..4e6ae33 100644 --- a/Sources/App/Views/Employees/EmployeeTable.swift +++ b/Sources/App/Views/Employees/EmployeeTable.swift @@ -15,7 +15,7 @@ struct EmployeeTable: HTML { .attributes( .style("padding: 0px 10px;"), .hx.get(route: .employee(.form)), - .hx.target(.float), + .hx.target(.id(.float)), .hx.swap(.outerHTML.transition(true).swap("0.5s")) ) } @@ -40,7 +40,7 @@ struct EmployeeTable: HTML { .attributes( .style("padding-left: 15px;"), .hx.get(route: .employee(.get(id: employee.id))), - .hx.target(.float), + .hx.target(.id(.float)), .hx.pushURL(true), .hx.swap(.outerHTML.transition(true).swap("0.5s")) ) diff --git a/Sources/App/Views/HTMXExtensions.swift b/Sources/App/Views/HTMXExtensions.swift index 268a5b4..005c909 100644 --- a/Sources/App/Views/HTMXExtensions.swift +++ b/Sources/App/Views/HTMXExtensions.swift @@ -22,92 +22,177 @@ extension HTMLAttribute.hx { } extension HTMLAttribute.hx { - static func target(_ target: HXTarget) -> HTMLAttribute { Self.target(target.selector) } } -extension HTMLAttribute where Tag: HTMLTrait.Attributes.Global { - static func id(_ target: HXTarget) -> Self { - id(target.id) +extension HTMLAttribute.hx where Tag: HTMLTrait.Attributes.Global { + + static func on(_ event: HXOnEvent, value: String) -> HTMLAttribute { + HTMLAttribute.custom(name: "hx-on::\(event.rawValue)", value: value) + } + + static func on(_ event: HXOnEvent, _ value: HXOnValue) -> HTMLAttribute { + on(event, value: value.value) + } + + static func on(_ event: HXOnEvent, _ values: HXOnValue...) -> HTMLAttribute { + on(event, value: values.value) } } -enum HXTarget { - case body - case employee(EmployeeKey) +enum HXOnEvent: String { + case afterRequest = "after-request" +} + +indirect enum HXOnValue { + case ifSuccessful([Self]) + case resetForm + case setWindowLocation(String) + case toggleContent(id: String) + + static func toggleContent(_ toggle: Toggle) -> Self { + toggleContent(id: toggle.rawValue) + } + + static func setWindowLocation(to route: ViewRoute) -> Self { + setWindowLocation(ViewRoute.router.path(for: route)) + } + + static func ifSuccessful(_ values: Self...) -> Self { + .ifSuccessful(values) + } + + fileprivate var value: String { + switch self { + case .resetForm: + return "this.reset();" + case let .toggleContent(toggle): + return "toggleContent('\(toggle)');" + case let .setWindowLocation(string): + return "window.location.href='\(string)';" + case let .ifSuccessful(values): + return "if(event.detail.successful) \(values.value)" + } + } + + enum Toggle: String { + case float + } +} + +extension Array where Element == HXOnValue { + + var value: String { + return map(\.value).joined(separator: " ") + } +} + +extension HTMLAttribute where Tag: HTMLTrait.Attributes.Global { + static func id(_ key: IDKey) -> Self { + id(key.description) + } +} + +enum IDKey: CustomStringConvertible { + case branch(Branch) + case employee(Employee) case float - case purchaseOrders(PurchaseOrdersKey? = nil) - case search + case purchaseOrder(PurchaseOrder? = nil) + case user(User) + case vendor(Vendor) + + var description: String { + switch self { + case let .branch(key): return "branch-\(key)" + case let .employee(key): return "employee-\(key)" + case .float: return "float" + case let .purchaseOrder(key): + guard let key else { return "purchase-order" } + return "purchase-order-\(key)" + case let .user(key): return "user-\(key)" + case let .vendor(key): return "vendor-\(key)" + } + } + + enum Branch: CustomStringConvertible { + case form + case row(id: VendorBranch.ID) + + var description: String { + switch self { + case .form: return "form" + case let .row(id): return id.uuidString + } + } + } + + enum Employee: CustomStringConvertible { + case table + case row(id: SharedModels.Employee.ID) + + var description: String { + switch self { + case .table: return "table" + case let .row(id): return "\(id)" + } + } + } + + enum PurchaseOrder: CustomStringConvertible { + case row(id: SharedModels.PurchaseOrder.ID) + case search + case table + + var description: String { + switch self { + case let .row(id): return "\(id)" + case .search: return "search" + case .table: return "table" + } + } + } + + enum User: CustomStringConvertible { + case form + case row(id: SharedModels.User.ID) + case table + + var description: String { + switch self { + case .form: return "form" + case .table: return "table" + case let .row(id): return "\(id)" + } + } + } + + enum Vendor: CustomStringConvertible { + case row(id: SharedModels.Vendor.ID) + + var description: String { + switch self { + case let .row(id): return "\(id)" + } + } + + } +} + +enum HXTarget: CustomStringConvertible { + case body + case id(IDKey) case this - case user(UserKey) var selector: String { switch self { case .body: return "body" + case let .id(key): return "#\(key)" case .this: return "this" - default: - return "#\(id)" } } - var id: String { - switch self { - case let .employee(key): return key.key - case .float: return "float" - case let .purchaseOrders(key): - guard let key else { return "purchase-orders" } - return key.key - case .search: return "search" - case let .user(key): return key.key - case .this, .body: - fatalError("'\(selector)' can not be used as an id.") - } - } + var description: String { selector } - enum PurchaseOrdersKey { - case table - case row(id: PurchaseOrder.ID) - - var key: String { - switch self { - case .table: return "purchase-orders-table" - case let .row(id): return "purchase_order_\(id)" - } - } - } - - enum EmployeeKey { - case table - case row(id: Employee.ID) - - var key: String { - switch self { - case .table: return "employee-table" - case let .row(id): return "employee_\(id.uuidString)" - } - } - } - - enum UserKey { - case form - case row(id: User.ID) - case table(Table? = nil) - - var key: String { - switch self { - case .form: return "user-form" - case let .row(id): return "user_\(id)" - case let .table(table): - let key = "user-table" - guard let table else { return key } - return "\(key)-\(table.rawValue)" - } - } - - enum Table: String { - case body - } - } } diff --git a/Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift b/Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift index 1df16c9..83b8ec5 100644 --- a/Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift +++ b/Sources/App/Views/PurchaseOrders/PurchaseOrderForm.swift @@ -16,7 +16,7 @@ struct PurchaseOrderForm: HTML { } var content: some HTML { - Float(shouldDisplay: shouldShow, resetURL: "/purchase-orders") { + Float(shouldDisplay: shouldShow, resetURL: .purchaseOrder(.index)) { if shouldShow { if purchaseOrder != nil { p { @@ -28,9 +28,9 @@ struct PurchaseOrderForm: HTML { } form( .hx.post(route: .purchaseOrder(.index)), - .hx.target(.purchaseOrders(.table)), + .hx.target(.id(.purchaseOrder(.table))), .hx.swap(.afterBegin), - .customToggleFloatAfterRequest + .hx.on(.afterRequest, .ifSuccessful(.toggleContent(.float))) ) { div(.class("row")) { label( diff --git a/Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift b/Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift index 3efefab..78c6f4d 100644 --- a/Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift +++ b/Sources/App/Views/PurchaseOrders/PurchaseOrderSearch.swift @@ -15,9 +15,9 @@ struct PurchaseOrderSearch: HTML { var content: some HTML { form( - .id(.search), + .id(.purchaseOrder(.search)), .hx.post(route: .purchaseOrder(.search(.index()))), - .hx.target(.purchaseOrders()), + .hx.target(.id(.purchaseOrder())), .hx.swap(.outerHTML) ) { div(.class("btn-row")) { @@ -31,7 +31,7 @@ struct PurchaseOrderSearch: HTML { select( .name("context"), .class("col-3"), .hx.get(route: .purchaseOrder(.search(.index()))), - .hx.target(.search), + .hx.target(.id(.purchaseOrder(.search))), .hx.swap(.outerHTML.transition(true).swap("0.5s")), .hx.pushURL(true) ) { diff --git a/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift b/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift index 5b79af8..7e688dc 100644 --- a/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift +++ b/Sources/App/Views/PurchaseOrders/PurchaseOrderTable.swift @@ -22,12 +22,12 @@ struct PurchaseOrderTable: HTML { } var content: some HTML { - table(.id(.purchaseOrders())) { + table(.id(.purchaseOrder())) { thead { buttonRow tableHeader } - tbody(.id(.purchaseOrders(.table))) { + tbody(.id(.purchaseOrder(.table))) { Rows(page: page) } } @@ -45,7 +45,7 @@ struct PurchaseOrderTable: HTML { if context != .search { Button.add() .attributes( - .hx.get(route: .purchaseOrder(.index)), .hx.target(.float), + .hx.get(route: .purchaseOrder(.index)), .hx.target(.id(.float)), .hx.swap(.outerHTML), .hx.pushURL(true) ) } @@ -100,7 +100,7 @@ struct PurchaseOrderTable: HTML { var content: some HTML { tr( - .id(.purchaseOrders(.row(id: purchaseOrder.id))) + .id(.purchaseOrder(.row(id: purchaseOrder.id))) ) { td { "\(purchaseOrder.id)" } td { purchaseOrder.workOrder != nil ? String(purchaseOrder.workOrder!) : "" } @@ -112,7 +112,7 @@ struct PurchaseOrderTable: HTML { Button.detail() .attributes( .hx.get(route: .purchaseOrder(.get(id: purchaseOrder.id))), - .hx.target("#float"), + .hx.target(.id(.float)), .hx.swap(.outerHTML.transition(true).swap("0.5s")), .hx.pushURL(true) ) diff --git a/Sources/App/Views/Users/UserDetail.swift b/Sources/App/Views/Users/UserDetail.swift index 3fbf982..f9d429b 100644 --- a/Sources/App/Views/Users/UserDetail.swift +++ b/Sources/App/Views/Users/UserDetail.swift @@ -9,13 +9,13 @@ struct UserDetail: HTML, Sendable { let user: User? var content: some HTML { - Float(shouldDisplay: user != nil, resetURL: "/users") { + Float(shouldDisplay: user != nil, resetURL: .user(.index)) { if let user { form( .hx.post(route: .user(.get(id: user.id))), .hx.swap(.outerHTML), - .hx.target(.user(.row(id: user.id))), - .custom(name: "hx-on::after-request", value: "toggleContent('float'); window.location.href='/users';") + .hx.target(.id(.user(.row(id: user.id)))), + .hx.on(.afterRequest, .toggleContent(.float)) ) { div(.class("row")) { makeLabel(for: "username", value: "Username:") @@ -39,9 +39,12 @@ struct UserDetail: HTML, Sendable { .hx.delete(route: .user(.get(id: user.id))), .hx.trigger(.event(.click)), .hx.swap(.outerHTML), - .hx.target(.user(.row(id: user.id))), + .hx.target(.id(.user(.row(id: user.id)))), .hx.confirm("Are you sure you want to delete this user?"), - .custom(name: "hx-on::after-request", value: "toggleContent('float'); window.location.href='/users';") + .hx.on( + .afterRequest, + .toggleContent(.float), .setWindowLocation(to: .user(.index)) + ) ) } } diff --git a/Sources/App/Views/Users/UserForm.swift b/Sources/App/Views/Users/UserForm.swift index fe87783..b5e77a0 100644 --- a/Sources/App/Views/Users/UserForm.swift +++ b/Sources/App/Views/Users/UserForm.swift @@ -24,10 +24,14 @@ struct UserForm: HTML, Sendable { .hx.pushURL(context.pushURL), .hx.target(context.target), .hx.swap(.outerHTML), - .custom( - name: "hx-on::after-request", - value: "if(event.detail.successful) this.reset(); toggleContent('float');" + .hx.on( + .afterRequest, + .ifSuccessful(.resetForm, .toggleContent(.float)) ) + // .custom( + // name: "hx-on::after-request", + // value: "if(event.detail.successful) this.reset(); toggleContent('float');" + // ) ) { if case let .login(next) = context, let next { input(.type(.hidden), .name("next"), .value(next)) @@ -46,9 +50,7 @@ struct UserForm: HTML, Sendable { if context.showConfirmPassword { div(.class("row")) { input( - .type(.password), - .id("confirmPassword"), - .name("confirmPassword"), + .type(.password), .id("confirmPassword"), .name("confirmPassword"), .placeholder("Confirm Password"), .required ) diff --git a/Sources/App/Views/Users/UserTable.swift b/Sources/App/Views/Users/UserTable.swift index e42c91e..54bb936 100644 --- a/Sources/App/Views/Users/UserTable.swift +++ b/Sources/App/Views/Users/UserTable.swift @@ -9,7 +9,7 @@ struct UserTable: HTML { let users: [User] var content: some HTML { - table(.id(.user(.table()))) { + table { thead { tr { th { "Username" } @@ -18,13 +18,13 @@ struct UserTable: HTML { Button.add() .attributes( .hx.get(route: .user(.form)), - .hx.target(.float), + .hx.target(.id(.float)), .hx.swap(.outerHTML) ) } } } - tbody(.id(.user(.table(.body)))) { + tbody(.id(.user(.table))) { for user in users { Row(user: user) } @@ -46,7 +46,7 @@ struct UserTable: HTML { td { Button.detail().attributes( .hx.get(route: .user(.get(id: user.id))), - .hx.target(.float), + .hx.target(.id(.float)), .hx.swap(.outerHTML), .hx.pushURL(true) ) diff --git a/Sources/App/Views/Utils/AttributeExtensions.swift b/Sources/App/Views/Utils/AttributeExtensions.swift deleted file mode 100644 index 83991e3..0000000 --- a/Sources/App/Views/Utils/AttributeExtensions.swift +++ /dev/null @@ -1,11 +0,0 @@ -import Elementary - -extension HTMLAttribute where Tag: HTMLTrait.Attributes.Global { - - static var customToggleFloatAfterRequest: Self { - .custom( - name: "hx-on::after-request", - value: "if(event.detail.successful) toggleContent('float')" - ) - } -} diff --git a/Sources/App/Views/Utils/Float.swift b/Sources/App/Views/Utils/Float.swift index 10ec012..cf459d7 100644 --- a/Sources/App/Views/Utils/Float.swift +++ b/Sources/App/Views/Utils/Float.swift @@ -1,4 +1,5 @@ import Elementary +import SharedModels struct Float: HTML { @@ -61,14 +62,17 @@ extension Float where B == DefaultCloseButton { init( id: String = "float", shouldDisplay: Bool, - resetURL: String? = nil, + resetURL route: ViewRoute? = nil, @HTMLBuilder body: () -> C ) { self.init( id: id, shouldDisplay: shouldDisplay, body: body, - closeButton: { DefaultCloseButton(id: id, resetURL: resetURL) } + closeButton: { DefaultCloseButton( + id: id, + resetURL: route != nil ? ViewRoute.router.path(for: route!) : nil + ) } ) } diff --git a/Sources/App/Views/Vendors/VendorDetail.swift b/Sources/App/Views/Vendors/VendorDetail.swift index 841bc52..13180ef 100644 --- a/Sources/App/Views/Vendors/VendorDetail.swift +++ b/Sources/App/Views/Vendors/VendorDetail.swift @@ -32,7 +32,8 @@ struct VendorDetail: HTML { .hx.post("/vendors/\(vendor.id)/branches"), .hx.target("#branches"), .hx.swap(.beforeEnd), - .custom(name: "hx-on::after-request", value: "if(event.detail.successful) this.reset();") + .hx.on(.afterRequest, .ifSuccessful(.resetForm)) + // .custom(name: "hx-on::after-request", value: "if(event.detail.successful) this.reset();") ) { input( .type(.text), .class("col-9"), .name("name"), .placeholder("Add branch..."), .required, @@ -65,12 +66,12 @@ struct VendorDetail: HTML { let branch: VendorBranch var content: some HTML { - li(.id("branch_\(branch.id)"), .class("branch-row")) { + li(.id(.branch(.row(id: branch.id))), .class("branch-row")) { span(.class("label")) { branch.name.capitalized } button( .class("btn"), .hx.delete(route: .vendorBranch(.delete(id: branch.id))), - .hx.target("#branch_\(branch.id)"), + .hx.target(.id(.branch(.row(id: branch.id)))), .hx.swap(.outerHTML.transition(true).swap("0.5s")) ) { img(.src("/images/trash-can.svg"), .width(30), .height(30), .style("margin-top: 5px;")) diff --git a/Sources/App/Views/Vendors/VendorTable.swift b/Sources/App/Views/Vendors/VendorTable.swift index bf82507..3858a01 100644 --- a/Sources/App/Views/Vendors/VendorTable.swift +++ b/Sources/App/Views/Vendors/VendorTable.swift @@ -16,7 +16,7 @@ struct VendorTable: HTML { .attributes( .style("padding: 0px 10px;"), .hx.get(route: .vendor(.form)), - .hx.target(.float), + .hx.target(.id(.float)), .hx.swap(.outerHTML) ) } @@ -34,7 +34,7 @@ struct VendorTable: HTML { let vendor: Vendor var content: some HTML { - tr(.id("vendor_\(vendor.id)")) { + tr(.id(.vendor(.row(id: vendor.id)))) { td { vendor.name.capitalized } td { "(\(vendor.branches?.count ?? 0)) Branches" } td { @@ -42,7 +42,7 @@ struct VendorTable: HTML { .attributes( .style("padding-left: 15px;"), .hx.get(route: .vendor(.get(id: vendor.id))), - .hx.target("#float"), + .hx.target(.id(.float)), .hx.pushURL(true), .hx.swap(.outerHTML) )