edge-uikit
TypeScript icon, indicating that this package has built-in type declarations

0.0.1-0 • Public • Published

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.

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 the input component. Also, the error component reads the error message using the name.
  • id (optional)- The id of the control. If not defined, the name will be used as the id.
@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

@!ui.form.label({
  text: 'Post title',
  class: 'text-gray-900 font-md',
})

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

@!ui.form.error()

HTML output

<span id="title_error">Required validation failed</span>

Change wrapper element to div

@!ui.form.error({ as: 'div' })

HTML output

<div id="title_error">Required validation failed</div>

Self render error
You can also self-render error messages by accessing them from the $context.errors property.

@ui.form.error({ as: 'ul' })
  @each(error in $context.errors)
    <li> {{ error }} </li>
  @end
@end

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 with role=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 role tablist.
  • 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 our trigger HTML element using the x-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 the tabs 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.

Package Sidebar

Install

npm i edge-uikit

Weekly Downloads

139

Version

0.0.1-0

License

MIT

Unpacked Size

147 kB

Total Files

20

Last publish

Collaborators

  • virk