@profiscience/knockout-contrib

KnockoutJS Goodies for building wicked awesome SPAs

Usage no npm install needed!

<script type="module">
  import profiscienceKnockoutContrib from 'https://cdn.skypack.dev/@profiscience/knockout-contrib';
</script>

README

@profiscience/knockout-contrib

KnockoutJS Build Status Coverage States Gitter

Metapackage containing all of the goodies from the @profiscience/knockout-contrib monorepo. Everything you need to start building SPAs with KnockoutJS. Some bits are more opinionated than others, use however much/little as makes sense for your project. Publishing everything together as a metapackage is intended to a) reduce typing and b) increase discoverability.

Installation / Usage

First, install the metapackage. This will install the latest version of each package in @profiscience/knockout-contrib.

yarn add @profiscience/knockout-contrib

Then somewhere in your app before you kick off ko.applyBindings(), add the following boilerplate code...

NOTE: Nothing will be added to the global scope, nor any Knockout registries modified simply by installing/importing this package. You must register any bindings, components, filters, or router middleware/plugins you wish to use. This allows you to use your own loading mechanism, naming schemes, etc. if you so choose, as well as keeping each package pure (exports only, no global scope modification), enabling tree-shake-ability with webpack, rollup, or the esm bundler du jor.

/**
 * Copy this to your project, and remove any bits you don't need/want.
 *
 * **Don't'** change the knockout-contrib import statement to an `import *`
 * if you remove anything, otherwise you will not be able to take advantage of
 * tree-shaking.
 */

import * as ko from 'knockout'
import 'knockout-punches'

import {
  altClickBindingHandler,
  ctrlClickBindingHandler,
  metaClickBindingHandler,
  shiftClickBindingHandler,
  draggableBindingHandler,
  jqueryBindingHandler,
  toggleBindingHandler,
  formateDateFilter,
  Route,
  Router,
  createScrollPositionMiddleware,
  childrenRoutePlugin,
  componentRoutePlugin,
  componentsRoutePlugin,
  componentInitializerRoutePlugin,
  redirectRoutePlugin,
  createtitleRoutePlugin,
  withRoutePlugin,
} from '@profiscience/knockout-contrib'

/**
 * BINDINGS
 */

ko.bindingHandlers['click.alt'] = altClickBindingHandler
ko.bindingHandlers['click.ctrl'] = ctrlClickBindingHandler
ko.bindingHandlers['click.meta'] = metaClickBindingHandler
ko.bindingHandlers['click.shift'] = shiftClickBindingHandler

ko.bindingHandlers.draggable = draggableBindingHandler

ko.bindingHandlers.jquery = jqueryBindingHandler
ko.bindingHandlers.$ = jqueryBindingHandler // alias

ko.bindingHandlers.toggle = toggleBindingHandler

/**
 * FILTERS
 */

// punches does not have type definitions...
const _ko: any = ko

// you may remove these 2 lines if you call ko.punches.enableAll() in your project
_ko.punches.textFilter.enableForBinding('text')
_ko.punches.textFilter.enableForBinding('attr')

_ko.filters['date.format'] = formatDateFilter

/**
 * ROUTING / "FRAMEWORK"
 */
Router.use(createScrollPositionMiddleware())

// NOTE: This _must_ be executed _before_ any routes are instantiated. If you're having issues,
// remember that ES imports are hoisted, so the file that registers these plugins _must_ be imported
// before are file that instatiates a route.
Route.usePlugin(
  withRoutePlugin, // should _probably_ be first (so addt'l values are available everywhere)
  redirectRoutePlugin, // should also _probably_ be towards the top of the list (to prevent unneccesary work)
  componentRoutePlugin,
  componentInitializerRoutePlugin, // **MUST** come after component plugin

  // for the rest of these, the order is irrelevant
  childrenRoutePlugin,
  componentsRoutePlugin,
  createtitleRoutePlugin(
    (titleSegments: string[]) => `My App | ${titleSegments.join(' > ')}`
  )
)

View the documentation for individual packages to see what they do / how to use them.

Using the ESNext build

This assumes you are using webpack. It is likely possible with most other modern bundlers, but that's on you to figure out. If you would like to contribute documentation, PRs are accepted.

The default build is transpiled to ES5 in order to be compatible in older browsers (*cough* IE).

However, if you only need to support modern runtimes, you can use the esnext build. This build has much less transpilation overhead and does not require any additional runtime libraries (e.g. babel-runtime). This can have a pretty decent impact on the size of your builds when used with babel-minify, and provides for a much nicer debugging experience should you find yourself digging through the stack somewhere in this library; transpiled code can be very difficult to reason about.

This library uses the proposed esnext package.json field.

Add esnext to resolve.mainFields in your webpack config to use this build. Additionally, you'll need to configure an additional rule so that this field is used transitively. See Webpack #6796.

e.g.

module.exports = {
  resolve: {
    mainFields: [
      'esnext', // es2017+esm (resolves to <package>/dist/esnext)

      // these are the default values
      'module', // es5+esm  (resolves to <package>/dist/default)
      'main', // es5+cjs  (resolves to <package>/dist/node)
    ],
  },

  module: {
    rules: [
      {
        // https://github.com/webpack/webpack/issues/6796
        test: path.resolve(__dirname, 'node_modules'),
        resolve: {
          mainFields: ['esnext', 'es2015', 'module', 'main'],
        },
      },
    ],
  },
}

TypeScript Caveats

For reasons that are mostly related to my own development comfort, TypeScript is configured to point at the source instead of built definitions. What this comes down to is allowing parallel transpilation, however there is also the very real restriction that TypeScript refuses to emit a declaration for a mixin that uses a private or protected property.

That said, using this library with TypeScript requires a few properties to be set in your tsconfig.json.

{
  "compilerOptions": {
    "jsx": "react",
    "jsxFactory": "h",
    "allowSyntheticDefaultImports": true,
    "lib": [
      "dom",
      "es5",
      "es2015",
      "esnext.asynciterable"
    ]
  }
}

You may omit jsx and jsxFactory if you are not using any component packages.