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
options
will need anid
and aname
to render.setOption
is anaction
which will receive theid
of the selected option (example below).value
is the initialid
of 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-left
is 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
: