ComponentAdded in v15.5

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.

import { Component } from '@angular/core';
import { WrComment, WrCommentComposer } from 'ngwr/comment';

@Component({ imports: [WrComment, WrCommentComposer] })
export class MyComponent {}

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>
AL
Ann Lee
Looks good. One question on the empty state below: does it fall back to the placeholder, or stay blank?
BR
Bob Ray
It falls back to the placeholder, same as the input.
AL
Ann Lee
Then this is fine, thanks.
CP
Cy Park
Approving. https://github.com/thekhegay/ngwr/pull/1234/files#diff-a-very-long-anchor-that-wraps-rather-than-overflowing

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)" />
Are the review notes in?
Pushed a minute ago. Two small ones on the composer.
On it.

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)"
/>
KeysDoes
EnterSends. With [enterSends]="false" it adds a line instead.
Shift+EnterAdds a line.
Ctrl+Enter, Cmd+EnterSends, whatever Enter does.
Enter during IME compositionConfirms 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 article named after its author.
  • Replies sit in a group named repliesLabel, from the comment.replies catalog key.
  • No heading level is assumed. Put a chat in role="log" so new messages are announced.
  • The send button is named sendLabel, from comment.send. Focus stays in the field after a send.

API

<wr-comment> inputs.

NameDescriptionTypeDefault
authorWho wrote it. Also the article's accessible name. Leave it empty when a [wrCommentAuthor] element draws the name instead.string''
timeWhen it was written, already formatted: the component does no date formatting. Run a Date through the wrDate pipe on the way in.string''
datetimeThe 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 | nullnull
variantrow is a discussion entry; bubble is a chat message, which takes no replies.WrCommentVariant'row'
sideWhich inline side a bubble sits on. end is the reader's own message. Ignored by the row variant.WrCommentSide'start'
repliesLabelAccessible name of the replies group. Falls back to comment.replies, then 'Replies'.string | nullnull

Composer API

<wr-comment-composer> inputs and outputs.

NameDescriptionTypeDefault
placeholderNative placeholder text.string''
ariaLabelAccessible 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 | nullnull
sendLabelAccessible name of the send button. Falls back to comment.send, then 'Send'.string | nullnull
rowsVisible row count while the field is empty.number1
maxRowsRows the field grows to before it scrolls on its own.number6
disabledDisable the field and the button. Bound automatically from the field's disabled state when used with [formField]. The text is kept.booleanfalse
readonlyRead-only state: the text is shown and announced, keeps its tab stop and cannot be edited or sent.booleanfalse
sendingA 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.booleanfalse
enterSendstrue: Enter sends and Shift+Enter adds a line. false: Enter adds a line and only Ctrl+Enter or Cmd+Enter sends.booleantrue
clearOnSendClear the field before send emits. With false the text stays until the app clears the model itself.booleantrue
valueThe 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.

VariableDefaultDeclared on
--wr-comment-actions-gapvar(--wr-space-sm).wr-comment
--wr-comment-author-colorvar(--wr-color-on-surface).wr-comment
--wr-comment-author-font-weightvar(--wr-font-weight-semibold).wr-comment
--wr-comment-body-colorvar(--wr-color-on-surface).wr-comment
--wr-comment-body-gapvar(--wr-space-xs).wr-comment
--wr-comment-bubble-bgvar(--wr-color-fill).wr-comment +1 variant override
--wr-comment-bubble-colorvar(--wr-color-on-surface).wr-comment +1 variant override
--wr-comment-bubble-max-width80%.wr-comment
--wr-comment-bubble-padding-xvar(--wr-space-md).wr-comment
--wr-comment-bubble-padding-yvar(--wr-space-sm).wr-comment
--wr-comment-bubble-radiusvar(--wr-border-radius-lg).wr-comment
--wr-comment-bubble-time-colorvar(--wr-color-on-surface-muted).wr-comment +1 variant override
--wr-comment-composer-bgvar(--wr-color-surface).wr-comment-composer +2 variant overrides
--wr-comment-composer-bordervar(--wr-border-base).wr-comment-composer +1 variant override
--wr-comment-composer-gapvar(--wr-space-xs).wr-comment-composer
--wr-comment-composer-paddingvar(--wr-space-xs).wr-comment-composer
--wr-comment-composer-radiusvar(--wr-control-radius-md).wr-comment-composer
--wr-comment-composer-ringtransparent.wr-comment-composer +1 variant override
--wr-comment-composer-send-size2.25rem.wr-comment-composer
--wr-comment-font-familyvar(--wr-font-family-base).wr-comment
--wr-comment-font-sizevar(--wr-text-sm).wr-comment
--wr-comment-gapvar(--wr-space-md).wr-comment
--wr-comment-head-gapvar(--wr-space-sm).wr-comment
--wr-comment-indentvar(--wr-space-xl).wr-comment
--wr-comment-line-heightvar(--wr-text-sm-leading).wr-comment
--wr-comment-replies-gapvar(--wr-space-lg).wr-comment
--wr-comment-replies-margin-topvar(--wr-space-lg).wr-comment
--wr-comment-time-colorvar(--wr-color-on-surface-muted).wr-comment
--wr-comment-time-font-sizevar(--wr-text-xs).wr-comment

Dependencies

Built onButton, Textarea

See also