Polymorphic Association

Polymorphic Association

Eine polymorphe Beziehung ermöglicht es einem Modell, zu verschiedenen Typen anderer Modelle zu gehören. Hier zeige ich, wie man ein einzelnes Kommentarmodell dazu bringt, Artikeln, Fotos und Veranstaltungen zugeordnet zu werden.

Vorbereitung


Eine Rails-Anwendung mit Artikeln, Fotos und Veranstaltungen erstellen

In diesem Tutorial erstellen wir eine Rails-Anwendung mit drei unterschiedlichen Modellen:
  • Article für Artikel
  • Photo für Fotos
  • Event für Veranstaltungen
Alle drei Modelle sollen später Kommentare erhalten können. Statt für jeden Typ ein eigenes Kommentar-Modell anzulegen, verwenden wir dafür eine polymorphe Assoziation.
Dadurch kann ein einziges Comment-Model mit unterschiedlichen Modellen unserer Anwendung verknüpft werden.
Zunächst erstellen wir eine neue Rails-Anwendung und wechseln in das Projektverzeichnis:
$ rails new polymorphic_app
$ cd polymorphic_app
Anschließend erzeugen wir unsere drei Ressourcen:
$ rails g scaffold Article name content:text
$ rails g scaffold Photo name filename
$ rails g scaffold Event name starts_at:datetime ends_at:datetime description:text
Danach führen wir die erzeugten Migrationen aus:
$ rails db:migrate
Damit besitzen wir zunächst drei voneinander unabhängige Ressourcen, die wir über die vom Scaffold erzeugten Controller und Views verwalten können.

Bildverarbeitung vorbereiten

Für unsere Fotos möchten wir zusätzlich Active Storage verwenden. Dafür ergänzen wir das Gem für die Bildverarbeitung in unserer Gemfile:
# Gemfile

gem "image_processing", "~> 1.2"
Anschließend installieren wir die Abhängigkeiten:
$ bundle install
Falls Active Storage in der Anwendung noch nicht eingerichtet wurde, installieren wir außerdem die benötigten Active-Storage-Tabellen:
$ bin/rails active_storage:install
$ bin/rails db:migrate
Nun können wir unser Photo-Model mit einem Bild verknüpfen:
# app/models/photo.rb

class Photo < ApplicationRecord
  has_one_attached :image
end
Damit das Bild über das Formular übergeben werden kann, ergänzen wir :image bei den erlaubten Parametern des PhotosController:
# app/controllers/photos_controller.rb

def photo_params
  params.expect(photo: [:name, :filename, :image])
end
Damit steht unser Grundgerüst: Wir können Artikel, Fotos und Veranstaltungen verwalten und einem Foto über Active Storage ein Bild zuordnen.

Warum eine polymorphe Assoziation?

Als Nächstes möchten wir Benutzern ermöglichen, alle drei Arten von Inhalten zu kommentieren.
Eine Möglichkeit wäre, drei unterschiedliche Kommentar-Modelle anzulegen:
ArticleComment
PhotoComment
EventComment
Diese Modelle würden jedoch weitgehend dieselben Aufgaben übernehmen und dieselben Attribute besitzen. Wir würden damit praktisch dieselbe Kommentarfunktion mehrfach implementieren.
Stattdessen erstellen wir ein einziges Comment-Model und verbinden es polymorph mit unseren verschiedenen Modellen.
Ein Kommentar kann dadurch beispielsweise zu einem Article, einem Photo oder einem Event gehören, ohne dass wir für jeden dieser Typen ein eigenes Kommentar-Modell benötigen.
Im nächsten Schritt richten wir genau diese polymorphe Beziehung ein.

1.webp 241 KB

Ein einzelnes Kommentarmodell erstellen

Eine polymorphe Beziehung für Kommentare einrichten

