npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

manuel

v0.66.0

Published

A super customizable VDOM autocomplete with *production ready* defaults.

Downloads

1,655

Readme

✨ manuel ✨

A super customizable VDOM autocomplete with production ready defaults.

build status coverage report

Features

  • Use any VDOM Library, your favourite, my favourite, anything goes!
  • Configure absolutely everything, from behaviour, data and rendering
  • Stateless, so it works the same way everytime.
  • Your model, your way. Streams/Observables,Lenses,POJO's,Redux, easy setup.
  • Tiny: 5KB minified

Examples

Getting the library

Via npm:

npm install manuel

Or reference it in a script tag via unpkg

<script src="https://unpkg.com/[email protected]/dist/">

There's an optional CSS file. Which you can find on unpkg as well.

Getting Started

Manuel works with any Virtual DOM framework. For this example we'll use mithil and tell this library how to read and write from a plain old javascript object.

See a live sample of this example here

First we'll create an empty html file.

touch index.html

Then paste this into that file in your favourite editor and save the file.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <link rel="stylesheet" href="https://unpkg.com/manuel@latest/dist/style.css">
  <title>Manuel</title>
</head>
<body>
  <script src="https://unpkg.com/manuel@latest/dist/manuel.js"></script>
  <script src="https://unpkg.com/[email protected]/mithril.min.js"></script>

  <script>

    const pojo = {

      // the current value in the input field
      input: ''

      // the value of the currently selected
      // value in the suggestions list
      , highlight: ''


      // the list of suggestions
      , list: [
        'Banana 🍌'
        ,'Cherry 🍒'
        ,'Watermelon 🍉'
        ,'Kiwi 🥝'
        ,'Strawberry 🍓'
        , 'Peaches 🍑'
      ]

      // whether or not the suggestions list should be open
      ,open: false

      // The value of an item that was explicitly
      //  selected from the list
      ,chosen: null
    }

    const autocomplete = manuel({
        hyperscript: m

        ,get: key => pojo[key]

        ,set: (key, value) => {
            pojo[key] = value
        }
    })

    const App = {
      view(){
        return m(
          'label', 'Search for fruit (Try Kiwi)'
          ,autocomplete({
            input: 'input'
            ,highlight: 'highlight'
            ,open: 'open'
            ,chosen: 'chosen'
            ,list: 'list'
          })
        )
      }
    }

    m.mount(document.body, App)
  </script>


</body>
</html>

Then open the index.html file in your browser whichever way you prefer.

You should now have a working sample.

Most of the above code is simply boilerplate for creating a mithril app.

Let's zoom in on the parts that are relevant to this library in particular.

This section tells manuel how to read and write from your state. This is completely within your control. manuel will always use these particular functions whenever it interacts with your state.

There are plenty of advantages to this approach. One is simply intercepting what the library is doing while debugging. Another is, being able to swap out your model backend without having to write a lot of heavy adapter code or writing a new library! 😱

const autocomplete = manuel({
    hyperscript: m

    ,get: key => pojo[key]

    ,set: (key, value) => {
        pojo[key] = value
    }
})

If you wanted a call to get to read from a redux store, and a call to set to dispatch a redux action you can. If you like optics libraries, you can use lenses instead.

As captain planet always says: The model is yours!

 const pojo = {

    // the current value in the input field
    input: ''

    // the value of the currently selected
    // value in the suggestions list
    , highlight: ''


    // the list of suggestions
    , list: [
        'Banana 🍌'
        ,'Cherry 🍒'
        ,'Watermelon 🍉'
        ,'Kiwi 🥝'
        ,'Strawberry 🍓'
        , 'Peaches 🍑'
    ]

    // whether or not the suggestions list should be open
    ,open: false

    // The value of an item that was explicitly
    //  selected from the list
    ,chosen: null
}

This is where your state lives. You can structure your model however you want, but it has to work with those get and set functions you set up earlier.

The fact the model is external is kind of unusual but its useful. It may seem inconvenient to have to be so explicit, but it's giving you complete control over your state, and because this library is just a stateless function, you can actually update the autocomplete by just writing to your state yourself and redrawing.

You don't need to do that. But you can.

A more common scenario will be to write a thin wrapper component in your favourite framework that abstracts over this interface. But you don't need to do that. It's totally fine to just pass the state in yourself in your view.

It makes the library's behaviour very predictable.

autocomplete({
    input: 'input'
    ,highlight: 'highlight'
    ,open: 'open'
    ,chosen: 'chosen'
    ,list: 'list'
})

Remember how our get and set function accepted a key and in the case of our set function, a key and a value argument. Well manuel doesn't know anything about how your get/set functions work. All it needs is something it can give to your functions in order to get or set a value. In our case our functions need a key, so that's exactly what we are giving it.

But imagine instead that we were using a stream library. We could instead pass the streams in:

autocomplete({
    input: inputStream
    ,highlight: highlightStream
    ,open: openStream
    ,chosen: chosenStream
    ,list: listStream
})

We'd now need our get and set functions to know what to do when we pass in a stream and a value, instead of a key and a value. But that's all you need to do to make a different model backend click

Here's an imaginary stream library example:

const streamAutocomplete = manuel({
    hyperscript: m
    ,get: stream => stream.value()
    ,set: (stream, value) => stream.next(value)
})

Hopefully that is enough to get you excited. But autocomplete also takes a second argument. That second argument allows you to override absolutely every default behaviour and state that manuel uses.

What's different about manuel?

Framework neutral and uber configurability.

This library's primary assumption is that any functionality can be overridden. From interacting with stores, specifying hyperscript libraries to specifying how to render any particular item and any feature the library provides.

But manuel also works just fine without specifying any config other than how to interact with your library. This step is going to be so common for so many users that it's likely in future we'll simply distribute this library preconfigured for that particular framework.

Stateless

This library is 100% stateless. If you need a stateful interface you can just call this library from within your own wrapper component. Statelessness means this library doesn't store anything internally that you cannot access, and that lets you have complete control over it's behaviour. It also means if the state gets out of sync, it's not this libraries fault 😄

Theming

Whether you like atomic css, css in js, or plain old stylesheets, manuel has you covered.

By default, manuel decorates the containing element with classes that make it's internal state themeable. But you can replace the config.classNames function and prevent or alter that behaviour.

The only other autogenerated class is on the currently selected item in the list of suggestions. By default that suggestion will have a class highlight added to it. But you can alter or replace that behaviour too, by overriding config.itemClassNames

Top level state classnames:

  • open
  • loaded
  • not-empty

The top level element will also always begin with manuel-complete.

In the interest of sensible defaults - this library comes with a stylesheet that you are free to use. But please keep in mind you are not locked into any particular workflow at all.

Testing

manuel is a stateless function that returns a tree of virtual dom. That means testing manuel is super easy.

Let's say your favourite library doesn't have a library for querying your hyperscript, we can interact with the structure manually. And this is pretty safe to do because you ultimately control the rendered output via config.renderRoot, config.renderInput, config.renderItems, config.renderItem, config.classNames, config.itemClassNames and more!

Using our Getting Started example with mithril.

var out = autocomplete(model)

var input = out.children[0]
var suggestions out.children[1].children

model.input = ''

input.oninput({ currentTarget: { value: 'New value' }})

assert.equals( model.input, 'New Value' )

As you can see, you can test this library without interacting with the browser at all. Technically you could run this library serverside, it's really up to you!

API

manuel

