Edge UI Kit
Design-less UI Kit for the Edge template engine
Edge UI Kit is a design-less components library for the Edge template engine. This UI Kit aims to extract all the repetitive parts of your templating layer without making any design choices for you.
The Edge UI Kit ships components in the following categories.
- Form controls (Finished)
- Formatted inputs (Finished)
- Interactive Alpine components (In progress)
- Icons (In progress)
Setup
The first step is to install the package from the npm registry.
npm i edge-uikit
The next step is to register the UI Kit as an Edge plugin. In AdonisJS, you can do it inside a preload file.
import View from '@ioc:Adonis/Core/View'
import uiKit from 'edge-uikit'
View.use(uiKit)
Alpine setup
Masked input components and interactive frontend components rely on Alpine.js. Therefore, you must install Alpine as a dependency in your project and then register the edge-uikit
frontend plugins with a specific version of Alpine.
npm i -D alpinejs
In AdonisJS, you can write the following code block inside the resources/js/app.js
file.
Note: Feel free to remove the plugins you are not using in your app.
import Alpine from 'alpinejs'
import { tabs } from 'edge-uikit/tabs'
import {
dateInput,
phoneInput,
numeralInput,
creditCardInput,
autoResizeTextarea
} from 'edge-uikit/inputs'
Alpine.plugin(tabs)
Alpine.plugin(dateInput)
Alpine.plugin(phoneInput)
Alpine.plugin(numeralInput)
Alpine.plugin(creditCardInput)
Alpine.plugin(autoResizeTextarea)
Alpine.start()
Why Edge UI Kit?
Accessible
All components shipped with Edge UI Kit are fully keyboard and screen reader accessible. We take care of those little details, so you don't have to.
Design less
The UI Kit does not get into your way with any pre-defined design or markup choices. You have complete control over the structure of your HTML.
Forms error handling
The form controls automatically perform the error handling for you by reading the old input values and errors from the flash messages.
Form controls
Following is the list of available form control components.
ui.form.control
The ui.form.control
component creates a scope for all its children components. In addition, the component accepts the following two props.
-
name
- The name of the control. The value is set as the name property on theinput
component. Also, the error component reads the error message using the name. -
id (optional)
- Theid
of the control. If not defined, thename
will be used as theid
.
@ui.form.control({ name: 'title' })
@end
ui.form.label
The ui.form.label
component create a label HTML tag. You can define the label text as a prop or as the component body.
Warning: The
ui.form.label
component must be inside the control component
@ui.form.control({ name: 'title' })
@!ui.form.label({ text: 'Post title' })
@end
@ui.form.control({ name: 'title' })
@ui.form.label()
<span> Post title </span>
@end
@end
All of the props except the text
prop will be added as an attribute to the underlying label HTML element. For example:
Edge component
|
HTML output <label for="title" class="text-gray-900 font-md">
Post title
</label> |
ui.form.input
Create an input element. All the props except the custom ones are added to the underlying input HTML element.
Warning: The
ui.form.input
component must be inside the control component
@ui.form.control({ name: 'title' })
@!ui.form.input()
@end
By default, the input type is set to text
. However, you can set the type to any of the supported input type values.
@!ui.form.input({ type: 'email' })
@!ui.form.input({ type: 'number' })
@!ui.form.input({ type: 'radio' })
@!ui.form.input({ type: 'checkbox' })
Displaying the old input value
During form validation failure, the AdonisJS server redirects the user to the form that stores the form input values inside the flashMessages
object.
The @ui.form.input
component automatically sets the input value by reading the old value from the flash messages.
Displaying the initial input value
You can pass the initial input value as the value
prop.
@!ui.form.input({ value: post.title })
@ui.form.error
The @ui.form.error
component displays the error message for the given input control.
Warning: The
ui.form.error
component must be inside the control component
Edge component
|
HTML output <span id="title_error">Required validation failed</span> |
Change wrapper element to div
|
HTML output <div id="title_error">Required validation failed</div> |
Self render error
|
HTML output <ul id="title_error">
<li> Required validation failed </li>
</ul> |
@ui.form.select
Create a select element. All the props except the custom ones are added to the underlying select HTML element.
You can define the select options as a prop. The value should be an array of objects with a value
and a text
property.
Warning: The
ui.form.select
component must be inside the control component
@ui.form.control({ name: 'category' })
@!ui.form.select({
options: [
{
value: 'node_js',
text: 'Node.js'
},
{
value: 'css',
text: 'Css'
}
]
})
@end
Defining selected option
Either you can set the selected
property on the options object itself or define a separate selected prop.
@!ui.form.select({
options: [
{
value: 'node_js',
text: 'Node.js'
},
{
value: 'css',
text: 'Css',
selected: true // 👈
}
]
})
@!ui.form.select({
selected: ['css'] // 👈,
options: [
{
value: 'node_js',
text: 'Node.js'
},
{
value: 'css',
text: 'Css'
}
]
})
Create empty option
You can also create an empty option by defining the emptyOption
prop.
@!ui.form.select({
emptyOption: true,
})
@!ui.form.select({
emptyOption: 'Select category',
})
@ui.form.textarea
Create a textarea element. All the props except the autoResize
prop are defined as attributes on the underlying textarea HTML element.
Warning: The
ui.form.textarea
component must be inside the control component.
@ui.form.control({ name: 'description' })
@!ui.form.textarea()
@end
Auto resizing textarea as the user types
To auto-resize the textarea, you must use the Alpine plugin. So make sure to register it first.
import Alpine from 'alpinejs'
// Import the plugin
import { autoResizeTextarea } from 'edge-uikit/inputs'
// Register plugin
Alpine.plugin(autoResizeTextarea)
Alpine.start()
Once the frontend setup is completed, you must define the autoResize
prop.
@!ui.form.textarea({ autoResize: true })
@ui.form.radioGroup
Radio groups are a little notorious when it comes to screen reader accessibility. This is how they should be structured for screenreaders to provide helpful information to the user.
- Wrap all radio inputs inside a group. The group can be the
fieldset
element or an HTML element withrole=radiogroup
. - The entire group should have a label.
- In case of error, the group must be marked invalid using the
aria-invalid
attribute. - The
aria-describedby
attribute on the group must point towards the error message element.
The ui.form.radioGroup
component hides all these implementation details and gives you a nicer API to work with.
@ui.form.radioGroup({ name: 'toppings' })
@!ui.form.groupLabel({ text: 'Select pizza toppings' })
@end
Formatted inputs
You can create formatted or masked inputs alongside the regular input elements by defining the format
prop.
The formatting is done on the client-side (as the user types) and requires you to register the Alpine plugins first.
Note: The formatting is performed using the Cleave.js library. Make sure to go through the Cleave documentation as well.
Numeral format
Format the input as a number. Make sure to consult Cleave numeral input documentation to see all the available options.
The first step is to register the Alpine plugin.
import Alpine from 'alpinejs'
import { numeralInput } from 'edge-uikit/inputs'
Alpine.plugin(numeralInput)
Alpine.start()
The next and final step is to define the format on the Edge input component.
@!ui.form.input({ format: 'numeral' })
You can define additional formatting props directly on the input component as follows.
@!ui.form.input({
format: 'numeral',
numeralThousandsGroupStyle: 'lakh',
numeralPositiveOnly: true,
})
Date format
Format the input as a date. Consult Cleave date input documentation to see all the available options.
The first step is to register the Alpine plugin.
import Alpine from 'alpinejs'
import { dateInput } from 'edge-uikit/inputs'
Alpine.plugin(dateInput)
Alpine.start()
The next and final step is to define the format on the Edge input component.
@!ui.form.input({ format: 'date' })
You can define additional formatting props directly on the input component as follows.
@!ui.form.input({
format: 'date',
datePattern: ['Y', 'm', 'd'],
delimiter: '-'
})
Time format
Format the input as time. Consult Cleave date input documentation to see all the available options.
The first step is to register the Alpine plugin.
import Alpine from 'alpinejs'
import { timeInput } from 'edge-uikit/inputs'
Alpine.plugin(timeInput)
Alpine.start()
The next and final step is to define the format on the Edge input component.
@!ui.form.input({ format: 'time' })
You can define additional formatting props directly on the input component as follows.
@!ui.form.input({
format: 'time',
timePattern: ['h', 'm'],
delimiter: ':',
timeFormat: '24'
})
Credit card format
Format the input as a credit card number. Make sure to consult Cleave date input documentation to see all the available options.
The first step is to register the Alpine plugin.
import Alpine from 'alpinejs'
import { creditCardInput } from 'edge-uikit/inputs'
Alpine.plugin(creditCardInput)
Alpine.start()
The next and final step is to define the format on the Edge input component.
@!ui.form.input({ format: 'creditCard' })
Phone number format
Format the input as a phone number. Make sure to consult Cleave date input documentation to see all the available options.
The first step is to register the Alpine plugin.
import Alpine from 'alpinejs'
import { phoneInput } from 'edge-uikit/inputs'
Alpine.plugin(phoneInput)
Alpine.start()
The next and final step is to define the format on the Edge input component.
@!ui.form.input({ format: 'phone' })
You can define additional formatting props directly on the input component as follows.
@!ui.form.input({
format: 'phone',
prefix: '+91 '
})
Formatted input event listeners
You can wrap your formatted inputs inside a custom Alpine component and listen for value change. Following is an example of the same.
<div
x-data="inputWrapper" {{-- 👈 Custom component --}}
>
@ui.form.control({ name: 'phone' })
@!ui.form.input({ format: 'phone' })
@end
</div>
Next, create the inputWrapper
Alpine component inside the frontend JavaScript codebase and define an onValueChanged
function.
Alpine.data('inputWrapper', function () {
return {
onValueChanged (event) {
console.log(event.target)
}
}
})
Similarly, for the creditCard
format, you can also listen for the credit card type change. The event receives the credit card type by inspecting the credit card number.
Alpine.data('inputWrapper', function () {
return {
onCreditCardTypeChanged (type) {
console.log(type)
}
}
})
Interactive components
Following is the list of all the interactive components. They are also design-less and fully keyboard/screen reader accessible.
Note: The list of interactive components is limited to what I need in my projects. Therefore, I will not be taking any requests to add additional components (the time and effort to create them is massive).
- Tabs (Finished)
- Alert and Dialog modal (In progress)
- Tooltips (In progress)
- Navbar Dropdown (In progress)
- Disclosure (In progress)
- Accordion (In progress)
Tabs
You can create a tabs layout by using a collection of tabs components. However, the first step is registering the Alpine plugin to make tabs interactive.
import Alpine from 'alpinejs'
import { tabs } from 'edge-uikit/tabs'
Alpine.plugin(tabs)
Alpine.start()
Once done. You can create a tab layout as follows.
@ui.tabs.group()
@ui.tabs.list()
@!ui.tabs.trigger({ text: 'Tab 1' })
@!ui.tabs.trigger({ text: 'Tab 2' })
@!ui.tabs.trigger({ text: 'Tab 3' })
@end
<div>
@ui.tabs.panel()
<p> Content for tab 1 </p>
@end
@ui.tabs.panel()
<p> Content for tab 2 </p>
@end
@ui.tabs.panel()
<p> Content for tab 3 </p>
@end
</div>
@end
- The
@tabs.group
component creates a wrapper for the tabs. Under the hood, it uses the github/tab-container-element library. - The
@tabs.list
component creates a div with the roletablist
. - The
@tabs.trigger
component create a clickable tab button. Either you can define the button text as a prop or define the body within the opening and the closing statement. - The
@tabs.panel
component creates a panel for the tab content. The number of panels and the triggers should be the same.
Styling the selected tab
Since tabs are switched within the browser (without making a round trip to the backend), you will have to define custom classes for the selected tab in your frontend JavaScript code.
The ergonomic way is to wrap the tabs group inside a custom Alpine component and use the x-bind
directive to bind custom attributes on the selected trigger.
<div
x-data="tabsWrapper" {{-- 👈 Custom component --}}
@ui.tabs.group()
@ui.tabs.list()
@!ui.tabs.trigger({
text: 'Tab 1',
'x-bind': 'trigger' {{-- 👈 --}}
})
@!ui.tabs.trigger({
text: 'Tab 2',
'x-bind': 'trigger' {{-- 👈 --}}
})
@!ui.tabs.trigger({
text: 'Tab 3',
'x-bind': 'trigger' {{-- 👈 --}}
})
@end
{{-- Rest of the markup --}}
@end
>
</div>
Alpine.data('tabsWrapper', function () {
return {
trigger: {
['x-bind:class'] () {
return this.isSelected(this.$el)
? 'text-blue-500'
: ''
}
}
}
})
Let's see how the above piece of code works. We have some Alpine magic here.
- The
tabsWrapper
is a standard Alpine component. - Next, we have an object called
trigger
. We apply this object to ourtrigger
HTML element using thex-bind
directive. - Therefore, all the properties
(ie. 'x-bind:class')
ends up on the trigger HTML element. - Inside the
'x-bind:class'
property we are checking if the current element(ie. $this.el)
is selected or not. - The
isSelected
method is exposed by thetabs
plugin shipped with edge-uikit.
All this may seem a bit complicated based on your familiarity with Alpine. But, I think this is the best API to express the client-side logic.
Defining the default selected tab
You can set the selected tab by defining the defaultIndex
prop. The index starts with 0
.
@ui.tabs.group({ defaultIndex: 2 })
@end
Listening for tab change
You can listen for the tab change events by wrapping the tab group inside a custom Alpine component.
<div
x-data="tabsWrapper" {{-- 👈 Custom component --}}
@ui.tabs.group()
@end
</div>
Define the beforeChange
and onChange
methods to listen for the tab change event.
Alpine.data('tabsWrapper', function () {
return {
beforeChange(event) {
/**
* Return "false" to cancel the event
*/
},
onChange(event) {
}
}
})
Icons
We are on bundling some of the popular icon sets as edge components.