Wir möchten Kommentare mit Article, Photo und Event verknüpfen, ohne für jedes dieser Modelle ein eigenes Kommentar-Modell erstellen zu müssen.
Dazu benötigen wir eine gemeinsame Bezeichnung für die Modelle, die kommentiert werden können. In unserem Fall nennen wir diese Beziehung:
commentable
Ein Kommentar gehört also zu einem kommentierbaren Objekt. Welches konkrete Model dahintersteht, entscheidet Rails anhand einer polymorphen Assoziation.
Wir können das Comment-Model einschließlich der polymorphen Referenz direkt mit dem Rails-Generator erstellen:
$ rails g model Comment content:text commentable:references{polymorphic}
Rails erzeugt dadurch für die polymorphe Beziehung zwei wichtige Datenbankspalten:
commentable_type
commentable_id
commentable_type speichert den Typ des zugehörigen Models, beispielsweise:
Article
Photo
Event
commentable_id enthält die ID des konkreten Datensatzes.
Ein Kommentar könnte intern beispielsweise folgende Zuordnung besitzen:
commentable_type: "Article"
commentable_id: 1
Rails weiß dadurch, dass dieser Kommentar zum Article mit der ID 1 gehört.

Die Migration

Die vom Generator erzeugte Migration enthält die polymorphe Referenz:
# db/migrate/XXXXXXXXXXXXXX_create_comments.rb

class CreateComments < ActiveRecord::Migration[8.0]
  def change
    create_table :comments do |t|
      t.text :content
      t.references :commentable, polymorphic: true, null: false

      t.timestamps
    end
  end
end
Die Zeile:
t.references :commentable, polymorphic: true, null: false
erzeugt sowohl commentable_type als auch commentable_id und legt den benötigten zusammengesetzten Index für die polymorphe Referenz an.
Wir müssen deshalb nicht separat Folgendes hinzufügen:
add_index :comments, %i[commentable_type commentable_id]
Anschließend erstellen wir die Tabelle:
$ rails db:migrate

Das Comment-Model

Auch das Comment-Model erhält durch den Generator bereits die passende belongs_to-Assoziation. Es sollte folgendermaßen aussehen:
# app/models/comment.rb

class Comment < ApplicationRecord
  belongs_to :commentable, polymorphic: true
end
commentable ist dabei keine eigene Modelklasse. Rails ermittelt über commentable_type und commentable_id, mit welchem Datensatz der Kommentar verbunden ist.
Haben wir beispielsweise einen Kommentar in der Variable comment, können wir einfach Folgendes aufrufen:
comment.commentable
Je nach Kommentar kann das Ergebnis ein Article, ein Photo oder ein Event sein.
Wir benötigen deshalb keine Aufrufe wie:
comment.article
comment.photo
comment.event
Die polymorphe Beziehung wird vollständig über commentable aufgelöst.

Die andere Seite der Beziehung einrichten

Jetzt müssen wir noch festlegen, dass unsere drei vorhandenen Models Kommentare besitzen können.
Im Article-Model ergänzen wir:
# app/models/article.rb

class Article < ApplicationRecord
  has_many :comments, as: :commentable, dependent: :destroy
end
Dasselbe machen wir für Veranstaltungen:
# app/models/event.rb

class Event < ApplicationRecord
  has_many :comments, as: :commentable, dependent: :destroy
end
Und schließlich ergänzen wir die Beziehung in unserem Photo-Model. Die bereits vorhandene Active-Storage-Assoziation bleibt dabei natürlich erhalten:
# app/models/photo.rb

class Photo < ApplicationRecord
  has_one_attached :image

  has_many :comments, as: :commentable, dependent: :destroy
end
Die Option:
as: :commentable
ist hierbei entscheidend. Sie sagt Rails, dass Article, Event beziehungsweise Photo die polymorphe Seite der commentable-Beziehung darstellen.
dependent: :destroy sorgt zusätzlich dafür, dass die zugehörigen Kommentare gelöscht werden, wenn beispielsweise ein Artikel gelöscht wird. Dadurch bleiben keine Kommentare zurück, deren ursprünglicher Inhalt nicht mehr existiert.

Die Beziehung verwenden

Jetzt können wir Kommentare direkt über die jeweilige Assoziation erstellen.
Bei einem Artikel funktioniert das beispielsweise so:
article = Article.first
comment = article.comments.create(content: "Mein erster Kommentar")
Rails setzt dabei die polymorphen Werte automatisch passend zum Artikel.
Wir müssen commentable_type und commentable_id also nicht selbst setzen.
Umgekehrt gelangen wir vom Kommentar wieder zu seinem zugehörigen Objekt:
comment.commentable
Damit haben wir ein einziges Comment-Model, das mit allen drei unterschiedlichen Models unserer Anwendung verwendet werden kann.

Polymorphe Beziehungen in der Anwendung verwenden

Kommentare als verschachtelte Ressource einrichten

Unsere Models sind nun miteinander verbunden. Als Nächstes möchten wir die Kommentare über unsere Anwendung aufrufen und verwalten können.
Dabei sollen URLs wie diese entstehen:
/articles/1/comments
/photos/1/comments
/events/1/comments
Alle drei URLs führen zum selben CommentsController. Anhand der übergebenen Parameter ermitteln wir dort, ob die Kommentare zu einem Artikel, einem Foto oder einer Veranstaltung gehören.
Zunächst erstellen wir den Controller:
$ rails g controller Comments index new

Verschachtelte Routes definieren

Kommentare gehören immer zu einem unserer kommentierbaren Objekte. Deshalb richten wir sie als verschachtelte Ressource ein:
# config/routes.rb

Rails.application.routes.draw do
  resources :photos do
    resources :comments
  end

  resources :events do
    resources :comments
  end

  resources :articles do
    resources :comments
  end

  root "articles#index"

  get "up" => "rails/health#show", as: :rails_health_check
end
Damit erzeugt Rails unter anderem passende Routen für die Kommentare unserer drei Ressourcen.
Wir können uns die erzeugten Routen im Terminal ansehen:
$ bin/rails routes
Für die Artikel finden wir beispielsweise Routen nach dem Muster:
/articles/:article_id/comments
Für Fotos:
/photos/:photo_id/comments
und für Veranstaltungen:
/events/:event_id/comments
Der entscheidende Unterschied liegt in den Parametern:
params[:article_id]
params[:photo_id]
params[:event_id]
Diese Parameter können wir verwenden, um im CommentsController das jeweilige kommentierbare Objekt zu laden.

Das kommentierbare Objekt dynamisch ermitteln

Wir verwenden einen before_action, damit das zugehörige Objekt vor den Aktionen des Controllers geladen wird:
# app/controllers/comments_controller.rb

class CommentsController < ApplicationController
  before_action :load_commentable

  def index
    @comments = @commentable.comments
  end

  def new
  end

  private

  def load_commentable
    if params[:article_id]
      @commentable = Article.find(params[:article_id])
    elsif params[:photo_id]
      @commentable = Photo.find(params[:photo_id])
    elsif params[:event_id]
      @commentable = Event.find(params[:event_id])
    end
  end

  def comment_params
    params.expect(comment: [:content])
  end
end
Besuchen wir beispielsweise:
/articles/1/comments
enthält params[:article_id] die ID des Artikels. Der Controller lädt entsprechend:
@commentable = Article.find(params[:article_id])
Bei:
/photos/1/comments
wird dagegen das entsprechende Photo geladen.
Anschließend funktioniert der Zugriff auf die Kommentare unabhängig davon, welcher Typ geladen wurde:
@comments = @commentable.comments
Genau darin liegt der Vorteil unserer polymorphen Beziehung: Ab diesem Punkt muss der restliche Controller nicht mehr unterscheiden, ob @commentable ein Article, Photo oder Event ist.

Kommentare anzeigen

Nun erstellen wir die Index-View:
<!-- app/views/comments/index.html.erb -->

<h1>Comments</h1>

<div id="comments">
  <% @comments.each do |comment| %>
    <div class="comment">
      <%= simple_format comment.content %>
    </div>
  <% end %>
</div>
Wenn wir jetzt beispielsweise:
/articles/1/comments
aufrufen, werden alle Kommentare des entsprechenden Artikels angezeigt.
Dasselbe funktioniert auch für:
/photos/1/comments
und:
/events/1/comments
Der CommentsController bleibt dabei derselbe. Lediglich das in @commentable gespeicherte Objekt ändert sich.

2.webp 32.2 KB

Kommentare hinzufügen

Dynamische Links für polymorphe Ressourcen verwenden

Unsere Kommentare können nun zu Artikeln, Fotos und Veranstaltungen gehören. Als Nächstes möchten wir einen Link anbieten, über den ein neuer Kommentar erstellt werden kann.
Bei einer ausschließlich für Fotos gedachten Kommentarfunktion könnten wir einen festen Route Helper verwenden:
<%= link_to "New Comment", new_photo_comment_path(@photo) %>
Unsere Kommentarfunktion soll jedoch mit allen drei Models funktionieren. Wir wissen innerhalb der gemeinsamen Comments-View nicht zwingend, ob @commentable gerade ein Article, Photo oder Event enthält.
Rails unterstützt dafür polymorphe Routen.
In unserer Index-View ergänzen wir:
<!-- app/views/comments/index.html.erb -->

<h1>Comments</h1>

<div id="comments">
  <% @comments.each do |comment| %>
    <div class="comment">
      <%= simple_format comment.content %>
    </div>
  <% end %>
</div>

<p>
  <%= link_to "New Comment", [:new, @commentable, :comment] %>
</p>
Das Array:
[:new, @commentable, :comment]
wird von Rails abhängig vom Typ von @commentable in die passende Route umgesetzt.
Enthält @commentable beispielsweise einen Artikel, entspricht das sinngemäß:
new_article_comment_path(@commentable)
Bei einem Foto wird daraus:
new_photo_comment_path(@commentable)
und bei einer Veranstaltung:
new_event_comment_path(@commentable)
Wir benötigen dadurch keine unterschiedlichen Views für unsere drei kommentierbaren Models.

Einen neuen Kommentar vorbereiten

Nun ergänzen wir die new-Action im CommentsController.
Da load_commentable bereits über unseren before_action ausgeführt wurde, können wir den neuen Kommentar direkt über die Assoziation erstellen:
# app/controllers/comments_controller.rb

def new
  @comment = @commentable.comments.new
end
Dadurch wird der neue Kommentar bereits dem richtigen @commentable zugeordnet.

Einen Kommentar speichern

Als Nächstes ergänzen wir die create-Action:
# app/controllers/comments_controller.rb

def create
  @comment = @commentable.comments.new(comment_params)

  if @comment.save
    redirect_to [@commentable, :comments], notice: "Comment created."
  else
    render :new, status: :unprocessable_entity
  end
end
Auch hier erstellen wir den Kommentar über:
@commentable.comments.new(comment_params)
Rails setzt dadurch automatisch die polymorphe Zuordnung.
Wir müssen also weder:
commentable_id
noch:
commentable_type
vom Benutzer entgegennehmen oder selbst über das Formular übergeben.
Unsere Strong Parameters enthalten deshalb ausschließlich den eigentlichen Kommentarinhalt:
def comment_params
  params.expect(comment: [:content])
end
Wird der Kommentar erfolgreich gespeichert, verwenden wir erneut eine polymorphe Route:
redirect_to [@commentable, :comments], notice: "Comment created."
Rails erzeugt daraus abhängig vom Typ von @commentable beispielsweise:
/articles/1/comments
oder:
/photos/1/comments
beziehungsweise:
/events/1/comments
Kann der Kommentar nicht gespeichert werden, zeigen wir das Formular erneut an:
render :new, status: :unprocessable_entity

Das Formular erstellen

Jetzt benötigen wir noch die View für einen neuen Kommentar.
In älteren Rails-Anwendungen wurde dafür häufig form_for verwendet. In unserer aktuellen Anwendung verwenden wir stattdessen form_with.
Auch hier können wir Rails die richtige verschachtelte Route anhand unserer beiden Objekte bestimmen lassen:
<!-- app/views/comments/new.html.erb -->

<h1>New Comment</h1>

<%= form_with model: [@commentable, @comment] do |form| %>
  <% if @comment.errors.any? %>
    <div class="error_messages">
      <h2>Please correct the following errors.</h2>

      <ul>
        <% @comment.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.text_area :content, rows: 8 %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>
Entscheidend ist auch hier das Array:
[@commentable, @comment]
Ist @commentable ein Artikel, erzeugt Rails das Formular für die verschachtelte Artikel-Kommentar-Route. Bei einem Foto oder einer Veranstaltung wird automatisch die jeweils passende Route verwendet.
Wir können dadurch dieselbe new-View und denselben CommentsController für alle drei Arten von Inhalten verwenden.
Rufen wir beispielsweise:
/photos/1/comments/new
auf, erstellen wir einen Kommentar für das entsprechende Foto.
Bei:
/articles/1/comments/new
wird dagegen derselbe Code verwendet, um einen Kommentar für einen Artikel zu erstellen.
Unsere polymorphe Beziehung funktioniert damit nicht nur auf Model-Ebene, sondern auch gemeinsam mit den verschachtelten Rails-Routen, dem Controller und unseren Formularen.

3.webp 30.5 KB

Kommentare direkt auf den Show-Seiten anzeigen

Die Kommentarfunktion in Partials aufteilen

Momentan besitzen unsere Kommentare eine eigene Index-Seite und eine eigene Seite zum Erstellen eines neuen Kommentars.
Für unsere Anwendung ist es jedoch komfortabler, die vorhandenen Kommentare und das Formular für einen neuen Kommentar direkt auf der jeweiligen Show-Seite eines Artikels, Fotos oder einer Veranstaltung anzuzeigen.
Damit wir denselben Code für alle drei Models verwenden können, lagern wir die Kommentaranzeige und das Formular in Partials aus.
Bevor wir das tun, ergänzen wir zunächst eine einfache Validierung in unserem Comment-Model:
# app/models/comment.rb

class Comment < ApplicationRecord
  belongs_to :commentable, polymorphic: true

  validates :content, presence: true
end
Dadurch können keine Kommentare ohne Inhalt gespeichert werden. Gleichzeitig kann unser Formular bei einem Fehler eine entsprechende Meldung anzeigen.

Das Formular in ein Partial verschieben

Unser bisheriges Formular verschieben wir aus new.html.erb in:
app/views/comments/_form.html.erb
Das Partial enthält:
<!-- app/views/comments/_form.html.erb -->

<%= form_with model: [@commentable, @comment] do |form| %>
  <% if @comment.errors.any? %>
    <div class="error_messages">
      <h2>Please correct the following errors.</h2>

      <ul>
        <% @comment.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.text_area :content, rows: 8 %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>
Unsere new-View wird dadurch deutlich kürzer:
<!-- app/views/comments/new.html.erb -->

<h1>New Comment</h1>

<%= render "form" %>
Die separate Seite zum Erstellen eines Kommentars funktioniert damit weiterhin. Gleichzeitig können wir dasselbe Formular nun auch an anderen Stellen unserer Anwendung einbinden.

Einzelne Kommentare als Partial darstellen

Auch die Darstellung eines Kommentars lagern wir in ein Partial aus:
<!-- app/views/comments/_comment.html.erb -->

<div class="comment">
  <%= simple_format comment.content %>
</div>
Wichtig ist, dass dieses Partial einen einzelnen Kommentar darstellt.
Dadurch können wir eine Collection von Kommentaren direkt mit Rails rendern:
<div id="comments">
  <%= render @comments %>
</div>
Rails verwendet für jedes Element aus @comments automatisch:
app/views/comments/_comment.html.erb
und stellt innerhalb des Partials den jeweiligen Datensatz als lokale Variable comment zur Verfügung.
Unsere bisherige Index-View können wir deshalb ebenfalls vereinfachen:
<!-- app/views/comments/index.html.erb -->

<h1>Comments</h1>

<div id="comments">
  <%= render @comments %>
</div>

<p>
  <%= link_to "New Comment", [:new, @commentable, :comment] %>
</p>

Kommentare auf der Artikel-Seite anzeigen

Nun integrieren wir die Kommentarfunktion direkt in die Show-Seite eines Artikels.
Unsere vom Scaffold erzeugte View erweitern wir folgendermaßen:
<!-- app/views/articles/show.html.erb -->

<%= render @article %>

<div>
  <%= link_to "Edit this article", edit_article_path(@article) %> |
  <%= link_to "Back to articles", articles_path %>

  <%= button_to "Destroy this article", @article, method: :delete %>
</div>

<h2>Comments</h2>

<div id="comments">
  <%= render @comments %>
</div>

<%= render "comments/form" %>
Damit diese beiden Partials funktionieren, müssen wir in der show-Action die benötigten Objekte vorbereiten.
Wir ergänzen den ArticlesController:
# app/controllers/articles_controller.rb

def show
  @commentable = @article
  @comments = @commentable.comments
  @comment = @commentable.comments.new
end
@commentable enthält auf dieser Seite unseren Artikel:
@commentable = @article
Über:
@comments = @commentable.comments
laden wir seine vorhandenen Kommentare.
Für das Formular erzeugen wir zusätzlich einen neuen Kommentar:
@comment = @commentable.comments.new
Da dieser Kommentar bereits über die Assoziation erzeugt wird, ist er direkt unserem Artikel zugeordnet.

Nach dem Erstellen zur Show-Seite zurückkehren

Bisher leitet unser CommentsController nach einem erfolgreich erstellten Kommentar auf die Kommentarübersicht weiter:
redirect_to [@commentable, :comments], notice: "Comment created."
Da sich unsere Kommentarfunktion jetzt direkt auf der Show-Seite befindet, möchten wir nach dem Speichern dorthin zurückkehren.
Wir ändern deshalb die create-Action:
# app/controllers/comments_controller.rb

def create
  @comment = @commentable.comments.new(comment_params)

  if @comment.save
    redirect_to @commentable, notice: "Comment created."
  else
    render :new, status: :unprocessable_entity
  end
end
Bei erfolgreichem Speichern reicht:
redirect_to @commentable, notice: "Comment created."
Rails erkennt anhand des Objekts automatisch die passende Show-Route.
Bei einem Artikel führt die Weiterleitung beispielsweise zu:
/articles/1
bei einem Foto zu:
/photos/1
und bei einer Veranstaltung zu:
/events/1
Schlägt die Validierung fehl, wird das Formular erneut angezeigt und die Validierungsfehler können direkt ausgegeben werden.

Dasselbe Prinzip für Fotos und Veranstaltungen

Unsere Partials sind nicht an Article gebunden. Wir können deshalb dieselbe Kommentarfunktion auch in den Show-Seiten von Photo und Event verwenden.
In den jeweiligen show-Actions benötigen wir dieselben drei Variablen.
Für Fotos:
# app/controllers/photos_controller.rb

def show
  @commentable = @photo
  @comments = @commentable.comments
  @comment = @commentable.comments.new
end
Für Veranstaltungen:
# app/controllers/events_controller.rb

def show
  @commentable = @event
  @comments = @commentable.comments
  @comment = @commentable.comments.new
end
Anschließend ergänzen wir sowohl in:
app/views/photos/show.html.erb
als auch in:
app/views/events/show.html.erb
denselben Kommentarbereich:
<h2>Comments</h2>

<div id="comments">
  <%= render @comments %>
</div>

<%= render "comments/form" %>
Damit steht uns dieselbe Inline-Kommentarfunktion für alle drei Models zur Verfügung.
Ein Article, Photo und Event verwendet jeweils dieselben Comment-Datensätze, denselben CommentsController und dieselben Kommentar-Partials. Welchem Objekt ein Kommentar tatsächlich gehört, verwaltet Rails über unsere polymorphe commentable-Assoziation.

5.webp 84.8 KB

Kommentare gestalten

Ein einfaches Styling für die Kommentarfunktion hinzufügen

