Skip to content

Instantly share code, notes, and snippets.

@marcoarthur
Last active June 14, 2024 16:22
Show Gist options
  • Save marcoarthur/d5a3c3433aed43f58cb2c6caff434bff to your computer and use it in GitHub Desktop.
Save marcoarthur/d5a3c3433aed43f58cb2c6caff434bff to your computer and use it in GitHub Desktop.
Mojolicious::Lite example with HTMX
use warnings;
use strict;
package App::Model::Product {
use Mojo::Base 'Mojo::EventEmitter', -strict, -signatures;
use Data::Fake qw/Core Names Text Dates/;
our %generated;
has [qw(name image description price id)];
has _fake_data => sub ($self) {
state $generator = fake_hash(
{
id => fake_digits('###-##-####'),
name => fake_name(),
description => fake_sentences(1),
created_at => fake_past_datetime("%Y-%m-%d"),
#image => "https://picsum.photos/200",
image => fake_template('%s.jpg', fake_pick(qw(image product))),
price => fake_template("%.2f", fake_float(1.0, 1200.0)),
details => fake_paragraphs(),
}
);
};
sub get_fake_data($self) {
my $fk = $self->_fake_data->();
$self->emit( new_data => $generated{$fk->{id}} = $fk );
return $fk;
}
sub from_id($self, $id) { return $generated{$id}; }
1;
}
# Mojolicious App
package main;
use Mojolicious::Lite -strict, -signatures;
our $prod_model = App::Model::Product->new; #fake product
$prod_model->on(
new_data => sub ($e, $product){
app->log->debug( "New Fake product with id $product->{id}" );
}
);
helper publish_prod => sub ($c, $id = undef) {
$id = $id ? $id : $c->param('id');
my $prod = $prod_model->from_id($id);
return $c->render( template => 'not_found_product', id => $id ) unless $prod;
$c->stash(%$prod);
return $c->render( template => 'product' );
};
helper generate_prod => sub($c, $total = 1) {
return map { $prod_model->get_fake_data } 1..$total;
};
get '/products' => sub ($c) {
my @products = $c->generate_prod(10);
$c->render(
template => 'products',
layout => 'products',
products => [@products],
);
};
get '/product' => sub ($c) { $c->publish_prod; };
app->start;
__DATA__
@@styles.css
body {
font-family: Arial, sans-serif;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.product-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
max-width: 100%;
padding: 10px;
box-sizing: border-box;
}
.product-list-h1 {
display: flex;
}
.product-card {
background-color: #f4f4f4;
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
flex: 0 1 280px;
display: flex;
flex-direction: column;
justify-content: space-between;
box-sizing: border-box;
cursor: pointer;
transition: transform 0.2s;
}
.product-card:hover {
transform: scale(1.05);
}
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: none;
justify-content: center;
align-items: center;
z-index: 10;
}
.overlay.active {
display: flex;
}
.highlighted {
background-color: #fff;
border: 2px solid #000;
padding: 20px;
z-index: 20;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
max-width: 100%;
}
#highlighted-product {
width: 80%;
}
.product-image {
max-width: 100%;
height: auto;
border-radius: 5px;
}
.product-name {
font-size: 24px;
margin: 10px 0;
}
.product-description {
font-size: 16px;
margin: 10px 0;
}
.product-price {
font-size: 20px;
color: #28a745;
margin: 10px 0;
}
.product-details {
font-size: 18px;
max-width: 80%;
margin: 10px auto;
}
@@products.html.ep
<h1 class="product-list-h1">Product List Page</h1>
<div class="product-list">
% for my $p (@$products) {
%= include 'product-card', %$p
% }
</div>
<div id="overlay" class="overlay">
<div id="highlighted-product"></div>
</div>
<script>
document.addEventListener('htmx:afterSwap', function(evt) {
if (evt.detail.target.id === 'highlighted-product') {
document.getElementById('overlay').classList.add('active');
}
});
document.getElementById('overlay').addEventListener('click', function(evt) {
if (evt.target === this) {
this.classList.remove('active');
document.getElementById('highlighted-product').innerHTML = '';
}
});
function closeOverlay() {
document.getElementById('overlay').classList.remove('active');
document.getElementById('highlighted-product').innerHTML = '';
}
</script>
@@product.html.ep
<div class="highlighted">
<img src="<%= $image %>" alt="<%= $name %>" class="product-image">
<h2 class="product-name"><%= $name %></h2>
<p class="product-description"><%= $description %></p>
<p class="product-price">$<%= $price %></p>
<p class="product-details"><%= $details %></p>
<div class="product-buttons">
<button>Buy Now</button>
<button onclick="closeOverlay()">Close</button>
</div>
</div>
@@product-card.html.ep
<div class="product-card"
hx-get="/product?id=<%= $id %>"
hx-target="#highlighted-product"
>
<img src="<%= $image %>" alt="<%= $name %>" class="product-image">
<h2 class="product-name"><%= $name %></h2>
<p class="product-description"><%= $description %></p>
<p class="product-price">$<%= $price %></p>
</div>
@@not_found_product.html.ep
<div class="product-not-found">
<p> NOT FOUND a product with <%= $id ? $id : 'undefined' %> id </p>
</div>
@@layouts/products.html.ep
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="/styles.css">
<script src="/htmx.min.js"></script>
</head>
<body>
<%= content %>
</body>
</html>
@marcoarthur
Copy link
Author

Just a simple example in using HTMX for UIX and Mojo backend. This seems to me simpler and more intuitive than the Svelte + Mojo, because of no generated (transpiled) JS code, still with most of capabilities.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment