Ember Addons (KB using Tailwind)

Application

Put the following into application.hbs:

<KbBase
  @actionSignOut={{this.actionSignOut}}
  @appName="My App"
  @appMenu={{@model.appMenu}}
  @appLogo="/assets/img/logo-white-small.png"
  @currentContact={{this.currentContact}}
  @isAuthenticated={{this.session.isAuthenticated}}
  @logoLink="about"
>
  <KbNotify />
  {{outlet}}
</KbBase>

Pages

Create a page component for each route.

For example, if your router contains the following:

this.route("course", function () {
  this.route("list", { path: "/" })
  this.route("create")
  this.route("detail", { path: "/:courseId" })
}

You create three page components:

./app/components/course/list/page.hbs
./app/components/course/create/page.hbs
./app/components/course/detail/page.hbs

The data for the page can be retrieved in the page components e.g:

./app/components/course/detail/page.js
import Component from "@glimmer/component"
import { service } from "@ember/service"
import { task } from "ember-concurrency"
import { tracked } from "@glimmer/tracking"

export default class CourseDetailPageComponent extends Component {
  @service kbMessages
  @service store

  @tracked course

  constructor() {
    super(...arguments)
    if (this.args.courseId) {
      this.courseTask.perform(this.args.courseId)
    } else {
      console.error("'CourseDetailPageComponent' needs 'this.args.courseId'")
    }
  }

  courseTask = task(async courseId => {
    try {
      this.course = await this.store.findRecord("course", courseId)
    } catch (e) {
      this.kbMessages.addError("Cannot load course", e)
    }
  })
}

The route can simply return the parameters e.g. for the CourseDetailRoute:

import Route from "@ember/routing/route"
import { service } from "@ember/service"

export default class CourseDetailRoute extends Route {
  @service kbMessages
  @service kbPage

  model(params) {
    this.kbPage.setTitle("Course")
    return { courseId: params.courseId }
  }
}

The template can simply call the page component passing in the parameters from the route e.g:

<Course::Detail::Page @courseId={{@model.courseId}} />

Page Component

Add Header and Content sections to your template:

Tip

Add a Profile and one or more Panel sections.

Tip

The Panel::Group separates groups of controls e.g. buttons on the left and buttons on the right.

<KbBase::Header>

  <KbBase::Header::Profile>
  </KbBase::Header::Profile>

  <KbBase::Header::Panel>

    <KbBase::Header::Panel::Group>
    </KbBase::Header::Panel::Group>

    <KbBase::Header::Panel::Group>
    </KbBase::Header::Panel::Group>

  </KbBase::Header::Panel>
  <KbBase::Header::Panel>
  </KbBase::Header::Panel>

</KbBase::Header>

<KbBase::Content>

</KbBase::Content>

To add labels to input controls in the panel:

<KbBase::Header::Panel>
  <KbForm::Form::Field>
    <KbForm::Form::Field::Label>
      State
    </KbForm::Form::Field::Label>

    <Input ...

  </KbForm::Form::Field>

Note

For now, you will need to add labels to all the controls so the spacing works correctly.

If you need two header rows, then add another header section. Setting mergeWithAbove to true will fix the spacing e.g:

</KbBase::Header>
<KbBase::Header @hasProfile={{false}} @mergeWithAbove={{true}}>
  <KbBase::Header::Panel>

Data Display

Tip

Also know as Data Table (DataTable).

Copied from Description Lists, Data Display, Left-aligned in card:

<KbBase::Content>
  <KbDataDisplay::Container>
    <KbDataDisplay::Head @heading="Variables">
      My description
    </KbDataDisplay::Head>

    <KbDataDisplay::Body>
      <KbDataDisplay::Body::Row @caption={{key}}>
        {{{value}}}
      </KbDataDisplay::Body::Row>

      <KbDataDisplay::Body::RowList @caption="Attachments">
        <KbDataDisplay::Body::RowList::Attachment
          @filename="workflow-variables.pdf"
          @download_type="variables"
          @download_url={{process.download_url}}
        />
      </KbDataDisplay::Body::RowList>

    </KbDataDisplay::Body>
  </KbDataDisplay::Container>
</KbBase::Content>

Note

03/09/2022, Revised in ember-kb-base except for RowList.

Button Grid

See Tab below for an example of buttons in a flex. A grid has also worked for me e.g:

<div class="grid grid-cols-3">
  <KbButton
    @onClick={{fn this.viewDocument document.document_id}}
    @verticalPadding={{false}}
  >
    <KbSvg::DocumentText />
    View Document
  </KbButton>
  <div class="text-right">
    <LinkTo
      @route="document.list"
      class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 justify-self-end"
    >
      <KbSvg::LeftArrowInCircle />
      Return to list
    </LinkTo>
  </div>
</div>

Tip

Try using class="justify-self-start" to align the first button.

Development

An addon will include an index.js file in the root of the package. To allow your project to auto-refresh, then set isDevelopingAddon to true in module.exports e.g:

module.exports = {
  name: require("./package").name,
  isDevelopingAddon: function () {
    return true
  },

Forms

Our standard form is copied from the TailwindUI Two-column with cards layout https://tailwindui.com/components/application-ui/forms/form-layouts

Tip

This was called Two-column cards with separate submit actions, but it no longer exists on the TailwindUI site.

The full width form uses styles from the Stacked form layout.

Tip

To submit the form using the keyboard and to make sure the form isn’t submitted twice, add the onSubmit to KbForm::Form (not the Button).

<KbBase::Content>

  <KbForm::Container>
    <KbForm::Help>
      <KbForm::Help::Heading>
        My Heading
      </KbForm::Help::Heading>
      <KbForm::Help::SubHeading>
        Some extra help...
      </KbForm::Help::SubHeading>
    </KbForm::Help>
    <KbForm::Form @onSubmit={{this.submitForm}}>
      <KbForm::Form::Field::Container @niceSpacing={{true}}>

        <KbForm::Form::Field>
          <KbForm::Form::Field::Label>
            Reason for deletion
          </KbForm::Form::Field::Label>

          <KbForm::Form::Field::TextArea>
            Please enter the reason for deleting the workflow process...

            <KbForm::Form::Field::ErrorMessageChangeset
              @errorField={{this.changeset.error.deleted_comment}}
            />

          </KbForm::Form::Field::TextArea>
        </KbForm::Form::Field>

        <!-- Checkbox uses a different label (not sure about the error message) -->

        <KbForm::Form::Field>
          <KbForm::Form::Field::InputCheckbox @key={{"archived"}} @value={{this.changeset.archived}}>
            <KbForm::Form::Field::LabelCheckbox @for={{"archived"}} @label="Archived">
              Is this archived?
            </KbForm::Form::Field::LabelCheckbox>
          </KbForm::Form::Field::InputCheckbox>
          <KbForm::Form::Field::ErrorMessageChangeset
            @errorField={{this.changeset.error.archived}}
          />
        </KbForm::Form::Field>

        <!-- other fields... -->

      </KbForm::Form::Field::Container>

      <KbForm::Form::Button::Container>
        <KbButton
          @buttonType={{"cancel"}}
          @onClick={{fn this.cancel}}
          @paddingRight={{false}}
          @verticalPadding={{false}}
        >
          Cancel
        </KbButton>
        <KbForm::Form::Button @buttonType="submit">
          Delete workflow
        </KbForm::Form::Button>
      </KbForm::Form::Button::Container>

    </KbForm::Form>
  </KbForm::Container>
  <KbForm::Separator />

</KbBase::Content>

Tip

The @niceSpacing option on the <KbForm::Form::Field::Container> component makes the form look nice!

Note

The KbForm::Form::Button::Container probably isn’t needed if you have one button…

Select

Tip

Initial version created August 2023. Currently does not handle required fields or multi-select.

<KbForm::Form::Field>
  <KbForm::Form::Field::Label>
    Category
  </KbForm::Form::Field::Label>
  <KbForm::Form::Field::Select
    @id='myFieldId'
    @captionEmpty={{"-- Select a category --"}}
    @options={{@flowListCategories}}
    @setOption={{this.setCategory}}
    @value={{this.changeset.category.id}}>
    Please select a category...
    <KbForm::Form::Field::ErrorMessageChangeset
      @errorField={{this.changeset.error.category}}
    />
  </KbForm::Form::Field::Select>
</KbForm::Form::Field>
  • The options will need an id and a name to render.

  • setOption is an action which will receive the id of the selected option (example below).

  • value is the initial id of the option (probably from the changeset).

@action
setCategory(optionId) {
  this.setCategoryTask.perform(optionId);
}

setCategoryTask = task(async (categoryId) => {
  try {
    let category = await this.store.findRecord(
      'flowListCategory',
      categoryId,
    );
    this.changeset.set('category', category);
  } catch (e) {
    this.kbMessages.addError(
      `Cannot find flow list category ${categoryId}`,
      e,
    );
  }
});

Markdown

Tip

13/10/2023, Our task/form/str component has support for Markdown in the help text, but has not been added elsewhere (yet)

Using https://github.com/empress/ember-cli-showdown

Update package.json:

"@tailwindcss/typography": "^0.5.10",
ember-cli-showdown": "^7.0.0",

Add the Tailwind typography plugin:

# front/tailwind.config.js
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')],

Format the text using Tailwind prose and the markdown-to-html component:

<p class="prose prose-sm">
  {{markdown-to-html @field.help_text}}
</p>

For more information, see, https://www.kbsoftware.co.uk/crm/ticket/6864/

SVG Icons

  • We use https://heroicons.com/.

  • There are two types of icons, default and solid. Solid icons should be used on buttons (because they fit).

  • The component name should match the name of the icon e.g. the solid icon for arrow-circle-left is named addon/components/kb-svg/arrow-circle-left-solid.hbs

Tip

Our icons are in the KbSvg icons repository…

Tab

This is a tab bar with two tabs and one button:

<KbTabBar
  @onTabChange={{this.onTabChange}}
  @startTab={{this.state}}
  as |tabBar|
>
  <KbTabBar::Tabs>
    {{#if @model.documentTask.isRunning}}{{else}}

      {{#let @model.documentTask.value as |document|}}

        <KbTabBar::Tabs::Tab @key="document" @label="Documents" @tabBar={{tabBar}} />
        <KbTabBar::Tabs::Tab @key="audit" @label="Audit" @tabBar={{tabBar}} />

        <!-- To add buttons to the right of the tabs -->
        <KbTabBar::Tabs::ButtonContainer>
          <KbButton
            @id='createButton'
            @onClick={{fn this.createDocument}}
            @verticalPadding={{false}}
          >
            <KbSvg::Plus/>
            Create
          </KbButton>
        </KbTabBar::Tabs::ButtonContainer>

      {{/let}}

    {{/if}}
  </KbTabBar::Tabs>
</KbTabBar>

Tip

Add @verticalPadding={{false}} to the KbButton.

Our standard pattern is to put tabs and buttons at the top of the page. If you want buttons, but don’t need a tab, then use the same pattern but with no parameters for the tabs e.g:

<KbTabBar>
  <KbTabBar::Tabs>
    <KbTabBar::Tabs::ButtonContainer>

      <KbButton
        @id='createButton'
        @onClick={{fn this.createDocument}}
        @verticalPadding={{false}}
      >
        <KbSvg::Plus/>
        Create
      </KbButton>

    </KbTabBar::Tabs::ButtonContainer>
  </KbTabBar::Tabs>
</KbTabBar>

Tip

To use a LinkTo within the <KbTabBar::Tabs::ButtonContainer> try enclosing it in <div class="text-right">

Table

A checklist for handling data and for adding a spinner / nothing found to your table can be found here Tables / Data:

<KbTable::Container>
  <KbTable::Table>

    <KbTable::Head>
      <KbTable::Head::Cell>
      </KbTable::Head::Cell>
    </KbTable::Head>

    <KbTable::Body>

      {{#if @model.processes.isRunning}}
        <KbTable::Body::Row>
          <KbTable::Body::Cell>
            <KbSpinner @caption="Please wait..." />
          </KbTable::Body::Cell>
        </KbTable::Body::Row>
      {{else}}
        <KbTable::Body::Row>

          <KbTable::Body::Cell>
          </KbTable::Body::Cell>
          <KbTable::Body::Cell>
          </KbTable::Body::Cell>

          <KbTable::Body::Cell @align="right">
            <KbTable::Body::Cell::Button @onClick={{fn this.editUser user}}>
              Edit
            </KbTable::Body::Cell::Button>
          </KbTable::Body::Cell>

        </KbTable::Body::Row>
      {{/if}}

    </KbTable::Body>

  </KbTable::Table>
</KbTable::Container>

Tip

For pagination, see Pagination (JSON API).

Table Header

If you want a title, description and button above your table:

<KbBase::Content>

  <KbTable::Header
    @title="Workflow List Type"
    @description="Drop-down lists in the workflow mapping"
  >
    <KbTable::Header::Container>
      <button type="button" class="block rounded-md bg-blue-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600">
        Add List Type
      </button>
    </KbTable::Header::Container>
  </KbTable::Header>

  <KbTable::Container>

Tip

You only need the <KbTable::Header::Container> section if you want to add a button.

Upload (File)

Is a legacy component. I added a new @maxFileSize attribute e.g:

<KbForm::Upload
  @maxFileSize={{32000}}

Source code:

Uses ember-file-upload: