Broadly: Open things and close them as well.
Technically: Swap states on a particular element.
Drawer was originally created to handle opening and closing website navigation menus. It still does this very well! But you can use it to do other things too.
npm install @murmurcreative/[email protected]
import {Cabinet} from 'murmur-drawer';
new Cabinet();
<div data-module="drawer" data-knob="#knob">
<!-- Drawer Contents -->
</div>
<button id="knob">Toggle</button>
Now you have a Drawer!
For more details on functionality, etc, take a look at the configuration and API sections.
The core of Drawer is, well, the Drawer. In an instance, the Drawer should be considered the source of truth: Its state is what you should trust, and what all other components should refer to for an authoritative answer. The module is structured in a way that assumes this relationship and encourages you to go along with it. You can chose not to, if you like, but things might get dicey.
A Drawer is an element in the DOM that has a state attached to it. That state can be manipulated. When certain states are active, the Drawer might do certain things, or take on certain properties. The module is written to be fairly minimal, and doesn’t provide a lot of bells and whistles out of the box, but it gives you the tools to cast your own bells and carve your won whistles.
Knobs are optional, but they are useful for 90% of the things a Drawer is likely to be created for.
A Knob is an element that observes one or more Drawers for changes in their state, and has the capacity to react to those changes. Out of the box, a Knob also has the capacity to respond to click events by cycling the states on its attached Drawer(s).
|
Knobs can have many Drawers, and Drawers can have many Knobs. In most cases, they won’t; your nav menu probably only needs one toggle button, after all. Still, Drawer is built to assume that there can always be more than one Drawer or Knob, which means you should be circumspect when describing your element selectors. |
With a few exceptions, when specifying an element (or set of elements) you can use a variety of arguments and trust that Drawer will just handle it for you.
Wherever you see the SelectorArgument
type, you can use one of the following types:
HTMLElement
-
i.e. the result of
document.querySelector('selector-name')
. string
-
This will be interpreted as a selector, i.e.
.class
,#id
, etc. Array<HTMLElement | string>
-
An array of either of the above.
ℹ️
|
Internally, Drawer almost always deals with elements as arrays; targeting a single element usually just means an array with one row. Keep this in mind when crafting your selectors: If you want to be certain Drawer targets only a single element, pass it that element directly. |
Both Drawer
and Cabinet
take a second argument that can contain more specific settings.
Passing an object to this argument will overwrite the defaults on a per-property basis.
This is done using a shallow merge.
When calling Drawer
the settings will apply only to that Drawer; When calling Cabinet
the settings will apply to all Drawers that match.
states
|
<Array<string>> Default: These are the states that appear in |
||
initState
|
<string> Default: The initial state.
Must be a string from Can be overridden by adding |
||
hiddenStates
|
<Array<string>> Default: An array of strings.
When the Drawer is in a state listed here, it will be considered hidden and the |
||
hash
|
<string> Default: If set, this is a string that will be appended to the URL as a
Can be overridden by adding |
||
hashState
|
<string> Default: The state that should correspond to your If there is no valid If Can be overridden by adding |
||
actions
|
<Array<Function>> Default: An array of callbacks, called when the Drawer observes its state changing.
See |
||
uuid
|
<Function> Default: (internal function) Drawer uses a simple internal function generate uuids. If you require something more cryptographically secure, add a callback here that returns a uuid. |
||
knobs
|
<SelectorArgument | Object> Default: If passed a If you need to attach knobs with different settings, instead pass an argument with the following shape: {
elements: ['.knob'], // SelectorArgument
settings: {
cycle: false,
accessibility: true,
actions: [
function doThing(list) {
doTheThing(list);
},
]
},
} All matching elements will be assigned those settings and actions. This is overridden by |
If you’re instantiating Knobs independently with new Knob()
then you can pass a settings object as the second parameter with the following options:
cycle
|
<boolean> Default: A boolean that determines whether or not clicking on a Knob will fire |
accessibility
|
<boolean> Default: This enables (or disables) accessibility features.
Generally you should not turn it off, but for some use cases (i.e. non-interactive knobs) it may be desirable to disable it, which you can do by passing |
actions
|
<Array<Function>> Default: An array of callbacks, called when a Drawer this Knob is attached to changes state.
See |
Drawers and Knobs have an API object attached to their elements in the DOM.
For Drawers, this is a .drawer
;
for Knobs, .knob
.
You can also get the API for either by calling getDrawer(element)
or getKnob(element)
.
To create a Drawer, do one of the following:
-
new Drawer(HTMLElement, userArguments)
-
new Cabinet(SelectorArgument, userArguments)
new Drawer
will return an API object
(described below)
while new Cabinet
will return an array of API objects.
new Cabinet
will create Drawers on whatever objects match its SelectorArgument
, but if the first parameter is undefined
(or an invalid selector)
it will use the default selector: data-module="drawer"
.
state
|
<string> The current state of the Drawer.
To change the state, assign a new one: |
hidden
|
<boolean> Whether or not the Drawer is hidden.
This is based on the current value of Although this value can be set by assigning a new value
( |
cycle(states?: Array<string>)
|
<Function> Cycles through states on the Drawer. If called without an argument, it advances to the next states. If called with an array of valid states, it will advance to the next valid state in that array. |
actions
|
<Map<string, Function>> Callbacks called by the MutationObserver.
See actions for how those callbacks are constructed.
To add one, assign it: |
knobs
|
<Map<HTMLElement, KnobAPI>> List of Knobs attached to this Drawer.
To add a new knob, assign it: |
detachKnob(knob: HTMLElement)
|
<Function> Pass the element for a Knob to this function to detach it from this Drawer. The Knob will stop observer and reacting to the Drawer, and will no longer toggle it when clicked. |
hash
|
<string> The string used for the URL hash feature. If this is a string, the feature is enabled; otherwise it is disabled. While you can assign it directly, usually |
mount
|
<HTMLElement> The element that this API is attached to. It is here to allow you access to the element from actions, etc. You cannot modify its value after the Drawer has been created. |
The above are the API endpoints you should be using; they are chosen to give you necessary access to the things required, take steps validate your input, and are extremely unlikely to change outside of a major version bump. If you need some deeper access the following properties are also exposed, but keep in mind that their shape is not as guaranteed, and they have fewer checks in place to help you not break things.
settings
|
Contains internal settings for the Drawer.
Settings are things that (generally) aren’t going to change after instantiation and describe
behavior, like |
store
|
Contains internal values and references for the Drawer.
Things in the store are more dynamic and likely to change, and are also often complex objects that the Drawer acts upon, or asks to act for it.
Modifying items in the store will often have side effects;
i.e. adding an item to |
To create a Knob, do one of the following:
-
Assign a
SelectorArgument
to theknobs
property of a Drawer. -
Add a
data-knob="selector"
attribute to a Drawer. -
new Knob(HTMLElement, userArguments)
new Knob
will return an API object
(described below)
and you can retrieve the API object for a Knob from the knobs
on a Drawer if you have its element:
const knobAPI = drawerAPI.knobs.get(knobElement);
|
Unless you pass a drawer property in |
drawers
|
<Map<HTMLElement, MutationObserver>> This contains all the Drawers to which this knob is attached. Assigning a Drawer to this property will add it to the list, and assigning an array of Drawers will add them all. Adding a Drawer will cause the Knob to begin observing it, and interacting with the Drawer as its settings dictate. |
detachDrawer(HTMLElement)
|
<Function> Pass a Drawer element to this function stop observing it. The Knob will no longer react to the Drawer’s state, and will no longer toggle that state via clicks. |
actions
|
<Map<string, Function>> Callbacks called by the MutationObserver.
See actions for how those callbacks are constructed.
To add one, assign it: |
mount
|
<HTMLElement> The element that this API is attached to. It is here to allow you access to the element from actions, etc. You cannot modify its value after the Knob has been created. |
The above are the API endpoints you should be using; they are chosen to give you necessary access to the things required, take steps validate your input, and are extremely unlikely to change outside of a major version bump. If you need some deeper access the following properties are also exposed, but keep in mind that their shape is not as guaranteed, and they have fewer checks in place to help you not break things.
settings
|
Contains internal settings for the Knob.
Settings are things that (generally) aren’t going to change after instantiation and describe
behavior, like |
store
|
Contains internal values and references for the Knob.
Things in the store are more dynamic and likely to change, and are also often complex objects that the Knob acts upon, or asks to act for it.
Modifying items in the store will often have side effects;
i.e. adding an item to |
Actions are an important part of how we interact with drawers and knobs. In both cases, actions have an essentially identical signature:
-
list
This is an array of MutationRecords, each of which describes an observed mutation change. For most actions, you will be primarily concerned with these items, because they tell you what has just happened.
-
api
This is the API for the thing that this action is attached to; A Knob or a Drawer. Notably this is not the element that is being observed; if you want that element it can be found in
MutationRecord.target
. The API is made available here so that the action can make its host do things in response to the event. -
observer
The observer that observed this mutation. In most cases you won’t need this, but it some situations it may be useful, i.e. if you want to respond to a particular mutation by ceasing to observe.
The MutationObservers here are limited:
Both watch only for changes to the data-state
and hidden
attributes on drawers, and only on the element itself (children are ignored).
However, sometimes both will trigger at the same time, i.e. if the Drawer moves into a hidden state.
MutationRecord.attributeName
will tell you which particular attribute generated a particular MutationRecord.
MutationRecord.oldValue
will tell you what the attribute mutated from.
The MutationRecord itself doesn’t contain the current value, but you can easily get it from MutationRecord.target
:
function someAction(list) {
list.map(record => {
console.log(record.target.getAttribute(record.attributeName));
})
}
ℹ️
|
If |
When adding actions, you are encouraged to write named functions and then pass those as callbacks, rather than using anonymous/arrow functions. This makes it easier to identify and potentially modify the actions assigned to a Drawer or Knob.
// good
function doSomeAction(list, el, observer) {
// do something
}
api.actions = doSomeAction;
// good
const doAnotherAction = (list, api, observer) => {
// do another thing
};
api.actions = doAnotherAction;
// bad
api.actions = (list, api, observer) => {
// do a mysterious thing
};
// later we could easily remove this action
api.store.repo.get('actions').delete('doSomeAction');
If a callback you pass doesn’t have a name that Drawer can determine, it will be given a randomly-generated name by uuid()
.
The following is a simple, complete example that will result in a drawer that can be opened and closed by clicking on the button:
import {Drawer} from "murmur-drawer";
new Drawer(document.querySelector('.drawer'));
<div class="drawer"
data-knob="button[data-controls='drawer']"> 🧦🧦🧦🧦🧦🧦🧦🧦 </div>
<button data-controls="drawer"> Toggle </button>
Drawer is several dozen lines of code that manage, essentially, one thing:
data-state="open"
This is the single source of truth for everything Drawer does, and by taking advantage of a number of native browser features it does so efficiently and extensibly.
Using MutationObserver, Drawer watches for state changes and reacts to them. You are of course encouraged to use Drawer’s simple API to interact with its state, but the beauty of MutationObserver is that it doesn’t matter:
const el = document.querySelector('.drawer');
// Drawer API
const {setState} = el.drawer;
setState('closed');
// Direct access
drawer.dataset.state = 'closed';
The API is largely built on the following ideas:
-
Relevant data should be easily accessible
-
Changing that data should cause the rest of the state and element to react
-
Behavior should be as consistent as possible across interfaces
As a result, most functionality is accessed by assigning data to properties, which then use getters and setters to
-
Validate inputs
-
Store data in internal Maps
-
Fire off side effects that accomplish the actual functionality
This makes the API easy to use, easy to discover, and hopefully fun.
Trying to get v1 of this module to work with IE11 was possible, but a huge hassle. By avoiding any framework, and keeping the source simple, my intent is to make v2 either compatible out of the box, or compatible with a minimal amount of work. This might look like distributing a separate transpiled source file for browsers that don’t support modern technologies, or a sort section in the Readme detailing how to get it working in IE11.
Whatever the case, you should be able to trust that this module will work, easily, in IE11.
Instead of getting fancy with things like web components, this keeps it simple: No frameworks or dependencies, just good old Vanilla JS.