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.
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
optionswill need anidand anameto render.setOptionis anactionwhich will receive theidof the selected option (example below).valueis the initialidof the option (probably from thechangeset).
@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-leftis namedaddon/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: