Skip to content

Commit

Permalink
Rename create_dom_id to dom_id and make it more general
Browse files Browse the repository at this point in the history
  • Loading branch information
treagod committed Aug 27, 2024
1 parent e8763f3 commit 1a562e3
Show file tree
Hide file tree
Showing 5 changed files with 354 additions and 12 deletions.
334 changes: 334 additions & 0 deletions docs/getting_started.md
Original file line number Diff line number Diff line change
@@ -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/<pk:int>", TodoUpdateHandler, name: "update"
+ path "/delete/<pk:int>", 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 %}
<h1>Todos</h1>

<div>
<a href="{% url 'todos:create' %}" class="button">Create New Todo</a>
</div>

<div id="todos">
{% for todo in records %}
<div class="todo card" style="display: flex; flex-direction: row; justify-content: space-between;align-items: center;">
<div class="todo-title">{{ todo.task }}</div>
<div class="todo-actions" style="display: flex; gap: 0.5rem;">
<a href="{% url 'todos:update' pk: todo.pk %}" class="action action-edit"><i class="bi bi-pencil"></i></a>
<form action="{% url 'todos:delete' pk: todo.pk %}" method="post">
<input type="hidden" name="csrftoken" value="{% csrf_token %}" />
<button class="action action-delete">
<i class="bi bi-trash3"></i>
</button>
</form>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
```

```html
<!-- src/templates/todos/create.html -->
{% extend "base.html" %}

{% block content %}
<h1>Create Todo</h1>

<div id="todo">
{% include "todos/todo_form.html" with button_label="Create Todo" %}
</div>
{% endblock %}
```

```html
{% extend "base.html" %}

{% block content %}
<h1>Update Todo</h1>

<div id="todo">
{% include "todos/todo_form.html" with button_label="Update Todo" %}
</div>
{% endblock %}
```

```html
<form class="card" action="" method="post" style="margin: 0 auto;">
<input type="hidden" name="csrftoken" value="{% csrf_token %}" />

<div class="field{% if schema.task.errored? %} field-error{% endif %}">
<label for="{{ schema.task.id }}">Title</label>
<input type="text" id="{{ schema.task.id }}" name="{{ schema.task.id }}" value="{{ schema.task.value }}"
autocomplete="off" required>
{% for error in schema.task.errors %}<p class="input-error">{{ error.message }}</p>{% endfor %}
</div>

<div class="field-actions">
<input class="button" type="submit" value="{{ button_label }}">
<a href="{% url 'todos:list' %}" class="button inverse">Back to Todos</a>
</div>
</form>
```

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/[email protected]/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
- <link rel="stylesheet" type="text/css" href="{% asset 'css/app.css' %}"/>
+ <link rel="stylesheet" type="text/css" href="{% asset 'css/app.css' %}" data-turbolinks-track="reload" />

+ <script type="module" src="{% asset 'js/turbo.js' %}"></script>
<title>{% block title %}turbo-blog{% endblock %}</title>
</head>
```

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 %}
<h1>Todos</h1>

<div>
- <a href="{% url 'todos:create' %}" class="button">Create New Todo</a>
+ <a href="{% url 'todos:create' %}" data-turbo-frame="{% dom_id 'new_todo' %}" class="button">
+ Create New Todo
+ </a>
</div>

+ <turbo-frame id="{% dom_id 'new_todo' %}"></turbo-frame>

<div id="todos">
{% for todo in records %}
- <div class="todo card" style="display: flex; flex-direction: row; justify-content: space-between;align-items: center;">
- <div class="todo-title">{{ todo.task }}</div>
- <div class="todo-actions" style="display: flex; gap: 0.5rem;">
- <a href="{% url 'todos:update' pk: todo.pk %}" class="action action-edit"><i class="bi bi-pencil"></i></a>
- <form action="{% url 'todos:delete' pk: todo.pk %}" method="post">
- <input type="hidden" name="csrftoken" value="{% csrf_token %}" />
- <button class="action action-delete">
- <i class="bi bi-trash3"></i>
- </button>
- </form>
- </div>
- </div>
+ {% include "todos/todo.html" %}
{% endfor %}
</div>
{% 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
<turbo-frame id="{% dom_id todo %}" class="todo card"
style="display: flex; flex-direction: row; justify-content: space-between;align-items: center;">
<div class="todo-title">{{ todo.task }}</div>
<div class="todo-actions" style="display: flex; gap: 0.5rem;">
<a href="{% url 'todos:update' pk: todo.pk %}" class="action action-edit"><i class="bi bi-pencil"></i></a>
<form action="{% url 'todos:delete' pk: todo.pk %}" method="post">
<input autofocus type="hidden" name="csrftoken" value="{% csrf_token %}" />
<button class="action action-delete">
<i class="bi bi-trash3"></i>
</button>
</form>
</div>
</turbo-frame>
```

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 %}
<h1>Create Todo</h1>

{% url 'todos:create' as action %}

- <div id="todo">
+ <turbo-frame id="{% dom_id 'new_todo' %}">
<div class="card" style="margin: 0 auto;">
{% include "todos/todo_form.html" with button_label="Create Todo", action=action %}
</div>
- </div>
+ </turbo-frame>
{% 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
<!-- src/templates/todos/create.turbo_stream.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
<!-- src/templates/todos/update.turbo_stream.html -->
{% turbo_stream "replace" record do %}
{% include "todos/todo.html" with todo=record %}
{% end_turbo_stream %}
```
24 changes: 15 additions & 9 deletions src/marten_turbo/concerncs/dom_identifier.cr
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/marten_turbo/template/tag/dom_id.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/marten_turbo/template/tag/turbo_stream.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/marten_turbo/turbo_stream.cr
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ module MartenTurbo
# stream.replace("append", Message.get(pk: 1), "<div>Updated Message</div>")
# ```
def action(action, target : Marten::Model, content)
target_id = create_dom_id(target)
target_id = dom_id(target)
@streams << <<-TURBO_STREAM_TAG
<turbo-stream action="#{action}" target="#{target_id}">
#{render_template_tag(content)}
Expand Down

0 comments on commit 1a562e3

Please sign in to comment.