| manuel | | |---------|----------------------------------------------------------| | Type | HyperScriptConstructor h => ({ hyperscript:: h, get :: a -> string, set :: (a, string) -> void) -> autocomplete | | Default | No default, must be set |

Phew! That looks scary. Let's break that down.

You have a lot of flexibility when initializing manuel so the types reflect that.

The first part HyperScriptConstructor h => means, the parameter h must satisfy the constraint HyperScriptConstructor. That means it's a function that can generate hyperscript. An example would be mithril's m.

The second part get :: a -> string means a function that accepts a value of a particular type and returns a string. This is your getter function, manuel will pass in a type you've provided elsewhere, and you need to write the logic that extracts that value. The particular type of a has to match whatever value you provide to the autocomplete record whenever you call the view.

The third set :: (a, string) -> void is your setter function. manuel will pass in a value you've provided elsewhere, and you need to write the logic that takes that value and a string and saves it to your model.

This seems more complex and convoluted than it actually is. Please refer to the Getting Started section for a simple example with a plain javascript object.

Finall we return autocomplete which is defined below.

Please note, there isn't a hyperscript standard, so you may need to write an adapter for your framework of choice if it has a custom format. This library supports mithril's vnode structure. Your library of choice is probably generating a datastructure that is the same or similar.

autocomplete

| manuel | | |---------|----------------------------------------------------------| | Type | (model :: StrMap a, config :: Config? ) -> VTree | | Default | Every model field must be provided, but every config field has a default. |

This is the view function itself. You'll be calling this from within your view or within your own component.

The individual model properties must be explicitly provided, they are all documented below. There are not very many, and it's highly likely you'll want to interact with every model property at some point.

The config properties all have provided defaults, and you can completely skip providing the config argument if you want.

The individual config and model properties are all documented below.

Because your model can be of any type, the type documentation is only specifying what the type should be after calling your view function on it.

model.list

| get(model.list) | | |------------------|-------------------------| | View Type | string[] | | Default | No default, must be set |

The list of suggestions to be rendered. Most other autocomplete libraries have a list of records with some kind of hint or property to tell the library how to render that record. E.g. awesomeplete's list can be of the form Array { value, label }.

But working with virtual dom gives us an opportunity to simplify this interface. This library will always expect get(model.list) to return a list of strings. And you can override config.renderItem and join that string back to your complete dataset in scope if you need to.

This means the library's interface is far simpler and we can internally make a lot more assumptions when filtering and sorting that dataset.

There are several places you can intercept behaviours that rely on this list.

You can override config.choose to add custom logic when writing to model.chosen. Or you can override config.renderItem or even just config.highlight to override the rendering of a particular value.

In fact, there are so many ways you could customize the behaviour of this library that it's hard to list them all. If you have a special requirement but can't find a way to configure the library to support, please open an issue I'd be happy to help!

model.highlighted

| get(model.highlighted) | | |-------------------------|-------------------------| | View Type | string? | | Default | No default, must be set |

This is used to store the value in a list that is currently selected via keyboard navigation, but has not been chosen yet.

You can override keyboard navigation by replacing config.onkeydown.

model.input

| get(model.input) | | |--------------------|-------------------------| | View Type | string | | Default | No default, must be set |

This is used for storing or overriding the input value.

It's used for filtering and rendering. If you override it, it will be as if the value was typed in by the user. This can come in handy for custom replacements of the search on selection.

For more advanced rendering changes, consider overriding config.renderItem.

model.open

| get(model.open) | | |------------------|-------------------------| | View Type | boolean | | Default | No default, must be set |

This values stores whether or not the suggestions list should be open. Please be advised that setting this to true will not necessarily open the drawer. If there are no suggestions to render, or there is no value in model.input the list will not open.

You can override this behaviour by directly modifying config.showingDrawer.

model.chosen

| get(model.chosen) | | |--------------------|-------------------------| | View Type | string? | | Default | No default, must be set |

This property stores a value that was explicitly selected in the suggestions list (as opposed to simply typed in).

This can be handy for enforcing strict constraints on your user input.

This library itself only ever writes to model.chosen so you have a lot of freedom in what is stored there yourself.

The chosen value be cleared if the value of model.input does not equal model.chosen e.g. if you select a suggestion and then type something else in the search box.

You can override this clearing behaviour by replacing config.oninput.

config.minChars

| config.minChars | | |---------|-------------------| | Type | PositiveInteger | | Default | 2 |

The minimum number of characters that you have to enter before the suggestion list will open.

You can override this behaviour by picking a different value, or by overriding config.oninput.

config.maxItems

| config.minChars | | |-----------------|-------------------| | Type | PositiveInteger | | Default | 10 |

The maximum number of items that will be rendered in the suggestion list. It's generally good practice to keep this number pretty small to encourage filtering instead of scrolling.

config.sort

| config.sort | | |-------------|---------------------------| | Type | (String, String) -> Int | | Default | Sort by length of string |

How manuel sorts the suggestion list when rendering.

You can customize this behaviour by overriding the config.sort function, or by replacing config.filteredList.

This function signature may seem a bit odd, but it's informed by Javascript's native Array::sort. The signature is often called a comparator function.

There are many libraries out there that support this interface, but generally you just want to get 2 numbers and subtract one from the other.

Whether the number returned is less than 0, 0, or greater than zero tells Javascript how to order the list.

Github's User Search API returns a score property. So we could sort by Github's score like so.



autocomplete(model, {
    sort(a, b){
        const A =
            realData.find( s => s.login == a )

        const B =
            realData.find( s => s.login == b )

        return A.score - B.score
    }
})

Usually, you'll want model.list to just be an Array of strings, and thanks to the wonders of virtual dom, connect that string back to a more complex object in your own component scope. Here we're assuming there is a list of the complete response from the github api called realData. We're using Array::find to find the complete object for each string that is being compared and then subtract one score from the other.

There's an example of doing exactly this in the examples section.

config.filter

| config.filter | | |---------------|--------------------------------------------------------------------| | Type | (input::String, suggestion::String) -> Boolean | | Default | Excludes if the user input is not contained in the suggestion text |

Exclude a suggestion from being rendered based on a condition.

You can override this behaviour by replacing the config.filter function.

For example, your dataset may already be filtered by an API you are communicating with. We may simply want to keep all values.

autocomplete(model, {
    filter(value, suggestion){
        // keep every value from the server
        return true
    }
})

config.filteredList

| config.filteredList | | |---------------------|--------------------------------------------------| | Type | string[] | | Default | Sort/filter/slice based on other config settings |

The list of suggestions post processing. Used for rendering and keyboard navigation logic

You likely shouldn't be editing this property, and instead should override config.sort, config.filter, config.maxItems or even model.list.

But sometimes you may want to temporarily opt out of some config that is otherwise correct, and the simplest way to do that is to override the filteredList completely.

A usecase might be: you have a stream that automatically filters and sorts a massive list externally, and you want to skip the entire sort/filter/slice pipeline because that work has already happened.

But unless you need this, you probably shouldn't use it. It's likely a sign your design needs a rethink.

config.showingDrawer

| config.showingDrawer | | |----------------------|--------------------------------------------------| | Type | boolean | | Default | Guards against opening the suggestions when there are no items loaded |

You probably will never need to override this. get(model.open) tells manuel whether or not the suggestions list should currently be rendered.

But manuel will keep the suggestions list closed if there are no items to render, or if the number of characters in the text input are less than config.minChars.

You can prevent this extra guard by setting this to true or by tweaking config.minChars.

autocomplete(model, {
    // force the suggestions list to be open.
    showingDrawer: true
})

In a similar sense to filteredItems you probably should never override this property, as it will create more work for you to keep it in sync, and you may end up replicating behaviour that already exists. But if you need to, it's there.

config.choose

| config.choose | | |---------------|--------------------------------------------------| | Type | (x :: string) -> void | | Default | Updates the chosen, input to x and sets open to false |

This function is used by both the keyboard and mouse selection. You may want to perform some additional validation at this step. Though I'd advise to instead use streams and react to values being chosen by subscribing to a stream as opposed to proxying things.

You can override this behaviour by replacing config.choose with your own logic.

autocomplete(model, {
    choose(x){
        callSomeOtherCustomLogic()

        model.chosen = x
        model.input  x
        model.open  false
    }
})

config.clickItem

| config.clickItem | | |------------------|-------------------------| | Type | (x :: string) -> void | | Default | Calls config.choose |

This function simply dispatches config.choose(x). You can replace this function if you'd like to have custom logic only when selecting an item via the mouse.

You can override this behaviour by replacing config.clickItem, changing config.choose, or by writing your own rendering logic for a list item by replacing config.renderItem.

autocomplete(model, {
    clickItem(x){
        if( someCondition ){
            model.chosen = x
            model.input = x
            model.open = false
        }
    }
})

config.PATTERN_INPUT

| config.PATTERN_INPUT | | |------------------|-------------------------| | Type | Global RegExp | | Default | Matches on the input's current value case insensitively |

A Regular Expression that is generated every render that will match on the input's current value (model.input) in a case insensitive way.

It is used by config.highlight to highlight sections of text in suggestions that match the search query.

You'll probably never want to override this. But there are some valid reasons to do so. If you aren't using config.highlight we're basically generating a Regular Expression for no reason. You could also prevent generating the regular expression every frame (which this library does because it is stateless), and instead may want to cache the regular expression.

I'd advise not to touch this unless you run into any performance issues.

You can override this behaviour by setting the config.PATTERN_INPUT to a pattern of your choice. Or by replacing config.highlight. Please note the Regular Expression must have a global flag as manuel iterates over matches to replace them.

config.highlight

| config.highlight | | |------------------|-------------------------| | Type | String -> VTree | | Default | Surrounds characters with some <mark> elements wherever the contents of the suggestion match the search query |

This function allows you to annotate the suggestion by highlighting sections of text that match the search query. It's unlikely you'll want to override this function, but there are some cases where you may want to.

  1. You may not want to write your own renderItem function, but perhaps you would like to do some custom formatting on just the text of a suggestion.

  2. You may want to disable highlighting altogether (for some reason).

// disable mark annotation
autocomplete( model, {
    // prevent generation of RegExp
    PATTERN_INPUT: null

    // To not mark suggestion, leave as is
    ,highlight: x => x
})

config.oninput

| config.oninput | | |------------------|---------------------------------| | Type | HTMLInputEvent -> VTree | | Default | Saved the input, opens the suggestion list and clears chosen if the input value does not match the current chosen value. |

You may want to override this function to detect when values are being entered. I'd recommend instead to have a stream on your model and subscribe to changes via the stream. But there are valid cases where you may want to override the defaults.

config.onfocus

| config.onfocus | | |----------------------|-----------------------------| | Type | FocusEvent -> void | | Default | Set model.open to true |

You may want to override this function to have finer grain control on focus states.

config.onblur

| config.onblur | | |----------------------|-----------------------------| | Type | BlurEvent -> void | | Default | Deferred set of model.open to false after the current frame via setTimeout(..., 0) |

You may want to override this function to have finer grain control on focus states.

config.renderInput

| config.renderInput | | |----------------------|---------------------------| | Type | Config -> VTree | | Default | Binds handlers for focus, blur and input and renders an input via the provided hyperscript constructor |

You may want to override this function to do some custom rendering of the input, like a multi select.

config.itemClassNames

| config.itemClassNames | | |----------------------|---------------------------| | Type | String -> String | | Default | Adds a highlight class to a suggestion item if it is selected via the keyboard navigation. |

You may want to override this class to prevent css classname generation or to add additional classes based on some other states.

// disable classname generation
autocomplete(model, {
    itemClassNames: () => ''
    ,classNames: () => ''
})
autocomplete(model, {
    itemClassnames(x){
        if( x == someValue ){
            return someClasses
        } else {
            return ''
        }
    }
})

config.renderItem

| config.renderItem | | |----------------------|-------------------------------| | Type | ( String, Config ) -> VTree | | Default | Creates a suggestion item via the provided hyperscript constructor, includes calls to config.highlight, config.classNames and config.clickItem |

You may want to override this function to customise the rendering for the suggestion list, perhaps to include thumbnails, avatars, or additional formatting.

autocomplete(model, {
    renderItem(x){
        // override default list item rendering and disable
        // built in functionality as a result
        return m('li', x)
    }
})

config.renderItems

| config.renderItems | | |----------------------|-------------------------------| | Type | ( Config ) -> VTree | | Default | Iterates over config.filteredList and generates a ul of suggestions using the logic contained within config.renderItem |

You may want to override this function to additional styles or wrapping elements to the suggestions list.

autocomplete(model, {
    renderItem(config){
        // override default list item rendering and disable
        // built in functionality as a result
        return m(
            'ul'
            , addSomeCustomAttributes()
            , config.filterList.map(
                x => config.renderItem(x, config)
            )
        )
    }
})

config.eventNames

| config.eventNames | | |----------------------|-----------------------| | Type | StrMap String | | Default | Sets all event names to be all lowercase. |

By default manuel uses all lowercase event names just like HTML. Some libraries like React use camelcase for their event handlers, config.eventNames allows you to support libraries that use alternate naming schemes for events.

Note, if you are completely swapping out your render functions (and therefore your event listeners) you do not need to override this field.

manuel uses the following events

  • oninput
  • onfocus
  • onblur
  • onkeydown
  • onmousedown

To support React.createElement as your hyperscript function you may want to override eventNames like so:

autocomplete(model, {
    eventNames: {
        oninput: 'onInput'
        ,onfocus: 'onFocus'
        ,onblur: 'onBlur'
        ,onkeydown: 'onKeyDown'
        ,onmousedown: 'onMouseDown'
    }
})

You could also handle this translation entirely in your hyperscript function by writing an adapter that transforms props prefixed with on.

You could also use this property to force manuel to call different events in different environments, do silly things like swap onfocus and onblur, or disable all event interaction entirely!

config.classNames

| config.classNames | | |----------------------|-----------------------| | Type | () -> String | | Default | Adds some classes that allow you to style specific states of the autocomplete. |

This function will add classnames to the root element of the component to allow you to visually depict certain states including

| State | Behaviour | | -------------|------------------------------------------------| | .loaded | If the list of items has more than 0 elements | | .not-empty | If the input value length > 0 | | .open | If the suggestions draw is open. |

There's a default stylesheet, you can use that relies on upon these default classes, but you can override these classnames and theme the autocomplete using whatever workflow you prefer.

You may want to override this class to prevent css classname generation or to add additional classes based on some other states.

// disable classname generation
autocomplete(model, {
    classNames: () => ''
    ,itemClassNames: () => ''
})
autocomplete(model, {
    classNames(x){
        if( x == someValue ){
            return someClasses
        } else {
            return ''
        }
    }
})

config.renderRoot

| config.renderRoot | | |----------------------|--------------------------| | Type | Config -> Vtree | | Default | Set up the component DOM structure, adds classnames and renders the input and items. |

You may want to override this function to use a completely custom rendering strategy.

config.onkeydown

| config.renderRoot | | |----------------------|-------------------------------| | Type | KeyDownEvent -> Void | | Default | Sets up logic for dismiss (ESC), submit (ENTER) and navigation (ARROW KEYS). |

You may want to override this function to add additional keydown logic or to disable keyboard interaction entirely.

Queries

Manuel's mission is to work out of the box but also be configurable to handle all reasonable use cases.

We've got sensible defaults, we've got overrideable hyperscript, overrideable configuration, overrideable model formats.

But what if you are happy with the defaults and you just want to tweak one simple thing after manuel has returned the vtree?

That's where queries come in.

A query is a function that allows you to visit and modify a particular point of interest in the vtree without having to know where exactly that point of interest is in the heirarchy.

We'll start with an example. Let's say you want to add a background-color to the input element, but maintain all other rendering and event listeners manuel gives you.

Well Manuel exposes a query that lets you access that exact VNode.

const bgRedInput = manuel.queries.input(function( vnode ){
    vnode.attrs.backgroundColor = 'red'
})

bgRedInput is now a decorator function that accepts and returns the complete vtree.

You can use it like so in your view.

bgRedInput(
    autocomplete(model, overrides)
)

Or you could use a utility like R.compose to create a styled version of the autocomplete.

const styledAutocomplete =
    R.compose(
        bgRedInput
        ,autocomplete
    )

styledAutocomplete(model, overrides)

The best part, if manuel changes its internal dom structure your query will not break!

What queries does manuel expose?

address | signature --|-- manuel.queries.input | ( VNode -> VNode | undefined ) -> VNode -> VNode manuel.queries.list | ( VNode -> VNode | undefined ) -> VNode -> VNode manuel.queries.listItems | ( VNode[] -> VNode[] | undefined ) -> VNode -> VNode manuel.queries.root | ( VNode -> VNode | undefined ) -> VNode -> VNode

Why do the queries mutate? Do they have to?

The VTree is always brand new, so it's mutable state, but its not shared mutable state. There's no risks involved in mutation here so there's no benefit in paying the performance cost.

The queries support somewhat magic behaviour, where if you return a non undefined value manuel will use what you return instead of the existing tree. This behaviour ensures compatibility with existing tools for transforming data immutably (like R.assoc, _.set and lenses) without forcing immutability upon you.

This is great! Where can I find more queries?

There's a secret project in the works that does exactly that. Stay tuned.

Why did you write this library?

Before writing this library we were using Awesomeplete, which is a brilliant library but I've found it's quite difficult to integrate into a unidirectional architecture. There were many times where we were performing lots of hacks to control behaviour that would have been trivial to solve in a virtual dom setting.

I've wanted to write this library for a long time but there were always higher priority issues. Recently I needed to test a UI with a lot of searching, and having access to the VNode would make it worth the time investment.

Roadmap

This library was written to be basically Awesomeplete but VDOM native. Doing so solves a bunch of problems that I have. But I think there's a lot of headroom for functionality that could be included, some of which could be in this library, other things could be in plugins or modules.

I'd really like added functionality to continue to be library neutral, basically a decorator that intercepts and overrides config to enable certain functionality.

I'd like to have some tests and examples for some common VDOM libraries particularly React. I'd also like to see some modules sitting on top of this base API to provide library neutral plugins for functionality you might see in Select2 or selectize (like e.g. multiselect boxes). It should be quite straight forward to do as a lot of that is rendering logic and all the render functions are overridable.

It'd be great to have a site with inline examples instead of redirecting people to little jsbin's (they're great as a separate resource). But that's fairly low priority for me seeing as this library has 0 users at time of writing 😆

But unless I get any requests I'll probably prioritize features that I need first. And all the features I currently need are in this initial release.

Please open an issue on Gitlab if you have ideas. Contributions are very welcome.

What's with the name?

This library is designed to allow you to do everything manually if need be.

It's also an inversion of the auto in autocomplete.

And I work with an Emmanuel. It's really a pretty good name!

Acknowledgements and Prior Art

This library takes a lot of inspiration from Awesomeplete. The default sort, filter logic and stylesheet were written by referencing that source. This library in a sense is like Awesomeplete but reimagined for Virtual DOM, and all the benefits of that. So I'd like to thank Lea Verou for all the hard work she does and all the contributors on that project.

Stephan Hoyer, wrote both mithril-query and mithril-node-render, which are supremely useful for testing the logic of this library. Without Stephan's libraries the mithril community would be having a lot less fun writing tests.

To the Ramda community for introducing me to lenses. This library doesn't use lenses but faciliates using them, and was inspired by the possibilities lenses facilitate.

While the library has no direct dependencies, it was designed to work well with particular interfaces. I'd like to thank Simon Vindum for writing flyd, which has completely changed how I write frontend logic, and has definitely affected the design of this library. Logic I would have usually embedded in the library, I've instead left to the user to implement via streams. It's the same amount of glue code, but far more flexible.

I'm sure I'll add more to this section in the near future! Thank you for reading.