import { Component } from '@angular/core';
import { WrComment, WrCommentComposer } from 'ngwr/comment';
@Component({ imports: [WrComment, WrCommentComposer] })
export class MyComponent {}Comment
A discussion row with replies, a chat bubble, and a composer that sends on Enter.
Installation
Import the class, and @use the entry point in your global styles. Nothing warns you when the @use is missing.
Thread
Project the avatar, the body, the actions and the replies. The app owns the data and formats the time; datetime makes it a real <time>.
<wr-comment author="Ann Lee" time="2 hours ago" [datetime]="posted">
<wr-avatar size="2.5rem" alt="">AL</wr-avatar>
Looks good. One question on the empty state below.
<div wrCommentActions>
<button wr-btn size="sm" outlined>Reply</button>
</div>
<wr-comment author="Bob Ray" time="an hour ago">
<wr-avatar size="2rem" alt="">BR</wr-avatar>
It falls back to the placeholder, same as the input.
</wr-comment>
</wr-comment>Chat
variant="bubble" draws a message; side="end" is the reader's own. Both sides follow the writing direction. The composer below sends on Enter.
<div role="log" aria-label="Chat">
@for (m of messages(); track m.id) {
<wr-comment variant="bubble" [side]="m.own ? 'end' : 'start'" [time]="m.at | wrDate: 'time'" [datetime]="m.at">
{{ m.text }}
</wr-comment>
}
</div>
<wr-comment-composer placeholder="Message" [(value)]="draft" (send)="post($event)" />Composer
sending holds the button while a request is out. With clearOnSend off the text stays until the app clears it, so a failed request loses nothing.
<wr-comment-composer
placeholder="Reply"
[sending]="posting()"
[clearOnSend]="false"
[(value)]="draft"
(send)="postSlowly($event)"
/>| Keys | Does |
|---|---|
Enter | Sends. With [enterSends]="false" it adds a line instead. |
Shift+Enter | Adds a line. |
Ctrl+Enter, Cmd+Enter | Sends, whatever Enter does. |
| Enter during IME composition | Confirms the composition and never sends. |
Forms
The composer is a value control: bind it with [formField], [(value)] or any Angular form.
<wr-comment-composer placeholder="Message" [formField]="form.draft" (send)="post($event)" />Accessibility
- Each comment is an
articlenamed after itsauthor. - Replies sit in a
groupnamedrepliesLabel, from thecomment.repliescatalog key. - No heading level is assumed. Put a chat in
role="log"so new messages are announced. - The send button is named
sendLabel, fromcomment.send. Focus stays in the field after a send.
API
<wr-comment> inputs.
| Name | Description | Type | Default |
|---|---|---|---|
author | Who wrote it. Also the article's accessible name. Leave it empty when a [wrCommentAuthor] element draws the name instead. | string | '' |
time | When it was written, already formatted: the component does no date formatting. Run a Date through the wrDate pipe on the way in. | string | '' |
datetime | The same moment, machine-readable. With it the time renders as a <time datetime>: a Date is written as ISO 8601, a string as given. | Date | string | null | null |
variant | row is a discussion entry; bubble is a chat message, which takes no replies. | WrCommentVariant | 'row' |
side | Which inline side a bubble sits on. end is the reader's own message. Ignored by the row variant. | WrCommentSide | 'start' |
repliesLabel | Accessible name of the replies group. Falls back to comment.replies, then 'Replies'. | string | null | null |
Composer API
<wr-comment-composer> inputs and outputs.
| Name | Description | Type | Default |
|---|---|---|---|
placeholder | Native placeholder text. | string | '' |
ariaLabel | Accessible name for the field. Falls back to the placeholder: the native <textarea> lives inside the component, so a <label for> outside cannot reach it. | string | null | null |
sendLabel | Accessible name of the send button. Falls back to comment.send, then 'Send'. | string | null | null |
rows | Visible row count while the field is empty. | number | 1 |
maxRows | Rows the field grows to before it scrolls on its own. | number | 6 |
disabled | Disable the field and the button. Bound automatically from the field's disabled state when used with [formField]. The text is kept. | boolean | false |
readonly | Read-only state: the text is shown and announced, keeps its tab stop and cannot be edited or sent. | boolean | false |
sending | A send is in flight: the button is disabled, the host is aria-busy and Enter does nothing. The field stays editable and the text is kept. | boolean | false |
enterSends | true: Enter sends and Shift+Enter adds a line. false: Enter adds a line and only Ctrl+Enter or Cmd+Enter sends. | boolean | true |
clearOnSend | Clear the field before send emits. With false the text stays until the app clears the model itself. | boolean | true |
value | The draft. Bound by [formField], or two-way via [(value)]. | string | '' |
(touch) | Emitted on blur so a bound field can mark itself touched. | void | — |
(send) | The trimmed text, once per send. Where it goes is the app's business. | string | — |
CSS variables
Set these on the element itself, with a rule more specific than its Declared on selector. In a global stylesheet, put a class of yours on an ancestor it really has: <body> for an overlay panel, which renders outside your markup. In a component stylesheet the selector alone works, but only for an element in that component's own template. A value on a wrapper or :root is shadowed, because ngwr/comment declares each default on its own element.
| Variable | Default | Declared on |
|---|---|---|
--wr-comment-actions-gap | var(--wr-space-sm) | .wr-comment |
--wr-comment-author-color | var(--wr-color-on-surface) | .wr-comment |
--wr-comment-author-font-weight | var(--wr-font-weight-semibold) | .wr-comment |
--wr-comment-body-color | var(--wr-color-on-surface) | .wr-comment |
--wr-comment-body-gap | var(--wr-space-xs) | .wr-comment |
--wr-comment-bubble-bg | var(--wr-color-fill) | .wr-comment +1 variant override |
--wr-comment-bubble-color | var(--wr-color-on-surface) | .wr-comment +1 variant override |
--wr-comment-bubble-max-width | 80% | .wr-comment |
--wr-comment-bubble-padding-x | var(--wr-space-md) | .wr-comment |
--wr-comment-bubble-padding-y | var(--wr-space-sm) | .wr-comment |
--wr-comment-bubble-radius | var(--wr-border-radius-lg) | .wr-comment |
--wr-comment-bubble-time-color | var(--wr-color-on-surface-muted) | .wr-comment +1 variant override |
--wr-comment-composer-bg | var(--wr-color-surface) | .wr-comment-composer +2 variant overrides |
--wr-comment-composer-border | var(--wr-border-base) | .wr-comment-composer +1 variant override |
--wr-comment-composer-gap | var(--wr-space-xs) | .wr-comment-composer |
--wr-comment-composer-padding | var(--wr-space-xs) | .wr-comment-composer |
--wr-comment-composer-radius | var(--wr-control-radius-md) | .wr-comment-composer |
--wr-comment-composer-ring | transparent | .wr-comment-composer +1 variant override |
--wr-comment-composer-send-size | 2.25rem | .wr-comment-composer |
--wr-comment-font-family | var(--wr-font-family-base) | .wr-comment |
--wr-comment-font-size | var(--wr-text-sm) | .wr-comment |
--wr-comment-gap | var(--wr-space-md) | .wr-comment |
--wr-comment-head-gap | var(--wr-space-sm) | .wr-comment |
--wr-comment-indent | var(--wr-space-xl) | .wr-comment |
--wr-comment-line-height | var(--wr-text-sm-leading) | .wr-comment |
--wr-comment-replies-gap | var(--wr-space-lg) | .wr-comment |
--wr-comment-replies-margin-top | var(--wr-space-lg) | .wr-comment |
--wr-comment-time-color | var(--wr-color-on-surface-muted) | .wr-comment |
--wr-comment-time-font-size | var(--wr-text-xs) | .wr-comment |
Dependencies
See also
Component AvatarA user's picture, with initials as the fallback.Component TextareaA multi-line text field that can grow with its content.Component TimelineA list of events in order, vertical or horizontal.Component MentionSuggest names or tags while typing @ in a text field.Pipe wrDateFormat a date with a named preset or a token string.Component ButtonA button for an action or a link.