From 1a562e38d04ca589dd123e0146e2215b6d92e55d Mon Sep 17 00:00:00 2001 From: Marvin Ahlgrimm Date: Tue, 27 Aug 2024 19:42:56 +0200 Subject: [PATCH] Rename `create_dom_id` to `dom_id` and make it more general --- docs/getting_started.md | 334 ++++++++++++++++++ src/marten_turbo/concerncs/dom_identifier.cr | 24 +- src/marten_turbo/template/tag/dom_id.cr | 2 +- src/marten_turbo/template/tag/turbo_stream.cr | 4 +- src/marten_turbo/turbo_stream.cr | 2 +- 5 files changed, 354 insertions(+), 12 deletions(-) create mode 100644 docs/getting_started.md diff --git a/docs/getting_started.md b/docs/getting_started.md new file mode 100644 index 0000000..dc22c69 --- /dev/null +++ b/docs/getting_started.md @@ -0,0 +1,334 @@ +# Getting Started + +This guide will walk you through building a simple example application that demonstrates the core features of Marten Turbo. + +## Prerequisites + +* Understanding of [Turbo](https://turbo.hotwired.dev/handbook/introduction) concepts. +* The Marten CLI installed (See [Installation Guide](./installation.md)). + + +## Starting a basic application + +### Model setup + +First we'll create a basic Marten Todo app. To quickly set this up we can leverage the Marten generators: + +```bash +marten new project turbo-blog && cd turbo-blog +shards install +marten gen model Todo task:string done:boolean +marten gen schema TodoSchema task:string +``` + +Change following line in `src/models/todo.cr`: + +```diff +- field :done, :bool ++ field :done, :bool, default: false +``` + +Now generate and execute the migrations: + +```bash +marten genmigrations +marten migrate +``` + +### Routes & Handler Setup + +First create following handlers: + +```ruby +# src/handlers/todo_handler.cr +class TodosHandler < Marten::Handlers::RecordList + template_name "todos/index.html" + model Todo + queryset Todo.order("-created_at") +end +``` + +```ruby +# src/handlers/todo_create_handler.cr +class TodoCreateHandler < Marten::Handlers::RecordCreate + model Todo + schema TodoSchema + template_name "todos/create.html" + success_route_name "todos:list" +end +``` + +```ruby +# src/handlers/todo_update_handler.cr +class TodoUpdateHandler < Marten::Handlers::RecordUpdate + model Todo + schema TodoSchema + template_name "todos/update.html" + success_route_name "todos:list" +end +``` + +```ruby +# src/handlers/todos_delete_handler.cr +class TodoDeleteHandler < Marten::Handlers::RecordDelete + model Todo + template_name "articles/delete.html" + success_route_name "todos:list" +end +``` + +And add these handlers to a namespaced route inside your `config/routes.cr`: + +```diff +# config/routes.cr ++ TODO_ROUTES = Marten::Routing::Map.draw do ++ path "/", TodosHandler, name: "list" ++ path "/create", TodoCreateHandler, name: "create" ++ path "/update/", TodoUpdateHandler, name: "update" ++ path "/delete/", TodoDeleteHandler, name: "delete" ++ end + +Marten.routes.draw do ++ path "/todos", TODO_ROUTES, name: "todos" +``` + +### Templates + +To keep this guide short, just copy & paste following templates: + +```html +{% extend "base.html" %} + +{% block content %} +

Todos

+ +
+ Create New Todo +
+ +
+{% for todo in records %} +
+
{{ todo.task }}
+
+ +
+ + +
+
+
+{% endfor %} +
+{% endblock %} +``` + +```html + +{% extend "base.html" %} + +{% block content %} +

Create Todo

+ +
+ {% include "todos/todo_form.html" with button_label="Create Todo" %} +
+{% endblock %} +``` + +```html +{% extend "base.html" %} + +{% block content %} +

Update Todo

