Table of contents
About §
Documentation §
We use Custom Elements to encapsulate and distribute reusable behaviors. Learn more about how we use Web Components.
- Form related
<auto-check>
<auto-complete>
<image-crop>
<file-attachment>
<password-strength>
- UI components
- Enhancements
- Catalyst controllers
<auto-check>
§
Use <auto-check>
to perform server-side validation on an input. Add [required]
attribute if this input should only be submittable if the validation passes.
When user types in the input, input value is sent to the endpoint specified by src
attribute, with value
as the parameter name. The endpoint should respond a 422 responses to signal an invalid state, and 200 for a valid state.
Styling classes for states are globally applied by github/behaviors/autocheck.ts
.
<auto-complete>
§
Use <auto-complete>
when you have an input that can be fill with a specific set of data.
When user types in an input, the input value is sent to an endpoint with q
as the parameter name, and the endpoint should respond with the filtered results as a HTML fragment. The response HTML will be appended to the page for user to select.
<clipboard-copy>
§
Copy element text content or input values to the clipboard.
.js-clipboard-copy
Temporarily display copy feedback icons.
When a copy event is triggered, this behavior will hide a .js-clipboard-clippy-icon
element if it exists, show a .js-clipboard-check-icon
, and then revert back after 2000ms. This is useful when you want to provide visual feedback to the user that the data has been copied to their clipboard.
[data-copy-feedback]
Provide text feedback with the value of [data-copy-feedback]
in a tooltip.
<details-dialog>
§
<details-dialog>
is a <details>
dependent dialog component. <form>
inside of the dialog will be reset when dialog is closed.
<details-dialog>
can also be created in JavaScript with dialog()
.
<details-menu>
§
<details-menu>
is a <details>
dependent dropdown menu component with keyboard support. Use it for all dropdown menus and customizable <select>
s.
-
details-menu[data-menu-input="targetid"]
On
details-menu-selected
, syncevent.detail.relatedTarget.value
toinput#targetid
. -
details-menu[data-menu-max-options="5"]
On
details-menu-selected
, check number of selected menu items, toggle a warning element, and disable the inputs if at limit.
<file-attachment>
§
<file-attachment>
adds support for uploading files with drag and drop and pasting.
<filter-input> [experimental]
§
An input element that filters a list of results on the client side using substring.
<g-emoji>
§
Backports native emoji characters to user agents that don't support them. If a browser does not support emoji, a fallback image tag is created. If a browser supports emoji, nothing happens.
In github/github
<g-emoji>
tag generation is mostly done with the emoji_filter
in HTML pipeline. Outside of HTML pipeline, it is recommended to use our custom Rails helper emoji_tag
instead of handcrafting a <g-emoji>
.
<image-crop>
§
Use <image-crop>
to user to set image cropping dimensions. The actual image crop process should be handled by the server. <image-crop>
provides coordinates (x, y) and dimensions (width, height).
image-crop
does not come with the default bundles. The bundle can be loaded in as needed.
<include-fragment>
§
A client-side includes tag.
-
[data-show-on-error]
,[data-hide-on-error]
On
error
, toggle visibility of elements. -
button[data-retry-button]
On
click
, retry<include-fragment>
.
<markdown-toolbar>
§
Markdown formatting buttons for text inputs.
<poll-include-fragment>
§An extension to <include-fragment>
. This element repeatedly fetches from the endpoint as long as a 202 response is returned. The fetch happens on an interval that starts from 1000ms, and increases by 1.5x with each attempt, so that pollers don't hammer the server while it's working on the task.
This is most commonly used to display the status of a background job.
<remote-input>
§
An input element that sends its value to a server endpoint and renders the response body.
This is useful for:
- Server side filtering
- HTML live preview based on user input
<remote-pagination>
§An element for controlling pagination behavior through form submission. Built with Catalyst.
<tab-container>
§
Displays the corresponding tab panel when a user navigates between tabs with the keyboard or mouse.
This element implements the WAI-ARIA best practices for tabs.
<task-lists>
§
<task-lists>
adds drag and drop support and dispatch events for check state changes and drag and drop actions. In github/github
, task list dependent markup is generated from the task_list_filter
in HTML pipeline.
<text-expander>
§
Activates a suggestion menu to expand text snippets as you type. See suggester for existing text-expander
behaviors.
Time elements
§
Internationalized <time>
element extensions:
<local-time>
<relative-time>
<time-ago>
<time-until>
In github/github
, use our custom helpers to generate these tags.
- Keyboard interactions
captureKeypresses()
throttled-input.ts
onfocus.ts
onKey()
onFocus()
onInput()
eventloop-tasks.ts
microtask()
animationFrame()
idle()
parseHTML()
debounce()
scrollTo()
ARIA live region
§announce(), announceFromElement()
These two accessibility helper functions set text content into a global aria-live
container so they get read out to screen reader users immediately. This call is essential for interaction feedback like "changes saved" or live updates, because content only gets read when they receive focus, which often isn't the case for dynamic content injections.
import {announceFromElement} from './aria-live'
on('submit', '.js-auto-save-changes', async function(event) {
const form = event.currentTarget as HTMLFormElement
const response = await fetch(form.action, {
method: form.method,
body: new FormData(form)
})
if (response && response.ok) {
const message = query(document, '.js-success-message')
message.hidden = false
announceFromElement(message)
} else {
const message = query(document, '.js-error-message')
message.hidden = false
announceFromElement(message)
}
})
dialog()
§Create a <details-dialog>
. This is useful when:
- The dialog can't/doesn't have a in-page trigger. For example, the keyboard shortcut dialog is only reachable via
[data-hotkey="?"]
. - The dialog invokes a JavaScript-dependent feature.
- A fresh copy of a dialog is needed for every interaction.
- An inline
<details-dialog>
would be cutoff byoverflow: hidden
parents.
import {dialog} from './details-dialog'
import {on} from 'delegated-events'
on('click', '.js-dialog-trigger', async function() {
const template = document.getElementById('dialog-template')
const dialogEl = await dialog({
content: template.content.cloneNode(true) // or Promise<DocumentFragment>
})
// dialog is closed via Escape, clicks on the overlay, or any other JS triggers
dialog.addEventListener('dialog:remove', () => {})
})
Form helpers
§changeValue(input, value)
- Validates the value is the correct type for input
- Updates an input value
- Fires a
change
event
Having the change
event fire is important for various behaviors to react, such as form validation.
import {changeValue} from './form'
import {on} from 'delegated-events'
on('click', '.js-change-color-button', function() {
const input = query(document, '.js-color-input', HTMLInputElement)
changeValue(input, generateRandomColor())
})
requestSubmit(form, ?submitter)
- Preserves the submitter value for form
- Fires a
submit
event - Check if event was canceled, if so, return
- Submits the form
Calling native form.submit()
method immediately submits the form without triggering the submit
event. As a result, code that wants to hook into form submits would never execute.
This helper will one day be deprecated in favor of the native form.requestSubmit(submitter)
once browsers we support have all implemented them.
import {submit} from './form'
import {on} from 'delegated-events'
on('click', '.js-should-submit-form', () =>
const form = closest(document, '.js-form')
if(confirm('really?')) requestSubmit(form)
})
fillFormValues(form, fields): void
Fill multiple form fields by item name.
isFormField(element): boolean
Check if an element is a form participant.
serialize(form): string
Serialize form data into URLSearchParams
.
hashChange()
§Variant on the native hashchange
event with extra features:
- Guarantees that handlers run on
DOMContentLoaded
, or immediately if page has already loaded; - If the anchor references an element by id, the
data.target
property of the argument passed to handlers will be a reference to that DOM element.
Each handler will called with a data
object containing properties:
oldURL
- String URL before the hash change, ornull
on initial page load;newURL
- String URL after hash change, or current page URL;target
- DOM element whose id matches the anchor value, ornull
.
// Redirect old anchor issue urls
hashChange(data => {
const match = data.newURL.match(/\/issues#issue\/(\d+)$/)
if (match) {
window.location.href = data.newURL.replace(/\/?#issue\/.+/, "/#{m[1]}")
}
})
// Ensure that the referenced comment's parent container is visible
hashChange(data => {
const container = data.target && data.target.closest('.js-comments-container')
if (container) {
container.hidden = false
}
})
memoize()
§Store the results of expensive function calls and return the cached result when the same inputs occur again.
const cachedFetch = memoize(fetch)
// Will take as long it takes for the server to respond.
await cachedFetch(url)
// Will return practically instantly with the results from the cache.
await cachedFetch(url)
Pjax helpers
§pjax({url, container, replace?})
- Makes a pjax request
- Replace container content with pjax response
- Optionally
replaceState
(instead of adding a browser history entry)
import pjax from './pjax'
import {on} from 'delegated-events'
on('click', '.js-trigger-pjax', function(event) {
if (event.shiftKey) {
event.preventDefault()
pjax({
url: window.location.href + "?from=0",
element: query(document, '#js-repo-pjax-container'),
replace: true
})
}
})
@github/query-selector
§Run time assertion functions to query the DOM. Unlike the query functions built into browsers, these are guaranteed to return elements or throw an error. This is useful when working in type systems such as TypeScript or Flow. Since the query functions built in the browser have a nullable return value, you need to check to make sure that there actually is an element with a if
statement.
Using built-in querySelector
:
function getForm() {
const element = document.querySelector('.js-submit-comment')
if (element instanceof HTMLButtonElement) {
return element.form
}
}
Using TypeScript:
function getForm() {
const element = document.querySelector<HTMLButtonElement>('.js-submit-comment')!
return element.form
}
Using @github/query-selector
:
import {query} from '@github/query-selector'
function getForm() {
const element = query(document, '.js-submit-comment', HTMLButtonElement)
return element.form
}
Return value when | document.querySelector(...) |
TypeScript | query(...) |
---|---|---|---|
element is null |
undefined |
errors | errors |
element is HTMLElement |
undefined |
undefined |
errors |
The @github/query-selector
package has query
, querySelectorAll
, closest
, namedItem
, getAttribute
functions and you can read more about them on the repo page.
remoteForm()
§Submit a form with JavaScript. See also: [data-replace-remote-form]
, github/remote-form.
import {remoteForm} from './remote-form'
remoteForm('.js-remote-form', async function (form, wants) => {
// form submits
let response
try {
// request kick-off
// response = await wants.text()
// response = await wants.json()
response = await wants.html()
} catch {
// error state
}
// form submission ends
})
Sortable button
§moveWithButton()
@github/sortablejs
only provides a sorting behavior through drag and drop, which isn't accessible to keyboard users. This function is used to manually bind sort on clicks as an accessible alternative.
import {on} from 'delegated-events'
import {moveWithButton} from 'sortable-button'
on('click', '.js-sortable-button', function(event) {
const button = event.currentTarget
moveWithButton(button, button.closest('.js-sortable-item'), submitSortORderUpdateForm)
})
<ul class="js-sortable-container">
...
<li class="js-sortable-item">
<button type="button" class="js-sortable-button" data-direction="up">Move up</button>
<button type="button" class="js-sortable-button" data-direction="down">Move down</button>
</li>
...
</ul>
Updatable content
§This helper is used to update an element's content by fetching from an endpoint specified via data-url
. This is like using fetchSafeDocumentFragment
+ innerHTML
, but on top of that, this also does the following:
- Restore focus if current focus is inside the element
- Restore
<details id>
open states after replacement if a match is found - Preserve scroll position
- Prevent replacing content if user is interacting with the element content via
hasInteractions()
check
import {updateContent} from './updatable-content'
on('click', '.js-refresh', function(event) {
const list = closest(event.currentTarget, '.js-list')
updateContent(list)
})
<button type="button" class="js-refresh">Refresh</button>
<ul class="js-list" data-url="/list"></ul>
Web Systems maintain a collection of Rails helpers and partials for frontend development.
Partial: site/details_dialog
§Use this shared partial for a generic <details-dialog>
markup including the trigger and close button.
<%= render layout: "shared/details_dialog", locals: {
title: "Sponsor monalisaoctocat",
button_text: "Sponsor"
} do %>
<!-- dialog content goes here -->
<% end %>
Helper: emoji_tag
§Create a <g-emoji>
tag with just an emoji name.
<%= emoji_tag(Emoji.find_by_alias("cat2")) %>
This outputs:
<g-emoji alias="cat2" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f408.png">
🐈
</g-emoji>
Helper: ensure_local_vars
§Annotate required, optional, and default variables for a partial. In development, this helper will throw exceptions if bad local variables are passed in.
Helper: file_attachment_tag
§Use this shared helper to create <file-attachment>
to get auto generated token and upload URL.
Helper: include_cached_fragment
§This caches the response from an <include-fragment>
or <poll-include-fragment>
request, and serves the cached content to subsequent requests.
<%= include_cached_fragment src: top_languages_path, cache_key: top_languages_cache_key do %>
<p>Loading…</p>
<% end %>
In the template for top_languages_path
, do:
<%= cache top_languages_cache_key do %>
<% languages.each do |name, percentage| %><%= name %>: <%= perceentage %>%<% end %>
<% end %>
Helper: markdown_toolbar
§Use this shared helper to create markdown buttons for a textarea with standard styles and keyboard shortcuts. Please refer to the helper code for optional product feature arguments.
<%= markdown_toolbar("edit-comment-#{comment.id}") %>
<textarea id="edit-comment-#{comment.id}"></textarea>
Helper: noscript
§Use noscript
helper (as opposed to <noscript>
) to render alternative code paths. This will cover not only JavaScript-disabled cases, but also unsupported browsers (to which we do not send JavaScript).
<%= noscript do %>
<button type="submit">Submit</button>
<% end %>
Helper: safe_data_attributes
§Convert a hash into data attributes. See background in github/github#116367
.
def hydro_payload
{
hydro_click_hmac: "123"
...
}
end
<button
type="submit"
data-disable-with="Saving..."
<%= safe_data_attributes(view.hydro_payload) %>>
Save
</button>
Outputs:
<button
type="submit"
data-disable-with="Saving..."
data-hydro-click-hmac="123">
Save
</button>
Helper: time
§A suite of helpers for displaying timestamps, backed by @github/time-elements
.
#time_ago_in_words_js
#time_ago_js
#date_with_time_tooltip
#local_or_relative_time_tag
Bulk actions
§.js-bulk-actions-container[data-bulk-actions-url], .js-bulk-actions, .js-bulk-actions-toggle
Provides a way to update bulk actions buttons/menus from a fetched url when check boxes are updated. This is often used along with check-all
. Flow as follows:
- User checks a
.js-bulk-actions-toggle
checkbox - A fetch request gets sent to
bulkActionsURL?bulkActionsParameter=checkboxValues
- The response HTML is set to
.js-bulk-actions
element
This is particularly useful when there are multiple actions available for a collections of objects, and each action contains HTML content that changes according to the checkboxes selected; for example, label menu in issue triage.
Check all
§.js-check-all-container
Support input check range, checked item count, and (un)check all inputs at once. Backed by @github/check-all
.
<details>
§-
button[data-toggle-for="targetid"]
On
click
, toggledetails#targetid
open. -
details[data-deferred-details-content-url=""]
,details[data-details-no-preload-on-hover]
On
toggle
andhover
, load content from URL by setting url to<include-fragment src>
. Ifdata-details-no-preload-on-hover
is present,hover
is disabled.
Details container
§.js-details-container
, .js-details-target
Toggle content on a page. This behavior is an alternative to <details>
.
Apply .js-details-target
to a <button>
that will trigger the toggle on click.
Apply .js-details-container
and .Details
to the element wrapping all the elements to be toggled. This behavior will toggle .Details--on
on container. and CSS is used to show and hide the elements.
Elements to be toggled: Add .Details-content--shown
to elements shown on initial state. Add .Details-content--hidden
elements hidden on initial state.
Dismiss notice
§form.js-notice-dismiss
A basic remote handler for hiding notice on success form submission. The endpoint should save the dismiss notice preference for a user.
Dropdown details
§details.js-dropdown-details
- Add keyboard shortcut
Escape
to close - Ensure only one
.js-dropdwn-details
can be open on a page at any given time
Client side filtering
§Filter a list of items based on user input.
Markup:
input.js-filterable-field
with an ID attribute.- A container containing direct children to be filtered, with
data-filterable-for=${input.id}
,data-filterable-type=${filterableType}
; and only iffilterableType
issubstring-memory
, a requireddata-filterable-src=${path}
.
Available filtering types:
prefix
(default)substring
fuzzy
substring-memory
: like substring, but instead of filtering down direct children, it obtains data from a JSON endpoint specified on the container by thedata-filterable-src
attribute. Use this when the list might be very long; for example, menus with 1000+ users.
Form interaction
§-
input[data-autoselect]
On
focus
, auto select the full range of text in the input. -
button[data-disable-with]
On
click
, mark button as disabled to prevent double submission. -
button[data-confirm], input[data-confirm], a[data-confirm]
On
click
, confirm the activation with a system dialog. -
input[data-indeterminate]
On
load
, setinput.indeterminate = true
. This exists because there is no HTML attribute for the indeterminate state.
Form submission
§-
form[data-remote]
Submit form with JavaScript.
-
input[data-autosubmit]
Submit
input.form
on inputchange
. -
form[data-replace-remote-form]
,[data-replace-remote-form-target]
Submit form with JavaScript and replace
[data-replace-remote-form-target]
with the form response. -
form[data-warn-unsaved-changes]
Prevent user from navigating away from a page without saving their changes with an alert dialog.
Form validation
§-
[data-required-trimmed]
Works for TextArea & Input elements. This invalidates the field unless the trimmed value is not empty.
-
input[data-required-change]
Disable form submission unless input value is changed.
-
button[data-disable-invalid]
On
change
, toggle button according to form validity. See also MDN: Client-side form validation.
Removed contents
§.has-removed-contents
When .has-removed-contents
is added to an element, its contents are detached from the document and stored in memory. Once the class is removed, the original contents are automatically reinserted into the document.
This behavior is useful when working with <form>
with multiple states. Using display: none
is only presentational and doesn't exclude the form fields from being submitted or participating in validation requirements.
Hotkey
§-
[data-hotkey]
Used on interactive elements (
<a>
,<summary>
,<button>
,<input>
,<textarea>
...) to bind keyboard shortcuts. Backed by@github/hotkey
.
Navigation
§This behavior provides keyboard navigation and activation support, as a power user feature only.
- Navigating up with
k
orctrl+n
- Navigating down with
j
orctrl+p
- Activating focused item with
Enter
oro
- Page up with
alt+v
- Page down with
ctrl+v
Markup:
- A
.js-naigation-container
element containing all the.js-navigation-item
. - A List of
.js-navigation-item
elements to be highlighted on navigation. - (optional) Adding
.js-active-navigation-container
on.js-naigation-container
will mark it as active and receiving keystrokes. This can be done programmatically viaactivate()
exported bynavigation.ts
. - (optional) Adding
a.js-navigation-open
on or within.js-navigation-item
will make it so that the activation behavior triggers a click on the link, on top of the defaultnavigation:keyopen
custom event dispatch.
Pjax
§Navigate or submit with pushstate
+ ajax
. This creates a navigation-like experience without refreshing the page.
Markup:
- An element with
data-pjax-container
attribute and an ID attribute. (Ingithub/github
there is a default pjax container in application layout.) - An
<a>
,<form>
, or<select>
withdata-pjax
attribute.data-pjax
attribute can be empty, or optionally be used to specify a pjax container selector. This custom container does not need to havedata-pjax-container
attribute, but is still required to have a unique ID. - (optional) Adding
data-pjax-preserve-scroll
on thedata-pjax
element will preserve the current scroll position after pjax request.
Pjax originally come from https://github.com/defunkt/jquery-pjax. The current behavior in GitHub has diverged from the original implementation.
Quote selection
§-
.js-quote-selection-container
,[data-quote-markdown]
Set container has quotable. Use
[data-quote-markdown='.optional-selector']
to scope quote area, and persist content styling with markdown. Backed by@github/quote-selection
.
Session resume
§.js-session-resumable
Restore field values when the user revisits the page in their current browser session (excludes separate tabs). Backed by @github/session-resume
.
Field values are persisted based on page path; therefore, when using URL params to autofill input values, those values might be overridden if there are stored values for the path. In those cases, you can:
- Disable session resume behavior when the param is present
- Set a unique ID for the state via
<meta name="session-resume-id content="[id]">
Textarea autosizing
§.js-size-to-fit
Autosize <textarea>
to its text contents' height. Backed by @github/textarea-autosize
.
Socket channel
§.js-socket-channel[data-channel]
Establish a web socket connection and subscribes elements to socket channel updates.
A custom event socket:message
is dispatched when a message is received; event.detail
contains name
as the channel name, and data
as a JSON payload sent by the server.
To send messages to the client from Rails in github/github
, check out GitHub::WebSocket
model.
.js-socket-channel.js-updatable-content
.js-updatable-content
listens to socket:message
event that's emitted on .js-socket-channel
, and calls updateContent(element)
. The updatable element can be specified with data.gid
.
Sudo
§form[data-sudo-required], button[data-sudo-required], summary[data-sudo-required]
Ensure a user is in a SudoSession
before taking an action. If the user is not currently in one, a dialog or flow will be triggered. A list of different sudo levels supported can be found in sudo.js
. Remember, users can always inspect and edit the markup to remove this attribute. Always make sure the action is protected server-side.
SudoSession
is the actual model responsible for checking if the current session has been granted access to perform an action at a given risk level. The backend pieces are owned by @github/appsec
.
Suggester
§Suggestion menus are based on the <text-expander>
custom element. We decorate expanders with data attributes containing the URL of the suggestions to fetch from the server.
Menu data sources can be applied individually or in combination depending on the context of the textarea it's decorating. Some inputs might suggest only emoji while others might suggest emoji, mentions, and issue references.
There are three suggestion types available:
-
<text-expander keys=":" data-emoji-url="…">
Suggest emoji on
:
. -
<text-expander keys="@" data-mention-url="…">
Suggest user and teams on
@
. -
<text-expander keys="#" data-issue-url="…">
Suggest issues and pull request references on
#
.
Combine activation keys with a space.
<text-expander keys=": @ #" data-emoji-url="…" data-mention-url="…" data-issue-url="…">
<textarea></textarea>
</text-expander>
View components are backed back github/view_component
as shareable pieces of markup with a finite set of predefind behaivors. The components listed here are for encapsulating JavaScript behaviors and the markup necessary to ensure the behaviors are accessible.
GitHub::DialogComponent
§This component renders a <details-dialog>
styled as Primer: Box Overlay. This ensures that dialog and triggers are properly labeled, has an overlay and a close button.
Render a basic dialog:
<%= render(GitHub::DialogComponent.new(title: "Verify")) do %>
Dialog content!
<% end %>
Render a dialog with a Catalyst event listener for the close event. The following example outputs a data-action="details-dialog-close:get-repo#refreshList"
attribute on the <details-dialog>
element:
<%= render(GitHub::DialogComponent.new(title: "Verify", onclose: "get-repo#refreshList")) do %>
Dialog content!
<% end %>
Render a dialog with a custom trigger and structured content:
<%= render(GitHub::DialogComponent.new(title: "Verify")) do |dialog| %>
<% dialog.with(:summary) do %>
<summary class="text-red">Don't click on me <img src="/warning.png" alt="danger"></summary>
<% end %>
<% dialog.with(:body) do %>
Dialog content!
<% end %>
<% dialog.with(:footer) do %>
<button type="button" class="btn btn-block" data-close-dialog>Close</button>
<% end %>
<% end %>
Render a dialog with Primer variants for Box Overlay:
<%= render(GitHub::DialogComponent.new(title: "Verify", variant: :wide)) do |dialog| %>
Dialog content!
<% end %>
Render a dialog with defer loaded content, where src
will be copied onto <include-fragment>
on open:
<%= render(GitHub::DialogComponent.new(title: "Verify", src: "/modal")) do |dialog| %>
<% dialog.with(:body) do %>
<include-fragment>Loading</include-fragment>
<% end %>
<% dialog.with(:footer) do %>
<button type="button" class="btn btn-block" data-close-dialog>Close</button>
<% end %>
<% end %>
Render a form within a dialog—To ensure that both body and footer are contained in a form, use the default content block rather than passing in content areas :body
and :footer
separately.
<%= render GitHub::DialogComponent.new(...) do %>
<%= form_tag form_path do %>
<div class="Box-body">
...
</div>
<div class="Box-footer">
<button type="submit" class="btn btn-primary">Save group</button>
</div>
<% end %>
<% end %>
GitHub::MenuComponent
§This component renders a <details-menu>
styled as Primer: SelectMenu. Feature supported:
filterable: true
adds a<filter-input
in the menu.src: <url>
would load menu content uponmenu activation. This can be used withpreload: true
.onselect
,onselected: "catalyst-element#action"
addsdata-action="details-menu-selected:catalyst-element#action"
for Catalyst event binding on<details-menu>
for its custom events.
The menu component takes an array of items what should be one of the following:
GitHub::Menu::ButtonComponent
GitHub::Menu::LinkComponent
GitHub::Menu::RadioComponent
More types of items and multi-select support are coming soon.
Frontend test suite
§Command: bin/npm test [--debug]
When live editing test cases, --debug
allows you to quickly re-run tests by going to the "Debug" view and refreshing the page. Make sure to actually open up the tests in your browser as the test suite doesn't open up your browser for you.
Code health
§Command: bin/testrb <test_file_path.rb>
We have a number of tests in place to guard against tech debt, and keep the code base heathy:
-
bundle_requires_test.rb
ensures that all JS files are used. If the last instance of a module import is removed, the test prompt users to remove the module. -
js_classes_get_used_test.rb
ensures that alljs-
prefixed classes used in JS are either referenced in markup or Rails helpers, and vice versa. -
data_attributes_test.rb
ensures that all data attributes used in JS are either referenced in markup or Rails helpers, and vice versa.
There is also script/check-js-bundle-size
, which fails the build if there is a drastic change in bundle sizes. It prompts for a review, and would require a manual size limit bump to pass.
ERB
§ERBLint
Command: bin/bundle exec erblint app/views
ERBLint is used to lint HTML. Custom ERBLint rules can be found at ./erb-linters
. Basic conditional in the template is parsed.
Avoid postfix conditions in HTML attributes § so that the attribute will be correctly syntax highlighted, and ERBLint will be able to parse required
in the example correctly as one of tag.attributes
.
<!-- Bad -->
<input type="text" <%= "required" if required %>>
<!-- Good -->
<input type="text" <% if required %>required<% end %>>
Avoid creating data attributes with data: {}
§ so that attributes are searchable and lintable.
<!-- Bad -->
<%= form_tag path, data: {warn_unsaved_changes: ""} %>
<!-- Good -->
<%= form_tag path, "data-warn-unsaved-changes": "" %>
Avoid HTML generation Rails helpers § like link_to
and content_tag
.
Rendering static markup strings is simpler than evaluating helper methods. Inspecting HTML in the browser and searching for it in the source code is easier with static markup in the templates. Raw HTML allows us to develop better static analysis tools more easily than dynamic Rails helpers, and helpers do not always scale to meet requirements - making them a leaky abstraction.
Use safe_data_attributes
to generate data attributes from Hash if necessary.
<!-- Bad -->
<%= content_tag :button, text, data: hydro.merge(disable_with: ""), type: "button" %>
<!-- Good -->
<button type="button" data-disable-with <%= safe_data_attributes(hydro) %>><%= text %></button>
To disable a ERBLint rule, add: <%# erblint:disable DisabledAttribute %>
RuboCop
Command: bin/rubocop app/views
RuboCop is used to lint Rails view helper usages in views. Rules can be found in rubocop.yml
.
To disable a rule, add: <%# rubocop:disable Rails:ButtonType %>
ESLint
§Command: bin/npm run lint
We use ESLint to enforce patterns and common pitfalls in JavaScript. You can check our custom config out in github/eslint-plugin-github
. The config includes TypeScript, as well as code formatting via Prettier.
The Web Systems team has written some custom rules for our specific needs. Find a complete list of custom ESLint rules and documentation in the docs folder.
Setting up your editor to warn you about these violations is highly recommended since this will save you time and CI failure cycles. A lot of rules and violations can be autofixed, and your editor can probably be set up to autofix them for you.
VS Code
- https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode
- https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint
- https://marketplace.visualstudio.com/items?itemName=adamwalzer.scss-lint
- https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint
Vim
- https://github.com/dgraham/vim-eslint
- https://github.com/prettier/vim-prettier
- https://github.com/Microsoft/TypeScript/wiki/TypeScript-Editor-Support#vim
If you are using the Asyncronous Linting Engine you might want to add the following snippet to your config so that ALE picks is aware of the tools we use when writing javascript at GitHub.
let g:ale_linters = {
\ 'javascript': ['eslint', 'prettier', 'prettier-eslint'],
\}
let g:ale_fixers = {
\ 'javascript': ['eslint', 'prettier', 'prettier-eslint'],
\}
Since prettier
and eslint
both have concepts of fixing code when it's in a violation of a rule, it's also a good idea to turn "fix on save" on so that instead of telling you about a problem ALE will just apply the fix for you:
let g:ale_fix_on_save = 1