Home

Awesome

Vue Spruce

Build codecov Maintainability

A collection of useful Vue 2 renderless components.

Check out the demo

Install

Package

yarn add -D @crishellco/vue-spruce
# or
npm i -D @crishellco/vue-spruce

Vue Plugin

Installs all components globally.

import Vue from 'vue';
import VueSpruce from '@crishellco/vue-spruce';

Vue.use(VueSpruce);
// or with options
Vue.use(VueSpruce, { componentPrefix: 's' });

Nuxt Module

Installs all components globally.

// nuxt.config.js
{
  modules: [['@crishellco/vue-spruce/nuxt', { componentPrefix: 's' }]];
}

Options

NameDescriptionDefault
componentPrefixThe prefix used when installing components globallyspruce

Named Imports

Alternatively, use only the components you need.

import {
  SpruceAtLeast,
  SpruceCling,
  SpruceEvent,
  SpruceFetch,
  SpruceFunction,
  SprucePaginate,
  SpruceSearch,
  SpruceSort,
  SpruceState,
  SpruceToggle,
  SpruceWatch,
} from '@crishellco/vue-spruce';

export default {
  components: {
    SpruceAtLeast,
    SpruceCling,
    SpruceEvent,
    SpruceFetch,
    SpruceFunction,
    SprucePaginate,
    SpruceSearch,
    SpruceSort,
    SpruceState,
    SpruceToggle,
    SpruceWatch,
  },
};

The Components

SpruceAtLeast

Ensures a component shows for at least a given amount of time, in milliseconds, before hiding.

<spruce-at-least :ms="5000" :show="shouldShowImage">
  <div slot-scope="{ disabled, show }">
    <img v-if="show" src="https://placebeard.it/g/200/300" alt="" />

    <button :disabled="disabled" @click="shouldShowImage = !shouldShowImage">
      {{ disabled ? 'waiting...' : show ? 'hide' : 'show' }}
    </button>
  </div>
</spruce-at-least>

Props

NameDescriptionTypeRequiredDefault
msMinimum amount of time to show, in millisecondsNumberYes
showWeather or not to show the contents (given enough time has passed)BooleanNotrue

Slots

NameRequired
defaultYes

Slot Scope

SlotNameDescriptionType
defaultdisabledTrue if waiting to hide content after ms time has passedBoolean
defaultshowIf the contents should be shownBoolean

SpruceCling

Clings the clinger slot's contents to the anchor slot's contents using popper.js. Great for things like dropdown menus. See the demo for more context.

<spruce-cling placement="bottom">
  <template #anchor>
    <button>
      i'm a button
    </button>
  </template>
  <template #clinger>
    <div>
      <div>i'm a clinger!</div>
    </div>
  </template>
</spruce-cling>

Props

NameDescriptionTypeRequiredDefault
modifiersThe popper.js modifiersArrayNo[]
placementThe popper.js placement of the clinger in relation to the anchorStringNoauto

Slots

NameRequired
anchorYes
clingerYes

Slot Scope

SlotNameDescriptionType
anchorupdateUpdates the popper instanceFunction
clingerupdateUpdates the popper instanceFunction

SpruceEvent

Track any window event occurance inside or outside of SpruceEvent's default slot.

<spruce-event event="mouseover" @mouseover="someMethod">
  <button>Hover over me!</button>
</spruce-event>

Props

NameDescriptionTypeRequiredDefault
eventThe event to listen toStringYes
immediateFirst event immediately (in mounted)BooleanNofalse
outsideListen for the even only outside of the default slot elementsBooleanNofalse

Events

NameDescriptionPayload
Same as the event propFired when the event happens.--

Slots

NameRequired
defaultNo

SpruceFetch

Make asynchronous API fetch calls.

<spruce-fetch url="https://dog-api.kinduff.com/api/facts" >
  <div slot-scope="{ loading, data, error, fetch }">
    <loading-indicator v-if="loading" />
    <div v-else-if="errors">Errors! {{ error.status }}</div>
    <div v-else>Data: {{ data }}</div>
    <button @click="fetch">Refetch</button>
  <div>
</spruce-fetch>

Props

NameDescriptionTypeRequiredDefault
urlThe API url (changing this will refetch and rest all data)StringYes
immediateWeather to immediate make the request on mountBooleanNotrue

Events

NameDescriptionPayload
successFires when the request is successful{data: Object, fetch: Function}
errorFires when the request fails{data: Object, fetch: Function}

Slots

NameRequired
defaultYes

Slot Scope

SlotNameDescriptionType
defaultcallsNumber of calls madeNumber
defaultdataThe response on a successful requestObject
defaulterrorThe response on a failed request{ data: Object, status: Number }
defaultloadingWhether a request is in progressBoolean
defaultfetchMakes another requestFunction

SpruceFunction

Create reusable functions on the fly (great for lists!).

<div v-for="num in 10">
  <spruce-function :fn="() => alert(num)">
    <button slot-scope="{ fn }" @click="fn">Click me!</button>
  </spruce-function>
</div>

Props

NameDescriptionTypeRequiredDefault
fnThe functionFunctionYes

Slots

NameRequired
defaultYes

Slot Scope

SlotNameDescriptionType
defaultfnThe functionFunction

SprucePaginate

Paginate an array and navigate through it's chunks.

<spruce-paginate :list="states" :size="15">
  <div slot-scope="{ page, next, prev, pageNum, totalPages, isFirst, isLast, rangeStart, rangeEnd }">
    <button :disabled="isFirst" @click="prev">
      prev
    </button>
    <div class="px-4 flex flex-col items-center">
      <div>Page: {{ pageNum }}/{{ totalPages }}</div>
      <div>Showing: {{ rangeStart }} - {{ rangeEnd }} of {{ states.length }}</div>
    </div>
    <button :disabled="isLast" @click="next">
      next
    </button>
  </div>
</spruce-paginate>

Props

NameDescriptionTypeRequiredDefault
sizePage sizeNumberYes
listThe items to paginateArray<any>Yes

Slots

NameRequired
defaultYes

Slot Scope

SlotNameDescriptionType
defaultfirstGo to first pageFunction
defaultgoGo to specific pageFunction
defaultisFirstIf currently on first pageBoolean
defaultisLastIf currently on last pageBoolean
defaultlastGo to last pageFunction
defaultnextGo to next pageFunction
defaultpageThe current pageany
defaultpagesThe chunked pagesArray
defaultlinksA calculated array of specific page numbers that can be used for links [1, 2, 3, '...', 40]Array
defaultpageNumThe current page numberNumber
defaultprevGo to previous pageFunction
defaultrangeEndThe end of the current pageNumber
defaultrangeStartThe start of the current pageNumber
defaultresetReset the state of paginationFunction
defaulttotalPagesTotal number of pagesNumber

SpruceSearch

Search an array of strings or objects by keys using fuse.js.

<spruce-search :list="states" :term="term" :keys="['name', 'email']">
  <div slot-scope="{ results }">
    <div v-for="(item, index) in results" :key="index">
      {{ item }}
    </div>
  </div>
</spruce-search>

Props

NameDescriptionTypeRequiredDefault
keysThe keys to search inArrayNoIf list is Array<Object> then all of the first object's keys. Otherwise [].
listThe list to searchArrayYes
termThe terms to search forStringNo''
thresholdFuse.js match thresholdFloatNo0.6

Slots

NameRequired
defaultYes

Slot Scope

SlotNameDescriptionType
defaultresultsThe searched listArray

SpruceSort

Sort an array of strings or objects in either direction by specific keys.

Note: string sorting is case insensitive.

<spruce-sort :list="people" :by="by" :direction="direction">
  <div slot-scope="{ results }">
    <div v-for="(item, index) in results" :key="index">
      {{ item }}
    </div>
  </div>
</spruce-sort>

Props

NameDescriptionTypeRequiredDefault
listThe list to searchArrayYes
directionThe direction to sort in, 'asc' or 'desc'StringNo'asc'
byThe object property to sort byStringNo''

Events

NameDescriptionPayload
changeFired when results change (the list is sorted).results

Slots

NameRequired
defaultYes

Slot Scope

SlotNameDescriptionType
defaultresultsThe searched listArray<String, Object>

SpruceState

Create and manage localized state.

<spruce-state :value="{ count: 0 }">
  <div slot-scope="{ count, set }">
    <button @click="set({count: count + 1})">
      Increment ({{ count }})
    </button>
  </div>
</spruce-state>

Props

NameDescriptionTypeRequiredDefault
valueThe state objectObjectYes

Events

NameDescriptionPayload
inputFired when state updatesstate

Slots

NameRequired
defaultYes

Slot Scope

SlotNameDescriptionType
default[key]Each key in the state propAny
defaultset(newValue)Merges newValue with the current stateFunction

SpruceTagInput

Renderless tag input.

<template>
  <spruce-tag-input v-model="colors" :validator="validator">
    <div
      slot-scope="{ events, focusedTagIndex, invalid, remove, state, tags }"
      :class="{ 'border-red-500': invalid }"
    >
      <button
        v-for="(tag, index) in tags"
        :key="index"
        type="button"
        :class="{ 'bg-red-500': focusedTagIndex === index }"
        title="Remove tag"
        @click="remove(tag)"
      >
        <span>{{ tag }}</span>
        <span>&times;</span>
      </button>
      <input v-bind="state" placeholder="Add tag with letters only..." v-on="events" />
    </div>
  </spruce-tag-input>
</template>

<script>
export default {
  data() {
    return { colors: ['red', 'blue'] };
  },

  methods: {
    validator(tag) {
      return /^[a-zA-Z]+$/.test(tag);
    },
  },
};
</script>

Props

NameDescriptionTypeRequiredDefault
allowDuplicatesAllows duplicate tagsBooleanNoFalse
allowPasteAllows pasting to automatically create tagsBooleanNoFalse
disabledDisables all interactionsBooleanNoFalse
keepOnBackspaceDisables deleting last tab on keyup.backspace in the inputBooleanNoFalse
maxTagsNumber of allowed tagsNumberNoNull
separatorSeparator used to process pasted tagsStringNo\t
v-modelThe tagsArrayYes
validatorFunction that receives the String argument tag and returns true or false to determine the validity of the input's valueFunctionNo() => true

Slot Scope

SlotNameDescriptionType
defaulteventsEvents to listen for on the input. input for binding value, keydown.backspace for delete last tag, keydown.enter for adding new unique tag, and keydown.escape for clearing input.Object
defaultfocusedTagIndexCurrently focused tag index (to be removed on next keydown.backspace). Used for styling.String
defaultremove()Removes a tagFunction
defaultstateState to bind to the input. value of the input.Object
defaulttagsArray of tagsArray

SpruceToggle

Toggle between on (true) and off (false).

<spruce-toggle :value="true">
  <div slot-scope="{ isOn, on, off, toggle }">
    <div>
      <span>Accordion header</span>
      <span @click="toggle">{{ isOn ? '▲' : '▼' }}</span>
    </div>
    <div v-if="isOn">
      Accordion content
    </div>
    <div>
      <button @click="on">Open</button>
      <button @click="off">Close</button>
    </div>
  </div>
</spruce-toggle>

Props

NameDescriptionTypeRequiredDefault
valueThe state of the toggleBooleanNoFalse

Events

NameDescriptionPayload
inputFired when isOn updatesisOn

Slots

NameRequired
defaultYes

Slot Scope

SlotNameDescriptionType
defaultisOnThe state of the toggleBoolean
defaulton()Sets isOn to trueFunction
defaultoff()Sets isOn to falseFunction
defaulttoggle()Toggles isOnFunction

SpruceWatch

Watches variables for changes and emits events when changes occur.

<spruce-watch :watch="{count}" @changed="handleAnyChange" @changed:count="handleCountChange">
  <button @click="count++">
    count++ ({{ count }})
  </button>
</spruce-watch>

Props

NameDescriptionTypeRequiredDefault
watchValues you wish to watchObjectYes

Events

NameDescriptionPayload
changedFired when any value in watch changes{key: count, oldValue: 0, newValue: 1}
changed:[key]Fired when a specific value in watch changes{oldValue: 0, newValue: 1}

Slots

NameRequired
defaultNo

Examples

See the demo source code for real-world examples. Check out the demo to see the components in action with code examples.

Development

Lint

yarn lint

Test

yarn test:unit

Build Dist

yarn build

Run Demo

yarn serve

Build Demo

yarn build:demo

How to Contribute

Pull Requests

  1. Fork the repository
  2. Create a new branch for each feature or improvement
  3. Send a pull request from each feature branch to the develop branch

License

MIT