Skip to content

Commit

Permalink
remove hidden input for storing currently selected as JSON
Browse files Browse the repository at this point in the history
  • Loading branch information
janosh committed May 8, 2021
1 parent 14dd38a commit 802a219
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 116 deletions.
34 changes: 15 additions & 19 deletions multiselect/MultiSelect.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
export let name = ``
const dispatch = createEventDispatcher()
let activeOption, filterValue, filterInput
let activeOption, filterValue
let showOptions = false
$: filtered = options.filter((option) =>
Expand All @@ -35,24 +35,22 @@
) {
filterValue = ``
selected = single ? token : [token, ...selected]
input.value = JSON.stringify(selected)
if (single) {
setOptionsVisible(false)
filterInput.blur()
input.blur()
}
}
}
function remove(token) {
if (readonly || single) return
selected = selected.filter((str) => str !== token)
input.value = JSON.stringify(selected)
}
function setOptionsVisible(show) {
if (readonly) return
showOptions = show
if (show) filterInput.focus()
if (show) input.focus()
else activeOption = undefined
}
Expand Down Expand Up @@ -106,30 +104,28 @@
{#if readonly}
<ReadOnlyIcon height="18pt" />
{:else}
<!-- for holding the component's value in a way accessible to the DOM -->
<input bind:this={input} {required} {name} id={name} readonly tabindex="-1" />
<!-- tabindex="-1" means skip element during tabbing, else we couldn't shift-tab out of filterInput as filterInput.focus() would jump right back -->
<input
bind:this={input}
{required}
{name}
id={name}
on:click|self={() => setOptionsVisible(true)}
on:blur={() => dispatch(`blur`)}
autocomplete="off"
bind:value={filterValue}
bind:this={filterInput}
on:keydown={handleKeydown}
on:focus={() => setOptionsVisible(true)}
on:blur={() => setOptionsVisible(false)}
style="flex: 1;"
placeholder={selected.length ? `` : placeholder} />
{#if !single}
<button
type="button"
class="remove-all"
title="Remove All"
on:click={removeAll}
style={selected.length === 0 && `display: none;`}>
<CrossIcon height="18pt" />
</button>
{/if}
<button
type="button"
class="remove-all"
title="Remove All"
on:click={removeAll}
style={selected.length === 0 && `display: none;`}>
<CrossIcon height="18pt" />
</button>
{/if}

{#if showOptions}
Expand Down
75 changes: 59 additions & 16 deletions multiselect/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@

## Key Features

- Single / multiple select
- Dropdowns
- Searchable
- Tagging
- Server-side rendering
- Configurable
- No dependencies
- Keyboard friendly
- **Single / multiple select**: pass `single` prop to only allow one selection
- **Dropdowns**: scrollable lists for large numbers of options
- **Searchable**: start typing to filter options
- **Tagging**: selected options are recorded as tags within the text input
- **Server-side rendering**: no reliance on browser objects like `window` or `document`
- **Configurable**
- **No dependencies**, needs only Svelte as dev dependency
- **Keyboard friendly** for mouse-less form completion

## Installation

Expand Down Expand Up @@ -54,13 +54,56 @@ Favorite Web Frameworks?
<MultiSelect bind:input {name} {placeholder} options={webFrameworks} {required} />
```

## Props

Full list of props/bindable variables for this component:

- `options` (required): Array of strings (or integers) that will be listed in the dropdown selection.
- `selected = []`: Array of currently/pre-selected options when binding/passing as props respectively.
- `readonly = false`: Disables the input. User won't be able to interact with it.
- `placeholder = ''`: String shown when no option is selected.
- `single = false`: Allows only a single option to be selected when true.
- `required = false`: Prevents submission in an HTML form when true.
- `input = undefined`: Handle to the DOM node storing the currently selected options in JSON format as its `value` attribute.
- `name = ''`: Used as reference for associating HTML form labels with this component as well as for the `input` `id`. That is, the same DOM node `input` bindable through `<MultiSelect bind:input />` is also retrievable via `document.getElementByID(name)` e.g. for use in a JS file outside a Svelte component.
| name | default | description |
| :------------ | :---------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `options` | [required] | Array of strings (or integers) that will be listed in the dropdown selection. |
| `selected` | `[]` | Array of currently/pre-selected options when binding/passing as props respectively. |
| `readonly` | `false` | Disables the input. User won't be able to interact with it. |
| `placeholder` | `''` | String shown when no option is selected. |
| `single` | `false` | Allows only a single option to be selected when true. |
| `required` | `false` | Prevents submission in an HTML form when true. |
| `input` | `undefined` | Handle to the DOM node storing the currently selected options in JSON format as its `value` attribute. |
| `name` | `''` | Used as reference for associating HTML form labels with this component as well as for the `input` `id`. That is, the same DOM node `input` bindable through `<MultiSelect bind:input />` is also retrievable via `document.getElementByID(name)` e.g. for use in a JS file outside a Svelte component. |

## Styling

You can style every part of this component by using the following selectors. Overriding properties that the component already sets internally requires the `!important` keyword.

```css
:global(.multiselect) {
/* top-level wrapper div */
}
:global(.multiselect span.token) {
/* selected options */
}
:global(.multiselect span.token button),
:global(.multiselect .remove-all) {
/* buttons to remove a single or all selected options at once */
}
:global(.multiselect ul) {
/* dropdown options */
}
:global(.multiselect ul li) {
/* dropdown options */
}
:global(li.selected) {
/* selected options in the dropdown list */
}
:global(li:not(.selected):hover) {
/* unselected but hovered options in the dropdown list */
}
:global(li.selected:hover) {
/* selected and hovered options in the dropdown list */
/* probably not necessary to style this state in most cases */
}
:global(li.active) {
/* active means element was navigated to with up/down arrow keys */
/* ready to be selected by pressing enter */
}
:global(li.selected.active) {
}
```
5 changes: 1 addition & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,10 @@
"multiselect",
"site"
],
"scripts": {
"publish": "yarn workspace multiselect publish"
},
"devDependencies": {
"eslint": "^7.25.0",
"eslint-plugin-svelte3": "^3.2.0",
"prettier": "^2.2.1",
"prettier-plugin-svelte": "^2.2.0"
}
}
}
1 change: 1 addition & 0 deletions site/netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ command = "yarn build"
publish = "build"

[build.environment]
NETLIFY_USE_YARN = "true"
NODE_VERSION = "16.1.0"
YARN_VERSION = "1.22.10"
36 changes: 22 additions & 14 deletions site/src/app.html
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Svelte MultiSelect</title>
<meta name="author" content="Janosh Riebesell" />
<meta
name="description"
content="Keyboard-friendly, zero-dependency Svelte MultiSelect component"
/>

<head>
<title>Svelte MultiSelect</title>
<meta name="author" content="Janosh Riebesell" />
<meta name="description" content="Keyboard-friendly Svelte MultiSelect component" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />

<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta property="og:type" content="article" />
<meta property="og:image" content="/favicon.svg" />
<meta property="og:url" content="https://svelte-multiselect.netlify.app" />
<meta property="og:site_name" content="Svelte MultiSelect" />

<link rel="icon" href="/favicon.svg" />
<link rel="stylesheet" href="/global.css">
<link rel="stylesheet" href="/prism-vsc-dark-plus.css" />
<link rel="icon" href="/favicon.svg" />
<link rel="stylesheet" href="/global.css" />
<link rel="stylesheet" href="/prism-vsc-dark-plus.css" />

%svelte.head%
</head>
%svelte.head%
</head>

<body>%svelte.body%</body>

</html>
<body>
%svelte.body%
</body>
</html>
72 changes: 33 additions & 39 deletions site/src/components/Example.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -26,60 +26,54 @@
`Theano`,
`CNTK`,
]
const name = `webFrameworks`
const placeholder = `Take your pick...`
const required = true
let input
let selectedWeb, selectedML
</script>

<h3>Multi Select</h3>
<section>
<div>
<h3>Multi Select</h3>

<p>Favorite Web Frameworks?</p>

Favorite Web Frameworks?
{#if selectedWeb?.length > 0}
<pre><code>selected = {JSON.stringify(selectedWeb)}</code></pre>
{/if}

<MultiSelect bind:input {name} {placeholder} options={webFrameworks} {required} />
<MultiSelect {placeholder} options={webFrameworks} bind:selected={selectedWeb} />
</div>
<div>
<h3>Single Select</h3>

<h3>Single Select</h3>
<p>Favorite Machine Learning Framework?</p>

Favorite Machine Learning Framework?
{#if selectedML?.length > 0}
<pre><code>selected = {JSON.stringify(selectedML)}</code></pre>
{/if}

<MultiSelect single bind:input {placeholder} options={mlFrameworks} {required} />
<MultiSelect single {placeholder} options={mlFrameworks} bind:selected={selectedML} />
</div>
</section>

<style>
:global(.multiselect) {
/* top-level wrapper div */
section {
display: flex;
gap: 1em;
}
:global(.multiselect span.token) {
/* selected options */
section div {
flex: 1;
background-color: black;
border-radius: 1ex;
padding: 0 1em;
height: max-content;
}
:global(.multiselect span.token button),
:global(.multiselect .remove-all) {
/* buttons to remove a single or all selected options at once */
font-size: 1em;
@media (max-width: 600px) {
section {
display: contents;
}
}
:global(.multiselect ul) {
/* dropdown options */
background: black !important;
}
:global(.multiselect ul li) {
/* dropdown options */
}
:global(li.selected) {
/* selected options in the dropdown list */
}
:global(li:not(.selected):hover) {
/* unselected but hovered options in the dropdown list */
}
:global(li.selected:hover) {
/* selected and hovered options in the dropdown list */
/* probably not necessary to style this state in most cases */
}
:global(li.active) {
/* active means element was navigated to with up/down arrow keys */
/* ready to be selected by pressing enter */
background: firebrick;
}
:global(li.selected.active) {
background: gray;
}
</style>
Loading

0 comments on commit 802a219

Please sign in to comment.