+ +
+ {% include "todos/todo_form.html" with button_label="Update Todo" %} +
+{% endblock %} +``` + +```html +
+ + +
+ + + {% for error in schema.task.errors %}

{{ error.message }}

{% endfor %} +
+ + +
+``` + +This will allow you to view all todos and to create or update a todo. For more information about templates [visit the official marten documentation](https://martenframework.com/docs/templates/introduction){:target="_blank"}. + +## Add Turbo to your application + +To add a touch of SPA feeling to the app we can leverage [Hotwire Turbo](https://turbo.hotwired.dev/){:target="_blank"}. Additionally to install the Marten Turbo library [follow the installation instructions](installation.md). + +First you have to add the Marten Turbo library to your installed apps: + +```diff +# src/project.cr +require "sqlite3" ++ require "marten_turbo" +``` + +```diff +# config/settings/base.cr +- config.installed_apps = [] of Marten::Apps::Config.class ++ config.installed_apps = [ ++ MartenTurbo::App ++ ] of Marten::Apps::Config.class +``` + +Because Marten Turbo does include the Turbo JavaScript library you have to download the library, for example from (unpkg.com)[https://unpkg.com/browse/@hotwired/turbo@8.0.4/dist/] as refered from the official Turbo documentation. Save the esm library to `src/assets/js/turbo.js` and include it inside `src/templates/base.html`: + +```diff +- ++ + ++ + {% block title %}turbo-blog{% endblock %} + +``` + +Notice we also added `data-turbolinks-track="reload"` to our CSS link, which ensures that `css/app.css` will be reloaded when necessary. + + +Now we have to extend our record handlers `TodoCreateHandler` and `TodoUpdateHandler`: + +```diff +- class TodoCreateHandler < Marten::Handlers::RecordCreate ++ class TodoCreateHandler < MartenTurbo::Handlers::RecordCreate + model Todo + schema TodoSchema + template_name "todos/create.html" ++ turbo_stream_name "todos/create.turbo_stream.html" + success_route_name "todos:list" +end +``` + +```diff +- class TodoUpdateHandler < Marten::Handlers::RecordUpdate ++ class TodoUpdateHandler < MartenTurbo::Handlers::RecordUpdate + model Todo + schema TodoSchema + template_name "todos/update.html" ++ turbo_stream_name "todos/update.turbo_stream.html" + success_route_name "todos:list" +end +``` + +We changed two things in both handlers: we changed `Marten::Handlers` to `MartenTurbo::Handlers` and added a `turbo_stream_name` definition. + +Now let's change our views step by step. We start with `src/templates/todos/index.html`. + +```diff +{% extend "base.html" %} + +{% block content %} +

Todos

+ +
+- Create New Todo ++ ++ Create New Todo ++ +
+ ++ + +
+{% for todo in records %} +-
+-
{{ todo.task }}
+-
+- +-
+- +- +-
+-
+-
++ {% include "todos/todo.html" %} +{% endfor %} +
+{% endblock %} +``` + +We did a few things here: + +- Changed the `Create New Todo` button to have a turbo frame target `new_todo`. We used to template tag `dom_id` template helper which is provided by Marten Turbo. +- We added a new empty `new_todo` turbo frame where the content of the todo creation form is loaded into +- We moved the HTML content of a single todo into `todos/todo.html`. This way we can reuse the markup everywhere, also in turbo streams, and only need to change it in one place. + +The content of the new `src/templates/todos/todo.html` is similar to the one we've removed from `src/templates/todos/index.html`: + +```html + +
{{ todo.task }}
+
+ +
+ + +
+
+
+``` + +We wrapped the todo in a Turbo Frame and also utilized the `dom_id` helper to make each todo frame unique. + +To avoid re-loading the entire page when creating/updating a todo we'll also move the form inside a frame. + +```diff +{% extend "base.html" %} + +{% block content %} +

Create Todo

+ +{% url 'todos:create' as action %} + +-
++ +
+ {% include "todos/todo_form.html" with button_label="Create Todo", action=action %} +
+-
++ +{% endblock %} +``` + +UPDATE FORM!EWQEASD + +In addition we create turbo stream files, which were defined in the augmented handlers before, to return lightweight responses which update the UI without reloading the page: + +```html + +{% turbo_stream "prepend" "todos" do %} +{% include "todos/todo.html" with todo=record %} +{% end_turbo_stream %} + +{% turbo_stream "update" "new_todo" do %}{% end_turbo_stream %} +``` + +```html + +{% turbo_stream "replace" record do %} +{% include "todos/todo.html" with todo=record %} +{% end_turbo_stream %} +``` diff --git a/src/marten_turbo/concerncs/dom_identifier.cr b/src/marten_turbo/concerncs/dom_identifier.cr index e9d9809..489c63b 100644 --- a/src/marten_turbo/concerncs/dom_identifier.cr +++ b/src/marten_turbo/concerncs/dom_identifier.cr @@ -1,16 +1,22 @@ module MartenTurbo module Identifiable - def create_dom_id(value : Marten::Template::Value, prefix : Marten::Template::Value? = nil) - if value.raw.is_a? Marten::Model - create_dom_id(value.raw.as(Marten::Model), prefix) - else - dom_id = value.to_s - prefix ? "#{prefix}_#{dom_id}" : dom_id - end + @[Deprecated("Use `#dom_id` instead")] + def create_dom_id(value, prefix : String | Symbol | Nil = nil) + dom_id(value, prefix) + end + + def dom_id(value, prefix : String | Symbol | Nil = nil) + dom_id = value.to_s + prefix ? "#{prefix}_#{dom_id}" : dom_id + end + + @[Deprecated("Use `#dom_id` instead")] + def create_dom_id(value : Marten::Model, prefix : String | Symbol | Nil = nil) + dom_id(value, prefix) end - def create_dom_id(model : Marten::Model, prefix : Marten::Template::Value? = nil) - generate_id_for_model(model, prefix) + def dom_id(value : Marten::Model, prefix : String | Symbol | Nil = nil) + generate_id_for_model(value, prefix) end private def formatted_prefix(prefix) diff --git a/src/marten_turbo/template/tag/dom_id.cr b/src/marten_turbo/template/tag/dom_id.cr index a98d627..9622fcb 100644 --- a/src/marten_turbo/template/tag/dom_id.cr +++ b/src/marten_turbo/template/tag/dom_id.cr @@ -22,7 +22,7 @@ module MartenTurbo end def render(context : Marten::Template::Context) : String - create_dom_id @instance_name.resolve(context), @prefix.try(&.resolve(context)) + dom_id @instance_name.resolve(context).raw, @prefix.try(&.resolve(context).try(&.to_s)) end end end diff --git a/src/marten_turbo/template/tag/turbo_stream.cr b/src/marten_turbo/template/tag/turbo_stream.cr index f7883a3..fd4cb05 100644 --- a/src/marten_turbo/template/tag/turbo_stream.cr +++ b/src/marten_turbo/template/tag/turbo_stream.cr @@ -89,7 +89,9 @@ module MartenTurbo content = template.render(context) end - MartenTurbo::TurboStream.action(action, create_dom_id(@target_id.resolve(context)), content).to_s + target = @target_id.resolve(context) + + MartenTurbo::TurboStream.action(action, dom_id(target.raw), content).to_s end end end diff --git a/src/marten_turbo/turbo_stream.cr b/src/marten_turbo/turbo_stream.cr index 98338b9..55f5c28 100644 --- a/src/marten_turbo/turbo_stream.cr +++ b/src/marten_turbo/turbo_stream.cr @@ -40,7 +40,7 @@ module MartenTurbo # stream.replace("append", Message.get(pk: 1), "
Updated Message
") # ``` def action(action, target : Marten::Model, content) - target_id = create_dom_id(target) + target_id = dom_id(target) @streams << <<-TURBO_STREAM_TAG #{render_template_tag(content)}