mirror of
https://github.com/danieleteti/delphimvcframework.git
synced 2024-11-15 07:45:54 +01:00
New demo instant_search_with_htmx
This commit is contained in:
parent
0f1e8f79a6
commit
641b89cf36
89
samples/instant_search_with_htmx/BooksSearch.dpr
Normal file
89
samples/instant_search_with_htmx/BooksSearch.dpr
Normal file
@ -0,0 +1,89 @@
|
||||
program BooksSearch;
|
||||
|
||||
{$APPTYPE CONSOLE}
|
||||
|
||||
uses
|
||||
System.SysUtils,
|
||||
Web.ReqMulti,
|
||||
Web.WebReq,
|
||||
Web.WebBroker,
|
||||
IdContext,
|
||||
IdHTTPWebBrokerBridge,
|
||||
MVCFramework,
|
||||
MVCFramework.Logger,
|
||||
MVCFramework.DotEnv,
|
||||
MVCFramework.Commons,
|
||||
MVCFramework.Serializer.Commons,
|
||||
MVCFramework.Signal,
|
||||
Controllers.BooksU in 'Controllers.BooksU.pas',
|
||||
WebModuleU in 'WebModuleU.pas' {MyWebModule: TWebModule},
|
||||
FDConnectionConfigU in 'FDConnectionConfigU.pas';
|
||||
|
||||
{$R *.res}
|
||||
|
||||
procedure RunServer(APort: Integer);
|
||||
var
|
||||
LServer: TIdHTTPWebBrokerBridge;
|
||||
begin
|
||||
LServer := TIdHTTPWebBrokerBridge.Create(nil);
|
||||
try
|
||||
LServer.OnParseAuthentication := TMVCParseAuthentication.OnParseAuthentication;
|
||||
LServer.DefaultPort := APort;
|
||||
LServer.KeepAlive := dotEnv.Env('dmvc.indy.keep_alive', True);
|
||||
LServer.MaxConnections := dotEnv.Env('dmvc.webbroker.max_connections', 0);
|
||||
LServer.ListenQueue := dotEnv.Env('dmvc.indy.listen_queue', 500);
|
||||
LServer.Active := True;
|
||||
LogI('Listening on port ' + APort.ToString);
|
||||
LogI('Application started. Press Ctrl+C to shut down.');
|
||||
WaitForTerminationSignal;
|
||||
EnterInShutdownState;
|
||||
LServer.Active := False;
|
||||
finally
|
||||
LServer.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
begin
|
||||
{ Enable ReportMemoryLeaksOnShutdown during debug }
|
||||
// ReportMemoryLeaksOnShutdown := True;
|
||||
IsMultiThread := True;
|
||||
|
||||
// DMVCFramework Specific Configurations
|
||||
// When MVCSerializeNulls = True empty nullables and nil are serialized as json null.
|
||||
// When MVCSerializeNulls = False empty nullables and nil are not serialized at all.
|
||||
MVCSerializeNulls := True;
|
||||
|
||||
// MVCNameCaseDefault defines the name case of property names generated by the serializers.
|
||||
// Possibile values are: ncAsIs, ncUpperCase, ncLowerCase (default), ncCamelCase, ncPascalCase, ncSnakeCase
|
||||
MVCNameCaseDefault := TMVCNameCase.ncLowerCase;
|
||||
|
||||
// UseConsoleLogger defines if logs must be emitted to also the console (if available).
|
||||
UseConsoleLogger := True;
|
||||
|
||||
LogI('** DMVCFramework Server ** build ' + DMVCFRAMEWORK_VERSION);
|
||||
|
||||
CreateSqlitePrivateConnDef(True);
|
||||
|
||||
TMVCSqids.SQIDS_ALPHABET := dotEnv.Env('dmvc.sqids.alphabet', 'JUeQArjG4z2RgYIP7HTaBxuni9cWK60VytwsDoldEqvfkLbOhXmpS153N8MZFC');
|
||||
TMVCSqids.SQIDS_MIN_LENGTH := dotEnv.Env('dmvc.sqids.min_length', 6);
|
||||
|
||||
try
|
||||
if WebRequestHandler <> nil then
|
||||
WebRequestHandler.WebModuleClass := WebModuleClass;
|
||||
|
||||
WebRequestHandlerProc.MaxConnections := dotEnv.Env('dmvc.handler.max_connections', 1024);
|
||||
|
||||
{$IF CompilerVersion >= 34} //SYDNEY+
|
||||
if dotEnv.Env('dmvc.profiler.enabled', false) then
|
||||
begin
|
||||
Profiler.ProfileLogger := Log;
|
||||
Profiler.WarningThreshold := dotEnv.Env('dmvc.profiler.warning_threshold', 2000);
|
||||
end;
|
||||
{$ENDIF}
|
||||
|
||||
RunServer(dotEnv.Env('dmvc.server.port', 8080));
|
||||
except
|
||||
on E: Exception do
|
||||
LogF(E.ClassName + ': ' + E.Message);
|
||||
end;
|
||||
end.
|
1080
samples/instant_search_with_htmx/BooksSearch.dproj
Normal file
1080
samples/instant_search_with_htmx/BooksSearch.dproj
Normal file
File diff suppressed because it is too large
Load Diff
63
samples/instant_search_with_htmx/Controllers.BooksU.pas
Normal file
63
samples/instant_search_with_htmx/Controllers.BooksU.pas
Normal file
@ -0,0 +1,63 @@
|
||||
unit Controllers.BooksU;
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
MVCFramework, MVCFramework.Commons, MVCFramework.Serializer.Commons, System.Generics.Collections;
|
||||
|
||||
type
|
||||
[MVCPath]
|
||||
TBooksController = class(TMVCController)
|
||||
public
|
||||
[MVCPath]
|
||||
[MVCPath('/search')]
|
||||
function Search([MVCFromQueryString('query','')] SearchQueryText: String): String;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
System.StrUtils, System.SysUtils, MVCFramework.Logger,
|
||||
MVCFramework.ActiveRecord,
|
||||
MVCFramework.HTMX,
|
||||
MVCFramework.Middleware.ActiveRecord,
|
||||
Data.DB;
|
||||
|
||||
|
||||
{ TBooksController }
|
||||
|
||||
function TBooksController.Search(SearchQueryText: String): String;
|
||||
var
|
||||
lDS: TDataSet;
|
||||
lBaseSelect, lOrdering: String;
|
||||
begin
|
||||
SearchQueryText := SearchQueryText.ToLower;
|
||||
lBaseSelect := 'select id, book_name, author_name, genre from books';
|
||||
lOrdering := 'order by book_name COLLATE NOCASE';
|
||||
if SearchQueryText.IsEmpty then
|
||||
begin
|
||||
lDS := TMVCActiveRecord.SelectDataSet(lBaseSelect + ' ' + lOrdering, [], True);
|
||||
end
|
||||
else
|
||||
begin
|
||||
lDS := TMVCActiveRecord.SelectDataSet(
|
||||
lBaseSelect + ' where instr(lower(book_name), ?) > 0 or instr(lower(author_name), ?) > 0 or instr(lower(genre), ?) > 0 ' + lOrdering,
|
||||
[SearchQueryText, SearchQueryText, SearchQueryText], True);
|
||||
end;
|
||||
try
|
||||
ViewData['books'] := lDS;
|
||||
ViewData['books_count'] := lDS.RecordCount;
|
||||
if Context.Request.IsHTMX then
|
||||
begin
|
||||
Result := PageFragment(['search_results']);
|
||||
end
|
||||
else
|
||||
begin
|
||||
Result := Page(['index']);
|
||||
end;
|
||||
finally
|
||||
lDS.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
end.
|
226
samples/instant_search_with_htmx/FDConnectionConfigU.pas
Normal file
226
samples/instant_search_with_htmx/FDConnectionConfigU.pas
Normal file
@ -0,0 +1,226 @@
|
||||
unit FDConnectionConfigU;
|
||||
|
||||
interface
|
||||
|
||||
const
|
||||
CON_DEF_NAME = 'MyConnX';
|
||||
|
||||
procedure CreateFirebirdPrivateConnDef(AIsPooled: boolean);
|
||||
procedure CreateInterbasePrivateConnDef(AIsPooled: boolean);
|
||||
procedure CreateMySQLPrivateConnDef(AIsPooled: boolean);
|
||||
procedure CreateMSSQLServerPrivateConnDef(AIsPooled: boolean);
|
||||
procedure CreatePostgresqlPrivateConnDef(AIsPooled: boolean);
|
||||
procedure CreateSqlitePrivateConnDef(AIsPooled: boolean);
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
System.Classes,
|
||||
System.IOUtils,
|
||||
FireDAC.Comp.Client,
|
||||
FireDAC.Moni.Base,
|
||||
FireDAC.Moni.FlatFile,
|
||||
FireDAC.Stan.Intf,
|
||||
MVCFramework.Commons
|
||||
;
|
||||
|
||||
|
||||
var
|
||||
gFlatFileMonitor: TFDMoniFlatFileClientLink = nil;
|
||||
|
||||
procedure CreateMySQLPrivateConnDef(AIsPooled: boolean);
|
||||
var
|
||||
LParams: TStringList;
|
||||
begin
|
||||
{
|
||||
docker run --detach --env MARIADB_USER=example-user --env MARIADB_PASSWORD=my_cool_secret --env MARIADB_ROOT_PASSWORD=root -p 3306:3306 mariadb:latest
|
||||
}
|
||||
|
||||
LParams := TStringList.Create;
|
||||
try
|
||||
LParams.Add('Database=activerecorddb');
|
||||
LParams.Add('Protocol=TCPIP');
|
||||
LParams.Add('Server=localhost');
|
||||
LParams.Add('User_Name=root');
|
||||
LParams.Add('Password=root');
|
||||
LParams.Add('TinyIntFormat=Boolean'); { it's the default }
|
||||
LParams.Add('CharacterSet=utf8mb4'); // not utf8!!
|
||||
LParams.Add('MonitorBy=FlatFile');
|
||||
if AIsPooled then
|
||||
begin
|
||||
LParams.Add('Pooled=True');
|
||||
LParams.Add('POOL_MaximumItems=100');
|
||||
end
|
||||
else
|
||||
begin
|
||||
LParams.Add('Pooled=False');
|
||||
end;
|
||||
FDManager.AddConnectionDef(CON_DEF_NAME, 'MySQL', LParams);
|
||||
finally
|
||||
LParams.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure CreateMSSQLServerPrivateConnDef(AIsPooled: boolean);
|
||||
var
|
||||
LParams: TStringList;
|
||||
begin
|
||||
{
|
||||
docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=!SA_password!" -p 1433:1433 -d mcr.microsoft.com/mssql/server:2019-latest
|
||||
}
|
||||
|
||||
// [ACTIVERECORDB_SQLSERVER]
|
||||
// Database=activerecorddb
|
||||
// OSAuthent=Yes
|
||||
// Server=DANIELETETI\SQLEXPRESS
|
||||
// DriverID=MSSQL
|
||||
//
|
||||
|
||||
LParams := TStringList.Create;
|
||||
try
|
||||
LParams.Add('Database=activerecorddb');
|
||||
LParams.Add('OSAuthent=Yes');
|
||||
LParams.Add('Server=DANIELETETI\SQLEXPRESS');
|
||||
if AIsPooled then
|
||||
begin
|
||||
LParams.Add('Pooled=True');
|
||||
LParams.Add('POOL_MaximumItems=100');
|
||||
end
|
||||
else
|
||||
begin
|
||||
LParams.Add('Pooled=False');
|
||||
end;
|
||||
FDManager.AddConnectionDef(CON_DEF_NAME, 'MSSQL', LParams);
|
||||
finally
|
||||
LParams.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure CreateFirebirdPrivateConnDef(AIsPooled: boolean);
|
||||
var
|
||||
LParams: TStringList;
|
||||
begin
|
||||
LParams := TStringList.Create;
|
||||
try
|
||||
LParams.Add('Database=' + TPath.GetFullPath(TPath.Combine('..\..',
|
||||
'data\ACTIVERECORDDB.FDB')));
|
||||
LParams.Add('Protocol=TCPIP');
|
||||
LParams.Add('Server=localhost');
|
||||
LParams.Add('User_Name=sysdba');
|
||||
LParams.Add('Password=masterkey');
|
||||
LParams.Add('CharacterSet=UTF8');
|
||||
if AIsPooled then
|
||||
begin
|
||||
LParams.Add('Pooled=True');
|
||||
LParams.Add('POOL_MaximumItems=100');
|
||||
end
|
||||
else
|
||||
begin
|
||||
LParams.Add('Pooled=False');
|
||||
end;
|
||||
FDManager.AddConnectionDef(CON_DEF_NAME, 'FB', LParams);
|
||||
finally
|
||||
LParams.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure CreateInterbasePrivateConnDef(AIsPooled: boolean);
|
||||
var
|
||||
LParams: TStringList;
|
||||
begin
|
||||
LParams := TStringList.Create;
|
||||
try
|
||||
LParams.Add('Database=' + TPath.GetFullPath(TPath.Combine('..\..',
|
||||
'data\ACTIVERECORDDB.IB')));
|
||||
LParams.Add('Protocol=TCPIP');
|
||||
LParams.Add('Server=localhost');
|
||||
LParams.Add('User_Name=sysdba');
|
||||
LParams.Add('Password=masterkey');
|
||||
LParams.Add('CharacterSet=UTF8');
|
||||
if AIsPooled then
|
||||
begin
|
||||
LParams.Add('Pooled=True');
|
||||
LParams.Add('POOL_MaximumItems=100');
|
||||
end
|
||||
else
|
||||
begin
|
||||
LParams.Add('Pooled=False');
|
||||
end;
|
||||
FDManager.AddConnectionDef(CON_DEF_NAME, 'IB', LParams);
|
||||
finally
|
||||
LParams.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure CreatePostgresqlPrivateConnDef(AIsPooled: boolean);
|
||||
var
|
||||
LParams: TStringList;
|
||||
begin
|
||||
LParams := TStringList.Create;
|
||||
try
|
||||
LParams.Add('Database=activerecorddb');
|
||||
LParams.Add('Protocol=TCPIP');
|
||||
LParams.Add('Server=localhost');
|
||||
LParams.Add('User_Name=postgres');
|
||||
LParams.Add('Password=postgres');
|
||||
LParams.Add('MonitorBy=FlatFile');
|
||||
|
||||
// https://quality.embarcadero.com/browse/RSP-19755?jql=text%20~%20%22firedac%20guid%22
|
||||
LParams.Add('GUIDEndian=Big');
|
||||
if AIsPooled then
|
||||
begin
|
||||
LParams.Add('Pooled=True');
|
||||
LParams.Add('POOL_MaximumItems=100');
|
||||
end
|
||||
else
|
||||
begin
|
||||
LParams.Add('Pooled=False');
|
||||
end;
|
||||
FDManager.AddConnectionDef(CON_DEF_NAME, 'PG', LParams);
|
||||
finally
|
||||
LParams.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure CreateSqlitePrivateConnDef(AIsPooled: boolean);
|
||||
var
|
||||
LParams: TStringList;
|
||||
lFName: string;
|
||||
begin
|
||||
LParams := TStringList.Create;
|
||||
try
|
||||
lFName := TPath.Combine(TPath.GetDirectoryName(ParamStr(0)), dotEnv.Env('database_path', '..\..\data\books.db'));
|
||||
LParams.Add('Database=' + lFName);
|
||||
LParams.Add('StringFormat=Unicode');
|
||||
if AIsPooled then
|
||||
begin
|
||||
LParams.Add('Pooled=True');
|
||||
LParams.Add('POOL_MaximumItems=100');
|
||||
end
|
||||
else
|
||||
begin
|
||||
LParams.Add('Pooled=False');
|
||||
end;
|
||||
FDManager.AddConnectionDef(CON_DEF_NAME, 'SQLite', LParams);
|
||||
finally
|
||||
LParams.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
initialization
|
||||
|
||||
gFlatFileMonitor := TFDMoniFlatFileClientLink.Create(nil);
|
||||
gFlatFileMonitor.FileColumns := [tiRefNo, tiTime, tiThreadID, tiClassName, tiObjID, tiMsgText];
|
||||
gFlatFileMonitor.EventKinds := [
|
||||
ekVendor, ekConnConnect, ekLiveCycle, ekError, ekConnTransact,
|
||||
ekCmdPrepare, ekCmdExecute, ekCmdDataIn, ekCmdDataOut];
|
||||
gFlatFileMonitor.ShowTraces := False;
|
||||
gFlatFileMonitor.FileAppend := False;
|
||||
gFlatFileMonitor.FileName := TPath.ChangeExtension(ParamStr(0), '.trace.log');
|
||||
gFlatFileMonitor.Tracing := True;
|
||||
|
||||
finalization
|
||||
|
||||
gFlatFileMonitor.Free;
|
||||
|
||||
end.
|
11
samples/instant_search_with_htmx/WebModuleU.dfm
Normal file
11
samples/instant_search_with_htmx/WebModuleU.dfm
Normal file
@ -0,0 +1,11 @@
|
||||
object MyWebModule: TMyWebModule
|
||||
OnCreate = WebModuleCreate
|
||||
OnDestroy = WebModuleDestroy
|
||||
Actions = <>
|
||||
Height = 230
|
||||
Width = 415
|
||||
object FDPhysSQLiteDriverLink1: TFDPhysSQLiteDriverLink
|
||||
Left = 192
|
||||
Top = 96
|
||||
end
|
||||
end
|
89
samples/instant_search_with_htmx/WebModuleU.pas
Normal file
89
samples/instant_search_with_htmx/WebModuleU.pas
Normal file
@ -0,0 +1,89 @@
|
||||
unit WebModuleU;
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
System.SysUtils,
|
||||
System.Classes,
|
||||
Web.HTTPApp,
|
||||
MVCFramework, FireDAC.Stan.ExprFuncs, FireDAC.Phys.SQLiteWrapper.Stat,
|
||||
MVCFramework.View.Renderers.Mustache,
|
||||
FireDAC.Phys.SQLiteDef, FireDAC.Stan.Intf, FireDAC.Phys, FireDAC.Phys.SQLite;
|
||||
|
||||
type
|
||||
TMyWebModule = class(TWebModule)
|
||||
FDPhysSQLiteDriverLink1: TFDPhysSQLiteDriverLink;
|
||||
procedure WebModuleCreate(Sender: TObject);
|
||||
procedure WebModuleDestroy(Sender: TObject);
|
||||
private
|
||||
fMVC: TMVCEngine;
|
||||
end;
|
||||
|
||||
var
|
||||
WebModuleClass: TComponentClass = TMyWebModule;
|
||||
|
||||
implementation
|
||||
|
||||
{$R *.dfm}
|
||||
|
||||
uses
|
||||
System.IOUtils,
|
||||
MVCFramework.Commons,
|
||||
MVCFramework.Middleware.ActiveRecord,
|
||||
MVCFramework.Middleware.StaticFiles,
|
||||
MVCFramework.Middleware.Analytics,
|
||||
MVCFramework.Middleware.Trace,
|
||||
MVCFramework.Middleware.CORS,
|
||||
MVCFramework.Middleware.ETag,
|
||||
MVCFramework.Middleware.Compression, Controllers.BooksU, FDConnectionConfigU;
|
||||
|
||||
procedure TMyWebModule.WebModuleCreate(Sender: TObject);
|
||||
begin
|
||||
FMVC := TMVCEngine.Create(Self,
|
||||
procedure(Config: TMVCConfig)
|
||||
begin
|
||||
// session timeout (0 means session cookie)
|
||||
Config[TMVCConfigKey.SessionTimeout] := dotEnv.Env('dmvc.session_timeout', '0');
|
||||
//default content-type
|
||||
Config[TMVCConfigKey.DefaultContentType] := dotEnv.Env('dmvc.default.content_type', TMVCMediaType.TEXT_HTML);
|
||||
//default content charset
|
||||
Config[TMVCConfigKey.DefaultContentCharset] := dotEnv.Env('dmvc.default.content_charset', TMVCConstants.DEFAULT_CONTENT_CHARSET);
|
||||
//unhandled actions are permitted?
|
||||
Config[TMVCConfigKey.AllowUnhandledAction] := dotEnv.Env('dmvc.allow_unhandled_actions', 'false');
|
||||
//enables or not system controllers loading (available only from localhost requests)
|
||||
Config[TMVCConfigKey.LoadSystemControllers] := dotEnv.Env('dmvc.load_system_controllers', 'true');
|
||||
//default view file extension
|
||||
Config[TMVCConfigKey.DefaultViewFileExtension] := dotEnv.Env('dmvc.default.view_file_extension', 'html');
|
||||
//view path
|
||||
Config[TMVCConfigKey.ViewPath] := dotEnv.Env('dmvc.view_path', 'templates');
|
||||
//use cache for server side views (use "false" in debug and "true" in production for faster performances
|
||||
Config[TMVCConfigKey.ViewCache] := dotEnv.Env('dmvc.view_cache', 'false');
|
||||
//Max Record Count for automatic Entities CRUD
|
||||
Config[TMVCConfigKey.MaxEntitiesRecordCount] := dotEnv.Env('dmvc.max_entities_record_count', IntToStr(TMVCConstants.MAX_RECORD_COUNT));
|
||||
//Enable Server Signature in response
|
||||
Config[TMVCConfigKey.ExposeServerSignature] := dotEnv.Env('dmvc.expose_server_signature', 'false');
|
||||
//Enable X-Powered-By Header in response
|
||||
Config[TMVCConfigKey.ExposeXPoweredBy] := dotEnv.Env('dmvc.expose_x_powered_by', 'true');
|
||||
// Max request size in bytes
|
||||
Config[TMVCConfigKey.MaxRequestSize] := dotEnv.Env('dmvc.max_request_size', IntToStr(TMVCConstants.DEFAULT_MAX_REQUEST_SIZE));
|
||||
end);
|
||||
|
||||
// Controllers
|
||||
FMVC.AddController(TBooksController);
|
||||
// Controllers - END
|
||||
|
||||
// Middleware
|
||||
fMVC.AddMiddleware(TMVCStaticFilesMiddleware.Create('/static', TPath.Combine(ExtractFilePath(GetModuleName(HInstance)), 'www')));
|
||||
fMVC.AddMiddleware(TMVCActiveRecordMiddleware.Create(CON_DEF_NAME));
|
||||
// Middleware - END
|
||||
|
||||
fMVC.SetViewEngine(TMVCMustacheViewEngine);
|
||||
|
||||
end;
|
||||
|
||||
procedure TMyWebModule.WebModuleDestroy(Sender: TObject);
|
||||
begin
|
||||
FMVC.Free;
|
||||
end;
|
||||
|
||||
end.
|
33
samples/instant_search_with_htmx/bin/templates/index.html
Normal file
33
samples/instant_search_with_htmx/bin/templates/index.html
Normal file
@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Books Search</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org/dist/htmx.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<section class="section">
|
||||
<div class="columns">
|
||||
<div class="column is-one-third is-offset-one-third">
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="Search"
|
||||
name="query"
|
||||
hx-get="/search"
|
||||
hx-trigger="keyup changed delay:500ms"
|
||||
hx-target="#results"
|
||||
hx-push-url="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="column is-one-third">
|
||||
<p class="subtitle is-6">⭐ DMVCFramework + HTMX :: Instant Search Demo ⭐</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div id="results">{{>search_results}}</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,28 @@
|
||||
<p>{{books_count}} book/s found</p>
|
||||
<table class="table is-fullwidth is-striped is-hoverable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Book Title</th>
|
||||
<th>Book Author</th>
|
||||
<th>Genre</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#books}}
|
||||
<tr>
|
||||
<td>{{id}}</td>
|
||||
<td>{{book_name}}</td>
|
||||
<td>{{author_name}}</td>
|
||||
<td>{{genre}}</td>
|
||||
</tr>
|
||||
{{/books}}
|
||||
{{^books}}
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<span>No books found</span>
|
||||
</td>
|
||||
</tr>
|
||||
{{/books}}
|
||||
</tbody>
|
||||
</table>
|
Loading…
Reference in New Issue
Block a user