Unsere polymorphe Kommentarfunktion ist jetzt vollständig eingerichtet. Zum Abschluss können wir die Darstellung der Kommentare und des Formulars noch etwas übersichtlicher gestalten.
In älteren Rails-Anwendungen wurde dafür häufig Sass beziehungsweise SCSS zusammen mit sassc-rails verwendet. Für unsere Kommentarfunktion benötigen wir jedoch keine speziellen Sass-Funktionen. Die wenigen Styles können wir deshalb mit normalem CSS umsetzen.
Wir erstellen eine eigene Stylesheet-Datei:
/* app/assets/stylesheets/comments.css */

#comments {
  max-width: 500px;
  margin: 1.5rem 0;
  padding: 0 1.25rem;
  border: 1px solid #777;
  background: #eee;
}

.comment {
  padding: 1rem 0;
  border-bottom: 1px solid #999;
}

.comment:last-child {
  border-bottom: 0;
}

.comment p {
  margin: 0;
}

.comment-form {
  max-width: 500px;
  margin-top: 1.5rem;
}

.comment-form textarea {
  box-sizing: border-box;
  width: 100%;
  min-height: 8rem;
  padding: 0.75rem;
  resize: vertical;
}

.error-messages {
  margin-bottom: 1rem;
}
Damit wir auch das Formular gezielt gestalten können, ergänzen wir eine umschließende Klasse in unserem Formular-Partial:
<!-- app/views/comments/_form.html.erb -->

<div class="comment-form">
  <%= form_with model: [@commentable, @comment] do |form| %>
    <% if @comment.errors.any? %>
      <div class="error-messages">
        <h2>Please correct the following errors.</h2>

        <ul>
          <% @comment.errors.full_messages.each do |message| %>
            <li><%= message %></li>
          <% end %>
        </ul>
      </div>
    <% end %>

    <div class="field">
      <%= form.text_area :content, rows: 8 %>
    </div>

    <div class="actions">
      <%= form.submit %>
    </div>
  <% end %>
</div>
Die Darstellung eines einzelnen Kommentars bleibt unverändert:
<!-- app/views/comments/_comment.html.erb -->

<div class="comment">
  <%= simple_format comment.content %>
</div>
Da wir die Kommentare als Collection rendern:
<div id="comments">
  <%= render @comments %>
</div>
erhält jeder einzelne Kommentar automatisch die Klasse:
comment
und kann über unser Stylesheet gestaltet werden.

Das Stylesheet einbinden

Wie Stylesheets eingebunden werden, hängt vom Asset-Setup der Rails-Anwendung ab.
Wenn unsere Anwendung die klassische Asset Pipeline verwendet und application.css die Stylesheets des Verzeichnisses automatisch einbindet, reicht es aus, comments.css unter:
app/assets/stylesheets/comments.css
abzulegen.
Verwendet unsere Anwendung stattdessen explizite CSS-Imports, binden wir comments.css entsprechend über das vorhandene Stylesheet-Setup ein.
Für die polymorphe Beziehung selbst spielt das verwendete CSS-System keine Rolle. Unsere Models, Routes, Controller und Views funktionieren unabhängig davon.

Das Ergebnis

Damit besitzen wir jetzt eine Kommentarfunktion, die wir für verschiedene Models wiederverwenden können.
Ein Kommentar gehört über:
belongs_to :commentable, polymorphic: true
zu einem beliebigen kommentierbaren Objekt.
Unsere drei Models stellen die andere Seite der Beziehung bereit:
has_many :comments, as: :commentable, dependent: :destroy
Rails speichert dabei automatisch, welcher Typ von Objekt kommentiert wurde und welcher konkrete Datensatz gemeint ist.
Dadurch benötigen wir nicht:
ArticleComment
PhotoComment
EventComment
sondern lediglich:
Comment
Dasselbe Comment-Model und dieselben Partials können damit für Artikel, Fotos und Veranstaltungen verwendet werden.
Weitere kommentierbare Models können später nach demselben Prinzip ergänzt werden, ohne dass wir dafür ein neues Kommentar-Modell entwickeln müssen.
Meld dich an und schreibe ein Kommentar