Add a new documentation site to the repository (#23)
* Update config * Update readme. * Update readme.md * Add getting started section to tutorials. * Added getting-started with updates. * Major update to getting started guide * Overhaul to tutorials.
This commit is contained in:
parent
31e567f9b1
commit
0721d52d04
31 changed files with 1465 additions and 392 deletions
.circleci
.gitignoredocs
examples
mkdocs.ymlpackage.jsonreadme.md
|
@ -38,45 +38,53 @@ jobs:
|
|||
paths:
|
||||
- ~/select/node_modules
|
||||
|
||||
# Verify all examples successfully build...
|
||||
- run:
|
||||
name: Build all components via docs
|
||||
command: yarn run build-docs
|
||||
name: Build example components
|
||||
command: |
|
||||
yarn build-docs
|
||||
|
||||
# Persist the specified paths into the workspace
|
||||
- persist_to_workspace:
|
||||
root: docs/dist
|
||||
root: docs
|
||||
paths:
|
||||
- .
|
||||
- js
|
||||
|
||||
site:
|
||||
working_directory: ~/select
|
||||
docker:
|
||||
- image: rustlang/rust:nightly
|
||||
css:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- run:
|
||||
name: Install Gutenberg
|
||||
name: Fetch and persist CSS
|
||||
command: |
|
||||
wget https://github.com/Keats/gutenberg/releases/download/v0.3.1/gutenberg-v0.3.1-x86_64-unknown-linux-gnu.tar.gz -O gb.tar.gz
|
||||
tar -xvzf gb.tar.gz
|
||||
yarn fetch-css && yarn move-css
|
||||
|
||||
- attach_workspace:
|
||||
at: docs/dist
|
||||
- persist_to_workspace:
|
||||
root: docs
|
||||
paths:
|
||||
- css
|
||||
|
||||
site:
|
||||
working_directory: ~/select
|
||||
docker:
|
||||
- image: jfloff/alpine-python:3.4-slim
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- setup_remote_docker
|
||||
|
||||
- run:
|
||||
name: Build site
|
||||
command: |
|
||||
mv gutenberg docs/site
|
||||
cd docs/site
|
||||
./gutenberg build
|
||||
cd ../
|
||||
cp -r site/public/. dist/
|
||||
name: Install dependencies
|
||||
command: pip install mkdocs mkdocs-material
|
||||
|
||||
- attach_workspace:
|
||||
at: docs
|
||||
|
||||
- run:
|
||||
name: Run site generation
|
||||
command: mkdocs build
|
||||
|
||||
# Persist the dist folder
|
||||
- persist_to_workspace:
|
||||
root: docs/dist
|
||||
root: site
|
||||
paths:
|
||||
- .
|
||||
|
||||
|
@ -98,7 +106,7 @@ jobs:
|
|||
git worktree add ../gh-pages gh-pages
|
||||
|
||||
- attach_workspace:
|
||||
at: dist
|
||||
at: site
|
||||
|
||||
# Enable building docs
|
||||
- add_ssh_keys:
|
||||
|
@ -108,14 +116,14 @@ jobs:
|
|||
- run:
|
||||
name: Copy over files to gh-pages branch
|
||||
command: |
|
||||
cp -r dist/. ../gh-pages
|
||||
cp -r site/. ../gh-pages
|
||||
cp -r .circleci/. ../gh-pages/.circleci
|
||||
|
||||
- run:
|
||||
name: Push update to GitHub
|
||||
command: |
|
||||
cd ../gh-pages
|
||||
git config --global user.email "dev@citizennet.com"
|
||||
git config --global user.email "admin@thomashoneyman.com"
|
||||
git config --global user.name "CircleCI Build"
|
||||
git add .
|
||||
git commit --allow-empty -m "Build triggered by CircleCI"
|
||||
|
@ -131,10 +139,22 @@ workflows:
|
|||
ignore: gh-pages
|
||||
|
||||
# On master branch, rebuild docs
|
||||
- css:
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- docs
|
||||
|
||||
requires:
|
||||
- test
|
||||
|
||||
- site:
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
only:
|
||||
- master
|
||||
- docs
|
||||
|
||||
requires:
|
||||
- test
|
||||
|
@ -142,7 +162,10 @@ workflows:
|
|||
- docs:
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
only:
|
||||
- master
|
||||
- docs
|
||||
|
||||
requires:
|
||||
- site
|
||||
- css
|
||||
|
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -11,8 +11,9 @@ package-lock.json
|
|||
generated-docs
|
||||
|
||||
# Dist
|
||||
docs/dist
|
||||
docs/site/public
|
||||
docs/css
|
||||
docs/js
|
||||
site
|
||||
|
||||
# IDE
|
||||
*.swo
|
||||
|
|
84
docs/concepts/understanding-free-queries.md
Normal file
84
docs/concepts/understanding-free-queries.md
Normal file
|
@ -0,0 +1,84 @@
|
|||
# Understanding Free Monad Queries
|
||||
|
||||
Most of the time Halogen queries look like this:
|
||||
|
||||
```hs
|
||||
data QueryF (… other type arguments omitted …) a
|
||||
= ...
|
||||
| SetVisibility Visibility a
|
||||
| GetVisibility (Visibility -> a)
|
||||
```
|
||||
|
||||
(where `#!hs QueryF` is used directly as the Halogen query functor)
|
||||
|
||||
This library takes a slightly different approach: the query functor is actually `#!hs Control.Monad.Free.Free QueryF`, the [free monad](https://pursuit.purescript.org/packages/purescript-free/4.2.0/docs/Control.Monad.Free) generated by the query functor.
|
||||
|
||||
This allows queries the full power of monadic (and applicative) composition: sequencing effects, determining control flow based on previous results, and my favorite: doing nothing (`#!hs pure unit`).
|
||||
|
||||
We now define smart query constructors for this Free pattern like so:
|
||||
|
||||
```hs
|
||||
-- | Set the container visibility (`On` or `Off`).
|
||||
setVisibility :: ∀ o item eff. Visibility -> Query o item eff Unit
|
||||
setVisibility v = liftF (SetVisibility v unit)
|
||||
|
||||
-- | Get the container visibility (`On` or `Off`). Most useful when sequenced
|
||||
-- | with other actions.
|
||||
getVisibility :: ∀ o item eff. Query o item eff Visibility
|
||||
getVisibility = liftF (GetVisibility id)
|
||||
```
|
||||
|
||||
## Different patterns
|
||||
In the simple cases, the helpers Halogen use to work with raw query constructors are folded into the smart Free query constructors already: `#!hs H.action (SetVisibility On)` becomes simply `#!hs setVisiblity On`, and similarly `#!hs H.request GetVisibility` is just `#!hs getVisibility`. This is because these patterns are typically present already smart constructors: `#!hs setVisibility` returns `#!hs Free QueryF Unit`, since it is an action, and `#!hs getVisibility` returns `#!hs Free QueryF Visibility`, since it requests the visibility. This allows for easy composition in `#!hs do` notation:
|
||||
|
||||
```hs
|
||||
toggleVisibility = do
|
||||
vis <- getVisibility
|
||||
setVisibility (not vis)
|
||||
```
|
||||
|
||||
C’est très facile!
|
||||
|
||||
Event handlers look a little different. This is one example:
|
||||
|
||||
```hs
|
||||
HE.onMouseDown \ev -> Just do
|
||||
Select.preventClick ev
|
||||
Select.select index
|
||||
when doBlur Select.triggerBlur
|
||||
```
|
||||
|
||||
(Of course you may return `#!hs Nothing` if you so wish, but its effect is just like `#!hs pure unit` now.)
|
||||
|
||||
If you do not need access to the argument `#!hs ev`, `#!hs Select.always` provides a simple shortcut for `#!hs const <<< Just`:
|
||||
|
||||
```hs
|
||||
HE.onMouseOver $ Select.always $ Select.highlight (Index index)
|
||||
```
|
||||
|
||||
## Returning non-unit values
|
||||
Use `#!hs map` or `#!hs <$` or `#!hs pure` to return other types of values from a query. So, instead of something like this:
|
||||
|
||||
```hs
|
||||
H.subscribe $ eventSource' someEventSource
|
||||
\value -> Just (SetVisibility value H.Listening)
|
||||
```
|
||||
|
||||
Use
|
||||
|
||||
```hs
|
||||
H.subscribe $ eventSource' someEventSource
|
||||
\value -> Just $ setVisibility value $> H.Listening
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```hs
|
||||
H.subscribe $ eventSource' someEventSource
|
||||
\value -> Just do
|
||||
setVisibility value
|
||||
pure H.Listening
|
||||
```
|
||||
|
||||
!!! info ""
|
||||
Many thanks to [Nicholas Scheel](https://github.com/MonoidMusician) for providing the implementation of `QueryF` and the documentation above.
|
23
docs/examples.md
Normal file
23
docs/examples.md
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Examples
|
||||
|
||||
You can play around with a few example components here. However, for a much richer set of components with much more functionality, check out the [Ocelot design system by CitizenNet](https://citizennet.github.io/purescript-ocelot/#typeaheads).
|
||||
|
||||
### Dropdown
|
||||
|
||||
Dropdowns are a common button-driven input type, especially for navigation. But most custom dropdowns sacrifice usability: unlike browser default dropdowns, you can't type on most custom dropdowns, nor are many built with accessibility in mind. With `Select` you can easily create rich, usable dropdowns with little code.
|
||||
|
||||
<div data-component="dropdown" class="ocelot-scoped"></div>
|
||||
|
||||
Curious how to build a dropdown with `Select`? Check out [the dropdown tutorial](https://citizennet.github.io/tutorials/build-a-dropdown).
|
||||
|
||||
### Typeahead / Autocomplete
|
||||
|
||||
This library was originally designed so that we could build typeaheads with all sorts of custom rendering and functionality. It was frustrating to find solutions that almost worked, but broke down as soon as you needed a moderate level of customization.
|
||||
|
||||
Building typeaheads with `Select` is only a little more complex than building dropdowns. Instead of a button as input, you'll use a text input, and you'll be responsible for deciding how to handle user searches. `Select` handles debouncing user input, keyboard navigation, and more on your behalf.
|
||||
|
||||
The typeahead below is quite simple; to see examples of more sophisticated typeaheads -- including ones that fetch and display data asynchronously -- check out the [Ocelot component library](https://citizennet.github.io/purescript-ocelot/#typeaheads).
|
||||
|
||||
<div data-component="typeahead" class="ocelot-scoped"></div>
|
||||
|
||||
Curious how to build a typeahead with `Select`? Check out [the typeahead tutorial](https://citizennet.github.io/tutorials/build-a-typehead).
|
3
docs/how-to/embed-parent-queries.md
Normal file
3
docs/how-to/embed-parent-queries.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# How to Embed Parent Queries
|
||||
|
||||
One of the most powerful ways to extend `Select` is to write functionality in your component, triggered by a query, and then embed that query into the `Select` component.
|
45
docs/index.md
Normal file
45
docs/index.md
Normal file
|
@ -0,0 +1,45 @@
|
|||
# Welcome
|
||||
|
||||
`Select` helps you build selection user interfaces in PureScript with Halogen. You can use it to build dropdowns, typeaheads and autocompletes, date pickers, image pickers, and more, with features like keyboard navigation, accessibility, and state management handled for you. This library takes a unique approach to component design to ensure you can leverage its features without compromising your design in any way.
|
||||
|
||||
## Installation
|
||||
|
||||
You can use `Select` in your PureScript project with a compatible package manager. The PureScript community typically leverages [psc-package](https://github.com/purescript/psc-package) or Bower:
|
||||
|
||||
```sh
|
||||
# Using psc-package
|
||||
$ psc-package install halogen-select
|
||||
|
||||
# Using Bower
|
||||
$ bower install --save purescript-halogen-select
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
If this is your first time using `Select`, start with the [tutorials](https://citizennet.github.io/purescript-halogen-select/tutorials/getting-started). I'd recommend starting with the simplest example where you'll learn to make this dropdown component:
|
||||
|
||||
<div class="ocelot-scoped" data-component="dropdown"></div>
|
||||
|
||||
!!! tip
|
||||
Don't want to build your own UI components? Check out the [Ocelot component library](https://citizennet.github.io/purescript-ocelot)!
|
||||
|
||||
If this isn't your first time, you'll find value in these resources:
|
||||
|
||||
* The [how-to](https://citizennet.github.io/purescript-halogen-select/how-to/embed-parent-queries) section contains plenty of short guides for common tasks you'll perform using `Select`. Consider it a grab-bag of useful strategies and examples you can refer to when developing new components.
|
||||
* The [concepts](https://citizennet.github.io/purescript-halogen-select/concepts/understanding-free-queries) section contains more detailed explanations on the design of the library. It will help you understand how to make good design choices and make the most of the tools available to you.
|
||||
* The [reference documentation on Pursuit](https://pursuit.purescript.org/packages/purescript-halogen-select) contains the module documentation and source code. It's a useful reference to verify what functions are available to you.
|
||||
* The [examples folder on Github](https://github.com/citizennet/purescript-halogen-select) contains the working source code for all the components in the [tutorials](https://citizennet.github.io/purescript-halogen-select/tutorials/getting-started). If you're building a similar component, this code can help you get started.
|
||||
|
||||
## Why Select?
|
||||
|
||||
`Select` provides essential behaviors for selection UI as a flexible, extensible Halogen component. But you won't find a single render function in the code. Instead, with a few helper functions, you can write your own `#!hs State -> HTML` function however you'd like. You can:
|
||||
|
||||
* Extend the component's functionality by embedding new queries in the HTML
|
||||
* Extend the component's data by including as much additional state from the parent as you want (which you can then use in your render function)
|
||||
* Leverage the provided features for user interaction, state management, accessibility, and logic
|
||||
* Retain complete freedom over the design and aesthetic of your selection component
|
||||
|
||||
!!! aside "For visual learners"
|
||||
I gave a talk at the Los Angeles PureScript meetup in April 2018 about the approach this library takes. It provides an overview of our design approach, including advantages and drawbacks, as well as a simple walkthrough of building a dropdown. No, the man in the preview isn't me -- that's [Phil Freeman](http://functorial.com/), the designer of the PureScript programming language.
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/igWrktC0m7E?rel=0&showinfo=0&start=2119" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
|
|
@ -1,6 +0,0 @@
|
|||
# Documentation
|
||||
|
||||
This section of the repository holds example components built with `Select` and demonstrated in a Github Pages site. These are only the source files; the build itself is managed by Circle CI and artifacts are kept on the `gh-pages` branch.
|
||||
|
||||
TODO:
|
||||
- Write a post explaining how other PureScript repositories can do the same
|
|
@ -1,12 +0,0 @@
|
|||
# The base URL pages are relative to
|
||||
base_url = "/purescript-halogen-select/"
|
||||
|
||||
# Don't provide or compile from Sass folder
|
||||
compile_sass = true
|
||||
|
||||
# Turn on syntax highlighting
|
||||
highlight_code = true
|
||||
highlight_theme = "inspired-github"
|
||||
|
||||
# Put custom variables in [extra]
|
||||
[extra]
|
|
@ -1,17 +0,0 @@
|
|||
+++
|
||||
title = "PureScript Halogen Select"
|
||||
description = "Some description"
|
||||
template = "home.html"
|
||||
+++
|
||||
|
||||
## Typeaheads
|
||||
|
||||
Use input-driven selects to support typeaheads and autocompletion.
|
||||
|
||||
{{ halogen(id="typeahead", header="Typeahead") }}
|
||||
|
||||
## Dropdowns
|
||||
|
||||
Use toggle-driven selects to support dropdowns.
|
||||
|
||||
{{ halogen(id="dropdown", header="Dropdown") }}
|
|
@ -1,5 +0,0 @@
|
|||
+++
|
||||
title = "Documentation"
|
||||
description = "Some description"
|
||||
template = "section.html"
|
||||
+++
|
|
@ -1,5 +0,0 @@
|
|||
+++
|
||||
title = "Functions"
|
||||
description = "Some description"
|
||||
template = "article.html"
|
||||
+++
|
|
@ -1,5 +0,0 @@
|
|||
+++
|
||||
title = "Tutorials"
|
||||
description = "Some description"
|
||||
template = "section.html"
|
||||
+++
|
|
@ -1,46 +0,0 @@
|
|||
+++
|
||||
title = "Typeahead Tutorial"
|
||||
description = "A tutorial for building typeaheads with Select."
|
||||
template = "article.html"
|
||||
+++
|
||||
|
||||
# Some Text
|
||||
|
||||
I've written an article here.
|
||||
|
||||
```hs
|
||||
eval = case _ of
|
||||
ToContainer q a -> H.query unit q *> pure a
|
||||
|
||||
HandleContainer m a -> case m of
|
||||
C.Emit q -> eval q *> pure a
|
||||
|
||||
C.ItemSelected item -> do
|
||||
H.modify \st -> st { selected = ( item : st.selected ) }
|
||||
st <- H.get
|
||||
_ <- H.query unit
|
||||
$ H.action
|
||||
$ C.ContainerReceiver
|
||||
$ { render: renderContainer
|
||||
, items: difference st.items st.selected }
|
||||
pure a
|
||||
|
||||
Removed item a -> do
|
||||
st <- H.get
|
||||
let newSelections = delete item st.selected
|
||||
newItems = difference newSelections st.items
|
||||
H.modify (_ { selected = newSelections })
|
||||
_ <- H.query unit
|
||||
$ H.action
|
||||
$ C.ContainerReceiver
|
||||
$ { render: renderContainer
|
||||
, items: newItems }
|
||||
pure a
|
||||
```
|
||||
|
||||
<br>
|
||||
## Typeaheads
|
||||
|
||||
Support asynchronous, continuously asynchronous, and synchronous typeaheads with autocompletion.
|
||||
|
||||
{{ halogen(id="typeahead", header="Typeahead") }}
|
|
@ -1,38 +0,0 @@
|
|||
$black: #22292f;
|
||||
$grey-95: #f0f1f2;
|
||||
$grey-lightest: #f8fafc;
|
||||
|
||||
$step-1: 0.25rem;
|
||||
$step-2: 0.5rem;
|
||||
$step-4: 1.0rem;
|
||||
$step-6: 1.5rem;
|
||||
|
||||
$semibold: 600;
|
||||
|
||||
$full: 100%;
|
||||
$half: 50%;
|
||||
|
||||
pre {
|
||||
padding: 0 $step-4 0 $step-4;
|
||||
margin-top: $step-6;
|
||||
margin-bottom: $step-6;
|
||||
border-left: $grey-95 $step-1 solid;
|
||||
}
|
||||
|
||||
table {
|
||||
width: $full;
|
||||
text-align: left;
|
||||
border-collapse: collapse;
|
||||
margin: $step-6 0;
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: $semibold;
|
||||
padding: $step-2;
|
||||
background-color: $grey-lightest;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: $step-2;
|
||||
border-top: $grey-95 1px solid;
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
{% extends "base-nav.html" %}
|
||||
|
||||
{% block page %}
|
||||
{{ page.content | safe }}
|
||||
{% endblock %}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<nav class="flex items-center justify-center py-6 mb-6">
|
||||
<div class="flex items-center flex-no-shrink block mr-6">
|
||||
<span class="font-light text-3xl"><a class="no-underline text-grey-darkest" href="{{ config.base_url }}">SELECT</a></span>
|
||||
</div>
|
||||
|
||||
<div class="w-full block flex-grow flex items-center w-auto">
|
||||
<div class="text-sm flex-grow">
|
||||
<a class="block inline-block text-grey-dark no-underline hover:text-grey-darker mr-2" href="{{ config.base_url }}/tutorials">Tutorials</a>
|
||||
<a class="block inline-block text-grey-dark no-underline hover:text-grey-darker mr-2" href="{{ config.base_url }}/documentation">Documentation</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a class="bg-teal hover:bg-teal-lighter text-white border-2 border-teal hover:teal-lighter no-underline py-2 px-4 rounded mx-2"
|
||||
href="https://github.com/citizennet/purescript-halogen-select">
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{% block page %}
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
|
@ -1,22 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
{% block head %}
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}{% if config.title %}{{ config.title }}{% else %}PureScript Halogen Select{% endif %}{% endblock title %}</title>
|
||||
<meta name="description" content="{% block description %}{{ config.description }}{% endblock description %}">
|
||||
<link rel="stylesheet" href="https://unpkg.com/tachyons@4.9.1/css/tachyons.min.css"/>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{{ config.base_url }}site.css" />
|
||||
<script src="{{ config.base_url }}app.js"></script>
|
||||
{% endblock head %}
|
||||
</head>
|
||||
|
||||
<body class="font-normal text-grey-darkest leading-normal">
|
||||
<div class="w-full max-w-lg mx-auto">
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,23 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ section.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="text-center">
|
||||
<h1 class="font-light text-3xl pt-8">Select for PureScript's Halogen<br></h1>
|
||||
<p class="text-xl text-grey-dark mb-4">Flexible, accessible primitives for selection user interfaces.</p>
|
||||
<div class="my-8">
|
||||
<a class="bg-teal hover:bg-teal-light text-white border-2 border-teal hover:border-teal-light no-underline py-2 px-4 rounded mx-2"
|
||||
href="https://github.com/citizennet/purescript-halogen-select">
|
||||
GitHub
|
||||
</a>
|
||||
<a class="bg-white hover:bg-teal-light hover:text-white text-teal border-2 border-teal-light no-underline py-2 px-4 rounded mx-2" href="{{ config.base_url }}/documentation">
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ section.content | safe }}
|
||||
|
||||
{% endblock %}
|
|
@ -1,18 +0,0 @@
|
|||
{% extends "base-nav.html" %}
|
||||
|
||||
{% block page %}
|
||||
|
||||
<h1>Table of Contents</h1>
|
||||
|
||||
<ul class="list-reset">
|
||||
{% for page in section.pages %}
|
||||
<li class="p-2">
|
||||
<a class="text-blue no-underline" href="{{ page.permalink }}">
|
||||
{{ page.title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{{ section.content | safe }}
|
||||
{% endblock %}
|
|
@ -1,7 +0,0 @@
|
|||
<div class="bg-grey-lighter flex flex-col items-center justify-center my-6 p-8">
|
||||
{% if header %}
|
||||
<h3 class="block font-light mb-6 text-grey-dark text-sm tracking-wide uppercase">{{ header }}</h3>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex flex-col items-center w-full" data-component-id="{{id}}"></div>
|
||||
</div>
|
767
docs/tutorials/dropdown.md
Normal file
767
docs/tutorials/dropdown.md
Normal file
|
@ -0,0 +1,767 @@
|
|||
# Let's Build a Dropdown
|
||||
|
||||
Dropdowns are among the simplest selection components you will build, but they can be tricky to get right. For example, you'll likely want to ensure that your users can type to highlight close text matches (like when you type "Ca" to highlight "California" in a state dropdown). You'll want to be accessible to folks using screen readers or keyboard-only navigation, too. And, of course, you'll want to achieve all this without compromising on your design.
|
||||
|
||||
In this tutorial, we'll build a dropdown together using `Select`. It will be ugly but functional; we won't worry much about CSS.
|
||||
|
||||
!!! note ""
|
||||
This tutorial assumes you've followed the steps in the [Project Setup](https://citizennet.github.io/purescript-halogen-select/tutorials/getting-started/) section. While not necessary, this code is tested with those steps in mind.
|
||||
|
||||
It also assumes familiarity with the Halogen framework. If you need a refresher, try the official [Halogen guide](https://github.com/slamdata/purescript-halogen/tree/master/docs) or the [whirlwind tour of the component we're building from](https://citizennet.github.io/purescript-halogen-select/tutorials/getting-started/#a-whirlwind-tour-of-our-starter-component)
|
||||
|
||||
**If you are already an intermediate or advanced PureScript developer, then this tutorial will read slowly for you. Feel free to skim, get the gist of how the library works, and then move on to the much [more advanced typeahead tutorial](https://citizennet.github.io/purescript-halogen-select/tutorials/typeahead).**
|
||||
|
||||
Your code should work at the end of every step. If you run into issues or your code doesn't compile, please come visit us on the [PureScript user forum](https://purescript-users.ml) or the [#fpchat Slack channel](https://functionalprogramming.slack.com).
|
||||
|
||||
We're going to build a dropdown that is functionally equivalent this one:
|
||||
|
||||
<div class="ocelot-scoped" data-component="dropdown"></div>
|
||||
|
||||
Our component will look a little bit worse, because we're not going to spend time on CSS.
|
||||
|
||||
## Basic Setup
|
||||
Let's get something on the screen!
|
||||
|
||||
The simplest sort of dropdown has a button that can toggle a menu open or closed, a list of items that can be selected from that menu, and zero, one, or more selected items. For our dropdown we'll assume that you can select at most one item, and that selecting an item will replace the text on the button with that item.
|
||||
|
||||
### Rendering a button and items
|
||||
|
||||
We'll start by rendering the button and the items. At this point our render function contains only an empty div, so let's fill in the rest of the HTML we need:
|
||||
|
||||
```hs
|
||||
render :: State -> H.ParentHTML Query ChildQuery ChildSlot m
|
||||
render st =
|
||||
HH.div_
|
||||
[ HH.h1_
|
||||
[ HH.text "Dropdown" ]
|
||||
, HH.button_
|
||||
[ HH.text "Click me to view some items" ]
|
||||
, HH.ul_
|
||||
[ HH.li_
|
||||
[ HH.text "Item 1" ]
|
||||
, HH.li_
|
||||
[ HH.text "Item 2" ]
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
!!! warn ""
|
||||
Make sure to compile this code and view the new output! You should see a header, a button, and two items in the list.
|
||||
|
||||
|
||||
### A better `#!hs State` type
|
||||
|
||||
It's already clear we're going to need more than `#!hs Unit` for our `#!hs State` type. We at least need to know three things:
|
||||
|
||||
- If the menu is toggled on or off
|
||||
- The currently-selected item (if there is one)
|
||||
- The list of items available for selection
|
||||
|
||||
We can represent each of these with simple types in our state:
|
||||
|
||||
```hs
|
||||
type State =
|
||||
{ isOpen :: Boolean
|
||||
, selectedItem :: Maybe String
|
||||
, availableItems :: Array String
|
||||
}
|
||||
```
|
||||
|
||||
Now that our state contains these three fields, we need to update our `#!hs initialState` function to produce the right type of values:
|
||||
|
||||
```hs
|
||||
initialState :: Input -> State
|
||||
initialState = const
|
||||
{ isOpen: false
|
||||
, selectedItem: Nothing
|
||||
, availableItems:
|
||||
[ "Item One"
|
||||
, "Item Two"
|
||||
, "Item Three"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Finally, lets update our render function to leverage the information now contained in `#!hs State`. If there's a selected item, that will be the button's text; if not, we'll fall back to a default message. If the menu is open, we'll list out the available items for selection.
|
||||
|
||||
For code clarity, we'll also break out the dropdown into its own helper function.
|
||||
|
||||
```hs
|
||||
import Data.Maybe (fromMaybe)
|
||||
|
||||
render :: State -> H.ParentHTML Query ChildQuery ChildSlot m
|
||||
render st =
|
||||
HH.div_
|
||||
[ HH.h1_
|
||||
[ HH.text "Dropdown" ]
|
||||
, dropdown st
|
||||
]
|
||||
|
||||
dropdown :: State -> H.ParentHTML Query ChildQuery ChildSlot m
|
||||
dropdown =
|
||||
HH.div_
|
||||
[ HH.button_
|
||||
[ HH.text $ fromMaybe "Click me to view some items" st.selectedItem ]
|
||||
, if st.isOpen
|
||||
then HH.ul_ $ (\item -> HH.li_ [ HH.text item ]) <$> st.availableItems
|
||||
else HH.text ""
|
||||
]
|
||||
```
|
||||
|
||||
!!! tip ""
|
||||
Since the dropdown has no behavior yet, try changing the `#!hs initialState` to set `#!hs isOpen` to `#!hs true` to verify your items are in fact being rendered out to the page.
|
||||
|
||||
It ain't pretty, but at this point we've got all the rendering we need for a basic dropdown! The next step is to actually wire in some behavior.
|
||||
|
||||
## Integrating the component
|
||||
|
||||
Let's integrate the `Select` component! In just a few steps, we'll turn our simple render function into a fully-functioning dropdown with keyboard navigation, toggling, debounced type-to-search, and several other features.
|
||||
|
||||
### On building components with Select
|
||||
The key idea behind the `Select` library is to **provide behaviors, not rendering**. The core component the library exposes doesn't have a render function at all! Of course, all Halogen components require a render function to work, and `Select` is no different. *You* are expected to provide that render function.
|
||||
|
||||
Why?
|
||||
|
||||
When you write the render function, not the library, you get to decide exactly what your component will look and feel like. You can also control what queries to trigger from HTML and when, effectively allowing you to control the behavior of the component *without configuration*. You can even extend it with new behavior and new state by using information from your parent component. The end result is a much smaller library component with a lot more flexibility and power for you.
|
||||
|
||||
We just wrote the rendering we need for an (admittedly ugly) dropdown. The render function we just wrote can actually serve almost as-is as the render function for `Select`! All we have to do is mount the `Select` component, make a few tweaks to our render code, and then pass in a little configuration information. Let's do that next.
|
||||
|
||||
### Importing the Select component
|
||||
|
||||
The first thing we'll do is bring in the `Select` library in the first place.
|
||||
|
||||
```hs
|
||||
import Select as Select
|
||||
import Select.Utils.Setters as Setters
|
||||
```
|
||||
|
||||
!!! tip ""
|
||||
You can always [view the module documentation for Select on Pursuit](https://pursuit.purescript.org/packages/purescript-halogen-select/1.0.0) or the [source code on GitHub](https://github.com/citizennet/purescript-halogen-select). This is useful when integrating with third-party components so that you can check out the `#!hs Input`, `#!hs State`, `#!hs Query`, and `#!hs Message` types.
|
||||
|
||||
Next, we need to update our `#!hs ChildSlot` and `#!hs ChildQuery` types. We're only going to have one dropdown so we can leave the child slot as `#!hs Unit`; we do need to add the `Select` component's query type to our `#!hs ChildQuery` synonym, however.
|
||||
|
||||
This code, unfortunately, won't work:
|
||||
|
||||
```hs
|
||||
type ChildQuery = Select.Query
|
||||
|
||||
Error found:
|
||||
Type synonym Select.Query is partially applied.
|
||||
Type synonyms must be applied to all of their type arguments.
|
||||
|
||||
in type synonym ChildQuery
|
||||
```
|
||||
|
||||
The compiler has noticed that `#!hs ChildQuery`, a type synonym, now partially applied. That's because `#!hs Select.Query`, itself a type synonym, takes several arguments as described in the [module documentation on Pursuit](https://pursuit.purescript.org/packages/purescript-halogen-select/1.0.0/docs/Select#t:Query). Let's walk through each one:
|
||||
|
||||
```hs
|
||||
type ChildQuery o item eff = Select.Query o item eff
|
||||
```
|
||||
|
||||
`o` is *your* query type. Remember how you can embed your own queries into `Select`, and in that way extend the component's functionality? This is how. So we can fill in the first argument:
|
||||
|
||||
```hs
|
||||
type ChildQuery item eff = Select.Query Query item eff
|
||||
```
|
||||
|
||||
`item` is the type of whatever items you want to be selectable. Commonly these are strings, but can also be custom data types. Later on, in the [typeahead tutorial](https://citizennet.github.io/purescript-halogen-select/tutorials/typeahead), we'll see how powerful custom data types can be for rendering purposes. For our simple dropdown we'll simply specialize this to `#!hs String`:
|
||||
|
||||
```hs
|
||||
type ChildQuery eff = Select.Query Query String eff
|
||||
```
|
||||
|
||||
`eff` is the type of whatever effect rows your component leverages. `Select` performs some effects, like manipulating the DOM or using threads to perform debouncing on your behalf asynchronously, and it must verify that its effects match your parent component effects. We'll leave this as an argument for now, but when we mount the component, we'll provide some concrete effects.
|
||||
|
||||
What happens if we try to save this?
|
||||
|
||||
```hs
|
||||
Error found in module Component :
|
||||
|
||||
Could not match kind
|
||||
Type
|
||||
|
||||
with kind
|
||||
# Control.Monad.Eff.Effect
|
||||
|
||||
while checking the kind of State -> ParentHTML Query ChildQuery ChildSlot m0
|
||||
```
|
||||
|
||||
Whoops! Our `#!hs render` and `#!hs eval` functions expect `#!hs ChildQuery` to have the kind `#!hs Type`, but instead we've provided a type synonym that's still awaiting an argument of kind `#!hs # Effect` (read: a row of effects). We need to supply that argument. Let's update those two functions:
|
||||
|
||||
```hs
|
||||
render :: State -> H.ParentHTML Query (ChildQuery eff) ChildSlot m
|
||||
dropdown :: State -> H.ParentHTML Query (ChildQuery eff) ChildSlot m
|
||||
eval :: Query ~> H.ParentDSL State Query (ChildQuery eff) ChildSlot Message m
|
||||
```
|
||||
|
||||
Now that `Select` has been imported and we've updated our `ChildQuery` and `ChildSlot` types to support it, we can worry about what to do when we receive a message from the component.
|
||||
|
||||
|
||||
### Mounting the component
|
||||
|
||||
We're finally ready to mount the `Select` component. Mounting any component in Halogen requires supplying a slot value, the component itself, the component's input, and the component's handler. We can put together all of these except for the input, which we haven't prepared yet.
|
||||
|
||||
Let's stub out our render function in preparation:
|
||||
|
||||
```hs
|
||||
import Halogen.HTML.Events as HE
|
||||
|
||||
render :: State -> H.ParentHTML Query (ChildQuery eff) ChildSlot m
|
||||
render st =
|
||||
HH.div_
|
||||
[ HH.h1_
|
||||
[ HH.text "Dropdown" ]
|
||||
, HH.slot unit Select.component ?input (HE.input <<< const Nothing)
|
||||
]
|
||||
```
|
||||
|
||||
Right away we get an error:
|
||||
|
||||
```hs
|
||||
Error in module Component:
|
||||
|
||||
Could not match type
|
||||
eff7
|
||||
|
||||
with type
|
||||
( dom :: DOM
|
||||
, avar :: AVAR
|
||||
| eff7
|
||||
)
|
||||
|
||||
when trying to match type QueryF Query String eff7
|
||||
with type QueryF Query String
|
||||
```
|
||||
|
||||
This happened because `Select` uses the `AVAR` and `DOM` effects, but we've asserted our component will work with ANY row. That's not true anymore! Our component will now work with any row that includes `DOM` and `AVAR`. It's easy enough to fix. We need to define our own effects row, extensible by `eff`:
|
||||
|
||||
```hs
|
||||
import Control.Monad.Aff.AVar (AVAR)
|
||||
import DOM (DOM)
|
||||
|
||||
type Effects eff =
|
||||
( dom :: DOM
|
||||
, avar :: AVAR
|
||||
| eff
|
||||
)
|
||||
```
|
||||
|
||||
Now we can update our various types to use our new row, verifying the effects are the same throughout the component. As a rule of thumb, anywhere a **function** uses `eff`, wrap it in our new `Effects` type synonym, but don't apply the same rule to types or type synonyms. As an example, we'll update `component`, but we won't update the `ChildQuery` type synonym:
|
||||
|
||||
```hs
|
||||
component :: ∀ eff m
|
||||
. MonadAff (Effects eff) m
|
||||
=> H.Component HH.HTML Query Input Message m
|
||||
|
||||
render :: State -> H.ParentHTML Query (ChildQuery (Effects eff)) ChildSlot m
|
||||
dropdown :: State -> H.ParentHTML Query (ChildQuery (Effects eff)) ChildSlot m
|
||||
eval :: Query ~> H.ParentDSL State Query (ChildQuery (Effects eff)) ChildSlot Message m
|
||||
```
|
||||
|
||||
With that out of the way, we can turn to filling in our component's input type. We can either look at the module documentation for `Select.Input` or look at the type error that resulted from our typed hole, `?input`. Both will tell us that we need to provide a value of this type:
|
||||
|
||||
```hs
|
||||
type Input o item eff =
|
||||
{ inputType :: InputType
|
||||
, items :: Array item
|
||||
, initialSearch :: Maybe String
|
||||
, debounceTime :: Maybe Milliseconds
|
||||
, render :: State item eff -> ComponentHTML o item eff
|
||||
}
|
||||
```
|
||||
|
||||
Let's build this up, step by step. First, we see we have to provide an `#!hs InputType`. This is described in the module documentation:
|
||||
|
||||
```hs
|
||||
-- | Text-driven inputs will operate like a normal search-driven selection component.
|
||||
-- | Toggle-driven inputs will capture key streams and debounce in reverse (only notify
|
||||
-- | about searches when time has expired).
|
||||
data InputType
|
||||
= TextInput
|
||||
| Toggle
|
||||
```
|
||||
|
||||
We don't have any text input for our dropdown -- its a button -- so we'll go with the `#!hs Toggle` constructor.
|
||||
|
||||
```hs
|
||||
selectInput :: Select.Input Query String (Effects eff)
|
||||
selectInput =
|
||||
{ inputType: Select.Toggle
|
||||
, ...
|
||||
}
|
||||
```
|
||||
|
||||
Next, we're expected to provide an array of items. Fortunately we already have those in our `#!hs State`. We can just send those items directly into the input.
|
||||
|
||||
```hs
|
||||
selectInput =
|
||||
{ ...
|
||||
, items: st.availableItems
|
||||
}
|
||||
```
|
||||
|
||||
Next, we're expected to provide an initial search. This would be more useful if we had a text input, but for our dropdown, we'll start off with no initial search.
|
||||
|
||||
```hs
|
||||
selectInput =
|
||||
{ ...
|
||||
, initialSearch: Nothing
|
||||
}
|
||||
```
|
||||
|
||||
What about a debounce time? For toggle-driven components, this is how long to aggregate key presses before the user's typing should affect the list of items. For search-driven components, this is how long to delay before raising a message with the new search. For our dropdown, we don't care:
|
||||
|
||||
```hs
|
||||
selectInput =
|
||||
{ ...
|
||||
, debounceTime: Nothing
|
||||
}
|
||||
```
|
||||
|
||||
Finally, we're expected to provide a render function to the component. Ah ha! We've actually already written a render function for a dropdown -- it's just that the type is wrong.
|
||||
|
||||
### Adapting the render function for `Select`
|
||||
|
||||
Let's look at the types side-by-side:
|
||||
|
||||
```hs
|
||||
Select.render :: Select.State item eff -> Select.ComponentHTML o item eff
|
||||
|
||||
dropdown :: State -> H.ParentHTML Query (ChildQuery (Effects eff)) ChildSlot m
|
||||
dropdown st =
|
||||
HH.div_
|
||||
[ HH.button_
|
||||
[ HH.text $ fromMaybe "Click me to view some items" st.selectedItem ]
|
||||
, if st.isOpen
|
||||
then HH.ul_ $ (\item -> HH.li_ [ HH.text item ]) <$> st.availableItems
|
||||
else HH.text ""
|
||||
]
|
||||
```
|
||||
|
||||
From this, we can see that we need to use the state type from `Select` to drive our render function, not the state from our parent component. Will our function still work? Let's look at [`Select`'s state type in the module documentation](https://pursuit.purescript.org/packages/purescript-halogen-select/1.0.0/docs/Select#t:State) to see what we have available:
|
||||
|
||||
```hs
|
||||
type State item eff =
|
||||
{ inputType :: InputType
|
||||
, search :: String
|
||||
, debounceTime :: Milliseconds
|
||||
, debouncer :: Maybe (Debouncer eff)
|
||||
, inputElement :: Maybe HTMLElement
|
||||
, items :: Array item
|
||||
, visibility :: Visibility
|
||||
, highlightedIndex :: Maybe Int
|
||||
, lastIndex :: Int
|
||||
}
|
||||
```
|
||||
|
||||
That's a lot of stuff! We have some of the data we need in `Select`'s state -- we have our list of items and whether the menu is open or closed. We even got new information, like which item is highlighted. But we're missing something crucial: which item is selected.
|
||||
|
||||
As a general rule, `Select` does not manage selections on your behalf. You are expected to decide what you want to happen when an item is selected and to store the selections yourself.
|
||||
|
||||
What can we do? We don't have all the information we need to write this function. Or do we?
|
||||
|
||||
In fact, *so long as we write the `Select` render function within the `where` clause of the parent component's render function*, we have access to the parent's state! Let's give it a shot.
|
||||
|
||||
```hs
|
||||
render parentState =
|
||||
HH.div_
|
||||
[ HH.h1_
|
||||
[ HH.text "Dropdown" ]
|
||||
, HH.slot unit Select.component ?input (HE.input <<< const Nothing)
|
||||
]
|
||||
where
|
||||
dropdown
|
||||
:: Select.State String (Effects eff)
|
||||
-> Select.ComponentHTML Query String (Effects eff)
|
||||
dropdown childState =
|
||||
HH.div_
|
||||
[ HH.button_
|
||||
[ HH.text $ fromMaybe "Click me to view some items" parentState.selectedItem ]
|
||||
, if childState.visibility == Select.On
|
||||
then HH.ul_ $ (\item -> HH.li_ [ HH.text item ]) <$> childState.items
|
||||
else HH.text ""
|
||||
]
|
||||
```
|
||||
|
||||
It works! Even better, we no longer have to manage things like `#!hs openState` in the parent anymore. Finally, now that we have the render function we need, we can finally finish our component's input type:
|
||||
|
||||
```hs
|
||||
render :: State -> H.ParentHTML Query (ChildQuery (Effects eff)) ChildSlot m
|
||||
render parentState =
|
||||
HH.div_
|
||||
[ HH.h1_
|
||||
[ HH.text "Dropdown" ]
|
||||
, HH.slot unit Select.component selectInput (HE.input <<< const Nothing)
|
||||
]
|
||||
where
|
||||
selectInput :: Select.Input Query String (Effects eff)
|
||||
selectInput =
|
||||
{ inputType: Select.Toggle
|
||||
, items: parentState.availableItems
|
||||
, initialSearch: Nothing
|
||||
, debounceTime: Nothing
|
||||
, render: dropdown
|
||||
}
|
||||
|
||||
dropdown = ...
|
||||
```
|
||||
|
||||
## Integrating Behavior
|
||||
|
||||
Everything up to this point has been standard Halogen except for writing the child component's render function. At this point, the `Select` component is running -- good work! However, it's not yet *doing* anything.
|
||||
|
||||
It's now time to turn your static HTML into a fully-functioning dropdown.
|
||||
|
||||
### Attaching behavior to Select
|
||||
|
||||
`Select` works by using a few helper functions that attach at critical points in your render function. The library assumes very little about what your rendering looks like, except that there _at least_ exists:
|
||||
|
||||
- One or more items that can be selected
|
||||
- An element that contains those items
|
||||
- A focusable element that can be used to toggle visibility and capture keystrokes
|
||||
|
||||
Accordingly, you'll need to use three helper functions, each exported by the `Select.Utils.Setters` module:
|
||||
|
||||
- `setItemProps`
|
||||
- `setContainerProps`
|
||||
- `setToggleProps` (for toggle-driven input)
|
||||
- `setInputProps` (for text-driven input)
|
||||
|
||||
Each of these functions should be used on the property array for the relevant element in your HTML. Let's walk through each one using our built render function.
|
||||
|
||||
First, let's augment our individual items. `#!hs setItemProps` takes an index and some properties and outputs some new properties, which include all sorts of event handlers necessary for keyboard events and click events to work. In order to provide it with the index it needs, we'll use the `#!hs mapWithIndex` function from `#!hs Data.Array`.
|
||||
|
||||
```hs
|
||||
import Data.Array (mapWithIndex)
|
||||
|
||||
dropdown childState =
|
||||
HH.div_
|
||||
[ HH.button_
|
||||
[ HH.text $ fromMaybe "Click me to view some items" parentState.selectedItem ]
|
||||
, case childState.visibility of
|
||||
Select.Off -> HH.text ""
|
||||
|
||||
Select.On -> HH.ul [] $
|
||||
mapWithIndex
|
||||
(\ix item -> HH.li (Setters.setItemProps ix []) [ HH.text item ])
|
||||
childState.items
|
||||
]
|
||||
```
|
||||
|
||||
Next, we'll move to the element that contains the items. The `#!hs setContainerProps` function takes and returns some properties, attaching all the behavior the library needs. We'll use this on the parent element, `ul`:
|
||||
|
||||
```hs
|
||||
dropdown childState =
|
||||
HH.div_
|
||||
[ HH.button_
|
||||
[ HH.text $ fromMaybe "Click me to view some items" parentState.selectedItem ]
|
||||
, case childState.visibility of
|
||||
Select.Off -> HH.text ""
|
||||
|
||||
Select.On -> HH.ul (Setters.setContainerProps []) $
|
||||
mapWithIndex
|
||||
(\ix item -> HH.li (Setters.setItemProps ix []) [ HH.text item ])
|
||||
childState.items
|
||||
]
|
||||
```
|
||||
|
||||
Finally, we can make sure that our button toggles the menu on and off, captures keyboard events, can be tabbed to, and all sorts of other stuff with the `#!hs setToggleProps` function.
|
||||
|
||||
```hs
|
||||
dropdown childState =
|
||||
HH.div_
|
||||
[ HH.button
|
||||
(Setters.setToggleProps [])
|
||||
[ HH.text $ fromMaybe "Click me to view some items" parentState.selectedItem ]
|
||||
, case childState.visibility of
|
||||
Select.Off -> HH.text ""
|
||||
|
||||
Select.On -> HH.ul [] $
|
||||
mapWithIndex
|
||||
(\ix item -> HH.li (Setters.setItemProps ix []) [ HH.text item ])
|
||||
childState.items
|
||||
]
|
||||
```
|
||||
|
||||
Whew! Your rendering code now contains everything it needs to provide a keyboard-accessible dropdown. If you open this up in the browser and click around, you'll notice it's properly toggling and can be tabbed to.
|
||||
|
||||
Let's make one last improvement. When you use your arrow keys on the dropdown, the highlighted index is changing, but since we didn't provide any CSS we can't see it. Let's add some bare-bones styling so we can watch the highlights:
|
||||
|
||||
```hs
|
||||
import Halogen.HTML.Properties as HP
|
||||
|
||||
Select.On -> HH.ul (Setters.setContainerProps []) $
|
||||
mapWithIndex
|
||||
(\ix item ->
|
||||
HH.li
|
||||
( Setters.setItemProps ix
|
||||
$ case Just ix == childState.highlightedIndex of
|
||||
true -> [ HP.attr (HH.AttrName "style") "color: red;" ]
|
||||
_ -> [] )
|
||||
[ HH.text item ]
|
||||
)
|
||||
childState.items
|
||||
```
|
||||
|
||||
There we go! Try toggling the menu on and off, using your arrow, enter, and escape keys, and so on. It works!
|
||||
|
||||
...almost. Alas, we aren't doing anything when the user makes a selection. `Select` is attempting to notify us that a selection occurred, but we never provided a handler. Let's fix that now.
|
||||
|
||||
### Handling messages from Select
|
||||
|
||||
When you add a new child component you invariably need to add a handler for its `#!hs Message` type. What should the parent do when something important has occurred in the child? To handle messages, add a new constructor to your query algebra that takes the child's `#!hs Message` type as an argument:
|
||||
|
||||
```hs
|
||||
data Query a
|
||||
= NoOp a
|
||||
| HandleSelect Select.Message a
|
||||
```
|
||||
|
||||
Ah -- this won't compile!
|
||||
|
||||
```hs
|
||||
Error found in module Component:
|
||||
|
||||
Could not match kind
|
||||
(Type -> Type) -> Type -> Type
|
||||
|
||||
with kind
|
||||
Type
|
||||
|
||||
in type constructor Query
|
||||
```
|
||||
|
||||
This looks similar to the type error we got when we tried to just use `Select.Query` in a type synonym. We need to provide a `#!hs Type` to `#!hs HandleSelect`, but `#!hs Select.Message` is still awaiting 2 arguments, the first of which is *itself* awaiting an argument! Let's go look at the [module documentation for `Select.Message`](https://pursuit.purescript.org/packages/purescript-halogen-select/1.0.0/docs/Select#t:Message).
|
||||
|
||||
```hs
|
||||
data Message o item
|
||||
```
|
||||
|
||||
We've seen both of these arguments before in the component's query type, so we should fill them in with the same values. `o` is our parent component query type, and `item` is a `#!hs String`:
|
||||
|
||||
```hs
|
||||
data Query a
|
||||
= NoOp a
|
||||
| HandleSelect (Select.Message Query String) a
|
||||
```
|
||||
|
||||
As soon as you save and rebuild you'll see another compiler error!
|
||||
|
||||
```hs
|
||||
Error found in module Component
|
||||
|
||||
A case expression could not be determined to cover all inputs.
|
||||
The following additional cases are required to cover all inputs:
|
||||
|
||||
(HandleSelect _ _)
|
||||
|
||||
in value declaration component
|
||||
```
|
||||
|
||||
This time it's because we've added a new query, but we never updated our `#!hs eval` function to describe what should happen when the query is triggered. What should we actually _do_ when a message comes up from the child component?
|
||||
|
||||
!!! tip ""
|
||||
You'll often see type errors that end in "... value declaration component." when the error occurred in any of the functions in the `where` clause for the component. It can be annoying to track down where the error actually is in your code. One way to help track these down is to move your code out of the `where` block and into the module level temporarily so the compiler can identify which particular function is causing the issue.
|
||||
|
||||
There are four possible sub-cases that we need to handle, each described in the module documentation:
|
||||
|
||||
```hs
|
||||
data Message o item
|
||||
= Searched String
|
||||
| Selected item
|
||||
| VisibilityChanged Visibility
|
||||
| Emit (o Unit)
|
||||
```
|
||||
|
||||
Let's stub out each of these cases and then decide what to do with them:
|
||||
|
||||
```hs
|
||||
eval :: Query ~> H.ParentDSL State Query (ChildQuery eff) ChildSlot Message m
|
||||
eval = case _ of
|
||||
NoOp next -> pure next
|
||||
|
||||
HandleSelect message next -> case message of
|
||||
Select.Searched string ->
|
||||
pure next
|
||||
|
||||
Select.Selected item ->
|
||||
pure next
|
||||
|
||||
Select.VisibilityChanged vis ->
|
||||
pure next
|
||||
|
||||
Select.Emit query ->
|
||||
pure next
|
||||
```
|
||||
|
||||
Let's take these case-by-case.
|
||||
|
||||
What should we do when the user has searched something on our dropdown? This is just a simple list of items, so we'll simply ignore their search. We can leave this as `#!hs pure next`.
|
||||
|
||||
What should we do when the user has selected an item? Ah! This is more interesting. We want to set their item as the currently-selected one, and then we want to remove it from the available items list. Once we've removed it from the available items, we'll update `Select` with its new items to display and we'll toggle it off.
|
||||
|
||||
We can use `#!hs difference` from `#!hs Data.Array` to filter out the selected item from the overall list of items. This is a common pattern in `Select`: the parent holds the immutable list of all possible items, and `Select` receives some subset of those items at each render. You might use the user's search to filter out items in a typeahead, for example, or only load 50 results at a time into a dropdown.
|
||||
|
||||
```hs
|
||||
import Data.Array (difference)
|
||||
|
||||
Select.Selected item -> do
|
||||
st <- H.get
|
||||
_ <- H.query unit $ Select.setVisibility Select.Off
|
||||
_ <- H.query unit $ Select.replaceItems $ difference st.availableItems [ item ]
|
||||
H.modify _ { selectedItem = Just item }
|
||||
```
|
||||
|
||||
What should we do when the dropdown's visibility has changed? This can often be useful to run validation, but for our dropdown, we don't care what its visibility is. We can leave this as `#!hs pure next`.
|
||||
|
||||
Finally, what should we do when the child component raises its `Emit` message? What does this even mean? `Emit` exists so you can embed your own queries into `Select` and extend its behavior. Since the message contains one of your own queries, all you have to do is evaluate it: you can call `eval` recursively to run your query.
|
||||
|
||||
You can think of `Emit` as notifying you that the query you embedded is ready to run.
|
||||
|
||||
```hs
|
||||
Select.Emit query -> do
|
||||
eval query
|
||||
pure next
|
||||
```
|
||||
|
||||
Nice and simple! While you may write all kinds of logic for the other messages raised by `Select`, you'll always write this same code for the `Emit` message.
|
||||
|
||||
## Conclusion
|
||||
|
||||
Congratulations! You have successfully built a keyboard-navigable dropdown using `Select`. You integrated the library, wrote your own render function, and then augmented it with helper functions from the library. Then, you handled the output messages and sent queries to update the component's state. You've done quite a lot of work!
|
||||
|
||||
### Next Steps
|
||||
|
||||
This tutorial was a slow, thorough introduction to the `Select` library. But we've only scratched the surface of what you can do with it. I'd recommend continuing on to the faster-paced and more advanced [typeahead tutorial](https://citizennet.github.io/purescript-halogen-select/tutorials/typeahead).
|
||||
|
||||
### Source Code
|
||||
|
||||
If you'd like to use this component as a starting point from which to build your own, feel free to copy/paste the source code below.
|
||||
|
||||
??? article "Full source code for the tutorial"
|
||||
```hs
|
||||
module Component where
|
||||
|
||||
import Prelude
|
||||
|
||||
import Control.Monad.Aff.AVar (AVAR)
|
||||
import Control.Monad.Aff.Class (class MonadAff)
|
||||
import DOM (DOM)
|
||||
import Data.Array (difference, mapWithIndex)
|
||||
import Data.Maybe (Maybe(..), fromMaybe)
|
||||
import Halogen as H
|
||||
import Halogen.HTML as HH
|
||||
import Halogen.HTML.Events as HE
|
||||
import Halogen.HTML.Properties (attr) as HP
|
||||
import Select as Select
|
||||
import Select.Utils.Setters as Setters
|
||||
|
||||
data Query a
|
||||
= NoOp a
|
||||
| HandleSelect (Select.Message Query String) a
|
||||
|
||||
type State =
|
||||
{ isOpen :: Boolean
|
||||
, selectedItem :: Maybe String
|
||||
, availableItems :: Array String
|
||||
}
|
||||
|
||||
type Input = Unit
|
||||
|
||||
type Message = Void
|
||||
|
||||
type ChildSlot = Unit
|
||||
type ChildQuery eff = Select.Query Query String eff
|
||||
|
||||
type Effects eff =
|
||||
( dom :: DOM
|
||||
, avar :: AVAR
|
||||
| eff
|
||||
)
|
||||
|
||||
component :: ∀ eff m
|
||||
. MonadAff (Effects eff) m
|
||||
=> H.Component HH.HTML Query Input Message m
|
||||
component =
|
||||
H.parentComponent
|
||||
{ initialState
|
||||
, render
|
||||
, eval
|
||||
, receiver: const Nothing
|
||||
}
|
||||
where
|
||||
|
||||
initialState :: Input -> State
|
||||
initialState = const
|
||||
{ isOpen: false
|
||||
, selectedItem: Nothing
|
||||
, availableItems:
|
||||
[ "Item One"
|
||||
, "Item Two"
|
||||
, "Item Three"
|
||||
]
|
||||
}
|
||||
|
||||
render :: State -> H.ParentHTML Query (ChildQuery (Effects eff)) ChildSlot m
|
||||
render parentState =
|
||||
HH.div_
|
||||
[ HH.h1_
|
||||
[ HH.text "Dropdown" ]
|
||||
, HH.slot unit Select.component selectInput (HE.input HandleSelect)
|
||||
]
|
||||
where
|
||||
selectInput :: Select.Input Query String (Effects eff)
|
||||
selectInput =
|
||||
{ inputType: Select.Toggle
|
||||
, items: parentState.availableItems
|
||||
, initialSearch: Nothing
|
||||
, debounceTime: Nothing
|
||||
, render: dropdown
|
||||
}
|
||||
|
||||
dropdown
|
||||
:: Select.State String (Effects eff)
|
||||
-> Select.ComponentHTML Query String (Effects eff)
|
||||
dropdown childState =
|
||||
HH.div_
|
||||
[ HH.button
|
||||
(Setters.setToggleProps [])
|
||||
[ HH.text $ fromMaybe "Click me to view some items" parentState.selectedItem ]
|
||||
, case childState.visibility of
|
||||
Select.Off -> HH.text ""
|
||||
|
||||
Select.On -> HH.ul (Setters.setContainerProps []) $
|
||||
mapWithIndex
|
||||
(\ix item ->
|
||||
HH.li
|
||||
( Setters.setItemProps ix
|
||||
$ case Just ix == childState.highlightedIndex of
|
||||
true -> [ HP.attr (HH.AttrName "style") "color: red;" ]
|
||||
_ -> [] )
|
||||
[ HH.text item ]
|
||||
)
|
||||
childState.items
|
||||
]
|
||||
|
||||
eval :: Query ~> H.ParentDSL State Query (ChildQuery (Effects eff)) ChildSlot Message m
|
||||
eval = case _ of
|
||||
NoOp next -> pure next
|
||||
|
||||
HandleSelect message next -> case message of
|
||||
Select.Searched string ->
|
||||
pure next
|
||||
|
||||
Select.Selected item -> do
|
||||
st <- H.get
|
||||
_ <- H.query unit $ Select.setVisibility Select.Off
|
||||
_ <- H.query unit $ Select.replaceItems $ difference st.availableItems [ item ]
|
||||
H.modify _ { selectedItem = Just item }
|
||||
pure next
|
||||
|
||||
Select.VisibilityChanged vis ->
|
||||
pure next
|
||||
|
||||
Select.Emit query -> do
|
||||
eval query
|
||||
pure next
|
||||
```
|
255
docs/tutorials/getting-started.md
Normal file
255
docs/tutorials/getting-started.md
Normal file
|
@ -0,0 +1,255 @@
|
|||
# Introduction
|
||||
Halogen is a powerful framework for building PureScript applications. It’s used by several companies, including SlamData and my own company, CitizenNet (a Condé Nast company), among others. The `Select` library is written for the Halogen framework, so if you don’t know how to use Halogen yet, you ought to start with the [Halogen guide](https://github.com/slamdata/purescript-halogen/tree/master/docs). That said, with only passing familiarity with Halogen, you should be able to follow along just fine!
|
||||
|
||||
## Setup
|
||||
Instead of creating a new Halogen project from scratch, we’ll start with a minimal starter template. This template includes the HTML, build scripts, and basic `Main.purs` file necessary to run your Halogen application. It also includes a component with the bare minimum definitions in place. This component does nothing at all, which is nice because we can easily use it to start building dropdowns, typeaheads, and other components.
|
||||
|
||||
!!! note ""
|
||||
We prefer Yarn over NPM for package management and scripts, but either one will work. Anywhere you see `#!sh yarn <script>`, you can substitute `#!sh npm run <script>` instead. Feel free to look at the `package.json` file if you want to see what these scripts are doing.
|
||||
|
||||
### Installation
|
||||
|
||||
First, clone the Halogen template project from CitizenNet, install dependencies, and make sure things build properly. If they don’t, please reach out on the [Purescript user forum](https://purescript-users.ml) so we can fix it!
|
||||
|
||||
Next, make sure to install `Select`:
|
||||
|
||||
```shell
|
||||
bower i --save purescript-halogen-select
|
||||
```
|
||||
|
||||
And that's it! You now have everything you need to complete the tutorials. This is the full set of steps you can follow to get all set up:
|
||||
|
||||
```shell
|
||||
# Get the CitizenNet starter Halogen project
|
||||
git clone git@github.com:citizennet/purescript-halogen-template.git
|
||||
|
||||
# Change into the directory and install packages
|
||||
cd purescript-halogen-template && yarn
|
||||
|
||||
# Install a new package: purescript-halogen-select
|
||||
bower i --save purescript-halogen-select
|
||||
|
||||
# Build the project
|
||||
yarn build
|
||||
|
||||
# Open the application in the browser
|
||||
open dist/index.html
|
||||
```
|
||||
|
||||
After you complete each step in the tutorial, make sure to rebuild the project and refresh your browser to see your updated component.
|
||||
|
||||
### Helpful tip: Watching for file changes
|
||||
It’s convenient to keep a terminal running which watches for file changes, rebuilds the project, and bundles JavaScript on your behalf. Then, when you make a change to a file, all you have to do is wait a moment and refresh the page to see your updates.
|
||||
|
||||
When I write PureScript, I usually work with two terminals open. I use the first to write code, and the second to watch those changes and rebuild. I recommend using the same technique as you walk through these tutorials. These three steps are all you need:
|
||||
|
||||
1. Open a new terminal and run the `#!sh watch` script
|
||||
2. Open your editor to a source file
|
||||
3. Open a new tab in your browser pointed to `dist/index.html` so you can see the app
|
||||
|
||||
To test everything is working, try editing `src/Component.purs` to change the title of the page. The project should automatically rebuild on save. Then, when you refresh the browser, you should see your new text rendered.
|
||||
|
||||
```shell
|
||||
# Watch for changes and rebuild (remember to refresh the page after builds)
|
||||
yarn watch
|
||||
```
|
||||
|
||||
## A whirlwind tour of our starter component
|
||||
|
||||
The project starts off with a minimal Halogen component. As a brief refresher, I'll step through each of the types and functions involved.
|
||||
|
||||
!!! info
|
||||
If you are already quite familiar with Halogen, feel free to skip this section entirely.
|
||||
|
||||
### Query Algebra
|
||||
|
||||
How does a component know what to do?
|
||||
|
||||
In Halogen, we give names to each computation we'd like a component to run. Computations that can have side effects but don't return anything are colloquially called *actions*; those that can have side effects and also return something are called *requests*. The type that lists out the possible actions and requests for a component is called the component's *query algebra*. The Halogen guide has a relevant [section about query algebras](https://github.com/slamdata/purescript-halogen/blob/master/docs/2%20-%20Defining%20a%20component.md#query-algebra) if you'd like to know more.
|
||||
|
||||
What actions and requests can our starter component perform? By looking at the query algebra, we see just one constructor:
|
||||
|
||||
```hs
|
||||
data Query a
|
||||
= NoOp a
|
||||
```
|
||||
|
||||
All we know so far is that this component can do one thing: evaluate a query called `NoOp`. We'll see what it does later on when we look at the `#!hs eval` function.
|
||||
|
||||
|
||||
### State
|
||||
|
||||
Every component encapsulates some state, described by its `#!hs State` type. You will usually see Halogen components use records to hold state, like this:
|
||||
|
||||
```hs
|
||||
type State = { on :: Boolean, name :: String }
|
||||
```
|
||||
|
||||
State is the core of your component. Most of the queries you see in Halogen components modify state in some way, and the render function that produces HTML for the component has only the `#!hs State` type as its argument.
|
||||
|
||||
For our starter component, we don't need any state just yet, so we've simply assigned it the `#!hs Unit` type. When we start building selection components, however, we'll soon create a record to hold our state.
|
||||
|
||||
```hs
|
||||
type State = Unit
|
||||
```
|
||||
|
||||
### Input
|
||||
|
||||
A component's `#!hs Input` type can be thought of as a container for any information you'd like to pass to the component. It's most commonly used to provide a component with some initial `#!hs State` values via the `#!hs initialState :: Input -> State` function. However, it's more powerful than that!
|
||||
|
||||
Once a Halogen component has been mounted to the DOM, there is only one way to continue sending it new information: its `#!hs Input` type paired with its `#!hs receiver` function. Every time the parent component re-renders, it will send a new `#!hs Input` to the child component.
|
||||
|
||||
For more information on the `#!hs Input` type, see the [Parent and Child Components](https://github.com/slamdata/purescript-halogen/blob/master/docs/5%20-%20Parent%20and%20child%20components.md#input-values) section of the Halogen guide.
|
||||
|
||||
Our starter component doesn't need any input, so we'll assign it the `#!hs Unit` type. However, once we build a dropdown or typeahead, we'll probably want to receive the list of items that can be selected as input.
|
||||
|
||||
```hs
|
||||
type Input = Unit
|
||||
```
|
||||
|
||||
### Message
|
||||
|
||||
How does a component tell its parent when something important has happened? In Halogen, this is accomplished with a `#!hs Message` type. Like the query algebra, this is just a type describing messages that can be raised, containing some information. To actually trigger sending a particular message, you can use the `#!hs raise` function provided by Halogen.
|
||||
|
||||
When we start building selection components, we'll use messages to notify parent components when items have been selected or removed. Our starter component doesn't need to raise any messages, however, so we've given it the `#!hs Void` type.
|
||||
|
||||
??? note "Why are we using `Void` when we have no messages?"
|
||||
Why use `Void` instead of `Unit` for the `Message` type when it has no constructors? This is common practice in Halogen because of how messages are used by parent components. When your component raises a message, it gets handled by the parent using a function like this:
|
||||
|
||||
`#!hs Child.Message -> Maybe (ParentQuery Unit)`
|
||||
|
||||
If you want to ignore all messages from the child, you could write an implementation like this:
|
||||
|
||||
`#!hs Halogen.HTML.Events.input <<< const Nothing`
|
||||
|
||||
However, if the child's message type is `Void`, then you can use the `absurd` function from `Data.Void`:
|
||||
|
||||
`#!hs absurd :: Void -> a`
|
||||
|
||||
This saves you a bit of typing when you mount a child component in a slot and makes it absolutely unambiguous that there are no messages to handle. It also ensures that if you add a message to the child component later on you'll get a compiler error -- this is a good thing!
|
||||
|
||||
Compare mounting a child component that uses `Unit` to represent "no messages" vs. using `Void`:
|
||||
|
||||
```hs
|
||||
-- It's unclear whether you're ignoring all messages or whether there are
|
||||
-- simply no messages to handle.
|
||||
HH.slot ComponentSlot component unit (Halogen.HTML.Events.input <<< const Nothing)
|
||||
|
||||
-- It's obvious there are no messages, and if that changes (the component adds a
|
||||
-- message) you'll get a nice compile-time error.
|
||||
HH.slot ComponentSlot component unit absurd
|
||||
```
|
||||
|
||||
|
||||
For more information on messages, see the [Parent and Child Components](https://github.com/slamdata/purescript-halogen/blob/master/docs/5%20-%20Parent%20and%20child%20components.md) section in the Halogen guide.
|
||||
|
||||
```hs
|
||||
type Message = Void
|
||||
```
|
||||
|
||||
### ChildQuery and ChildSlot
|
||||
|
||||
Halogen components often have further child components. To maintain type safety when managing multiple child components, Halogen uses a pair of concepts: *child queries* and *child slots*.
|
||||
|
||||
- The **ChildQuery** type lists out each unique type of child component your component has. For each type of child component, you'll add its query type here.
|
||||
|
||||
- The **ChildSlot** type works like an address book for the various child components. If you only have one child component of any distinct `ChildQuery`, then you can just use `unit`. However, if you have multiple children with the same query type, you need some way to distinguish between them. It's common to use custom types or integers for this.
|
||||
|
||||
See the [Multiple Types of Child Component](https://github.com/slamdata/purescript-halogen/blob/master/docs/5%20-%20Parent%20and%20child%20components.md#multiple-types-of-child-component) section of the Halogen guide for more details.
|
||||
|
||||
For now, our component has no children. Once we bring in the `Select` component we'll update these types.
|
||||
|
||||
```hs
|
||||
type ChildQuery = Const Void
|
||||
type ChildSlot = Unit
|
||||
```
|
||||
|
||||
### Component
|
||||
|
||||
Ah! We can finally create our component. The actual component definition is simple: we call the `#!hs parentComponent` function from Halogen to assert we're creating a component that can have further child components and provide it with the four functions it needs to operate. More on those in a moment!
|
||||
|
||||
```hs
|
||||
component :: ∀ eff m
|
||||
. MonadAff eff m
|
||||
=> H.Component HH.HTML Query Input Message m
|
||||
component =
|
||||
H.parentComponent
|
||||
{ initialState
|
||||
, render
|
||||
, eval
|
||||
, receiver: const Nothing
|
||||
}
|
||||
|
||||
where
|
||||
```
|
||||
|
||||
Next, lets look at those function definitions, defined in the `#! where` clause:
|
||||
|
||||
#### initialState
|
||||
|
||||
The `#!hs initialState` function describes how to go from the component's `#!hs Input` type to its `#!hs State` type. In this case, our `#!hs State` type is just `#!hs Unit`, so we'll throw away the input and return `#!hs unit`.
|
||||
|
||||
```hs
|
||||
initialState :: Input -> State
|
||||
initialState = const unit
|
||||
|
||||
-- Could also be written this way:
|
||||
initialState = id
|
||||
```
|
||||
|
||||
#### render
|
||||
|
||||
The `#!hs render` function describes how to go from the component's `#!hs State` type to some HTML, where that HTML can include any of the components listed in the `#!hs ChildQuery` type. You'll use plenty of code from these modules when writing render functions:
|
||||
|
||||
```hs
|
||||
import Halogen.HTML as HH
|
||||
import Halogen.HTML.Events as HE
|
||||
import Halogen.HTML.Properties as HP
|
||||
```
|
||||
|
||||
We're going to spend a lot of time writing render functions in the following tutorials. You can refer to the Halogen guide's [section on rendering](https://github.com/slamdata/purescript-halogen/tree/master/docs) for more information.
|
||||
|
||||
For now we won't render anything to the page, represented by an empty div.
|
||||
|
||||
```hs
|
||||
render :: State -> H.ParentHTML Query ChildQuery ChildSlot m
|
||||
render st = HH.div_ []
|
||||
```
|
||||
|
||||
#### eval
|
||||
|
||||
The `#!hs eval` function describes what to do when one of the queries from the component's query algebra is called. There are various ways a query can be triggered:
|
||||
|
||||
- The parent component can trigger a child component query using the `#!hs query` function
|
||||
- A user can trigger a query with an event in HTML, like `#!hs onClick`
|
||||
- The `#!hs eval` function can recursively call itself while evaluating a query
|
||||
|
||||
The `#! eval` function is where you get to actually define what all your queries *do*. Unlike the render function, you can actually perform all kinds of side effects here, like make API calls, update state, trigger queries in child components, raise messages, and more.
|
||||
|
||||
As usual, our starter component won't do much in its `#!hs eval`. When it receives the `#!hs NoOp` constructor, it will do nothing and return the next query.
|
||||
|
||||
```hs
|
||||
eval :: Query ~> H.ParentDSL State Query ChildQuery ChildSlot Message m
|
||||
eval = case _ of
|
||||
NoOp next -> pure next
|
||||
```
|
||||
|
||||
#### receiver
|
||||
|
||||
The `#!hs receiver` function describes what to do when a parent component sends in new `#!hs Input`. Its type signature looks like this:
|
||||
|
||||
```hs
|
||||
receiver :: Input -> Maybe (Query Unit)
|
||||
```
|
||||
|
||||
Once a Halogen component has been mounted, the only way to send it new input is via its `#!hs receiver` function. When its parent re-renders, it will automatically send the child component's input type again, and it's up to the `#!hs receiver` function to decide what to do with it.
|
||||
|
||||
This function can either provide a query to call, or `#!hs Nothing` if you'd like to ignore new input. If you elect to provide a query then you unlock all the power available in the `eval` function and can describe all sorts of things to do on new input, like making API calls or updating state.
|
||||
|
||||
In our case, we don't care about new input, so we'll ignore the input and return `#!hs Nothing`.
|
||||
|
||||
```hs
|
||||
{ ...
|
||||
, receiver: const Nothing
|
||||
}
|
||||
```
|
88
examples/Components/CSS.purs
Normal file
88
examples/Components/CSS.purs
Normal file
|
@ -0,0 +1,88 @@
|
|||
module Docs.CSS where
|
||||
|
||||
import Prelude
|
||||
import Halogen.HTML as HH
|
||||
|
||||
baseContainer :: Array HH.ClassName
|
||||
baseContainer = HH.ClassName <$>
|
||||
[ "bg-white"
|
||||
, "border-grey-80"
|
||||
, "border-l-2"
|
||||
, "border-r-2"
|
||||
, "w-full"
|
||||
]
|
||||
|
||||
selectionContainer :: Array HH.ClassName
|
||||
selectionContainer = baseContainer <>
|
||||
( HH.ClassName <$>
|
||||
[ "border-t-2"
|
||||
]
|
||||
)
|
||||
|
||||
itemContainer :: Array HH.ClassName
|
||||
itemContainer = baseContainer <>
|
||||
( HH.ClassName <$>
|
||||
[ "absolute"
|
||||
, "shadow"
|
||||
, "max-h-80"
|
||||
, "overflow-y-scroll"
|
||||
, "pin-t"
|
||||
, "pin-l"
|
||||
, "z-50"
|
||||
, "border-b-2"
|
||||
]
|
||||
)
|
||||
|
||||
menu :: Array HH.ClassName
|
||||
menu = HH.ClassName <$> [ "relative z-50" ]
|
||||
|
||||
ul :: Array HH.ClassName
|
||||
ul = HH.ClassName <$> [ "list-reset" ]
|
||||
|
||||
li :: Array HH.ClassName
|
||||
li = HH.ClassName <$>
|
||||
[ "px-3"
|
||||
, "rounded-sm"
|
||||
, "text-black-20"
|
||||
, "group"
|
||||
, "hover:bg-grey-97"
|
||||
, "cursor-pointer"
|
||||
]
|
||||
|
||||
button :: Array HH.ClassName
|
||||
button = HH.ClassName <$>
|
||||
[ "no-outline"
|
||||
, "px-4"
|
||||
, "py-2"
|
||||
, "!active:border-b"
|
||||
, "active:border-t"
|
||||
, "disabled:opacity-50"
|
||||
, "disabled:cursor-default"
|
||||
, "bg-blue-88"
|
||||
, "border-blue-88"
|
||||
, "hover:!disabled:bg-blue-82"
|
||||
, "focus:bg-blue-82"
|
||||
, "text-white"
|
||||
, "rounded"
|
||||
]
|
||||
|
||||
input :: Array HH.ClassName
|
||||
input = HH.ClassName <$>
|
||||
[ "bg-white"
|
||||
, "border-t-2"
|
||||
, "border-b-2"
|
||||
, "font-light"
|
||||
, "cc-blue-88"
|
||||
, "border-grey-80"
|
||||
, "disabled:bg-grey-95"
|
||||
, "disabled:text-grey-70"
|
||||
, "focus:no-outline"
|
||||
, "py-2"
|
||||
, "transition-1/4-bounce"
|
||||
, "border-l-2"
|
||||
, "border-r-2"
|
||||
, "w-full"
|
||||
, "px-3"
|
||||
, "focus:border-blue-88"
|
||||
, "!focus:!disabled:hover:border-grey-70"
|
||||
]
|
|
@ -14,12 +14,22 @@ import Halogen.HTML.Events as HE
|
|||
import Halogen.HTML.Properties as HP
|
||||
import Select as Select
|
||||
import Select.Utils.Setters as Setters
|
||||
import Docs.CSS as CSS
|
||||
|
||||
type Effects eff = ( avar :: AVAR, dom :: DOM, console :: CONSOLE | eff )
|
||||
type State = { items :: Array String, text :: String }
|
||||
type Input = { items :: Array String }
|
||||
data Query a = HandleSelect (Select.Message Query String) a
|
||||
data Message = Void
|
||||
|
||||
type State =
|
||||
{ items :: Array String
|
||||
, text :: String }
|
||||
|
||||
type Input =
|
||||
{ items :: Array String }
|
||||
|
||||
data Query a
|
||||
= HandleSelect (Select.Message Query String) a
|
||||
|
||||
data Message
|
||||
= Void
|
||||
|
||||
type ChildSlot = Unit
|
||||
type ChildQuery eff = Select.Query Query String eff
|
||||
|
@ -45,8 +55,7 @@ component =
|
|||
HandleSelect (Select.Selected item) a -> do
|
||||
st <- H.get
|
||||
_ <- H.query unit $ Select.setVisibility Select.Off
|
||||
_ <- H.query unit $ Select.triggerBlur
|
||||
_ <- H.query unit $ Select.replaceItems (difference st.items [ item ])
|
||||
_ <- H.query unit $ Select.replaceItems $ difference st.items [ item ]
|
||||
H.modify _ { text = item }
|
||||
pure a
|
||||
|
||||
|
@ -57,7 +66,7 @@ component =
|
|||
-> H.ParentHTML Query (ChildQuery (Effects e)) ChildSlot m
|
||||
render st =
|
||||
HH.div
|
||||
[ class_ "w-full" ]
|
||||
[ HP.class_ $ HH.ClassName "w-full" ]
|
||||
[ HH.slot unit Select.component input (HE.input HandleSelect) ]
|
||||
where
|
||||
input =
|
||||
|
@ -68,38 +77,36 @@ component =
|
|||
, render: renderDropdown
|
||||
}
|
||||
|
||||
class_ :: ∀ p i. String -> H.IProp ( "class" :: String | i ) p
|
||||
class_ = HP.class_ <<< HH.ClassName
|
||||
|
||||
renderDropdown :: Select.State String (Effects e) -> Select.ComponentHTML Query String (Effects e)
|
||||
renderDropdown state = HH.div_ [ renderToggle, renderContainer ]
|
||||
renderDropdown state = HH.div_ [ renderToggle, renderMenu ]
|
||||
where
|
||||
renderToggle =
|
||||
HH.button
|
||||
( Setters.setToggleProps props )
|
||||
( Setters.setToggleProps [ HP.classes CSS.button ] )
|
||||
[ HH.text st.text ]
|
||||
where
|
||||
props = [ class_ "bg-blue hover:bg-blue-dark text-white font-bold py-2 px-4 rounded-sm w-full flex" ]
|
||||
|
||||
renderContainer =
|
||||
HH.div [ class_ "relative z-50" ]
|
||||
renderMenu =
|
||||
HH.div [ HP.classes CSS.menu ]
|
||||
$ if state.visibility == Select.Off
|
||||
then []
|
||||
else [ renderItems $ renderItem `mapWithIndex` state.items ]
|
||||
else [ renderContainer $ renderItem `mapWithIndex` state.items ]
|
||||
where
|
||||
renderItems html =
|
||||
renderContainer html =
|
||||
HH.div
|
||||
( Setters.setContainerProps props )
|
||||
[ HH.ul [ class_ "list-reset" ] html ]
|
||||
where
|
||||
props = [ class_ "absolute bg-white shadow rounded-sm pin-t pin-l w-full" ]
|
||||
( Setters.setContainerProps [ HP.classes CSS.itemContainer ] )
|
||||
[ HH.ul [ HP.classes CSS.ul ] html ]
|
||||
|
||||
renderItem index item =
|
||||
HH.li
|
||||
( Setters.setItemProps index props )
|
||||
[ HH.text item ]
|
||||
where
|
||||
props = [ class_
|
||||
$ "px-4 py-1 text-grey-darkest"
|
||||
<> if state.highlightedIndex == Just index then " bg-grey-lighter" else ""
|
||||
props =
|
||||
[ HP.classes
|
||||
( CSS.li <>
|
||||
if state.highlightedIndex == Just index
|
||||
then [ HH.ClassName "bg-grey-lighter" ]
|
||||
else []
|
||||
)
|
||||
]
|
||||
|
|
@ -18,6 +18,8 @@ import Halogen.HTML.Properties as HP
|
|||
import Select as Select
|
||||
import Select.Utils.Setters as Setters
|
||||
|
||||
import Docs.CSS as CSS
|
||||
|
||||
type TypeaheadItem = String
|
||||
|
||||
type Effects eff = ( avar :: AVAR, dom :: DOM, console :: CONSOLE | eff )
|
||||
|
@ -134,7 +136,7 @@ renderInputContainer :: ∀ e
|
|||
renderInputContainer state = HH.div_ [ renderInput, renderContainer ]
|
||||
where
|
||||
renderInput = HH.input $ Setters.setInputProps
|
||||
[ class_ "rounded-sm bg-white w-full flex py-2 px-3"
|
||||
[ HP.classes CSS.input
|
||||
, HP.placeholder "Type to search..." ]
|
||||
|
||||
renderContainer =
|
|
@ -1,6 +1,6 @@
|
|||
-- | A centralized module ready for use to mount components into documentation pages.
|
||||
|
||||
module Docs.App.Component where
|
||||
module Docs.Internal.Component where
|
||||
|
||||
import Prelude
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
-- | A proxy that hides both the Query and Message of wrapped component.
|
||||
-- | Adapted from `Halogen.Component.Proxy` and `Halogen.Storybook.Proxy`.
|
||||
|
||||
module Docs.App.Proxy
|
||||
module Docs.Internal.Proxy
|
||||
( ProxyS
|
||||
, proxy
|
||||
) where
|
|
@ -28,22 +28,23 @@ import Halogen.Aff as HA
|
|||
import Halogen.HTML as HH
|
||||
import Halogen.VDom.Driver (runUI)
|
||||
|
||||
import Docs.App.Proxy (ProxyS, proxy)
|
||||
|
||||
import Docs.App.Component as Component
|
||||
import Docs.Internal.Proxy (ProxyS, proxy)
|
||||
import Docs.Internal.Component as Component
|
||||
|
||||
-- Finds all nodes labeled "data-component-id" and retrieves the associated attribute.
|
||||
-- Then, mounts the right component at each node.
|
||||
main :: ∀ eff. Eff (HA.HalogenEffects (Component.Effects eff)) Unit
|
||||
main = HA.runHalogenAff do
|
||||
elements <- awaitSelectAll $ { query: QuerySelector "div[data-component-id]", attr: "data-component-id" }
|
||||
elements <- awaitSelectAll
|
||||
{ query: QuerySelector "div[data-component]"
|
||||
, attr: "data-component"
|
||||
}
|
||||
flip traverse_ elements $ \e -> runUI app e.attr e.element
|
||||
|
||||
----------
|
||||
-- Routes
|
||||
|
||||
type ComponentQuery = ProxyS (Const Void) Unit
|
||||
|
||||
type Components m = Map.Map String (H.Component HH.HTML ComponentQuery Unit Void m)
|
||||
|
||||
routes :: ∀ eff m. MonadAff ( Component.Effects eff ) m => Components m
|
87
mkdocs.yml
Normal file
87
mkdocs.yml
Normal file
|
@ -0,0 +1,87 @@
|
|||
# Project information
|
||||
site_name: 'Select Documentation'
|
||||
site_description: 'Building blocks for common selection user interfaces in PureScript & Halogen'
|
||||
site_author: 'Thomas Honeyman'
|
||||
site_url: 'https://citizennet.github.io/purescript-halogen-select'
|
||||
|
||||
# Repository
|
||||
repo_name: 'purescript-halogen-select'
|
||||
repo_url: 'https://github.com/citizennet/purescript-halogen-select'
|
||||
|
||||
# Copyright
|
||||
copyright: 'Copyright © 2018 CitizenNet'
|
||||
|
||||
# Config
|
||||
theme:
|
||||
name: 'material'
|
||||
custom_dir: 'docs/theme'
|
||||
language: 'en'
|
||||
palette:
|
||||
primary: 'cyan'
|
||||
accent: 'cyan'
|
||||
font:
|
||||
text: 'Open Sans'
|
||||
code: 'Roboto Mono'
|
||||
logo:
|
||||
icon: 'school'
|
||||
|
||||
# To mount components
|
||||
extra_javascript:
|
||||
- js/app.js
|
||||
|
||||
# To style components
|
||||
extra_css:
|
||||
- css/cn-tailwind.scoped.css
|
||||
|
||||
# Google Analytics
|
||||
# google_analytics:
|
||||
# - 'UA-XXXXXXXX-X'
|
||||
# - 'auto'
|
||||
|
||||
# Extensions
|
||||
# Recommended here:
|
||||
# https://squidfunk.github.io/mkdocs-material/getting-started/#extensions
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- codehilite:
|
||||
guess_lang: false
|
||||
linenums: true
|
||||
- toc:
|
||||
permalink: true
|
||||
- pymdownx.arithmatex
|
||||
- pymdownx.betterem:
|
||||
smart_enable: all
|
||||
- pymdownx.caret
|
||||
- pymdownx.critic
|
||||
- pymdownx.details
|
||||
- pymdownx.emoji:
|
||||
emoji_generator: !!python/name:pymdownx.emoji.to_svg
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.magiclink
|
||||
- pymdownx.mark
|
||||
- pymdownx.smartsymbols
|
||||
- pymdownx.superfences
|
||||
- pymdownx.tasklist:
|
||||
custom_checkbox: true
|
||||
- pymdownx.tilde
|
||||
|
||||
# Site architecture
|
||||
pages:
|
||||
- Home: 'index.md'
|
||||
# - Design Philosophy: 'index.md'
|
||||
- Tutorials:
|
||||
- 'Project setup': 'tutorials/getting-started.md'
|
||||
- 'Build a dropdown': 'tutorials/dropdown.md'
|
||||
# - 'Build a typeahead': 'index.md'
|
||||
# - 'Build a date picker': 'index.md'
|
||||
- How To:
|
||||
- 'How to embed parent queries': 'how-to/embed-parent-queries.md'
|
||||
# - 'How to leverage the parent state for rendering': 'index.md'
|
||||
# - 'How to watch a key on the parent state': 'index.md'
|
||||
# - 'How to understand common type errors': 'index.md'
|
||||
# - 'How to write your own setProps functions': 'index.md'
|
||||
- Concepts:
|
||||
# - 'Advice for writing better render functions': 'index.md'
|
||||
# - 'Controlling component behavior from HTML': 'index.md'
|
||||
- 'Understanding free queries': 'concepts/understanding-free-queries.md'
|
||||
- Examples: 'examples.md'
|
13
package.json
13
package.json
|
@ -1,13 +1,12 @@
|
|||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build-all": "yarn run build-docs && yarn run build-site",
|
||||
"watch-all": "pulp -w --before 'yarn run build-docs' build -I docs/src --to docs/dist/app.js",
|
||||
"clean": "rm -rf output bower_components node_modules",
|
||||
"postinstall": "bower i --silent",
|
||||
"build-docs": "pulp build -I docs/src --to docs/dist/app.js",
|
||||
"watch-docs": "pulp -w build -I docs/src --to docs/dist/app.js",
|
||||
"build-site": "cd docs/site && gutenberg build && cd ../ && cp -r site/public/. dist/"
|
||||
"build-docs": "pulp build -I examples --to docs/js/app.js",
|
||||
"watch-docs": "pulp -w --then 'mkdocs serve' build -I examples --to docs/js/app.js",
|
||||
"clean": "rm -rf output bower_components node_modules site docs/js docs/css",
|
||||
"postinstall": "bower i --silent && yarn fetch-css && yarn move-css",
|
||||
"fetch-css": "curl https://cdn.rawgit.com/citizennet/purescript-ocelot/dev/dist/cn-tailwind.scoped.css --output cn-tailwind.scoped.css",
|
||||
"move-css": "mkdir -p docs/css/ && mv cn-tailwind.scoped.css docs/css/"
|
||||
},
|
||||
"devDependencies": {
|
||||
"pulp": "^12.0.1",
|
||||
|
|
93
readme.md
93
readme.md
|
@ -2,36 +2,28 @@
|
|||
|
||||
`Select` provides flexible building blocks for selection interfaces in Halogen. If you need a dropdown menu, typeahead, autocomplete, multi-select, calendar, image picker, or other selection interface, and you want it to be accessible, and you also want complete visual control over the component, then you're in the right place.
|
||||
|
||||
- [Module Documentation on Pursuit](https://pursuit.purescript.org/packages/purescript-halogen-select)
|
||||
- [Official Documentation / Tutorials / Getting Started](https://citizennet.github.io/purescript-halogen-select)
|
||||
- [CitizenNet UI Repository](https://github.com/citizennet/purescript-ocelot)
|
||||
- [Module Documentation on Pursuit](https://pursuit.purescript.org/packages/purescript-halogen-select)
|
||||
|
||||
# Get Started / Learn More
|
||||
|
||||
There are a few ways to get started with the `Select` library.
|
||||
|
||||
**Installation**
|
||||
**Installation**
|
||||
|
||||
`Select` is available on Bower and Pursuit:
|
||||
|
||||
```sh
|
||||
# Using psc-package
|
||||
psc-package install halogen-select
|
||||
|
||||
# Using bower
|
||||
bower i --save purescript-halogen-select
|
||||
```
|
||||
|
||||
**Examples**
|
||||
You can see working examples of components built using this library in a few places:
|
||||
For more information, try the [official documentation](https://citizennet.github.io/purescript-halogen-select).
|
||||
|
||||
- See the [Components](https://github.com/citizennet/purescript-halogen-select/tree/master/docs/src/Components) folder from the documentation site, or
|
||||
- Check out the official [CitizenNet UI Repository](https://github.com/citizennet/purescript-ocelot) where we use this library to make our app's design system.
|
||||
|
||||
Have an example of a component you've built with `Select`? Open a PR or drop us a message and we can help review and showcase your work.
|
||||
|
||||
To build the documentation site locally:
|
||||
1. Download the static site builder [Gutenberg](https://www.getgutenberg.io)
|
||||
2. Build the static html with `npm run -s build-site`
|
||||
3. Build the PureScript with `npm run -s build-docs`
|
||||
4. Open the `index.html` file in `docs/dist`
|
||||
|
||||
# Design Principles
|
||||
# Design Principles
|
||||
|
||||
The library provides essential behaviors for selection user interfaces as a group of Halogen components. But you won't find a single render function in the code. Instead, with the help of a few `setProps` helpers, you can write your HTML rendering however you'd like. You can freely include your own queries and the library will return them to be run. You can even use any data you want from your parent state in your render functions. The library manages user interaction, state, accessibility, and logic; you are responsible for rendering HTML depending on that state.
|
||||
|
||||
|
@ -67,73 +59,6 @@ For example, you can make your container compatible with the component with the
|
|||
|
||||
> Warning: If your events are duplicated by ours, they will be overwritten and fail to trigger.
|
||||
|
||||
# Free Monad Queries
|
||||
Most of the time Halogen queries look like this:
|
||||
```purescript
|
||||
data QueryF (… other type arguments omitted …) a
|
||||
= ...
|
||||
| SetVisibility Visibility a
|
||||
| GetVisibility (Visibility -> a)
|
||||
```
|
||||
(where `QueryF` is used directly as the Halogen query functor)
|
||||
|
||||
This library takes a slightly different approach: the query functor is actually `Control.Monad.Free.Free QueryF`, the [free monad](https://pursuit.purescript.org/packages/purescript-free/4.2.0/docs/Control.Monad.Free) generated by the query functor.
|
||||
|
||||
This allows queries the full power of monadic (and applicative) composition: sequencing effects, determining control flow based on previous results, and my favorite: doing nothing (`pure unit`).
|
||||
|
||||
We now define smart query constructors for this Free pattern like so:
|
||||
```purescript
|
||||
-- | Set the container visibility (`On` or `Off`).
|
||||
setVisibility :: ∀ o item eff. Visibility -> Query o item eff Unit
|
||||
setVisibility v = liftF (SetVisibility v unit)
|
||||
|
||||
-- | Get the container visibility (`On` or `Off`). Most useful when sequenced
|
||||
-- | with other actions.
|
||||
getVisibility :: ∀ o item eff. Query o item eff Visibility
|
||||
getVisibility = liftF (GetVisibility id)
|
||||
```
|
||||
|
||||
## Different patterns
|
||||
In the simple cases, the helpers Halogen use to work with raw query constructors are folded into the smart Free query constructors already: `H.action (SetVisibility On)` becomes simply `setVisiblity On`, and similarly `H.request GetVisibility` is just `getVisibility`. This is because these patterns are typically present already smart constructors: `setVisibility` returns `Free QueryF Unit`, since it is an action, and `getVisibility` returns `Free QueryF Visibility`, since it requests the visibility. This allows for easy composition in `do` notation:
|
||||
```purescript
|
||||
toggleVisibility = do
|
||||
vis <- getVisibility
|
||||
setVisibility (not vis)
|
||||
```
|
||||
C’est très facile!
|
||||
|
||||
Event handlers look a little different. This is one example:
|
||||
```purescript
|
||||
HE.onMouseDown \ev -> Just do
|
||||
Select.preventClick ev
|
||||
Select.select index
|
||||
when doBlur Select.triggerBlur
|
||||
```
|
||||
(Of course you may return `Nothing` if you so wish, but its effect is just like `pure unit` now.)
|
||||
|
||||
If you do not need access to the argument `ev`, `Select.always` provides a simple shortcut for `const <<< Just`:
|
||||
```purescript
|
||||
HE.onMouseOver $ Select.always $ Select.highlight (Index index)
|
||||
```
|
||||
|
||||
## Returning non-unit values
|
||||
Use `map` or `<$` or `pure` to return other types of values from a query. So, instead of something like this:
|
||||
```purescript
|
||||
H.subscribe $ eventSource' someEventSource
|
||||
\value -> Just (SetVisibility value H.Listening)
|
||||
```
|
||||
Use
|
||||
```purescript
|
||||
H.subscribe $ eventSource' someEventSource
|
||||
\value -> Just $ setVisibility value $> H.Listening
|
||||
```
|
||||
or
|
||||
```purescript
|
||||
H.subscribe $ eventSource' someEventSource
|
||||
\value -> Just do
|
||||
setVisibility value
|
||||
pure H.Listening
|
||||
```
|
||||
|
||||
## Inspiration & Thanks
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue