├── .circleci └── config.yml ├── .dependabot └── config.yml ├── .dockerignore ├── .github └── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── feature_request.md │ └── help.md ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── README.md ├── __mocks__ └── toprogress2.ts ├── __tests__ └── integration.ts ├── _config.yml ├── build.ts ├── build.worker.ts ├── lerna.json ├── package.json ├── packages ├── _ │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ └── package.json ├── bindings.click.alt │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.tsx ├── bindings.click.ctrl │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.tsx ├── bindings.click.meta │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.tsx ├── bindings.click.shift │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.tsx ├── bindings.draggable │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ └── package.json ├── bindings.infiniteScroll │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ └── package.json ├── bindings.jquery │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.tsx ├── bindings.repeat │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.tsx ├── bindings.toggle │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.tsx ├── component-loader │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts ├── components.flash-message │ ├── CHANGELOG.md │ ├── README.md │ ├── examples │ │ ├── index.html │ │ └── index.ts │ ├── index.tsx │ ├── package.json │ └── test.tsx ├── filters.date.format │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.tsx ├── jest-matchers │ ├── CHANGELOG.MD │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ └── observable.test.ts │ ├── package.json │ └── src │ │ ├── index.ts │ │ └── observable.ts ├── model.builders.base │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts ├── model.builders.data │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts ├── model.builders.view │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts ├── model.mixins.disposalAggregator │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts ├── model.mixins.pager │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts ├── model.mixins.query │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts ├── model.mixins.subscriptionDisposal │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts ├── model.mixins.transform │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts ├── query │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── utils.ts │ └── test.ts ├── router.middleware.flashMessage │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts ├── router.middleware.loading │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts ├── router.middleware.progressBar │ ├── CHANGELOG.md │ ├── README.md │ ├── examples │ │ ├── index.html │ │ └── index.ts │ ├── index.ts │ ├── package.json │ └── test.ts ├── router.middleware.scrollPosition │ ├── CHANGELOG.md │ ├── README.md │ ├── examples │ │ ├── index.html │ │ └── index.ts │ ├── index.ts │ ├── package.json │ └── test.ts ├── router.plugins.children │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts ├── router.plugins.component │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts ├── router.plugins.components │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts ├── router.plugins.init │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts ├── router.plugins.redirect │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts ├── router.plugins.title │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts ├── router.plugins.with │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts ├── router │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ ├── .eslintrc │ │ ├── anchor.js │ │ ├── basepath.js │ │ ├── before-navigate-callbacks.js │ │ ├── bindings │ │ │ ├── active-path.js │ │ │ ├── index.js │ │ │ └── path.js │ │ ├── element.js │ │ ├── force-update.js │ │ ├── hashbang.js │ │ ├── helpers │ │ │ └── ko-overwrite-component-registration.js │ │ ├── history.js │ │ ├── index.js │ │ ├── middleware.js │ │ ├── plugins.js │ │ ├── preserve-query.js │ │ ├── preserve-state.js │ │ ├── queue.js │ │ ├── redirect.js │ │ ├── routing │ │ │ ├── ambiguous.js │ │ │ ├── basic.js │ │ │ ├── index.js │ │ │ ├── init.js │ │ │ ├── nested.js │ │ │ ├── params.js │ │ │ ├── route-constructor.js │ │ │ └── similar.js │ │ └── with.js │ ├── docs │ │ ├── README.md │ │ ├── basic.md │ │ ├── best-practices.md │ │ ├── context.md │ │ ├── middleware.md │ │ ├── nested.md │ │ ├── path-binding.md │ │ ├── plugins.md │ │ ├── router.md │ │ ├── typescript.md │ │ └── utils.md │ ├── examples │ │ ├── index.html │ │ ├── index.ts │ │ ├── lazy-loading │ │ │ ├── index.html │ │ │ ├── index.ts │ │ │ └── views │ │ │ │ ├── bar │ │ │ │ ├── index.ts │ │ │ │ └── template.html │ │ │ │ ├── baz │ │ │ │ ├── index.ts │ │ │ │ └── template.html │ │ │ │ ├── foo │ │ │ │ ├── index.ts │ │ │ │ └── template.html │ │ │ │ ├── list │ │ │ │ ├── index.ts │ │ │ │ └── template.html │ │ │ │ └── qux │ │ │ │ ├── index.ts │ │ │ │ └── template.html │ │ ├── loading-animation │ │ │ ├── index.html │ │ │ ├── index.ts │ │ │ └── views │ │ │ │ ├── bar │ │ │ │ ├── index.ts │ │ │ │ └── template.html │ │ │ │ └── foo │ │ │ │ ├── index.ts │ │ │ │ └── template.html │ │ ├── path-binding │ │ │ ├── index.html │ │ │ └── index.ts │ │ ├── simple-auth │ │ │ ├── index.html │ │ │ └── index.ts │ │ ├── transition-animation │ │ │ ├── index.html │ │ │ └── index.ts │ │ └── yarn.lock │ ├── karma.conf.js │ ├── package.json │ └── src │ │ ├── bindings │ │ ├── active-path.ts │ │ ├── index.ts │ │ └── path.ts │ │ ├── component.ts │ │ ├── context.ts │ │ ├── index.ts │ │ ├── route.ts │ │ ├── router.ts │ │ └── utils.ts ├── utils.assign │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts ├── utils.defaults │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts ├── utils.fromJS │ ├── CHANGELOG.md │ ├── README.md │ ├── benchmark.ts │ ├── index.ts │ ├── package.json │ └── test.ts ├── utils.increment │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts ├── utils.modify │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts ├── utils.once │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts └── utils.toggle │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ └── test.ts ├── support └── xvfb_entrypoint.sh ├── tsconfig.json ├── tslint.json └── yarn.lock /.dependabot/config.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | update_configs: 3 | - package_manager: javascript 4 | directory: / 5 | update_schedule: live 6 | automerged_updates: 7 | - match: 8 | dependency_type: 'development' 9 | update_type: 'all' 10 | - match: 11 | dependency_type: 'production' 12 | update_type: 'semver:minor' 13 | version_requirement_updates: widen_ranges 14 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | .* 3 | 4 | !support/ 5 | !yarn.lock 6 | !package.json 7 | !packages/ 8 | !packages/*/package.json 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | **Package(s)** 7 | 8 | - [e.g. router, bindings.jquery, etc.] 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | Link to minimal example: (providing a minimal reproduction repo is not required but _highly encouraged_) 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - Browser [e.g. chrome, safari] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | **Package(s)** 7 | 8 | - [router, bindings.jquery, etc.] 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/help.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Help 3 | about: Get help using @profiscience/knockout-contrib 4 | --- 5 | 6 | > Please consider using the [gitter channel](https://gitter.im/Profiscience/knockout-contrib) instead! 7 | 8 | **Package(s)** 9 | 10 | - [router, bindings.jquery, etc.] 11 | 12 | **What I'm trying to do** 13 | 14 | Get this to do that. 15 | 16 | **What I've tried** 17 | 18 | ```typescript 19 | console.log('foo') 20 | ``` 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | *.log 4 | package-lock.json 5 | 6 | *.map 7 | *.css.d.ts 8 | 9 | .cache/ 10 | .chrome/ 11 | .nyc_output/ 12 | .vscode/ 13 | coverage/ 14 | dist/ 15 | node_modules/ 16 | 17 | packages/*/yarn.lock 18 | 19 | tasks/TASK_COUNT -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | RUN apk add --no-cache firefox-esr git xvfb 3 | WORKDIR /repo 4 | COPY . . 5 | RUN yarn install --pure-lockfile && rm -rf node_modules packages/*/node_modules 6 | ENTRYPOINT ["/repo/support/xvfb_entrypoint.sh"] 7 | CMD /bin/sh -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2017 Casey Webb (https://caseyWebb.xyz) 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | 12 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 13 | 14 | 0. You just DO WHAT THE FUCK YOU WANT TO. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @profiscience/knockout-contrib 2 | 3 | [![KnockoutJS][knockout-shield]][knockoutjs] 4 | [![Build Status][drone-ci-shield]][drone-ci] 5 | [![Coverage States][codecov-shield]][codecov] 6 | [![Gitter][gitter-shield]][gitter] 7 | [![Dependabot][dependabot-shield]][dependabot] 8 | 9 | Goodies for building modern SPAs and rich UIs with [KnockoutJS][knockoutjs] 10 | 11 | [Docs](./packages/_) 12 | 13 | [knockoutjs]: https://knockoutjs.com 14 | [knockout-shield]: https://img.shields.io/badge/KnockoutJS-3.5.0-red.svg 15 | [drone-ci]: https://ci.caseywebb.xyz/Profiscience/knockout-contrib/ 16 | [drone-ci-shield]: https://img.shields.io/drone/build/Profiscience/knockout-contrib?server=https%3A%2F%2Fci.caseywebb.xyz 17 | [codecov]: https://codecov.io/gh/Profiscience/knockout-contrib 18 | [codecov-shield]: https://img.shields.io/codecov/c/github/Profiscience/knockout-contrib.svg 19 | [gitter]: https://gitter.im/Profiscience/ko-component-router 20 | [gitter-shield]: https://img.shields.io/gitter/room/profiscience/ko-component-router.svg 21 | [dependabot-shield]: https://api.dependabot.com/badges/status?host=github&repo=Profiscience/knockout-contrib 22 | [dependabot]: https://dependabot.com 23 | -------------------------------------------------------------------------------- /__mocks__/toprogress2.ts: -------------------------------------------------------------------------------- 1 | export const start = jest.fn(() => Promise.resolve()) 2 | export const finish = jest.fn(() => Promise.resolve()) 3 | 4 | let _initializedWith: any 5 | export function initializedWith() { 6 | return _initializedWith 7 | } 8 | 9 | export function reset() { 10 | start.mockClear() 11 | start.mockImplementation(() => Promise.resolve()) 12 | finish.mockClear() 13 | finish.mockImplementation(() => Promise.resolve()) 14 | } 15 | 16 | export class ToProgress { 17 | public start = start 18 | public finish = finish 19 | constructor(opts: any) { 20 | _initializedWith = opts 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /__tests__/integration.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DataModelConstructorBuilder, 3 | ViewModelConstructorBuilder, 4 | } from '@profiscience/knockout-contrib' 5 | 6 | test("DisposalAggregatorMixin doesn't re-dispose deleted data model", async () => { 7 | const spy = jest.fn() 8 | 9 | class DataModel extends DataModelConstructorBuilder { 10 | public fetch = async () => ({}) 11 | public dispose() { 12 | spy() 13 | super.dispose() 14 | } 15 | } 16 | class ViewModel extends ViewModelConstructorBuilder { 17 | public data = new DataModel() 18 | } 19 | 20 | const vm = new ViewModel() 21 | 22 | await vm.data.delete() 23 | 24 | vm.dispose() 25 | 26 | expect(spy).toBeCalledTimes(1) 27 | }) 28 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal 2 | include: 3 | - packages/_ 4 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.4.0", 3 | "version": "independent", 4 | "npmClient": "yarn", 5 | "useWorkspaces": true, 6 | "command": { 7 | "publish": { 8 | "ignoreChanges": [ 9 | "**/docs/**", 10 | "**/examples/**", 11 | "**/__tests__/**", 12 | "**/*.test.ts", 13 | "**/*.test.tsx", 14 | "**/test.ts", 15 | "**/test.tsx", 16 | "**/*.md" 17 | ] 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/bindings.click.alt/README.md: -------------------------------------------------------------------------------- 1 | # bindings.click.alt 2 | 3 | [![Version][npm-version-shield]][npm] 4 | [![Dependency Status][david-dm-shield]][david-dm] 5 | [![Peer Dependency Status][david-dm-peer-shield]][david-dm-peer] 6 | [![Dev Dependency Status][david-dm-dev-shield]][david-dm-dev] 7 | [![Downloads][npm-stats-shield]][npm-stats] 8 | 9 | [david-dm]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/bindings.click.alt 10 | [david-dm-shield]: https://david-dm.org/Profiscience/knockout-contrib/status.svg?path=packages/bindings.click.alt 11 | [david-dm-peer]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/bindings.click.alt&type=peer 12 | [david-dm-peer-shield]: https://david-dm.org/Profiscience/knockout-contrib/peer-status.svg?path=packages/bindings.click.alt 13 | [david-dm-dev]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/bindings.click.alt&type=dev 14 | [david-dm-dev-shield]: https://david-dm.org/Profiscience/knockout-contrib/dev-status.svg?path=packages/bindings.click.alt 15 | [npm]: https://www.npmjs.com/package/@profiscience/knockout-contrib-bindings-click-alt 16 | [npm-version-shield]: https://img.shields.io/npm/v/@profiscience/knockout-contrib-bindings-click-alt.svg 17 | [npm-stats]: http://npm-stat.com/charts.html?package=@profiscience/knockout-contrib-bindings-click-alt&author=&from=&to= 18 | [npm-stats-shield]: https://img.shields.io/npm/dt/@profiscience/knockout-contrib-bindings-click-alt.svg?maxAge=2592000 19 | 20 | > This package is intended for consumption via the [@profiscience/knockout-contrib][] metapackage 21 | 22 | Built-in [click binding][], filtered for alt+click 23 | 24 | **NOTE:** Consider mobile users (where there is no access to alt) when using this binding 25 | 26 | ## Usage 27 | 28 | Accepts a function with the same API as the built-in [click-binding][] 29 | 30 | ```html 31 |
32 | ``` 33 | 34 | [@profiscience/knockout-contrib]: https://github.com/Profiscience/knockout-contrib 35 | [click binding]: https://knockoutjs.com/documentation/click-binding.html 36 | -------------------------------------------------------------------------------- /packages/bindings.click.alt/index.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | 3 | export const altClickBindingHandler: ko.BindingHandler = { 4 | init(el, valueAccessor, allBindings, viewModel, bindingContext) { 5 | ko.applyBindingsToNode( 6 | el, 7 | { 8 | click($data: any, e: MouseEvent) { 9 | if (e.altKey) valueAccessor().call(this, $data, e) 10 | }, 11 | }, 12 | bindingContext 13 | ) 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /packages/bindings.click.alt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-bindings-click-alt", 3 | "version": "2.0.2", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/bindings.click.alt", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "knockout": "3.5.1" 19 | }, 20 | "dependencies": { 21 | "babel-runtime": "^6.26.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/bindings.click.alt/test.tsx: -------------------------------------------------------------------------------- 1 | import { h } from 'jsx-dom' 2 | import * as ko from 'knockout' 3 | 4 | import { altClickBindingHandler } from './index' 5 | 6 | ko.bindingHandlers['click.alt'] = altClickBindingHandler 7 | 8 | const clickEvent = new Event('click') 9 | const altClickEvent = new Event('click') 10 | 11 | { 12 | ;(altClickEvent as any).altKey = true 13 | } 14 | 15 | describe('bindings.altClick', () => { 16 | test('calls handler only when alt depressed', () => { 17 | const actualEl = 18 | const handler = jest.fn() 19 | const context = { handler } 20 | ko.applyBindings(context, actualEl) 21 | 22 | actualEl.dispatchEvent(clickEvent) 23 | expect(handler).not.toBeCalled() 24 | 25 | actualEl.dispatchEvent(altClickEvent) 26 | expect(handler).toBeCalledWith(context, altClickEvent) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /packages/bindings.click.ctrl/README.md: -------------------------------------------------------------------------------- 1 | # bindings.click.ctrl 2 | 3 | [![Version][npm-version-shield]][npm] 4 | [![Dependency Status][david-dm-shield]][david-dm] 5 | [![Peer Dependency Status][david-dm-peer-shield]][david-dm-peer] 6 | [![Dev Dependency Status][david-dm-dev-shield]][david-dm-dev] 7 | [![Downloads][npm-stats-shield]][npm-stats] 8 | 9 | [david-dm]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/bindings.click.shift 10 | [david-dm-shield]: https://david-dm.org/Profiscience/knockout-contrib/status.svg?path=packages/bindings.click.shift 11 | [david-dm-peer]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/bindings.click.shift&type=peer 12 | [david-dm-peer-shield]: https://david-dm.org/Profiscience/knockout-contrib/peer-status.svg?path=packages/bindings.click.shift 13 | [david-dm-dev]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/bindings.click.shift&type=dev 14 | [david-dm-dev-shield]: https://david-dm.org/Profiscience/knockout-contrib/dev-status.svg?path=packages/bindings.click.shift 15 | [npm]: https://www.npmjs.com/package/@profiscience/knockout-contrib-bindings-click-shift 16 | [npm-version-shield]: https://img.shields.io/npm/v/@profiscience/knockout-contrib-bindings-click-shift.svg 17 | [npm-stats]: http://npm-stat.com/charts.html?package=@profiscience/knockout-contrib-bindings-click-shift&author=&from=&to= 18 | [npm-stats-shield]: https://img.shields.io/npm/dt/@profiscience/knockout-contrib-bindings-click-shift.svg?maxAge=2592000 19 | 20 | > This package is intended for consumption via the [@profiscience/knockout-contrib][] metapackage 21 | 22 | Built-in [click binding][], filtered for ctrl+click 23 | 24 | **NOTE:** Consider MacOS (where meta+click is more intuitive) and mobile users (where there is no access to ctrl) when using this binding 25 | 26 | ## Usage 27 | 28 | Accepts a function with the same API as the built-in [click-binding][] 29 | 30 | ```html 31 | 32 | ``` 33 | 34 | [@profiscience/knockout-contrib]: https://github.com/Profiscience/knockout-contrib 35 | [click binding]: https://knockoutjs.com/documentation/click-binding.html 36 | -------------------------------------------------------------------------------- /packages/bindings.click.ctrl/index.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | 3 | export const ctrlClickBindingHandler: ko.BindingHandler = { 4 | init(el, valueAccessor, allBindings, viewModel, bindingContext) { 5 | ko.applyBindingsToNode( 6 | el, 7 | { 8 | click($data: any, e: MouseEvent) { 9 | if (e.ctrlKey) valueAccessor().call(this, $data, e) 10 | }, 11 | }, 12 | bindingContext 13 | ) 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /packages/bindings.click.ctrl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-bindings-click-ctrl", 3 | "version": "2.0.2", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/bindings.click.ctrl", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "knockout": "3.5.1" 19 | }, 20 | "dependencies": { 21 | "babel-runtime": "^6.26.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/bindings.click.ctrl/test.tsx: -------------------------------------------------------------------------------- 1 | import { h } from 'jsx-dom' 2 | import * as ko from 'knockout' 3 | 4 | import { ctrlClickBindingHandler } from './index' 5 | 6 | ko.bindingHandlers['click.ctrl'] = ctrlClickBindingHandler 7 | 8 | const clickEvent = new Event('click') 9 | const ctrlClickEvent = new Event('click') 10 | 11 | { 12 | ;(ctrlClickEvent as any).ctrlKey = true 13 | } 14 | 15 | describe('bindings.ctrlClick', () => { 16 | test('calls handler only when ctrl depressed', () => { 17 | const actualEl = 18 | const handler = jest.fn() 19 | const context = { handler } 20 | ko.applyBindings(context, actualEl) 21 | 22 | actualEl.dispatchEvent(clickEvent) 23 | expect(handler).not.toBeCalled() 24 | 25 | actualEl.dispatchEvent(ctrlClickEvent) 26 | expect(handler).toBeCalledWith(context, ctrlClickEvent) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /packages/bindings.click.meta/README.md: -------------------------------------------------------------------------------- 1 | # bindings.click.meta 2 | 3 | [![Version][npm-version-shield]][npm] 4 | [![Dependency Status][david-dm-shield]][david-dm] 5 | [![Peer Dependency Status][david-dm-peer-shield]][david-dm-peer] 6 | [![Dev Dependency Status][david-dm-dev-shield]][david-dm-dev] 7 | [![Downloads][npm-stats-shield]][npm-stats] 8 | 9 | [david-dm]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/bindings.click.meta 10 | [david-dm-shield]: https://david-dm.org/Profiscience/knockout-contrib/status.svg?path=packages/bindings.click.meta 11 | [david-dm-peer]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/bindings.click.meta&type=peer 12 | [david-dm-peer-shield]: https://david-dm.org/Profiscience/knockout-contrib/peer-status.svg?path=packages/bindings.click.meta 13 | [david-dm-dev]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/bindings.click.meta&type=dev 14 | [david-dm-dev-shield]: https://david-dm.org/Profiscience/knockout-contrib/dev-status.svg?path=packages/bindings.click.meta 15 | [npm]: https://www.npmjs.com/package/@profiscience/knockout-contrib-bindings-click-meta 16 | [npm-version-shield]: https://img.shields.io/npm/v/@profiscience/knockout-contrib-bindings-click-meta.svg 17 | [npm-stats]: http://npm-stat.com/charts.html?package=@profiscience/knockout-contrib-bindings-click-meta&author=&from=&to= 18 | [npm-stats-shield]: https://img.shields.io/npm/dt/@profiscience/knockout-contrib-bindings-click-meta.svg?maxAge=2592000 19 | 20 | > This package is intended for consumption via the [@profiscience/knockout-contrib][] metapackage 21 | 22 | Built-in [click binding][], filtered for meta+click 23 | 24 | **NOTE:** Consider mobile users (where there is no access to meta) when using this binding 25 | 26 | ## Usage 27 | 28 | Accepts a function with the same API as the built-in [click-binding][] 29 | 30 | ```html 31 | 32 | ``` 33 | 34 | [@profiscience/knockout-contrib]: https://github.com/Profiscience/knockout-contrib 35 | [click binding]: https://knockoutjs.com/documentation/click-binding.html 36 | -------------------------------------------------------------------------------- /packages/bindings.click.meta/index.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | 3 | export const metaClickBindingHandler: ko.BindingHandler = { 4 | init(el, valueAccessor, allBindings, viewModel, bindingContext) { 5 | ko.applyBindingsToNode( 6 | el, 7 | { 8 | click($data: any, e: MouseEvent) { 9 | if (e.metaKey) valueAccessor().call(this, $data, e) 10 | }, 11 | }, 12 | bindingContext 13 | ) 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /packages/bindings.click.meta/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-bindings-click-meta", 3 | "version": "2.0.2", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/bindings.click.meta", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "knockout": "3.5.1" 19 | }, 20 | "dependencies": { 21 | "babel-runtime": "^6.26.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/bindings.click.meta/test.tsx: -------------------------------------------------------------------------------- 1 | import { h } from 'jsx-dom' 2 | import * as ko from 'knockout' 3 | 4 | import { metaClickBindingHandler } from './index' 5 | 6 | ko.bindingHandlers['click.meta'] = metaClickBindingHandler 7 | 8 | const clickEvent = new Event('click') 9 | const metaClickEvent = new Event('click') 10 | 11 | { 12 | ;(metaClickEvent as any).metaKey = true 13 | } 14 | 15 | describe('bindings.metaClick', () => { 16 | test('calls handler only when meta depressed', () => { 17 | const actualEl = 18 | const handler = jest.fn() 19 | const context = { handler } 20 | ko.applyBindings(context, actualEl) 21 | 22 | actualEl.dispatchEvent(clickEvent) 23 | expect(handler).not.toBeCalled() 24 | 25 | actualEl.dispatchEvent(metaClickEvent) 26 | expect(handler).toBeCalledWith(context, metaClickEvent) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /packages/bindings.click.shift/README.md: -------------------------------------------------------------------------------- 1 | # bindings.click.shift 2 | 3 | [![Version][npm-version-shield]][npm] 4 | [![Dependency Status][david-dm-shield]][david-dm] 5 | [![Peer Dependency Status][david-dm-peer-shield]][david-dm-peer] 6 | [![Dev Dependency Status][david-dm-dev-shield]][david-dm-dev] 7 | [![Downloads][npm-stats-shield]][npm-stats] 8 | 9 | [david-dm]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/bindings.click.shift 10 | [david-dm-shield]: https://david-dm.org/Profiscience/knockout-contrib/status.svg?path=packages/bindings.click.shift 11 | [david-dm-peer]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/bindings.click.shift&type=peer 12 | [david-dm-peer-shield]: https://david-dm.org/Profiscience/knockout-contrib/peer-status.svg?path=packages/bindings.click.shift 13 | [david-dm-dev]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/bindings.click.shift&type=dev 14 | [david-dm-dev-shield]: https://david-dm.org/Profiscience/knockout-contrib/dev-status.svg?path=packages/bindings.click.shift 15 | [npm]: https://www.npmjs.com/package/@profiscience/knockout-contrib-bindings-click-shift 16 | [npm-version-shield]: https://img.shields.io/npm/v/@profiscience/knockout-contrib-bindings-click-shift.svg 17 | [npm-stats]: http://npm-stat.com/charts.html?package=@profiscience/knockout-contrib-bindings-click-shift&author=&from=&to= 18 | [npm-stats-shield]: https://img.shields.io/npm/dt/@profiscience/knockout-contrib-bindings-click-shift.svg?maxAge=2592000 19 | 20 | > This package is intended for consumption via the [@profiscience/knockout-contrib][] metapackage 21 | 22 | Built-in [click binding][], filtered for shift+click 23 | 24 | **NOTE:** Consider mobile users (where there is no access to shift) when using this binding 25 | 26 | ## Usage 27 | 28 | Accepts a function with the same API as the built-in [click-binding][] 29 | 30 | ```html 31 | 32 | ``` 33 | 34 | [@profiscience/knockout-contrib]: https://github.com/Profiscience/knockout-contrib 35 | [click binding]: https://knockoutjs.com/documentation/click-binding.html 36 | -------------------------------------------------------------------------------- /packages/bindings.click.shift/index.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | 3 | export const shiftClickBindingHandler: ko.BindingHandler = { 4 | init(el, valueAccessor, allBindings, viewModel, bindingContext) { 5 | ko.applyBindingsToNode( 6 | el, 7 | { 8 | click($data: any, e: MouseEvent) { 9 | if (e.shiftKey) valueAccessor().call(this, $data, e) 10 | }, 11 | }, 12 | bindingContext 13 | ) 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /packages/bindings.click.shift/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-bindings-click-shift", 3 | "version": "2.0.2", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/bindings.click.shift", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "knockout": "3.5.1" 19 | }, 20 | "dependencies": { 21 | "babel-runtime": "^6.26.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/bindings.click.shift/test.tsx: -------------------------------------------------------------------------------- 1 | import { h } from 'jsx-dom' 2 | import * as ko from 'knockout' 3 | 4 | import { shiftClickBindingHandler } from './index' 5 | 6 | ko.bindingHandlers['click.shift'] = shiftClickBindingHandler 7 | 8 | const clickEvent = new Event('click') 9 | const shiftClickEvent = new Event('click') 10 | 11 | { 12 | ;(shiftClickEvent as any).shiftKey = true 13 | } 14 | 15 | describe('bindings.shiftClick', () => { 16 | test('calls handler only when shift depressed', () => { 17 | const actualEl = 18 | const handler = jest.fn() 19 | const context = { handler } 20 | ko.applyBindings(context, actualEl) 21 | 22 | actualEl.dispatchEvent(clickEvent) 23 | expect(handler).not.toBeCalled() 24 | 25 | actualEl.dispatchEvent(shiftClickEvent) 26 | expect(handler).toBeCalledWith(context, shiftClickEvent) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /packages/bindings.draggable/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [1.1.2](https://github.com/Profiscience/knockout-contrib/compare/@profiscience/knockout-contrib-bindings-draggable@1.1.1...@profiscience/knockout-contrib-bindings-draggable@1.1.2) (2020-10-09) 7 | 8 | **Note:** Version bump only for package @profiscience/knockout-contrib-bindings-draggable 9 | 10 | 11 | 12 | 13 | 14 | ## [1.1.1](https://github.com/Profiscience/knockout-contrib/compare/@profiscience/knockout-contrib-bindings-draggable@1.1.0...@profiscience/knockout-contrib-bindings-draggable@1.1.1) (2019-12-12) 15 | 16 | **Note:** Version bump only for package @profiscience/knockout-contrib-bindings-draggable 17 | 18 | # 1.1.0 (2019-06-13) 19 | 20 | ### Features 21 | 22 | - **bindings.draggable:** init :tada: ([3c4b432](https://github.com/Profiscience/knockout-contrib/commit/3c4b432)) 23 | -------------------------------------------------------------------------------- /packages/bindings.draggable/README.md: -------------------------------------------------------------------------------- 1 | # bindings.draggable 2 | 3 | [![Version][npm-version-shield]][npm] 4 | [![Dependency Status][david-dm-shield]][david-dm] 5 | [![Peer Dependency Status][david-dm-peer-shield]][david-dm-peer] 6 | [![Dev Dependency Status][david-dm-dev-shield]][david-dm-dev] 7 | [![Downloads][npm-stats-shield]][npm-stats] 8 | 9 | [david-dm]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/bindings.draggable 10 | [david-dm-shield]: https://david-dm.org/Profiscience/knockout-contrib/status.svg?path=packages/bindings.draggable 11 | [david-dm-peer]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/bindings.draggable&type=peer 12 | [david-dm-peer-shield]: https://david-dm.org/Profiscience/knockout-contrib/peer-status.svg?path=packages/bindings.draggable 13 | [david-dm-dev]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/bindings.draggable&type=dev 14 | [david-dm-dev-shield]: https://david-dm.org/Profiscience/knockout-contrib/dev-status.svg?path=packages/bindings.draggable 15 | [npm]: https://www.npmjs.com/package/@profiscience/knockout-contrib-bindings-draggable 16 | [npm-version-shield]: https://img.shields.io/npm/v/@profiscience/knockout-contrib-bindings-draggable.svg 17 | [npm-stats]: http://npm-stat.com/charts.html?package=@profiscience/knockout-contrib-bindings-draggable&author=&from=&to= 18 | [npm-stats-shield]: https://img.shields.io/npm/dt/@profiscience/knockout-contrib-bindings-draggable.svg?maxAge=2592000 19 | 20 | > This package is intended for consumption via the [@profiscience/knockout-contrib][] metapackage 21 | 22 | Drag and drop list re-ordering 23 | 24 | ## Usage 25 | 26 | ```html 27 |flashMessage
has been attached to the window. Change its value to
10 | modify the component.
11 |
12 | (opts: Q, group?: string) { 12 | return < 13 | P extends IQuery, 14 | T extends new (...args: any[]) => DataModelConstructorBuilder15 | >( 16 | ctor: T 17 | ) => 18 | class extends ctor { 19 | public query!: Query & IQuery
20 | 21 | public async init() { 22 | if (!this.query) { 23 | const query = Query.create(opts, group) 24 | this.query = query 25 | nonenumerable(this, 'query') 26 | Object.assign(this.params, query) 27 | } 28 | return super.init() 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/model.mixins.query/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-model-mixins-query", 3 | "version": "1.1.21", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/model.mixins.query", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "@profiscience/knockout-contrib-model-builders-data": ">=2.0.0", 19 | "@profiscience/knockout-contrib-query": ">=2.0.0", 20 | "knockout": "3.5.1" 21 | }, 22 | "dependencies": { 23 | "babel-runtime": "^6.26.0" 24 | }, 25 | "devDependencies": { 26 | "@profiscience/knockout-contrib-model-builders-data": "^2.3.11", 27 | "@profiscience/knockout-contrib-query": "^2.1.8" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/model.mixins.subscriptionDisposal/README.md: -------------------------------------------------------------------------------- 1 | # @profiscience/knockout-contrib-model-mixins-subscription-disposal 2 | 3 | @TODO 4 | 5 | > In the meantime, see the [tests](./test.ts) and [example usage](https://github.com/caseyWebb/knockout-realworld). 6 | -------------------------------------------------------------------------------- /packages/model.mixins.subscriptionDisposal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-model-mixins-subscription-disposal", 3 | "version": "2.1.0", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/model.mixins.subscriptionDisposal", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "knockout": "3.5.1" 19 | }, 20 | "dependencies": { 21 | "@profiscience/knockout-contrib-model-builders-base": "^2.0.1", 22 | "babel-runtime": "^6.26.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/model.mixins.transform/README.md: -------------------------------------------------------------------------------- 1 | # @profiscience/knockout-contrib-model-mixins-transform 2 | 3 | @TODO 4 | 5 | > In the meantime, see the [tests](./test.ts) and [example usage](https://github.com/caseyWebb/knockout-realworld). 6 | -------------------------------------------------------------------------------- /packages/model.mixins.transform/index.ts: -------------------------------------------------------------------------------- 1 | import { DataModelConstructorBuilder } from '@profiscience/knockout-contrib-model-builders-data' 2 | 3 | export function TransformMixin< 4 | TParams extends void | Record, 5 | TData extends Record 6 | >(transform: (fetchData: TData, params: TParams) => any) { 7 | return < 8 | T extends new (...args: any[]) => DataModelConstructorBuilder< 9 | TParams, 10 | TData 11 | > 12 | >( 13 | ctor: T 14 | ) => 15 | class extends ctor { 16 | protected async fetch(initData?: TData) { 17 | return transform(await super.fetch(initData), this.params) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/model.mixins.transform/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-model-mixins-transform", 3 | "version": "2.1.11", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/model.mixins.transform", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "@profiscience/knockout-contrib-model-builders-data": ">=2.0.0", 19 | "knockout": "3.5.1" 20 | }, 21 | "dependencies": { 22 | "babel-runtime": "^6.26.0" 23 | }, 24 | "devDependencies": { 25 | "@profiscience/knockout-contrib-model-builders-data": "^2.3.11" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/model.mixins.transform/test.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable max-classes-per-file 2 | 3 | import * as ko from 'knockout' 4 | import { DataModelConstructorBuilder } from '@profiscience/knockout-contrib-model-builders-data' 5 | 6 | import { TransformMixin } from './index' 7 | 8 | const FOOS = ['foo', 'bar', 'baz', 'qux'] 9 | 10 | function reverse(arr: any[]) { 11 | // Array.prototype.reverse() modifies arrays in place b/c JS is stupid, 12 | // so clone and reverse the clone 13 | const tmp = [...arr] 14 | tmp.reverse() 15 | return tmp 16 | } 17 | 18 | function FoosMixin< 19 | P extends { page: number }, 20 | T extends new (...args: any[]) => DataModelConstructorBuilder 21 | >(ctor: T) { 22 | return class extends ctor { 23 | public foos = ko.observableArray() 24 | protected async fetch(): Promise
{ 25 | return { 26 | foos: FOOS, 27 | } 28 | } 29 | } 30 | } 31 | describe('model.mixins.transform', () => { 32 | test('transforms the response from mixed in fetch', async () => { 33 | const reverseFoos = (obj: any) => ({ ...obj, foos: reverse(obj.foos) }) 34 | const ReverseFoosMixin = TransformMixin(reverseFoos) // tslint:disable-line variable-name 35 | class DataModel extends DataModelConstructorBuilder.Mixin( 36 | FoosMixin 37 | ).Mixin(ReverseFoosMixin)
{} 38 | 39 | const model = await DataModel.create({}) 40 | 41 | expect(ko.toJS(model.foos())).toEqual(reverse(FOOS)) 42 | 43 | model.dispose() 44 | }) 45 | 46 | test('transforms initData', async () => { 47 | const reverseFoos = (obj: any) => ({ ...obj, foos: reverse(obj.foos) }) 48 | const ReverseFoosMixin = TransformMixin(reverseFoos) // tslint:disable-line variable-name 49 | class DataModel
extends DataModelConstructorBuilder.Mixin( 50 | ReverseFoosMixin 51 | )
{ 52 | public foos = ko.observableArray() 53 | } 54 | 55 | const model = await DataModel.create({}, { foos: FOOS }) 56 | 57 | expect(ko.toJS(model.foos())).toEqual(reverse(FOOS)) 58 | 59 | model.dispose() 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /packages/query/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-query", 3 | "version": "2.1.8", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/query", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "src/index.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "knockout": "3.5.1" 19 | }, 20 | "dependencies": { 21 | "babel-runtime": "^6.26.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/query/src/utils.ts: -------------------------------------------------------------------------------- 1 | export type Primitive = string | boolean | number | null | undefined 2 | export type MaybeArray
= T | T[] 3 | 4 | export function isBool(x: any) { 5 | return typeof x === 'boolean' 6 | } 7 | 8 | export function isEmpty(x: any) { 9 | return x.length === 0 10 | } 11 | 12 | export function isNumber(x: any) { 13 | return !isNaN(parseFloat(x)) 14 | } 15 | 16 | export function isUndefined(x: any) { 17 | return typeof x === 'undefined' 18 | } 19 | 20 | export function entries(obj: { [k: string]: any }) { 21 | return Object.keys(obj).map((k) => [k, obj[k]]) 22 | } 23 | 24 | export function omit( 25 | obj: { [k: string]: any }, 26 | fn: (x: any) => boolean | void 27 | ) { 28 | const ret = {} as { [k: string]: any } 29 | for (const [k, v] of entries(obj)) { 30 | if (!fn(v)) { 31 | ret[k] = v 32 | } 33 | } 34 | return ret 35 | } 36 | -------------------------------------------------------------------------------- /packages/router.middleware.flashMessage/index.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | import { LifecycleMiddleware } from '@profiscience/knockout-contrib-router' 3 | 4 | export const FLASH_MESSAGE = Symbol('FLASH_MESSAGE') 5 | 6 | // tslint:disable-next-line no-empty-interface 7 | export interface IFlashMessage {} 8 | 9 | declare module '@profiscience/knockout-contrib-router' { 10 | // tslint:disable-next-line no-shadowed-variable 11 | interface IContext { 12 | [FLASH_MESSAGE]?: boolean | string | IFlashMessage 13 | } 14 | } 15 | 16 | export const flashMessage: ko.Observable< 17 | boolean | string | IFlashMessage | undefined 18 | > = ko.observable(false) 19 | 20 | export const flashMessageMiddleware: LifecycleMiddleware = (ctx) => ({ 21 | afterRender() { 22 | if (ctx[FLASH_MESSAGE]) flashMessage(ctx[FLASH_MESSAGE]) 23 | }, 24 | afterDispose() { 25 | flashMessage(false) 26 | }, 27 | }) 28 | -------------------------------------------------------------------------------- /packages/router.middleware.flashMessage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-router-middleware-flash-message", 3 | "version": "2.0.17", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/router.middleware.flashMessage", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "@profiscience/knockout-contrib-router": ">=2.0.0", 19 | "knockout": "3.5.1" 20 | }, 21 | "devDependencies": { 22 | "@profiscience/knockout-contrib-router": "^2.1.5" 23 | }, 24 | "dependencies": { 25 | "babel-runtime": "^6.26.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/router.middleware.flashMessage/test.ts: -------------------------------------------------------------------------------- 1 | import { Context, IContext } from '@profiscience/knockout-contrib-router' 2 | import { 3 | IFlashMessage, 4 | FLASH_MESSAGE, 5 | flashMessageMiddleware, 6 | flashMessage, 7 | } from './index' 8 | 9 | declare module './index' { 10 | // tslint:disable-next-line no-shadowed-variable 11 | interface IFlashMessage { 12 | text: string 13 | } 14 | } 15 | 16 | describe('router.middleware.flashMessage', () => { 17 | test('sets flashMessage after render', () => { 18 | const expected = 'This is a flash message' 19 | const ctx: Context & IContext = { [FLASH_MESSAGE]: expected } as Context & 20 | IContext 21 | const lifecycle = flashMessageMiddleware(ctx) as any 22 | 23 | expect(lifecycle.beforeRender).toBeUndefined() 24 | expect(flashMessage()).toBe(false) 25 | 26 | lifecycle.afterRender() 27 | expect(flashMessage()).toBe(expected) 28 | 29 | expect(lifecycle.beforeDispose).toBeUndefined() 30 | expect(flashMessage()).toBe(expected) 31 | 32 | lifecycle.afterDispose() 33 | expect(flashMessage()).toBe(false) 34 | }) 35 | 36 | test('works with custom flash message format', () => { 37 | const expected: IFlashMessage = { text: 'This is a flash message' } 38 | const ctx: Context & IContext = { [FLASH_MESSAGE]: expected } as Context & 39 | IContext 40 | const lifecycle = flashMessageMiddleware(ctx) as any 41 | 42 | lifecycle.afterRender() 43 | expect(flashMessage()).toEqual(expected) 44 | 45 | lifecycle.afterDispose() 46 | expect(flashMessage()).toBe(false) 47 | }) 48 | 49 | test("doesn't blow up when not used", () => { 50 | const ctx = {} as Context & IContext 51 | const lifecycle = flashMessageMiddleware(ctx) as any 52 | 53 | expect(() => { 54 | lifecycle.afterRender() 55 | lifecycle.afterDispose() 56 | }).not.toThrow() 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /packages/router.middleware.loading/README.md: -------------------------------------------------------------------------------- 1 | # router.middleware.loading 2 | 3 | [![Version][npm-version-shield]][npm] 4 | [![Dependency Status][david-dm-shield]][david-dm] 5 | [![Peer Dependency Status][david-dm-peer-shield]][david-dm-peer] 6 | [![Dev Dependency Status][david-dm-dev-shield]][david-dm-dev] 7 | [![Downloads][npm-stats-shield]][npm-stats] 8 | 9 | [david-dm]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/router.middleware.loading 10 | [david-dm-shield]: https://david-dm.org/Profiscience/knockout-contrib/status.svg?path=packages/router.middleware.loading 11 | [david-dm-peer]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/router.middleware.loading&type=peer 12 | [david-dm-peer-shield]: https://david-dm.org/Profiscience/knockout-contrib/peer-status.svg?path=packages/router.middleware.loading 13 | [david-dm-dev]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/router.middleware.loading&type=dev 14 | [david-dm-dev-shield]: https://david-dm.org/Profiscience/knockout-contrib/dev-status.svg?path=packages/router.middleware.loading 15 | [npm]: https://www.npmjs.com/package/@profiscience/knockout-contrib-router-middleware-loading 16 | [npm-version-shield]: https://img.shields.io/npm/v/@profiscience/knockout-contrib-router-middleware-loading.svg 17 | [npm-stats]: http://npm-stat.com/charts.html?package=@profiscience/knockout-contrib-router-middleware-loading&author=&from=&to= 18 | [npm-stats-shield]: https://img.shields.io/npm/dt/@profiscience/knockout-contrib-router-middleware-loading.svg?maxAge=2592000 19 | 20 | > **NOTE:** This package is intended for consumption via the [@profiscience/knockout-contrib metapackage](../_) 21 | 22 | Eases creation of loading middlware 23 | 24 | ## Usage 25 | 26 | ```typescript 27 | import { Router, createLoadingMiddleware } from '@profiscience/knockout-contrib' 28 | 29 | Router.use( 30 | createLoadingMiddleware({ 31 | start() { 32 | showLoader() 33 | }, 34 | end() { 35 | hideLoader() 36 | }, 37 | 38 | /* OPTIONAL */ 39 | minDuration: 0, // prevent flickering on fast navigation (show loader for at least ms) 40 | }) 41 | ) 42 | ``` 43 | -------------------------------------------------------------------------------- /packages/router.middleware.loading/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Context, 3 | IContext, 4 | LifecycleMiddleware, 5 | } from '@profiscience/knockout-contrib-router' 6 | 7 | let startTime = Date.now() 8 | 9 | export function createLoadingMiddleware({ 10 | start, 11 | end, 12 | minDuration, 13 | }: { 14 | start: (ctx: Context & IContext) => any 15 | end: (ctx: Context & IContext) => any 16 | minDuration?: number 17 | }): LifecycleMiddleware { 18 | let i = 0 19 | return (ctx: Context & IContext) => ({ 20 | beforeRender() { 21 | if (i++ === 0) { 22 | startTime = Date.now() 23 | start(ctx) 24 | } 25 | }, 26 | afterRender() { 27 | if (--i === 0) { 28 | if (minDuration) { 29 | const elapsed = Date.now() - startTime 30 | const delay = minDuration - elapsed 31 | // negative delay will be called immediately 32 | setTimeout(() => end(ctx), delay) 33 | } else { 34 | end(ctx) 35 | } 36 | } 37 | }, 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /packages/router.middleware.loading/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-router-middleware-loading", 3 | "version": "1.0.18", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/router.middleware.loading", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "@profiscience/knockout-contrib-router": ">=2.0.0", 19 | "knockout": "3.5.1" 20 | }, 21 | "dependencies": { 22 | "babel-runtime": "^6.26.0" 23 | }, 24 | "devDependencies": { 25 | "@profiscience/knockout-contrib-router": "^2.1.5" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/router.middleware.progressBar/README.md: -------------------------------------------------------------------------------- 1 | # router.middleware.progressBar 2 | 3 | [![Version][npm-version-shield]][npm] 4 | [![Dependency Status][david-dm-shield]][david-dm] 5 | [![Peer Dependency Status][david-dm-peer-shield]][david-dm-peer] 6 | [![Dev Dependency Status][david-dm-dev-shield]][david-dm-dev] 7 | [![Downloads][npm-stats-shield]][npm-stats] 8 | 9 | [david-dm]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/router.middleware.progressBar 10 | [david-dm-shield]: https://david-dm.org/Profiscience/knockout-contrib/status.svg?path=packages/router.middleware.progressBar 11 | [david-dm-peer]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/router.middleware.progressBar&type=peer 12 | [david-dm-peer-shield]: https://david-dm.org/Profiscience/knockout-contrib/peer-status.svg?path=packages/router.middleware.progressBar 13 | [david-dm-dev]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/router.middleware.progressBar&type=dev 14 | [david-dm-dev-shield]: https://david-dm.org/Profiscience/knockout-contrib/dev-status.svg?path=packages/router.middleware.progressBar 15 | [npm]: https://www.npmjs.com/package/@profiscience/knockout-contrib-router-middleware-progress-bar 16 | [npm-version-shield]: https://img.shields.io/npm/v/@profiscience/knockout-contrib-router-middleware-progress-bar.svg 17 | [npm-stats]: http://npm-stat.com/charts.html?package=@profiscience/knockout-contrib-router-middleware-progress-bar&author=&from=&to= 18 | [npm-stats-shield]: https://img.shields.io/npm/dt/@profiscience/knockout-contrib-router-middleware-progress-bar.svg?maxAge=2592000 19 | 20 | > **NOTE:** This package is intended for consumption via the [@profiscience/knockout-contrib metapackage](../_) 21 | 22 | Displays a progress bar using [toprogress2][] during navigation. Accepts all toprogress2 options. 23 | 24 | ## Usage 25 | 26 | ```typescript 27 | import { 28 | Router, 29 | createProgressBarMiddleware, 30 | } from '@profiscience/knockout-contrib' 31 | 32 | Router.use( 33 | createProgressBarMiddleware({ 34 | color: '#fff', 35 | height: '5px', 36 | // ... any toprogress2 option 37 | }) 38 | ) 39 | ``` 40 | -------------------------------------------------------------------------------- /packages/router.middleware.progressBar/examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/router.middleware.progressBar/examples/index.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | import { Router, Route } from '@profiscience/knockout-contrib-router' 3 | import { createProgressBarMiddleware } from '@profiscience/knockout-contrib-router-middleware' 4 | 5 | declare module '@profiscience/knockout-contrib-router' { 6 | interface IRouteConfig { 7 | delay?: number 8 | } 9 | } 10 | 11 | Router.setConfig({ 12 | base: '/router.middleware.progressBar', 13 | hashbang: true, 14 | }) 15 | 16 | ko.components.register('view', { 17 | template: ` 18 | Instant 19 | 200ms 20 | 400ms 21 | 2s 22 | 5s 23 | 10s 24 | `, 25 | }) 26 | 27 | // Use `view` component for all routes 28 | Route.usePlugin(() => 'view') 29 | 30 | // for simulating ajax, indexeddb, etc. delay 31 | Route.usePlugin(({ delay }) => () => 32 | new Promise((resolve) => setTimeout(resolve, delay)) 33 | ) 34 | 35 | Router.use(createProgressBarMiddleware()) 36 | 37 | Router.useRoutes([ 38 | new Route('/', 'view'), 39 | new Route('/200ms', { delay: 200 }), 40 | new Route('/400ms', { delay: 400 }), 41 | new Route('/2s', { delay: 2000 }), 42 | new Route('/5s', { delay: 5000 }), 43 | new Route('/10s', { delay: 10000 }), 44 | ]) 45 | 46 | ko.applyBindings() 47 | -------------------------------------------------------------------------------- /packages/router.middleware.progressBar/index.ts: -------------------------------------------------------------------------------- 1 | import { ToProgress, ToProgressOptions } from 'toprogress2' 2 | import { LifecycleMiddleware } from '@profiscience/knockout-contrib-router' 3 | import { createLoadingMiddleware } from '@profiscience/knockout-contrib-router-middleware-loading' 4 | 5 | export function createProgressBarMiddleware( 6 | opts?: ToProgressOptions 7 | ): LifecycleMiddleware { 8 | const progressBar = new ToProgress(opts) 9 | 10 | return createLoadingMiddleware({ 11 | start: () => 12 | progressBar.start().catch(() => { 13 | /* noop */ 14 | }), 15 | end: () => 16 | progressBar.finish().catch(() => { 17 | /* noop */ 18 | }), 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /packages/router.middleware.progressBar/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-router-middleware-progress-bar", 3 | "version": "2.0.17", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/router.middleware.progressBar", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "@profiscience/knockout-contrib-router": ">=2.0.0", 19 | "knockout": "3.5.1" 20 | }, 21 | "dependencies": { 22 | "@profiscience/knockout-contrib-router-middleware-loading": "^1.0.18", 23 | "babel-runtime": "^6.26.0", 24 | "toprogress2": "^2.0.0" 25 | }, 26 | "devDependencies": { 27 | "@profiscience/knockout-contrib-router": "^2.1.5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/router.middleware.progressBar/test.ts: -------------------------------------------------------------------------------- 1 | import * as ToProgress from 'toprogress2' 2 | import { Context, IContext } from '@profiscience/knockout-contrib-router' 3 | import { createProgressBarMiddleware } from './index' 4 | 5 | beforeEach(() => { 6 | ;(ToProgress as any).reset() 7 | }) 8 | 9 | describe('router.middleware.progressBar', () => { 10 | test('passes options to toprogress2', () => { 11 | const ctx: Context & IContext = { router: {} } as Context & IContext 12 | const opts = { color: '#fff' } 13 | const lifecycle = createProgressBarMiddleware(opts)(ctx) as any 14 | lifecycle.beforeRender() 15 | expect((ToProgress as any).initializedWith()).toBe(opts) 16 | }) 17 | test('starts progress bar before render at most once', () => { 18 | const topCtx: Context & IContext = { 19 | $child: {}, 20 | router: { 21 | isRoot: true, 22 | }, 23 | } as Context & IContext 24 | const bottomCtx: Context & IContext = { 25 | router: { 26 | isRoot: false, 27 | }, 28 | $child: undefined, 29 | } as any 30 | const middleware = createProgressBarMiddleware() 31 | const topLifecycle = middleware(topCtx) as any 32 | const bottomLifecycle = middleware(bottomCtx) as any 33 | topLifecycle.beforeRender() 34 | bottomLifecycle.beforeRender() 35 | expect((ToProgress as any).start).toHaveBeenCalledTimes(1) 36 | }) 37 | test('starts progress bar after render at most once', () => { 38 | const topCtx: Context & IContext = { 39 | $child: {}, 40 | router: { 41 | isRoot: true, 42 | }, 43 | } as Context & IContext 44 | const bottomCtx: Context & IContext = { 45 | router: { 46 | isRoot: false, 47 | }, 48 | $child: undefined, 49 | } as any 50 | const middleware = createProgressBarMiddleware() 51 | const topLifecycle = middleware(topCtx) as any 52 | const bottomLifecycle = middleware(bottomCtx) as any 53 | topLifecycle.beforeRender() 54 | bottomLifecycle.beforeRender() 55 | topLifecycle.afterRender() 56 | bottomLifecycle.afterRender() 57 | expect((ToProgress as any).finish).toHaveBeenCalledTimes(1) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /packages/router.middleware.scrollPosition/README.md: -------------------------------------------------------------------------------- 1 | # router.middleware.scrollPosition 2 | 3 | [![Version][npm-version-shield]][npm] 4 | [![Dependency Status][david-dm-shield]][david-dm] 5 | [![Peer Dependency Status][david-dm-peer-shield]][david-dm-peer] 6 | [![Dev Dependency Status][david-dm-dev-shield]][david-dm-dev] 7 | [![Downloads][npm-stats-shield]][npm-stats] 8 | 9 | [david-dm]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/router.middleware.scrollPosition 10 | [david-dm-shield]: https://david-dm.org/Profiscience/knockout-contrib/status.svg?path=packages/router.middleware.scrollPosition 11 | [david-dm-peer]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/router.middleware.scrollPosition&type=peer 12 | [david-dm-peer-shield]: https://david-dm.org/Profiscience/knockout-contrib/peer-status.svg?path=packages/router.middleware.scrollPosition 13 | [david-dm-dev]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/router.middleware.scrollPosition&type=dev 14 | [david-dm-dev-shield]: https://david-dm.org/Profiscience/knockout-contrib/dev-status.svg?path=packages/router.middleware.scrollPosition 15 | [npm]: https://www.npmjs.com/package/@profiscience/knockout-contrib-router-middleware-scroll-position 16 | [npm-version-shield]: https://img.shields.io/npm/v/@profiscience/knockout-contrib-router-middleware-scroll-position.svg 17 | [npm-stats]: http://npm-stat.com/charts.html?package=@profiscience/knockout-contrib-router-middleware-scroll-position&author=&from=&to= 18 | [npm-stats-shield]: https://img.shields.io/npm/dt/@profiscience/knockout-contrib-router-middleware-scroll-position.svg?maxAge=2592000 19 | 20 | Add support for: 21 | 22 | - restoring scroll position on back-button navigation 23 | - scrolling to anchors (#hash), or the top on forward navigation 24 | 25 | ## Usage 26 | 27 | ```typescript 28 | import { 29 | Router, 30 | createScrollPositionMiddleware 31 | } from '@profiscience/knockout-contrib' 32 | 33 | Router.use( 34 | createScrollPositionMiddleware({ 35 | // optionally supply a custom scroll function (add smooth scrolling, use Velocity, etc.) 36 | scrollTo(x, y) { ... } 37 | }) 38 | ) 39 | ``` 40 | -------------------------------------------------------------------------------- /packages/router.middleware.scrollPosition/examples/index.html: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /packages/router.middleware.scrollPosition/examples/index.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | import { Context, Route, Router } from '@profiscience/knockout-contrib-router' 3 | import { createScrollPositionMiddleware } from '@profiscience/knockout-contrib-router-middleware-scroll-position' 4 | 5 | function range(n: number) { 6 | const nums = [] 7 | for (let i = 0; i < n; i++) nums.push(i) 8 | return nums 9 | } 10 | 11 | ko.components.register('view', { 12 | viewModel: class { 13 | public nums = range(200) 14 | public otherPath: string 15 | constructor(ctx: Context) { 16 | this.otherPath = ctx.pathname === '/' ? '/2' : '/' 17 | } 18 | }, 19 | template: ` 20 | 25 | 26 | 27 |
28 | `, 29 | synchronous: true, 30 | }) 31 | 32 | Router.setConfig({ 33 | base: '/router.middleware.scrollPosition', 34 | hashbang: true, 35 | }) 36 | 37 | Router.use( 38 | createScrollPositionMiddleware({ 39 | scrollTo: (x, y) => window.scroll({ top: y, behavior: 'smooth' }), 40 | }) 41 | ) 42 | 43 | Router.useRoutes([new Route('/', 'view'), new Route('/2', 'view')]) 44 | 45 | ko.applyBindings() 46 | -------------------------------------------------------------------------------- /packages/router.middleware.scrollPosition/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Context, 3 | IContext, 4 | LifecycleMiddleware, 5 | } from '@profiscience/knockout-contrib-router' 6 | 7 | export type ScrollPositionMiddlewareOpts = { 8 | scrollTo?(x: number, y: number): void 9 | } 10 | 11 | export function createScrollPositionMiddleware( 12 | opts: ScrollPositionMiddlewareOpts = {} 13 | ): LifecycleMiddleware { 14 | const scrollTo = opts.scrollTo || window.scrollTo 15 | 16 | return (ctx: Context & IContext) => ({ 17 | afterRender() { 18 | const hash = (location.pathname + location.hash) 19 | .replace(/#!/, '') 20 | .replace(/^[^#]+#?/, '') 21 | let y = 0 22 | if (history.state && history.state.scrollPosition) { 23 | y = history.state.scrollPosition 24 | } else if (hash) { 25 | const anchor = document.getElementById(hash) 26 | if (anchor !== null) { 27 | y = anchor.offsetTop 28 | } else { 29 | // tslint:disable-next-line no-console 30 | console.warn( 31 | '[@profiscience/knockout-contrib-middleware-scroll-position]', 32 | `Navigated to page with #${hash}, but no element with id ${hash} found.` 33 | ) 34 | } 35 | } 36 | scrollTo(0, y) 37 | }, 38 | beforeDispose() { 39 | history.replaceState( 40 | { 41 | ...(history.state || {}), 42 | scrollPosition: window.scrollY, 43 | }, 44 | document.title 45 | ) 46 | }, 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /packages/router.middleware.scrollPosition/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-router-middleware-scroll-position", 3 | "version": "2.0.17", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/router.middleware.scrollPosition", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "@profiscience/knockout-contrib-router": ">=2.0.0", 19 | "knockout": "3.5.1" 20 | }, 21 | "devDependencies": { 22 | "@profiscience/knockout-contrib-router": "^2.1.5" 23 | }, 24 | "dependencies": { 25 | "babel-runtime": "^6.26.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/router.plugins.children/index.ts: -------------------------------------------------------------------------------- 1 | import { Route, IRouteConfig } from '@profiscience/knockout-contrib-router' 2 | 3 | declare module '@profiscience/knockout-contrib-router' { 4 | // tslint:disable-next-line no-shadowed-variable 5 | interface IRouteConfig { 6 | /** 7 | * Nested routes, only allows explicit route constructor syntax routes 8 | */ 9 | children?: Route[] 10 | } 11 | } 12 | 13 | export function childrenRoutePlugin({ children }: IRouteConfig) { 14 | return children || [] 15 | } 16 | -------------------------------------------------------------------------------- /packages/router.plugins.children/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-router-plugins-children", 3 | "version": "2.0.17", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/router.plugins.children", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "@profiscience/knockout-contrib-router": ">=2.0.0", 19 | "knockout": "3.5.1" 20 | }, 21 | "devDependencies": { 22 | "@profiscience/knockout-contrib-router": "^2.1.5" 23 | }, 24 | "dependencies": { 25 | "babel-runtime": "^6.26.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/router.plugins.children/test.ts: -------------------------------------------------------------------------------- 1 | import { Route, IRouteConfig } from '@profiscience/knockout-contrib-router' 2 | 3 | import { childrenRoutePlugin } from './index' 4 | 5 | Route.usePlugin(childrenRoutePlugin) 6 | 7 | describe('router.plugins.children', () => { 8 | test('allows registering children with children property', () => { 9 | const children = [new Route('/a'), new Route('/b')] 10 | const route = new Route('/', { children }) 11 | 12 | expect(route.children).toEqual(children) 13 | }) 14 | 15 | test('works with object shorthand', () => { 16 | // this usage is discouraged with TypeScript, because adding it to the 17 | // type definition greatly weakens the type checking ability of the route 18 | // constructor syntax because this requires an index type. So, to use it 19 | // with TS, it must explicitly be the `any` type 20 | const children: any = { 21 | '/a': 'a', 22 | '/b': 'b', 23 | } 24 | const route = new Route('/', { children }) 25 | 26 | expect(route.children).toHaveLength(2) 27 | expect(route.children[0].path).toBe('/a') 28 | expect(route.children[1].path).toBe('/b') 29 | }) 30 | 31 | test("doesn't blow up when not used", () => { 32 | const routeConfig: IRouteConfig = {} 33 | expect(() => childrenRoutePlugin(routeConfig)).not.toThrow() 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /packages/router.plugins.component/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-router-plugins-component", 3 | "version": "2.0.17", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/router.plugins.component", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "@profiscience/knockout-contrib-router": ">=2.0.0", 19 | "knockout": "3.5.1" 20 | }, 21 | "devDependencies": { 22 | "@profiscience/knockout-contrib-router": "^2.1.5" 23 | }, 24 | "dependencies": { 25 | "babel-runtime": "^6.26.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/router.plugins.components/README.md: -------------------------------------------------------------------------------- 1 | # router.plugins.components 2 | 3 | [![Version][npm-version-shield]][npm] 4 | [![Dependency Status][david-dm-shield]][david-dm] 5 | [![Peer Dependency Status][david-dm-peer-shield]][david-dm-peer] 6 | [![Dev Dependency Status][david-dm-dev-shield]][david-dm-dev] 7 | [![Downloads][npm-stats-shield]][npm-stats] 8 | 9 | [david-dm]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/router.plugins.components 10 | [david-dm-shield]: https://david-dm.org/Profiscience/knockout-contrib/status.svg?path=packages/router.plugins.components 11 | [david-dm-peer]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/router.plugins.components&type=peer 12 | [david-dm-peer-shield]: https://david-dm.org/Profiscience/knockout-contrib/peer-status.svg?path=packages/router.plugins.components 13 | [david-dm-dev]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/router.plugins.components&type=dev 14 | [david-dm-dev-shield]: https://david-dm.org/Profiscience/knockout-contrib/dev-status.svg?path=packages/router.plugins.components 15 | [npm]: https://www.npmjs.com/package/@profiscience/knockout-contrib-router-plugins-components 16 | [npm-version-shield]: https://img.shields.io/npm/v/@profiscience/knockout-contrib-router-plugins-components.svg 17 | [npm-stats]: http://npm-stat.com/charts.html?package=@profiscience/knockout-contrib-router-plugins-components&author=&from=&to= 18 | [npm-stats-shield]: https://img.shields.io/npm/dt/@profiscience/knockout-contrib-router-plugins-components.svg?maxAge=2592000 19 | 20 | Register components only for the life of the page (unregister before dispose). 21 | 22 | Allows breaking views into multiple components while helping to avoid naming conflicts. 23 | 24 | ## Usage 25 | 26 | ```typescript 27 | import { Route, componentsRoutePlugin } from '@profiscience/knockout-contrib' 28 | 29 | Route.usePlugin(componentsRoutePlugin) 30 | 31 | new Route('/', { 32 | components: () => ({ 33 | // will register thecomponent for use in this view and its children 34 | toolbar: import('./toolbar'), 35 | }), 36 | }) 37 | ``` 38 | 39 | **toolbar.ts** 40 | 41 | ```typescript 42 | export const template = 'Hello, World!' 43 | export class viewModel {} 44 | ``` 45 | -------------------------------------------------------------------------------- /packages/router.plugins.components/index.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | import { 3 | Context, 4 | IContext, 5 | IRouteConfig, 6 | LifecycleMiddleware, 7 | } from '@profiscience/knockout-contrib-router' 8 | 9 | declare module '@profiscience/knockout-contrib-router' { 10 | // tslint:disable-next-line no-shadowed-variable 11 | interface IRouteConfig { 12 | components?: LazyComponentsAccessor 13 | } 14 | } 15 | 16 | export type LazyComponentsAccessor = () => ILazyComponents 17 | 18 | export interface ILazyComponents { 19 | [k: string]: Promise<{ 20 | template: string 21 | viewModel?: ko.components.ViewModelConstructor 22 | }> 23 | } 24 | 25 | interface IComponentMap { 26 | [k: string]: { 27 | template: string 28 | viewModel?: ko.components.ViewModelConstructor 29 | } 30 | } 31 | 32 | export function componentsRoutePlugin({ 33 | components, 34 | }: IRouteConfig): LifecycleMiddleware | void { 35 | if (!components) return 36 | 37 | return (ctx: Context & IContext) => { 38 | let componentNames: string[] = [] 39 | 40 | return { 41 | beforeRender() { 42 | ctx.queue( 43 | fetchComponents(components).then((componentMap) => { 44 | componentNames = Object.keys(componentMap) 45 | componentNames.forEach((componentName) => { 46 | ko.components.register(componentName, componentMap[componentName]) 47 | }) 48 | }) 49 | ) 50 | }, 51 | beforeDispose() { 52 | componentNames.forEach((c) => ko.components.unregister(c)) 53 | }, 54 | } 55 | } 56 | } 57 | 58 | async function fetchComponents(componentsAccessor: LazyComponentsAccessor) { 59 | const lazyComponents = componentsAccessor() 60 | const components: IComponentMap = {} 61 | await Promise.all( 62 | Object.keys(lazyComponents).map(async (componentName) => { 63 | const componentConfig = await lazyComponents[componentName] 64 | components[componentName] = componentConfig 65 | }) 66 | ) 67 | return components 68 | } 69 | -------------------------------------------------------------------------------- /packages/router.plugins.components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-router-plugins-components", 3 | "version": "1.0.17", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/router.plugins.components", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "@profiscience/knockout-contrib-router": ">=2.0.0", 19 | "knockout": "3.5.1" 20 | }, 21 | "devDependencies": { 22 | "@profiscience/knockout-contrib-router": "^2.1.5" 23 | }, 24 | "dependencies": { 25 | "babel-runtime": "^6.26.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/router.plugins.components/test.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | import { 3 | Context, 4 | IContext, 5 | Route, 6 | Lifecycle, 7 | } from '@profiscience/knockout-contrib-router' 8 | 9 | import { componentsRoutePlugin } from './index' 10 | 11 | Route.usePlugin(componentsRoutePlugin) 12 | 13 | describe('router.plugins.components', () => { 14 | test('registers components before render', async () => { 15 | ko.components.register = jest.fn() 16 | ko.components.unregister = jest.fn() 17 | 18 | const helloWorldComponent = { template: 'Hello, World!' } 19 | const queue = jest.fn() 20 | const ctx = { queue: queue as any } as Context & IContext 21 | const route = new Route('/', { 22 | components: () => ({ 23 | 'hello-world': Promise.resolve(helloWorldComponent), 24 | }), 25 | }) 26 | const [middleware] = route.middleware 27 | const lifecycle = middleware(ctx) as Lifecycle 28 | 29 | if (lifecycle.beforeRender) lifecycle.beforeRender() 30 | await queue.mock.calls[0][0] 31 | // beforeRender 32 | expect(ko.components.register).lastCalledWith( 33 | 'hello-world', 34 | helloWorldComponent 35 | ) 36 | 37 | if (lifecycle.beforeDispose) lifecycle.beforeDispose() 38 | // beforeDispose 39 | expect(ko.components.unregister).lastCalledWith('hello-world') 40 | }) 41 | 42 | test("doesn't blow up when not used", () => { 43 | const route = new Route('/', {}) 44 | expect(route.middleware.length).toBe(0) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /packages/router.plugins.init/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Context, 3 | IContext, 4 | SimpleMiddleware, 5 | IRouteConfig, 6 | } from '@profiscience/knockout-contrib-router' 7 | import { IRoutedComponentInstance } from '@profiscience/knockout-contrib-router-plugins-component' 8 | 9 | export const INITIALIZED = Symbol('INITIALIZED') 10 | 11 | export function componentInitializerRoutePlugin( 12 | routeConfig: IRouteConfig 13 | ): SimpleMiddleware | void { 14 | if (!routeConfig.component) return 15 | 16 | return (ctx: Context & IContext) => { 17 | ctx.queue( 18 | (async () => { 19 | const { viewModel } = await (ctx.component as Promise< 20 | IRoutedComponentInstance 21 | >) 22 | 23 | if (!viewModel) return 24 | 25 | const initializers = Object.keys(viewModel) 26 | .filter((prop: any) => { 27 | const v = (viewModel as any)[prop] 28 | return typeof v === 'undefined' || v === null 29 | ? false 30 | : v[INITIALIZED] 31 | }) 32 | .map((prop: any) => (viewModel as any)[prop][INITIALIZED]) 33 | 34 | if (viewModel[INITIALIZED]) initializers.push(viewModel[INITIALIZED]) 35 | 36 | await Promise.all(initializers) 37 | 38 | if (viewModel.init) { 39 | await viewModel.init() 40 | } 41 | })() 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/router.plugins.init/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-router-plugins-init", 3 | "version": "3.0.9", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/router.plugins.init", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "@profiscience/knockout-contrib-router": ">=2.0.0", 19 | "@profiscience/knockout-contrib-router-plugins-component": ">=2.0.0", 20 | "knockout": "3.5.1" 21 | }, 22 | "dependencies": { 23 | "babel-runtime": "^6.26.0" 24 | }, 25 | "devDependencies": { 26 | "@profiscience/knockout-contrib-jest-matchers": "^2.0.4", 27 | "@profiscience/knockout-contrib-router": "^2.1.5", 28 | "@profiscience/knockout-contrib-router-plugins-component": "^2.0.17" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/router.plugins.redirect/README.md: -------------------------------------------------------------------------------- 1 | # router.plugins.redirect 2 | 3 | [![Version][npm-version-shield]][npm] 4 | [![Dependency Status][david-dm-shield]][david-dm] 5 | [![Peer Dependency Status][david-dm-peer-shield]][david-dm-peer] 6 | [![Dev Dependency Status][david-dm-dev-shield]][david-dm-dev] 7 | [![Downloads][npm-stats-shield]][npm-stats] 8 | 9 | [david-dm]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/router.plugins.redirect 10 | [david-dm-shield]: https://david-dm.org/Profiscience/knockout-contrib/status.svg?path=packages/router.plugins.redirect 11 | [david-dm-peer]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/router.plugins.redirect&type=peer 12 | [david-dm-peer-shield]: https://david-dm.org/Profiscience/knockout-contrib/peer-status.svg?path=packages/router.plugins.redirect 13 | [david-dm-dev]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/router.plugins.redirect&type=dev 14 | [david-dm-dev-shield]: https://david-dm.org/Profiscience/knockout-contrib/dev-status.svg?path=packages/router.plugins.redirect 15 | [npm]: https://www.npmjs.com/package/@profiscience/knockout-contrib-router-plugins-redirect 16 | [npm-version-shield]: https://img.shields.io/npm/v/@profiscience/knockout-contrib-router-plugins-redirect.svg 17 | [npm-stats]: http://npm-stat.com/charts.html?package=@profiscience/knockout-contrib-router-plugins-redirect&author=&from=&to= 18 | [npm-stats-shield]: https://img.shields.io/npm/dt/@profiscience/knockout-contrib-router-plugins-redirect.svg?maxAge=2592000 19 | 20 | Support `redirect` option in route configurations. 21 | 22 | ## Usage 23 | 24 | Redirects to the returned path, if any. Supports async via promise. 25 | 26 | ```typescript 27 | import { Route, redirectRoutePlugin } from '@profiscience/knockout-contrib' 28 | 29 | Route.usePlugin(redirectRoutePlugin) 30 | 31 | function shouldRedirect(ctx) { 32 | // ...do something... 33 | } 34 | 35 | // Sync 36 | new Route('/', { 37 | redirect: (ctx) => { 38 | if (shouldRedirect(ctx)) return '//redirect/to/this/route' 39 | }, 40 | }) 41 | 42 | // Async 43 | new Route('/', { 44 | redirect: async (ctx) => { 45 | if (await shouldRedirect(ctx)) return '//redirect/to/this/route' 46 | }, 47 | }) 48 | ``` 49 | -------------------------------------------------------------------------------- /packages/router.plugins.redirect/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Context, 3 | IContext, 4 | IRouteConfig, 5 | Middleware, 6 | } from '@profiscience/knockout-contrib-router' 7 | 8 | declare module '@profiscience/knockout-contrib-router' { 9 | // tslint:disable-next-line no-shadowed-variable 10 | interface IRouteConfig { 11 | redirect?: ( 12 | ctx: Context & IContext 13 | ) => string | void | Promise 14 | } 15 | } 16 | 17 | export function redirectRoutePlugin({ 18 | redirect, 19 | }: IRouteConfig): Middleware | void { 20 | if (!redirect) return 21 | 22 | return async (ctx: Context & IContext) => { 23 | const redirectUrl = await redirect(ctx) 24 | if (redirectUrl) { 25 | ctx.redirect(redirectUrl) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/router.plugins.redirect/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-router-plugins-redirect", 3 | "version": "2.0.17", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/router.plugins.redirect", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "@profiscience/knockout-contrib-router": ">=2.0.0", 19 | "knockout": "3.5.1" 20 | }, 21 | "devDependencies": { 22 | "@profiscience/knockout-contrib-router": "^2.1.5" 23 | }, 24 | "dependencies": { 25 | "babel-runtime": "^6.26.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/router.plugins.title/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Context, 3 | IContext, 4 | IRouteConfig, 5 | RoutePlugin, 6 | } from '@profiscience/knockout-contrib-router' 7 | 8 | type MaybePromise = T | Promise 9 | 10 | declare module '@profiscience/knockout-contrib-router' { 11 | // tslint:disable-next-line no-shadowed-variable 12 | interface IRouteConfig { 13 | /** 14 | * Document title for view, can be async or sync accessor function 15 | */ 16 | title?: string | ((ctx: Context & IContext) => MaybePromise ) 17 | } 18 | } 19 | 20 | export function createTitleRoutePlugin( 21 | compose = (ts: string[]) => ts.join(' | ') 22 | ): RoutePlugin { 23 | const titles: MaybePromise [] = [] 24 | let terminalTitledRouteContext: null | (Context & IContext) 25 | 26 | return (routeConfig: IRouteConfig) => { 27 | const titleOrAccessor = routeConfig.title 28 | 29 | if (!titleOrAccessor) return 30 | 31 | return (ctx: Context & IContext) => ({ 32 | beforeRender() { 33 | terminalTitledRouteContext = ctx 34 | }, 35 | afterRender() { 36 | titles.push( 37 | typeof titleOrAccessor === 'function' 38 | ? titleOrAccessor(ctx) 39 | : titleOrAccessor 40 | ) 41 | if (ctx === terminalTitledRouteContext) { 42 | terminalTitledRouteContext = null 43 | ctx.queue( 44 | Promise.all(titles) 45 | .then((ts) => { 46 | document.title = compose(ts) 47 | }) 48 | .catch((err) => { 49 | throw new Error(`Error setting title: ${err}`) 50 | }) 51 | ) 52 | } 53 | }, 54 | beforeDispose() { 55 | titles.pop() 56 | }, 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/router.plugins.title/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-router-plugins-title", 3 | "version": "2.0.18", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/router.plugins.title", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "@profiscience/knockout-contrib-router": ">=2.0.0", 19 | "knockout": "3.5.1" 20 | }, 21 | "devDependencies": { 22 | "@profiscience/knockout-contrib-router": "^2.1.5" 23 | }, 24 | "dependencies": { 25 | "babel-runtime": "^6.26.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/router.plugins.with/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Context, 3 | IContext, 4 | IRouteConfig, 5 | } from '@profiscience/knockout-contrib-router' 6 | 7 | type MaybePromise = T | Promise 8 | 9 | declare module '@profiscience/knockout-contrib-router' { 10 | // tslint:disable-next-line no-shadowed-variable 11 | interface IRouteConfig { 12 | /** 13 | * Additional data to extend context with. 14 | * 15 | * Can be used for overriding url params, e.g. 16 | * 17 | * ```typescript 18 | * with: { params: { id: 0 } } 19 | * ``` 20 | */ 21 | with?: 22 | | Context 23 | | IContext 24 | | ((ctx: Context & IContext) => MaybePromise ) 25 | } 26 | } 27 | 28 | export function withRoutePlugin({ with: _with }: IRouteConfig) { 29 | if (!_with) return 30 | 31 | return async (ctx: Context & IContext) => { 32 | let src = _with 33 | if (typeof _with === 'function') { 34 | src = await _with(ctx) 35 | } 36 | Object.assign(ctx, src) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/router.plugins.with/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-router-plugins-with", 3 | "version": "2.0.17", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/router.plugins.with", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "@profiscience/knockout-contrib-router": ">=2.0.0", 19 | "knockout": "3.5.1" 20 | }, 21 | "devDependencies": { 22 | "@profiscience/knockout-contrib-router": "^2.1.5" 23 | }, 24 | "dependencies": { 25 | "babel-runtime": "^6.26.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/router.plugins.with/test.ts: -------------------------------------------------------------------------------- 1 | import { Context, IContext, Route } from '@profiscience/knockout-contrib-router' 2 | import { withRoutePlugin } from './index' 3 | 4 | const FOO = Symbol('foo') 5 | 6 | declare module '@profiscience/knockout-contrib-router' { 7 | // tslint:disable-next-line no-shadowed-variable 8 | interface IContext { 9 | foo?: string 10 | [FOO]?: string 11 | } 12 | } 13 | 14 | Route.usePlugin(withRoutePlugin) 15 | 16 | describe('router.plugins.with', () => { 17 | test('extends context with object passed to with', () => { 18 | const route = new Route('/', { with: { foo: 'bar' } }) 19 | const ctx = { route } as Context & IContext 20 | const [middleware] = route.middleware 21 | 22 | middleware(ctx) 23 | 24 | expect(ctx.foo).toBe('bar') 25 | }) 26 | 27 | test('works with sync accessor', async () => { 28 | const route = new Route('/', { with: () => ({ foo: 'bar' }) }) 29 | const ctx = { route } as Context & IContext 30 | const [middleware] = route.middleware 31 | 32 | await middleware(ctx) 33 | 34 | expect(ctx.foo).toBe('bar') 35 | }) 36 | 37 | test('works with async accessor', async () => { 38 | const route = new Route('/', { 39 | with: () => Promise.resolve({ foo: 'bar' }), 40 | }) 41 | const ctx = { route } as Context & IContext 42 | const [middleware] = route.middleware 43 | 44 | await middleware(ctx) 45 | 46 | expect(ctx.foo).toBe('bar') 47 | }) 48 | 49 | test('accessor recieves context as argument', async () => { 50 | const accessor = jest.fn(() => ({ foo: 'bar' })) 51 | const route = new Route('/', { with: accessor }) 52 | const ctx = { route } as Context & IContext 53 | const [middleware] = route.middleware 54 | 55 | await middleware(ctx) 56 | 57 | expect(accessor).toBeCalledWith(ctx) 58 | }) 59 | 60 | test('works with symbol keys', () => { 61 | const route = new Route('/', { with: { [FOO]: 'bar' } }) 62 | const ctx = { route } as Context & IContext 63 | const [middleware] = route.middleware 64 | 65 | middleware(ctx) 66 | 67 | expect((ctx as any)[FOO]).toBe('bar') 68 | }) 69 | 70 | test("doesn't blow up when not used", () => { 71 | const middleware = withRoutePlugin({}) 72 | expect(middleware).toBeUndefined() 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /packages/router/__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "sourceType": "module" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/router/__tests__/basepath.js: -------------------------------------------------------------------------------- 1 | import ko from 'knockout' 2 | import $ from 'jquery' 3 | 4 | import { Router } from '../' 5 | 6 | ko.components.register('basepath', { 7 | template: ` 8 | 9 | 10 | `, 11 | viewModel: class BasePath { 12 | constructor({ t, done }) { 13 | Router.setConfig({ 14 | base: '/base', 15 | }) 16 | 17 | Router.useRoutes({ 18 | '/foo': { 19 | '/foo': 'foo', 20 | }, 21 | '/bar': { 22 | '/bar': 'bar', 23 | }, 24 | }) 25 | 26 | history.pushState(null, null, '/base/foo/foo') 27 | 28 | ko.components.register('foo', { 29 | viewModel: class { 30 | constructor(ctx) { 31 | t.pass('initializes with basepath') 32 | t.equals( 33 | location.pathname, 34 | '/base/foo/foo', 35 | 'uses basepath in url on init' 36 | ) 37 | t.equals( 38 | ctx.canonicalPath, 39 | '/foo/foo', 40 | 'ctx.canonicalPath is correct' 41 | ) 42 | 43 | ctx.router.initialized.then(() => 44 | setTimeout(() => { 45 | // Dirty hack for CI 46 | t.equals( 47 | $('#foo-link').attr('href'), 48 | '/base/foo/foo', 49 | 'sets href correctly in path binding' 50 | ) 51 | Router.update('/bar/bar') 52 | }) 53 | ) 54 | } 55 | }, 56 | }) 57 | 58 | ko.components.register('bar', { 59 | viewModel: class { 60 | constructor() { 61 | t.pass('navigates correctly with basepath') 62 | t.equals( 63 | '/base/bar/bar', 64 | location.pathname, 65 | 'uses basepath in url on update' 66 | ) 67 | 68 | done() 69 | } 70 | }, 71 | }) 72 | } 73 | 74 | dispose() { 75 | Router.setConfig({ 76 | base: '', 77 | }) 78 | } 79 | }, 80 | }) 81 | -------------------------------------------------------------------------------- /packages/router/__tests__/bindings/index.js: -------------------------------------------------------------------------------- 1 | import './path' 2 | import './active-path' 3 | -------------------------------------------------------------------------------- /packages/router/__tests__/element.js: -------------------------------------------------------------------------------- 1 | import ko from 'knockout' 2 | 3 | import { Router } from '../' 4 | 5 | ko.components.register('element', { 6 | template: ' ', 7 | viewModel: class Element { 8 | constructor({ t, done }) { 9 | Router.useRoutes({ 10 | '/': [ 11 | (ctx) => ({ 12 | beforeRender() { 13 | ctx.redirect('/foo') 14 | }, 15 | afterRender() { 16 | t.equals( 17 | ctx.element, 18 | undefined, 19 | 'ctx.element is undefined in redirection' 20 | ) 21 | }, 22 | }), 23 | ], 24 | '/foo': [ 25 | (ctx) => ({ 26 | beforeRender() { 27 | t.equals( 28 | ctx.element, 29 | undefined, 30 | 'ctx.element is undefined before render' 31 | ) 32 | }, 33 | afterRender() { 34 | const actual = document.getElementById('foo-view').parentElement 35 | t.equals( 36 | ctx.element, 37 | actual, 38 | 'ctx.element is router-view container after render' 39 | ) 40 | done() 41 | }, 42 | }), 43 | 'foo', 44 | ], 45 | }) 46 | 47 | history.pushState(null, null, '/') 48 | 49 | ko.components.register('foo', { template: '' }) 50 | } 51 | }, 52 | }) 53 | -------------------------------------------------------------------------------- /packages/router/__tests__/force-update.js: -------------------------------------------------------------------------------- 1 | import ko from 'knockout' 2 | 3 | import { Router } from '../' 4 | 5 | ko.components.register('force-update', { 6 | template: ' ', 7 | viewModel: class ForceUpdate { 8 | constructor({ t, done }) { 9 | let count = 0 10 | 11 | Router.useRoutes({ 12 | '/': 'foo', 13 | }) 14 | 15 | history.pushState(null, null, '/') 16 | 17 | ko.components.register('foo', { 18 | viewModel: class { 19 | constructor(ctx) { 20 | if (++count === 1) { 21 | ctx.router.update('/', { force: true }) 22 | } else { 23 | t.pass('can force same-route update') 24 | done() 25 | } 26 | } 27 | }, 28 | }) 29 | } 30 | }, 31 | }) 32 | -------------------------------------------------------------------------------- /packages/router/__tests__/hashbang.js: -------------------------------------------------------------------------------- 1 | import ko from 'knockout' 2 | import $ from 'jquery' 3 | 4 | import { Router } from '../' 5 | 6 | ko.components.register('hashbang', { 7 | template: ` 8 | 9 | 10 | `, 11 | viewModel: class Hashbang { 12 | constructor({ t, done }) { 13 | Router.setConfig({ 14 | hashbang: true, 15 | base: '/base', 16 | }) 17 | 18 | Router.useRoutes({ 19 | '/foo': { 20 | '/foo': 'foo', 21 | }, 22 | '/bar': { 23 | '/bar': 'bar', 24 | }, 25 | }) 26 | 27 | history.pushState(null, null, '/base/#!/foo/foo') 28 | 29 | ko.components.register('foo', { 30 | viewModel: class { 31 | constructor(ctx) { 32 | t.pass('initializes with hashbang') 33 | t.true( 34 | location.href.indexOf('/base/#!/foo/foo') > -1, 35 | 'uses hash in url on init' 36 | ) 37 | 38 | ctx.router.initialized.then(() => 39 | setTimeout(() => { 40 | // dirty hack for CI 41 | t.equals( 42 | $('#foo-link').attr('href'), 43 | '/base/#!/foo/foo', 44 | 'sets href correctly in path binding' 45 | ) 46 | Router.update('/bar/bar') 47 | }) 48 | ) 49 | } 50 | }, 51 | }) 52 | 53 | ko.components.register('bar', { 54 | viewModel: class { 55 | constructor() { 56 | t.pass('navigates correctly with hashbang') 57 | t.true( 58 | location.href.indexOf('/base/#!/bar/bar') > -1, 59 | 'uses hash in url on update' 60 | ) 61 | 62 | done() 63 | } 64 | }, 65 | }) 66 | } 67 | 68 | dispose() { 69 | Router.setConfig({ 70 | hashbang: false, 71 | base: '', 72 | }) 73 | } 74 | }, 75 | }) 76 | -------------------------------------------------------------------------------- /packages/router/__tests__/helpers/ko-overwrite-component-registration.js: -------------------------------------------------------------------------------- 1 | import ko from 'knockout' 2 | 3 | const _register = ko.components.register 4 | 5 | ko.components.register = ( 6 | name, 7 | { template = '', viewModel = class {} } 8 | ) => { 9 | if (ko.components.isRegistered(name)) { 10 | ko.components.unregister(name) 11 | ko.components.clearCachedDefinition(name) 12 | } 13 | return _register(name, { template, viewModel }) 14 | } 15 | -------------------------------------------------------------------------------- /packages/router/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import ko from 'knockout' 2 | import $ from 'jquery' 3 | import tape from 'tape' 4 | 5 | import { Router } from '../' 6 | 7 | import './helpers/ko-overwrite-component-registration' 8 | 9 | import './anchor' 10 | import './basepath' 11 | import './bindings' 12 | import './routing' 13 | import './hashbang' 14 | import './history' 15 | import './force-update' 16 | import './with' 17 | import './middleware' 18 | import './queue' 19 | import './redirect' 20 | import './before-navigate-callbacks' 21 | import './element' 22 | import './plugins' 23 | import './preserve-query' 24 | import './preserve-state' 25 | 26 | const tests = [ 27 | 'routing', 28 | 'basepath', 29 | 'hashbang', 30 | 'history', 31 | 'force-update', 32 | 'with', 33 | 'anchor', 34 | 'bindings-path', 35 | 'bindings-active-path', 36 | 'middleware', 37 | 'queue', 38 | 'redirect', 39 | 'before-navigate-callbacks', 40 | 'element', 41 | 'plugins', 42 | 'preserve-query', 43 | 'preserve-state', 44 | ] 45 | 46 | class TestRunner { 47 | constructor() { 48 | $('body').append(` 49 | 50 | 54 |55 | `) 56 | this.test = ko.observable(null) 57 | this.runTests() 58 | } 59 | 60 | async runTests() { 61 | for (const test of tests) { 62 | await this.runTest(test) 63 | } 64 | } 65 | 66 | async runTest(test) { 67 | history.pushState(null, null, '/') 68 | 69 | return await new Promise((resolve) => 70 | tape(test, (t) => { 71 | Router.routes = [] 72 | this.t = t 73 | this.done = () => { 74 | t.end() 75 | resolve() 76 | } 77 | this.test(test) 78 | }) 79 | ) 80 | } 81 | } 82 | 83 | ko.applyBindings(new TestRunner()) 84 | -------------------------------------------------------------------------------- /packages/router/__tests__/preserve-query.js: -------------------------------------------------------------------------------- 1 | import ko from 'knockout' 2 | 3 | import { Router } from '../' 4 | 5 | ko.components.register('preserve-query', { 6 | template: '', 7 | viewModel: class ForceUpdate { 8 | constructor({ t, done }) { 9 | Router.setConfig({ 10 | preserveQueryStringOnNavigation: true, 11 | }) 12 | 13 | Router.useRoutes({ 14 | '/foo': 'foo', 15 | '/bar': 'bar', 16 | '/baz': 'baz', 17 | }) 18 | 19 | history.pushState(null, null, '/foo?qux=qux') 20 | 21 | ko.components.register('foo', { 22 | viewModel: class { 23 | constructor(ctx) { 24 | ctx.router.update('/bar') 25 | } 26 | }, 27 | }) 28 | 29 | ko.components.register('bar', { 30 | viewModel: class { 31 | constructor(ctx) { 32 | t.equals(window.location.search, '?qux=qux', 'works as described') 33 | Router.update('/baz?qux=notqux') 34 | } 35 | }, 36 | }) 37 | 38 | ko.components.register('baz', { 39 | viewModel: class { 40 | constructor(ctx) { 41 | t.equals( 42 | window.location.search, 43 | '?qux=notqux', 44 | 'uses explicit querystring instead if specified' 45 | ) 46 | 47 | Router.setConfig({ 48 | preserveQueryStringOnNavigation: false, 49 | }) 50 | 51 | done() 52 | } 53 | }, 54 | }) 55 | } 56 | }, 57 | }) 58 | -------------------------------------------------------------------------------- /packages/router/__tests__/preserve-state.js: -------------------------------------------------------------------------------- 1 | import ko from 'knockout' 2 | 3 | import { Router } from '../' 4 | 5 | ko.components.register('preserve-state', { 6 | template: ' ', 7 | viewModel: class { 8 | constructor({ t, done }) { 9 | Router.useRoutes({ 10 | '/foo': 'foo', 11 | '/bar': 'bar', 12 | '/baz': 'baz', 13 | }) 14 | 15 | history.pushState({ foo: 'foo' }, null, '/foo') 16 | 17 | ko.components.register('foo', { 18 | viewModel: class { 19 | constructor(ctx) { 20 | t.deepEquals( 21 | history.state, 22 | { foo: 'foo' }, 23 | 'initializes with correct history.state regardless' 24 | ) 25 | 26 | history.replaceState({ bar: 'bar' }, document.title) 27 | 28 | Router.setConfig({ 29 | preserveHistoryStateOnNavigation: true, 30 | }) 31 | 32 | ctx.router.update('/bar') 33 | } 34 | }, 35 | }) 36 | 37 | ko.components.register('bar', { 38 | viewModel: class { 39 | constructor(ctx) { 40 | t.deepEquals(history.state, { bar: 'bar' }, 'works as described') 41 | 42 | history.replaceState({ baz: 'notbaz' }, document.title) 43 | 44 | Router.update('/baz', { state: { baz: 'baz' } }) 45 | } 46 | }, 47 | }) 48 | 49 | ko.components.register('baz', { 50 | viewModel: class { 51 | constructor(ctx) { 52 | t.deepEquals( 53 | history.state, 54 | { baz: 'baz' }, 55 | 'uses explicit state instead if specified' 56 | ) 57 | 58 | Router.setConfig({ 59 | preserveQueryStringOnNavigation: false, 60 | }) 61 | 62 | done() 63 | } 64 | }, 65 | }) 66 | } 67 | }, 68 | }) 69 | -------------------------------------------------------------------------------- /packages/router/__tests__/queue.js: -------------------------------------------------------------------------------- 1 | import ko from 'knockout' 2 | 3 | import { Router } from '../' 4 | 5 | ko.components.register('queue', { 6 | template: ' ', 7 | viewModel: class QueueTest { 8 | constructor({ t, done }) { 9 | let queuedPromiseAResolved = false 10 | let queuedPromiseBResolved = false 11 | 12 | Router.useRoutes({ 13 | '/': [ 14 | (ctx) => 15 | ctx.queue( 16 | new Promise((resolve) => { 17 | setTimeout(() => { 18 | queuedPromiseAResolved = true 19 | resolve() 20 | }, 1000) 21 | }) 22 | ), 23 | () => { 24 | t.notOk( 25 | queuedPromiseAResolved, 26 | 'queued promises let middleware continue' 27 | ) 28 | }, 29 | { 30 | '/': [ 31 | 'foo', 32 | (ctx) => 33 | ctx.queue( 34 | new Promise((resolve) => { 35 | setTimeout(() => { 36 | queuedPromiseBResolved = true 37 | resolve() 38 | }, 1000) 39 | }) 40 | ), 41 | () => { 42 | t.notOk( 43 | queuedPromiseAResolved, 44 | 'queued promises in parent router does not prevent child middleware from executing' 45 | ) 46 | }, 47 | ], 48 | }, 49 | ], 50 | }) 51 | 52 | ko.components.register('foo', { 53 | viewModel: class { 54 | constructor() { 55 | t.ok( 56 | queuedPromiseAResolved, 57 | 'queued promise in parent router resolves before component render' 58 | ) 59 | t.ok( 60 | queuedPromiseBResolved, 61 | 'queued promise in child router resolves before component render' 62 | ) 63 | done() 64 | } 65 | }, 66 | }) 67 | } 68 | }, 69 | }) 70 | -------------------------------------------------------------------------------- /packages/router/__tests__/routing/ambiguous.js: -------------------------------------------------------------------------------- 1 | import ko from 'knockout' 2 | import { Route, Router } from '../..' 3 | 4 | ko.components.register('ambiguous', { 5 | template: ' ', 6 | viewModel: class AmbiguousRoutingTest { 7 | constructor({ t, done }) { 8 | ko.components.register('wrong', { 9 | viewModel: class { 10 | constructor() { 11 | t.fail('fails on ambiguous routes') 12 | done() 13 | } 14 | }, 15 | }) 16 | 17 | ko.components.register('right-1', { 18 | viewModel: class { 19 | constructor() { 20 | t.pass('handles navigating to ambiguous route from unique parent') 21 | Router.update('//ambiguous/a/b/c/d') 22 | } 23 | }, 24 | }) 25 | 26 | ko.components.register('right-2', { 27 | viewModel: class { 28 | constructor() { 29 | /** 30 | * This ensures that the router doesn't incorrectly assume we are on the same page 31 | * just because paths match. 32 | * 33 | * View the commit this comment was added for context. 34 | */ 35 | t.pass( 36 | 'handles navigating to ambiguous route from matching (but wrong) parent' 37 | ) 38 | done() 39 | } 40 | }, 41 | }) 42 | } 43 | }, 44 | }) 45 | 46 | export const path = '/ambiguous/a/b/c' 47 | 48 | export const routes = [ 49 | new Route('/ambiguous', 'ambiguous', [ 50 | new Route('/a', [new Route('/b', 'wrong')]), 51 | new Route('/a', [new Route('/b', [new Route('/c', 'right-1')])]), 52 | new Route('/a', [ 53 | new Route('/b', [new Route('/c', [new Route('/d', 'right-2')])]), 54 | ]), 55 | ]), 56 | ] 57 | -------------------------------------------------------------------------------- /packages/router/__tests__/routing/basic.js: -------------------------------------------------------------------------------- 1 | import ko from 'knockout' 2 | 3 | ko.components.register('basic', { 4 | viewModel: class BasicRoutingTest { 5 | constructor({ t, done }) { 6 | t.pass('navigates to basic route') 7 | done() 8 | } 9 | }, 10 | }) 11 | 12 | export const path = '/basic' 13 | 14 | export const routes = { 15 | '/basic': 'basic', 16 | } 17 | -------------------------------------------------------------------------------- /packages/router/__tests__/routing/index.js: -------------------------------------------------------------------------------- 1 | import ko from 'knockout' 2 | import map from 'lodash/map' 3 | 4 | import { Router } from '../../' 5 | 6 | import * as init from './init' 7 | import * as basic from './basic' 8 | import * as params from './params' 9 | import * as nested from './nested' 10 | import * as similar from './similar' 11 | import * as ambiguous from './ambiguous' 12 | import * as routeConstructor from './route-constructor' 13 | 14 | const tests = [basic, params, nested, similar, ambiguous, routeConstructor] 15 | 16 | const paths = map(tests, 'path') 17 | 18 | ko.components.register('routing', { 19 | template: ' ', 20 | viewModel: class RoutingTestSuite { 21 | constructor({ t, done }) { 22 | Router.useRoutes(init.routes) 23 | history.pushState(null, null, init.path) 24 | 25 | tests.forEach((test) => Router.useRoutes(test.routes)) 26 | 27 | let resolve 28 | new Promise((_resolve) => (resolve = _resolve)).then(() => { 29 | this.runTests(t).then(done) 30 | }) 31 | 32 | this.t = t 33 | this.done = () => resolve() 34 | } 35 | 36 | async runTests(t) { 37 | for (const path of paths) { 38 | await new Promise((resolve) => { 39 | Router.update(path, { with: { t, done: resolve } }) 40 | }) 41 | } 42 | } 43 | }, 44 | }) 45 | -------------------------------------------------------------------------------- /packages/router/__tests__/routing/init.js: -------------------------------------------------------------------------------- 1 | import ko from 'knockout' 2 | 3 | ko.components.register('init', { 4 | viewModel: class RoutingInitializationTest { 5 | constructor({ t, done }) { 6 | t.pass('initializes') 7 | done() 8 | } 9 | }, 10 | }) 11 | 12 | export const path = '/init' 13 | 14 | export const routes = { 15 | '/init': 'init', 16 | } 17 | -------------------------------------------------------------------------------- /packages/router/__tests__/routing/params.js: -------------------------------------------------------------------------------- 1 | /* [path-to-regexp](https://github.com/pillarjs/path-to-regexp) is well tested, 2 | * so there isn't too much room for error in this as long as the params are being 3 | * attached to context and the route is working 4 | **/ 5 | 6 | import ko from 'knockout' 7 | 8 | ko.components.register('params', { 9 | viewModel: class ParamsTest { 10 | constructor(ctx) { 11 | const { t, done, params, search, hash } = ctx 12 | if (ctx.firstRun !== false) { 13 | t.equal('foo', params.foo, 'parses param to ctx.params') 14 | t.equals('?bar=bar', search, 'adds querystring to ctx.search') 15 | t.equals('#baz', hash, 'adds hash to ctx.hash') 16 | ctx.router.update('/params/bar', { 17 | with: { t, done, firstRun: false }, 18 | }) 19 | } else { 20 | t.equal( 21 | 'bar', 22 | params.foo, 23 | 're-initializes component if navigated to w/ different params' 24 | ) 25 | done() 26 | } 27 | } 28 | }, 29 | }) 30 | 31 | export const path = '/params/foo?bar=bar#baz' 32 | 33 | export const routes = { 34 | '/params/:foo': 'params', 35 | } 36 | -------------------------------------------------------------------------------- /packages/router/__tests__/routing/route-constructor.js: -------------------------------------------------------------------------------- 1 | import ko from 'knockout' 2 | 3 | import { Route, Router } from '../../' 4 | 5 | ko.components.register('route-constructor', { 6 | template: ' ', 7 | viewModel: class RouteConstructorTest { 8 | constructor(ctx) { 9 | const { t, done } = ctx 10 | 11 | ko.components.register('spread', { 12 | viewModel: class { 13 | constructor() { 14 | t.pass('Route constructor works with spread children') 15 | Router.update('/nested/arr') 16 | } 17 | }, 18 | }) 19 | 20 | ko.components.register('arr', { 21 | viewModel: class { 22 | constructor() { 23 | t.pass('Route constructor works with child routes in array') 24 | Router.update('/middleware') 25 | } 26 | }, 27 | }) 28 | 29 | ko.components.register('route-constructor-middleware', { 30 | viewModel: class { 31 | constructor(ctx) { 32 | t.true( 33 | ctx.spreadMiddlewareHit, 34 | 'middleware spread in arguments is used' 35 | ) 36 | t.true(ctx.arrMiddlewareHit, 'middleware in an array is flattened') 37 | done() 38 | } 39 | }, 40 | }) 41 | } 42 | }, 43 | }) 44 | 45 | export const path = '/nested/spread' 46 | 47 | /** 48 | * You'd probably never format routes like this, and for good reason. 49 | * The intent is to be as flexible as possible and to prevent users from 50 | * encountering annoying bugs when all that is wrong is an array somewhere 51 | * needs to be flattened. 52 | */ 53 | export const routes = [ 54 | new Route('/', 'route-constructor', [ 55 | new Route('/nested', new Route('/spread', 'spread'), [ 56 | new Route('/arr', 'arr'), 57 | ]), 58 | new Route( 59 | '/middleware', 60 | 'route-constructor-middleware', 61 | (ctx) => { 62 | ctx.spreadMiddlewareHit = true 63 | }, 64 | [ 65 | (ctx) => { 66 | ctx.arrMiddlewareHit = true 67 | }, 68 | ] 69 | ), 70 | ]), 71 | ] 72 | -------------------------------------------------------------------------------- /packages/router/__tests__/routing/similar.js: -------------------------------------------------------------------------------- 1 | import ko from 'knockout' 2 | 3 | ko.components.register('similar', { 4 | viewModel: class SimilarRoutesTest { 5 | constructor({ t, done, params }) { 6 | t.notOk(params.foo, 'should use most restrictive route') 7 | done() 8 | } 9 | }, 10 | }) 11 | 12 | export const path = '/similar/foo/bar' 13 | 14 | export const routes = { 15 | '/similar/:foo/:bar': 'similar', 16 | '/similar/foo/:bar': 'similar', 17 | } 18 | -------------------------------------------------------------------------------- /packages/router/__tests__/with.js: -------------------------------------------------------------------------------- 1 | import ko from 'knockout' 2 | 3 | import { Router } from '../' 4 | 5 | ko.components.register('with', { 6 | template: ' ', 7 | viewModel: class With { 8 | constructor({ t, done }) { 9 | Router.useRoutes({ 10 | '/a': 'a', 11 | '/b': 'b', 12 | }) 13 | 14 | history.pushState(null, null, '/a') 15 | 16 | ko.components.register('a', { 17 | viewModel: class { 18 | constructor(ctx) { 19 | ctx.router.update('/b', { with: { foo: 'foo' } }) 20 | } 21 | }, 22 | }) 23 | 24 | ko.components.register('b', { 25 | viewModel: class { 26 | constructor(ctx) { 27 | t.equals(ctx.foo, 'foo', 'can pass data using with') 28 | done() 29 | } 30 | }, 31 | }) 32 | } 33 | }, 34 | }) 35 | -------------------------------------------------------------------------------- /packages/router/docs/README.md: -------------------------------------------------------------------------------- 1 | #### Table of Contents 2 | 3 | - [Basic Usage](./basic.md) 4 | - [Context](./context.md) 5 | - [Router](./router.md) 6 | - [Middleware](./middleware.md) 7 | - [Nested Routing](./nested.md) 8 | - [Path Binding](./path-binding.md) 9 | - [Plugins](./plugins.md) 10 | - [Best Practices](./best-practices.md) 11 | - [TypeScript Support](./typescript.md) 12 | - [Wiki (Usage Examples)](https://github.com/Profiscience/knockout-contrib/wiki/router) 13 | 14 | #### Further Reading 15 | 16 | - [Ali (KnockoutJS SPA Boilerplate)](https://github.com/caseyWebb/ali) 17 | - [Building a Better Router](https://medium.com/@notCaseyWebb/building-a-better-router-ef42896e2e5a) 18 | -------------------------------------------------------------------------------- /packages/router/docs/utils.md: -------------------------------------------------------------------------------- 1 | # Utils 2 | 3 | ## API 4 | 5 | #### isActivePath({ router, path }): Computed 6 | 7 | Returns computed which is `true` if `path` for `router` is currently active 8 | 9 | #### resolveHref({ router, path }) 10 | 11 | Gets an absolute path for `path` on `router` 12 | 13 | #### traversePath(router, path) 14 | 15 | Resolves `path` in relation to `router` 16 | 17 | ###### Local 18 | 19 | '/foo' route to the `/foo` route on `router` 20 | 21 | ###### Absolute 22 | 23 | '//foo' will route to the `/foo` route on `router.$root` 24 | 25 | ###### Relative 26 | 27 | **parent** 28 | '../foo' will route to the `/foo` route on `router.$parent` 29 | 30 | **child** 31 | './foo' will route to the `/foo` route on `router.$child` 32 | -------------------------------------------------------------------------------- /packages/router/examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 |9 | 10 |11 | 12 |15 | -------------------------------------------------------------------------------- /packages/router/examples/index.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | import { ViewModelConstructorBuilder } from '@profiscience/knockout-contrib-model-builders-view' 3 | import { Router } from '@profiscience/knockout-contrib-router' 4 | 5 | location.hash = '!/' 6 | 7 | class ViewModel extends ViewModelConstructorBuilder { 8 | public examples = [ 9 | 'lazy-loading', 10 | 'loading-animation', 11 | 'path-binding', 12 | 'simple-auth', 13 | 'transition-animation', 14 | ] 15 | public selectedExample = ko.observable('') 16 | public exampleComponent = ko.observable('') 17 | 18 | constructor() { 19 | super() 20 | 21 | this.subscribe(this.selectedExample, async (v) => { 22 | await import(`./${v}`) 23 | this.exampleComponent(v) 24 | }) 25 | } 26 | } 27 | 28 | Router.setConfig({ 29 | base: '/router', 30 | hashbang: true, 31 | }) 32 | 33 | ko.applyBindings(new ViewModel()) 34 | -------------------------------------------------------------------------------- /packages/router/examples/lazy-loading/index.html: -------------------------------------------------------------------------------- 1 |
13 | Refresh the page to select another example 14 |Check the network pane in the dev tools
2 |3 | -------------------------------------------------------------------------------- /packages/router/examples/lazy-loading/index.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | import { RoutePlugin, Router } from '@profiscience/knockout-contrib-router' 3 | 4 | // @ts-ignore 5 | import template from './index.html' 6 | 7 | const lazyLoadPlugin: RoutePlugin = (componentName: string) => [ 8 | // we return an array that the router understands, so first we'll 9 | // include the name of the component 10 | componentName, 11 | 12 | // then some middleware to load that component... 13 | () => { 14 | // bail if already loaded 15 | if (ko.components.isRegistered(componentName)) { 16 | return 17 | } 18 | 19 | // https://webpack.js.org/guides/code-splitting-import/ 20 | return ( 21 | import('./views/' + componentName) 22 | .then((exports) => ko.components.register(componentName, exports)) 23 | // tslint:disable-next-line no-console 24 | .catch((err) => 25 | console.error('Error fetching component', componentName, err) 26 | ) 27 | ) 28 | }, 29 | ] 30 | 31 | Router.usePlugin(lazyLoadPlugin) 32 | 33 | Router.useRoutes({ 34 | '/': 'list', 35 | '/foo': 'foo', 36 | '/bar': 'bar', 37 | '/baz': 'baz', 38 | '/qux': 'qux', 39 | }) 40 | 41 | ko.components.register('lazy-loading', { template }) 42 | -------------------------------------------------------------------------------- /packages/router/examples/lazy-loading/views/bar/index.ts: -------------------------------------------------------------------------------- 1 | export { default as template } from './template.html' 2 | -------------------------------------------------------------------------------- /packages/router/examples/lazy-loading/views/bar/template.html: -------------------------------------------------------------------------------- 1 | bar
2 | 3 |
4 | 5 | back 6 | -------------------------------------------------------------------------------- /packages/router/examples/lazy-loading/views/baz/index.ts: -------------------------------------------------------------------------------- 1 | export { default as template } from './template.html' 2 | -------------------------------------------------------------------------------- /packages/router/examples/lazy-loading/views/baz/template.html: -------------------------------------------------------------------------------- 1 |baz
2 | 3 |
4 | 5 | back 6 | -------------------------------------------------------------------------------- /packages/router/examples/lazy-loading/views/foo/index.ts: -------------------------------------------------------------------------------- 1 | export { default as template } from './template.html' 2 | -------------------------------------------------------------------------------- /packages/router/examples/lazy-loading/views/foo/template.html: -------------------------------------------------------------------------------- 1 |foo
2 | 3 |
4 | 5 | back 6 | -------------------------------------------------------------------------------- /packages/router/examples/lazy-loading/views/list/index.ts: -------------------------------------------------------------------------------- 1 | export { default as template } from './template.html' 2 | -------------------------------------------------------------------------------- /packages/router/examples/lazy-loading/views/list/template.html: -------------------------------------------------------------------------------- 1 | foo 2 |
3 | 4 | bar 5 |
6 | 7 | baz 8 |
9 | 10 | qux 11 |
12 | -------------------------------------------------------------------------------- /packages/router/examples/lazy-loading/views/qux/index.ts: -------------------------------------------------------------------------------- 1 | export { default as template } from './template.html' 2 | -------------------------------------------------------------------------------- /packages/router/examples/lazy-loading/views/qux/template.html: -------------------------------------------------------------------------------- 1 |qux
2 | 3 |
4 | 5 | back 6 | -------------------------------------------------------------------------------- /packages/router/examples/loading-animation/index.html: -------------------------------------------------------------------------------- 1 | 67 | 68 | 78 | 79 |80 | -------------------------------------------------------------------------------- /packages/router/examples/loading-animation/views/bar/index.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | import template from './template.html' 3 | 4 | ko.components.register('bar', { template }) 5 | -------------------------------------------------------------------------------- /packages/router/examples/loading-animation/views/bar/template.html: -------------------------------------------------------------------------------- 1 | bar
2 | 3 |
4 | 5 | go to foo 6 | -------------------------------------------------------------------------------- /packages/router/examples/loading-animation/views/foo/index.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | import template from './template.html' 3 | 4 | ko.components.register('foo', { template }) 5 | -------------------------------------------------------------------------------- /packages/router/examples/loading-animation/views/foo/template.html: -------------------------------------------------------------------------------- 1 |foo
2 | 3 |
4 | 5 | go to bar 6 | -------------------------------------------------------------------------------- /packages/router/examples/path-binding/index.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | These paths exist outside any router, so '/' is good 11 |
12 | /foo 13 | /bar 14 | 15 |16 | -------------------------------------------------------------------------------- /packages/router/examples/path-binding/index.ts: -------------------------------------------------------------------------------- 1 | import ko from 'knockout' 2 | import { Router } from '@profiscience/knockout-contrib-router' 3 | import template from './index.html' 4 | 5 | function createOuterTemplate(foo) { 6 | return ` 7 | ${foo}
8 | 9 | These begin with '/', so they route using the current (containing) router 10 |
11 | /foo 12 | /bar 13 | 14 |
15 |
16 | 17 | This begins with './', so it is routed using the child (adjacent) router 18 |
19 | ./baz 20 | 21 |
22 |
23 | 24 | This begins with '//', so it is routed using the root router 25 |
26 | //${foo}/qux 27 | 28 |29 | ` 30 | } 31 | 32 | function createInnerTemplate(foo) { 33 | return ` 34 | ${foo}
35 | 36 | These begin with '/', so they route using the current (containing) router 37 |
38 | /baz 39 | /qux 40 | ` 41 | } 42 | 43 | ko.components.register('empty', { template: '' }) 44 | 45 | ko.components.register('foo', { template: createOuterTemplate('foo') }) 46 | ko.components.register('bar', { template: createOuterTemplate('bar') }) 47 | ko.components.register('baz', { template: createInnerTemplate('baz') }) 48 | ko.components.register('qux', { template: createInnerTemplate('qux') }) 49 | 50 | Router.useRoutes({ 51 | '/': 'empty', 52 | '/foo': [ 53 | 'foo', 54 | { 55 | '/': 'empty', 56 | '/baz': 'baz', 57 | '/qux': 'qux', 58 | }, 59 | ], 60 | '/bar': [ 61 | 'bar', 62 | { 63 | '/': 'empty', 64 | '/baz': 'baz', 65 | '/qux': 'qux', 66 | }, 67 | ], 68 | }) 69 | 70 | ko.components.register('path-binding', { template }) 71 | -------------------------------------------------------------------------------- /packages/router/examples/simple-auth/index.html: -------------------------------------------------------------------------------- 1 |2 | -------------------------------------------------------------------------------- /packages/router/examples/simple-auth/index.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | import { 3 | Context, 4 | IContext, 5 | Middleware, 6 | Router, 7 | } from '@profiscience/knockout-contrib-router' 8 | import template from './index.html' 9 | 10 | const authMiddleware: Middleware = (ctx: Context & IContext) => { 11 | const isLoginPage = ctx.path === '/login' 12 | const isLoggedIn = sessionStorage.getItem('authenticated') 13 | 14 | if (!isLoggedIn && !isLoginPage) { 15 | ctx.redirect('//login') 16 | } else if (isLoggedIn && isLoginPage) { 17 | ctx.redirect('//') 18 | } 19 | } 20 | 21 | // globally registered auth middleware, runs for every route 22 | Router.use(authMiddleware) 23 | 24 | Router.useRoutes({ 25 | '/': 'home', 26 | '/login': 'login', 27 | '/logout': (ctx) => { 28 | sessionStorage.removeItem('authenticated') 29 | ctx.redirect('/login') 30 | }, 31 | }) 32 | 33 | ko.components.register('home', { 34 | template: 'Logout', 35 | }) 36 | 37 | ko.components.register('login', { 38 | viewModel: class { 39 | public login() { 40 | sessionStorage.setItem('authenticated', 'true') 41 | Router.update('/').catch((err) => console.error('Error navigating', err)) // tslint:disable-line no-console 42 | } 43 | }, 44 | template: ` 45 | Login
46 | 47 | `, 48 | }) 49 | 50 | ko.components.register('simple-auth', { template }) 51 | -------------------------------------------------------------------------------- /packages/router/examples/transition-animation/index.html: -------------------------------------------------------------------------------- 1 | 6 | 7 |8 | -------------------------------------------------------------------------------- /packages/router/examples/transition-animation/index.ts: -------------------------------------------------------------------------------- 1 | import * as $ from 'jquery' 2 | import * as ko from 'knockout' 3 | import { 4 | Context, 5 | IContext, 6 | Middleware, 7 | Router, 8 | } from '@profiscience/knockout-contrib-router' 9 | import template from './index.html' 10 | 11 | const transitionAnimationMiddleware: Middleware = function* ( 12 | ctx: Context & IContext 13 | ): IterableIterator > { 14 | // ctx.element does not exist before render, for obvious reasons. 15 | yield 16 | 17 | // For less obvious reasons, ctx.element is undefined during redirection, 18 | // so we need to add a guard otherwise the following promises will never 19 | // resolve. 20 | if (ctx.element) { 21 | const $el = $(ctx.element) 22 | 23 | yield new Promise((resolve) => $el.fadeIn(resolve)) 24 | yield new Promise((resolve) => $el.fadeOut(resolve)) 25 | } 26 | } 27 | // This is semantically the same, but uses an async generator (which yields a promise) 28 | // instead of yielding a promise directly 29 | // 30 | // const transitionAnimationMiddleware = async function*(ctx: Context & IContext): AsyncIterableIterator { 31 | // yield 32 | 33 | // if (ctx.element) { 34 | // const $el = $(ctx.element) 35 | 36 | // await new Promise((resolve) => $el.fadeIn(resolve)) 37 | // yield 38 | 39 | // await new Promise((resolve) => $el.fadeOut(resolve)) 40 | // yield 41 | // } 42 | // } 43 | 44 | Router.use(transitionAnimationMiddleware) 45 | 46 | Router.useRoutes({ 47 | '/': (ctx: any) => ctx.redirect('/foo'), 48 | '/foo': 'foo', 49 | '/bar': 'bar', 50 | }) 51 | 52 | ko.components.register('foo', { 53 | template: ` 54 | foo
55 | 56 |
57 | 58 | go to bar 59 | `, 60 | }) 61 | ko.components.register('bar', { 62 | template: ` 63 |bar
64 | 65 |
66 | 67 | go to foo 68 | `, 69 | }) 70 | 71 | ko.components.register('transition-animation', { template }) 72 | -------------------------------------------------------------------------------- /packages/router/karma.conf.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | const path = require('path') 6 | 7 | const { DEBUG } = process.env 8 | 9 | module.exports = (config) => { 10 | config.set({ 11 | basePath: __dirname, 12 | 13 | plugins: [ 14 | require('karma-firefox-launcher'), 15 | require('karma-tap-pretty-reporter'), 16 | require('karma-tap'), 17 | require('karma-webpack'), 18 | require('karma-remap-istanbul'), 19 | ], 20 | 21 | frameworks: ['tap'], 22 | 23 | files: ['__tests__/index.js'], 24 | 25 | preprocessors: { 26 | '__tests__/index.js': 'webpack', 27 | }, 28 | 29 | browsers: ['_Firefox'], 30 | 31 | browserConsoleLogOptions: { 32 | terminal: false, 33 | }, 34 | 35 | customLaunchers: { 36 | _Firefox: { 37 | base: 'Firefox', 38 | flags: ['-private'], 39 | }, 40 | }, 41 | 42 | // to debug, comment out singleRun, and uncomment autoWatch 43 | singleRun: !DEBUG, 44 | autoWatch: DEBUG, 45 | 46 | reporters: ['tap-pretty', 'karma-remap-istanbul'], 47 | 48 | tapReporter: { 49 | // prettify: require('tap-diff') 50 | }, 51 | 52 | remapIstanbulReporter: { 53 | reports: { 54 | lcovonly: 'coverage/lcov.info', 55 | html: 'coverage/html', 56 | }, 57 | }, 58 | 59 | webpack: { 60 | context: __dirname, 61 | mode: 'development', 62 | node: { 63 | fs: 'empty', 64 | }, 65 | devtool: 'inline-source-map', 66 | module: { 67 | rules: [ 68 | { 69 | test: /\.js$/, 70 | use: { 71 | loader: 'istanbul-instrumenter-loader', 72 | options: { esModules: true }, 73 | }, 74 | include: path.resolve('dist'), 75 | }, 76 | ], 77 | }, 78 | }, 79 | 80 | webpackMiddleware: {}, 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /packages/router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-router", 3 | "version": "2.1.5", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/router", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "scripts": { 13 | "test": "karma start" 14 | }, 15 | "main": "dist/node/index.js", 16 | "module": "dist/default/index.js", 17 | "esnext": "dist/esnext/index.js", 18 | "types": "src/index.ts", 19 | "peerDependencies": { 20 | "knockout": "3.5.1" 21 | }, 22 | "dependencies": { 23 | "babel-runtime": "^6.26.0", 24 | "path-to-regexp": "^6.0.0" 25 | }, 26 | "devDependencies": { 27 | "istanbul-instrumenter-loader": "^3.0.1", 28 | "karma": "^6.0.1", 29 | "karma-firefox-launcher": "^2.0.0", 30 | "karma-remap-istanbul": "^0.6.0", 31 | "karma-tap": "^4.0.0", 32 | "karma-tap-pretty-reporter": "^4.0.0", 33 | "karma-webpack": "^5.0.0", 34 | "tap-diff": "^0.1.1", 35 | "tape": "^5.0.0", 36 | "webpack": "^4.33.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/router/src/bindings/active-path.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | import { Router } from '../router' 3 | import { 4 | isActivePath, 5 | traversePath, 6 | getRouterForBindingContext, 7 | log, 8 | } from '../utils' 9 | 10 | export const activePathBinding: ko.BindingHandler = { 11 | init(el, valueAccessor, allBindings, viewModel, bindingCtx) { 12 | const activePathCSSClass = 13 | allBindings.get('pathActiveClass') || Router.config.activePathCSSClass 14 | const path = ko.unwrap(valueAccessor()) 15 | 16 | Router.initialized 17 | .then(() => { 18 | const router = getRouterForBindingContext(bindingCtx) 19 | const route = ko.pureComputed(() => traversePath(router, path)) 20 | ko.applyBindingsToNode( 21 | el, 22 | { 23 | css: { 24 | [activePathCSSClass]: ko.pureComputed(() => 25 | isActivePath(route()) 26 | ), 27 | }, 28 | }, 29 | bindingCtx 30 | ) 31 | }) 32 | .catch((err) => log.error('Error initializing activePath binding', err)) 33 | }, 34 | } 35 | 36 | ko.bindingHandlers.activePath = activePathBinding 37 | -------------------------------------------------------------------------------- /packages/router/src/bindings/index.ts: -------------------------------------------------------------------------------- 1 | import './path' 2 | import './active-path' 3 | -------------------------------------------------------------------------------- /packages/router/src/bindings/path.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | import { Router } from '../router' 3 | import { 4 | resolveHref, 5 | traversePath, 6 | getRouterForBindingContext, 7 | log, 8 | } from '../utils' 9 | import { activePathBinding } from './active-path' 10 | 11 | export const pathBinding: ko.BindingHandler = { 12 | init(el, valueAccessor, allBindings, viewModel, bindingCtx) { 13 | ;(activePathBinding.init as any).call( 14 | this, 15 | el, 16 | valueAccessor, 17 | allBindings, 18 | viewModel, 19 | bindingCtx 20 | ) 21 | 22 | const path = ko.unwrap(valueAccessor()) 23 | 24 | Router.initialized 25 | .then(() => { 26 | const router = getRouterForBindingContext(bindingCtx) 27 | const route = ko.pureComputed(() => traversePath(router, path)) 28 | ko.applyBindingsToNode( 29 | el, 30 | { 31 | attr: { 32 | href: ko.pureComputed(() => resolveHref(route())), 33 | }, 34 | }, 35 | bindingCtx 36 | ) 37 | }) 38 | .catch((err) => log.error('Error initializing path binding', err)) 39 | }, 40 | } 41 | 42 | ko.bindingHandlers.path = pathBinding 43 | -------------------------------------------------------------------------------- /packages/router/src/index.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable no-empty-interface */ 2 | 3 | import './bindings' 4 | import './component' 5 | 6 | export * from './context' 7 | export * from './route' 8 | export * from './router' 9 | export { isActivePath, resolveHref } from './utils' 10 | 11 | /** 12 | * IContext exists for the sole purpose that classes do not support declaration 13 | * merging. 14 | * 15 | * See https://github.com/Microsoft/TypeScript/issues/9532 for why this is here, and 16 | * not ./context.ts where it belongs... 17 | * 18 | * tl;dr, re-exported interfaces can't be declared w/ the top level name, so 19 | * to augment (declaration merging) IContext in consumer code, you'd need to do... 20 | * 21 | * declare module "@profiscience/knockout-contrib-router/dist/typings/context" { 22 | * // ... 23 | * } 24 | * 25 | * and IMO that's just bad; at least way worse than this. 26 | */ 27 | export interface IContext {} 28 | export interface IRouteConfig {} 29 | -------------------------------------------------------------------------------- /packages/utils.assign/index.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | import { fromJS } from '@profiscience/knockout-contrib-utils-from-js' 3 | 4 | export type MergeOptions = { 5 | mapArrayElements?: boolean 6 | strict?: boolean 7 | } 8 | 9 | export function assign< 10 | TSrc extends Record, 11 | TDest extends Record 12 | >( 13 | dest: TDest, 14 | src: TSrc, 15 | opts: MergeOptions = { mapArrayElements: false, strict: false } 16 | ): Record { 17 | const props = Object.keys(src) as (keyof TSrc)[] 18 | 19 | const ret = dest as Record 20 | 21 | for (const prop of props) { 22 | if (isUndefined(ret[prop])) { 23 | if (opts.strict) { 24 | ret[prop] = src[prop] 25 | } else { 26 | ret[prop] = fromJS( 27 | src[prop], 28 | (src[prop] as any) instanceof Array && opts.mapArrayElements 29 | ) 30 | } 31 | } else if (ko.isObservable(ret[prop])) { 32 | // skip non-writable computeds 33 | if (!ko.isWriteableObservable(ret[prop])) { 34 | continue 35 | } 36 | ret[prop]( 37 | (src[prop] as any) instanceof Array && opts.mapArrayElements 38 | ? ko.unwrap(fromJS(src[prop], true)) 39 | : src[prop] 40 | ) 41 | } else if (src[prop] && src[prop].constructor === Object) { 42 | // tslint:disable-next-line 43 | if (ret[prop] === null) { 44 | ret[prop] = {} 45 | } 46 | assign(ret[prop], src[prop], opts) 47 | } else { 48 | ret[prop] = src[prop] 49 | } 50 | } 51 | 52 | return ret 53 | } 54 | 55 | function isUndefined(foo: any): boolean { 56 | return typeof foo === 'undefined' 57 | } 58 | -------------------------------------------------------------------------------- /packages/utils.assign/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-utils-assign", 3 | "version": "2.0.6", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/utils.assign", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.d.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "knockout": "3.5.1" 19 | }, 20 | "dependencies": { 21 | "@profiscience/knockout-contrib-utils-from-js": "^2.0.4", 22 | "babel-runtime": "^6.26.0" 23 | }, 24 | "devDependencies": { 25 | "@profiscience/knockout-contrib-jest-matchers": "^2.0.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/utils.defaults/README.md: -------------------------------------------------------------------------------- 1 | # utils.defaults 2 | 3 | [![Version][npm-version-shield]][npm] 4 | [![Dependency Status][david-dm-shield]][david-dm] 5 | [![Peer Dependency Status][david-dm-peer-shield]][david-dm-peer] 6 | [![Dev Dependency Status][david-dm-dev-shield]][david-dm-dev] 7 | [![Downloads][npm-stats-shield]][npm-stats] 8 | 9 | [david-dm]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/utils.defaults 10 | [david-dm-shield]: https://david-dm.org/Profiscience/knockout-contrib/status.svg?path=packages/utils.defaults 11 | [david-dm-peer]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/utils.defaults&type=peer 12 | [david-dm-peer-shield]: https://david-dm.org/Profiscience/knockout-contrib/peer-status.svg?path=packages/utils.defaults 13 | [david-dm-dev]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/utils.defaults&type=dev 14 | [david-dm-dev-shield]: https://david-dm.org/Profiscience/knockout-contrib/dev-status.svg?path=packages/utils.defaults 15 | [npm]: https://www.npmjs.com/package/@profiscience/knockout-contrib-utils-defaults 16 | [npm-version-shield]: https://img.shields.io/npm/v/@profiscience/knockout-contrib-utils-defaults.svg 17 | [npm-stats]: http://npm-stat.com/charts.html?package=@profiscience/knockout-contrib-utils-defaults&author=&from=&to= 18 | [npm-stats-shield]: https://img.shields.io/npm/dt/@profiscience/knockout-contrib-utils-defaults.svg?maxAge=2592000 19 | 20 | > This package is intended for consumption via the [@profiscience/knockout-contrib] metapackage 21 | 22 | ## Usage 23 | 24 | > defaults(dest, defaultValues[, mapArrays = false]) 25 | 26 | Creates observables for enumerable properties of `defaultValues` where undefined in the destination object. 27 | 28 | If `mapArrayElements` is true, array elements will be created using [utils.fromJS](../utils.fromJS). 29 | 30 | ```javascript 31 | import { defaults } from '@profiscience/knockout-contrib' 32 | 33 | const foos = { foo: 'foo' } 34 | defaults(foos, { foo: 'bar', bar: 'bar' }) 35 | 36 | foos() 37 | // { foo: 'foo', bar: 'bar' } 38 | ``` 39 | -------------------------------------------------------------------------------- /packages/utils.defaults/index.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | import { fromJS } from '@profiscience/knockout-contrib-utils-from-js' 3 | 4 | export function defaults< 5 | TSrc extends Record , 6 | TDest extends { 7 | [P in keyof TSrc]?: TSrc[P] extends [] 8 | ? ko.ObservableArray 9 | : ko.Observable 10 | } 11 | >(dest: TDest, defaultValues: TSrc, mapArraysDeep = false): TDest { 12 | for (const prop in defaultValues) { 13 | if (defaultValues.hasOwnProperty(prop)) { 14 | const maybeDestObservable = dest[prop] 15 | if (isUndefined(maybeDestObservable)) { 16 | dest[prop] = fromJS( 17 | defaultValues[prop], 18 | (defaultValues[prop] as any) instanceof Array && mapArraysDeep 19 | ) 20 | } else if ( 21 | ko.isObservable(maybeDestObservable) && 22 | isUndefined(maybeDestObservable()) 23 | ) { 24 | maybeDestObservable(defaultValues[prop]) 25 | } 26 | } 27 | } 28 | return dest 29 | } 30 | 31 | function isUndefined(foo: any): boolean { 32 | return typeof foo === 'undefined' 33 | } 34 | -------------------------------------------------------------------------------- /packages/utils.defaults/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-utils-defaults", 3 | "version": "2.1.4", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/utils.defaults", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.d.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "knockout": "3.5.1" 19 | }, 20 | "dependencies": { 21 | "@profiscience/knockout-contrib-utils-from-js": "^2.0.4", 22 | "babel-runtime": "^6.26.0" 23 | }, 24 | "devDependencies": { 25 | "@profiscience/knockout-contrib-jest-matchers": "^2.0.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/utils.defaults/test.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | import '@profiscience/knockout-contrib-jest-matchers' 3 | import { defaults } from './index' 4 | 5 | test('assigns default values', () => { 6 | const actual = { 7 | foo: 'foo', 8 | bar: ko.observable('bar'), 9 | qux: ko.observable(undefined), 10 | } as any 11 | 12 | defaults(actual, { 13 | foo: 'default', 14 | bar: 'default', 15 | baz: 'default', 16 | qux: 'default', 17 | }) 18 | 19 | expect(actual.foo).toBe('foo') 20 | expect(actual.bar()).toBe('bar') 21 | expect(actual.baz()).toBe('default') 22 | expect(actual.qux()).toBe('default') 23 | }) 24 | 25 | const testArrays = ( 26 | mapArraysArg: undefined | boolean | null, 27 | shouldMapArrays: boolean 28 | ) => () => { 29 | const actual: any = {} 30 | 31 | defaults(actual, { foo: ['foo'] }, mapArraysArg as boolean) 32 | 33 | expect(actual.foo).toBeObservable() 34 | 35 | shouldMapArrays 36 | ? expect(actual.foo()[0]).toBeObservable() 37 | : expect(actual.foo()[0]).not.toBeObservable() 38 | 39 | expect(ko.toJS(actual.foo()[0])).toBe('foo') 40 | } 41 | 42 | test( 43 | 'creates shallow arrays when 3rd arg is undefined', 44 | testArrays(undefined, false) 45 | ) 46 | test('creates shallow arrays when 3rd arg is falsy', testArrays(null, false)) 47 | test('creates shallow arrays when 3rd arg is false', testArrays(false, false)) 48 | test('creates deep arrays when 3rd arg is true', testArrays(true, true)) 49 | -------------------------------------------------------------------------------- /packages/utils.fromJS/benchmark.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | import * as benchmark from 'benchmark' 3 | import 'knockout-mapping' 4 | import { padEnd } from 'lodash' 5 | import { fromJS } from './index' 6 | 7 | const obj = { 8 | foo: { 9 | bar: { 10 | baz: { 11 | qux: true, 12 | }, 13 | }, 14 | }, 15 | foos: [{ foo: true }, { bar: true }, { baz: true }, { qux: true }], 16 | } 17 | 18 | const suite = new benchmark.Suite() 19 | 20 | suite 21 | .add(padEnd('utils.fromJS', 40), () => { 22 | fromJS({ ...obj }) 23 | }) 24 | .add(padEnd('utils.fromJS (mapArrayElements)', 40), () => { 25 | fromJS({ ...obj }, true) 26 | }) 27 | .add(padEnd('mapping.fromJS', 40), () => { 28 | ;(ko as any).mapping.fromJS({ ...obj }) 29 | }) 30 | .on('cycle', (e: any) => { 31 | // tslint:disable:no-console 32 | if (e.target.error) { 33 | console.error(e.target.error) 34 | process.exit(1) 35 | return 36 | } 37 | console.log(e.target.toString()) 38 | }) 39 | .run() 40 | -------------------------------------------------------------------------------- /packages/utils.fromJS/index.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | 3 | type ObservableTree = T extends any[] 4 | ? ko.ObservableArray 5 | : T extends Date 6 | ? ko.Observable 7 | : T extends RegExp 8 | ? ko.Observable 9 | : T extends ko.Observable 10 | ? T 11 | : T extends ko.ObservableArray 12 | ? T 13 | : T extends ko.Computed 14 | ? T 15 | : T extends (...args: any[]) => any 16 | ? T 17 | : T extends { [k: string]: any } 18 | ? { readonly [P in keyof T]: ObservableTree } 19 | : ko.Observable 20 | 21 | export function fromJS (obj: T, mapArrayElements = false): ObservableTree { 22 | if (ko.isObservable(obj)) { 23 | return obj as any 24 | } else if (Array.isArray(obj)) { 25 | return ko.observableArray( 26 | mapArrayElements ? obj.map((v) => fromJS(v, mapArrayElements)) : obj 27 | ) as any 28 | } else if (obj instanceof Date || obj instanceof RegExp) { 29 | return ko.observable(obj) as any 30 | } else if (obj instanceof Function) { 31 | return obj as any 32 | } else if (obj instanceof Object) { 33 | const obs = Object.create(Object.getPrototypeOf(obj)) 34 | for (const p of Object.keys(obj)) { 35 | obs[p] = fromJS((obj as any)[p], mapArrayElements) 36 | } 37 | return obs 38 | } else { 39 | return ko.observable(obj) as any 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/utils.fromJS/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-utils-from-js", 3 | "version": "2.0.4", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/utils.fromJS", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "scripts": { 13 | "bench": "ts-node ./benchmark.ts" 14 | }, 15 | "main": "dist/node/index.js", 16 | "module": "dist/default/index.js", 17 | "esnext": "dist/esnext/index.js", 18 | "types": "index.d.ts", 19 | "sideEffects": false, 20 | "peerDependencies": { 21 | "knockout": "3.5.1" 22 | }, 23 | "devDependencies": { 24 | "@profiscience/knockout-contrib-jest-matchers": "^2.0.4", 25 | "knockout-mapping": "^2.6.0" 26 | }, 27 | "dependencies": { 28 | "babel-runtime": "^6.26.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/utils.increment/README.md: -------------------------------------------------------------------------------- 1 | # utils.increment / utils.decrement 2 | 3 | [![Version][npm-version-shield]][npm] 4 | [![Dependency Status][david-dm-shield]][david-dm] 5 | [![Peer Dependency Status][david-dm-peer-shield]][david-dm-peer] 6 | [![Dev Dependency Status][david-dm-dev-shield]][david-dm-dev] 7 | [![Downloads][npm-stats-shield]][npm-stats] 8 | 9 | [david-dm]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/utils.increment 10 | [david-dm-shield]: https://david-dm.org/Profiscience/knockout-contrib/status.svg?path=packages/utils.increment 11 | [david-dm-peer]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/utils.increment&type=peer 12 | [david-dm-peer-shield]: https://david-dm.org/Profiscience/knockout-contrib/peer-status.svg?path=packages/utils.increment 13 | [david-dm-dev]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/utils.increment&type=dev 14 | [david-dm-dev-shield]: https://david-dm.org/Profiscience/knockout-contrib/dev-status.svg?path=packages/utils.increment 15 | [npm]: https://www.npmjs.com/package/@profiscience/knockout-contrib-utils-increment 16 | [npm-version-shield]: https://img.shields.io/npm/v/@profiscience/knockout-contrib-utils-increment.svg 17 | [npm-stats]: http://npm-stat.com/charts.html?package=@profiscience/knockout-contrib-utils-increment&author=&from=&to= 18 | [npm-stats-shield]: https://img.shields.io/npm/dt/@profiscience/knockout-contrib-utils-increment.svg?maxAge=2592000 19 | 20 | > This package is intended for consumption via the [@profiscience/knockout-contrib] metapackage. 21 | 22 | Increments/decrements a numeric observable. Returns the new value. 23 | 24 | ## Usage 25 | 26 | ```javascript 27 | import { increment, decrement } from '@profiscience/knockout-contrib' 28 | 29 | const foo = ko.observable(0) 30 | 31 | increment(foo) // 1 32 | increment(foo, 3) // 4 33 | decrement(foo) // 3 34 | decrement(foo, 2) // 1 35 | ``` 36 | -------------------------------------------------------------------------------- /packages/utils.increment/index.ts: -------------------------------------------------------------------------------- 1 | export function increment( 2 | obs: ko.Observable | ko.Computed , 3 | amt = 1 4 | ): number { 5 | const v = obs() + amt 6 | obs(v) 7 | return v 8 | } 9 | 10 | export function decrement( 11 | obs: ko.Observable | ko.Computed , 12 | amt = 1 13 | ): number { 14 | return increment(obs, -amt) 15 | } 16 | -------------------------------------------------------------------------------- /packages/utils.increment/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-utils-increment", 3 | "version": "2.0.1", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/utils.increment", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.d.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "knockout": "3.5.1" 19 | }, 20 | "dependencies": { 21 | "babel-runtime": "^6.26.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/utils.increment/test.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | import { increment, decrement } from './index' 3 | 4 | describe('utils.increment', () => { 5 | test('increment by 1 by default', () => { 6 | const count = ko.observable(0) 7 | increment(count) 8 | expect(count()).toBe(1) 9 | }) 10 | 11 | test('increment by n', () => { 12 | const count = ko.observable(0) 13 | increment(count, 2) 14 | expect(count()).toBe(2) 15 | }) 16 | 17 | test('increment writable computeds', (done) => { 18 | const count = ko.pureComputed({ 19 | read: () => 0, 20 | write: (v) => { 21 | expect(v).toBe(1) 22 | done() 23 | }, 24 | }) 25 | increment(count) 26 | }) 27 | 28 | test('increment throws with non writable computeds', () => { 29 | const count = ko.pureComputed(() => 0) 30 | expect(() => increment(count)).toThrow() 31 | }) 32 | 33 | test('returns the new value', () => { 34 | const count = ko.observable(0) 35 | const v = increment(count) 36 | expect(v).toBe(count()) 37 | }) 38 | }) 39 | 40 | describe('utils.decrement', () => { 41 | test('decrement by 1 by default', () => { 42 | const count = ko.observable(0) 43 | decrement(count) 44 | expect(count()).toBe(-1) 45 | }) 46 | 47 | test('decrement by n', () => { 48 | const count = ko.observable(0) 49 | decrement(count, 2) 50 | expect(count()).toBe(-2) 51 | }) 52 | 53 | test('decrement writable computeds', (done) => { 54 | const count = ko.pureComputed({ 55 | read: () => 0, 56 | write: (v) => { 57 | expect(v).toBe(-1) 58 | done() 59 | }, 60 | }) 61 | decrement(count) 62 | }) 63 | 64 | test('decrement throws with non writable computeds', () => { 65 | const count = ko.pureComputed(() => 0) 66 | expect(() => decrement(count)).toThrow() 67 | }) 68 | 69 | test('returns the new value', () => { 70 | const count = ko.observable(0) 71 | const v = decrement(count) 72 | expect(v).toBe(count()) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /packages/utils.modify/README.md: -------------------------------------------------------------------------------- 1 | # utils.modify 2 | 3 | [![Version][npm-version-shield]][npm] 4 | [![Dependency Status][david-dm-shield]][david-dm] 5 | [![Peer Dependency Status][david-dm-peer-shield]][david-dm-peer] 6 | [![Dev Dependency Status][david-dm-dev-shield]][david-dm-dev] 7 | [![Downloads][npm-stats-shield]][npm-stats] 8 | 9 | [david-dm]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/utils.modify 10 | [david-dm-shield]: https://david-dm.org/Profiscience/knockout-contrib/status.svg?path=packages/utils.modify 11 | [david-dm-peer]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/utils.modify&type=peer 12 | [david-dm-peer-shield]: https://david-dm.org/Profiscience/knockout-contrib/peer-status.svg?path=packages/utils.modify 13 | [david-dm-dev]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/utils.modify&type=dev 14 | [david-dm-dev-shield]: https://david-dm.org/Profiscience/knockout-contrib/dev-status.svg?path=packages/utils.modify 15 | [npm]: https://www.npmjs.com/package/@profiscience/knockout-contrib-utils-modify 16 | [npm-version-shield]: https://img.shields.io/npm/v/@profiscience/knockout-contrib-utils-modify.svg 17 | [npm-stats]: http://npm-stat.com/charts.html?package=@profiscience/knockout-contrib-utils-modify&author=&from=&to= 18 | [npm-stats-shield]: https://img.shields.io/npm/dt/@profiscience/knockout-contrib-utils-modify.svg?maxAge=2592000 19 | 20 | > This package is intended for consumption via the [@profiscience/knockout-contrib] metapackage 21 | 22 | Modifies an observable using a transform function. Returns the new value. 23 | 24 | Useful for wrapping general purpose utilities functions for Knockout observables. 25 | 26 | ## Usage 27 | 28 | ```javascript 29 | import { modify } from '@profiscience/knockout-contrib' 30 | 31 | const foobar = ko.observable('foobar') 32 | 33 | function reverseString(str) { 34 | return str.split('').reverse().join('') 35 | } 36 | 37 | const ret = modify(foobar, reverseString) 38 | ;(foobar() === ret) === 'raboof' 39 | ``` 40 | -------------------------------------------------------------------------------- /packages/utils.modify/index.ts: -------------------------------------------------------------------------------- 1 | export function modify ( 2 | obs: ko.Observable | ko.Computed , 3 | fn: (v: T) => T 4 | ): T { 5 | const v = fn(obs()) 6 | obs(v) 7 | return v 8 | } 9 | -------------------------------------------------------------------------------- /packages/utils.modify/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-utils-modify", 3 | "version": "2.0.1", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/utils.modify", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.d.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "knockout": "3.5.1" 19 | }, 20 | "dependencies": { 21 | "babel-runtime": "^6.26.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/utils.modify/test.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | import { modify } from './index' 3 | 4 | describe('utils.modify', () => { 5 | test('works with observables', () => { 6 | const str = ko.observable('foobar') 7 | modify(str, reverseString) 8 | expect(str()).toBe('raboof') 9 | }) 10 | test('works with observable arrays', () => { 11 | const arr = ko.observableArray(['foo', 'bar']) 12 | modify(arr, (v) => v.reverse()) 13 | expect(arr()).toEqual(['bar', 'foo']) 14 | }) 15 | test('works with (writeable) computeds', () => { 16 | const _str = ko.observable('foobar') 17 | const str = ko.pureComputed({ 18 | read: () => _str(), 19 | write: (v) => _str(v), 20 | }) 21 | modify(str, reverseString) 22 | expect(str()).toBe('raboof') 23 | }) 24 | test('returns the new value', () => { 25 | const foo = ko.observable('foo') 26 | expect(modify(foo, () => 'bar')).toBe('bar') 27 | }) 28 | }) 29 | 30 | function reverseString(str: string) { 31 | return str.split('').reverse().join('') 32 | } 33 | -------------------------------------------------------------------------------- /packages/utils.once/README.md: -------------------------------------------------------------------------------- 1 | # utils.once 2 | 3 | [![Version][npm-version-shield]][npm] 4 | [![Dependency Status][david-dm-shield]][david-dm] 5 | [![Peer Dependency Status][david-dm-peer-shield]][david-dm-peer] 6 | [![Dev Dependency Status][david-dm-dev-shield]][david-dm-dev] 7 | [![Downloads][npm-stats-shield]][npm-stats] 8 | 9 | [david-dm]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/utils.once 10 | [david-dm-shield]: https://david-dm.org/Profiscience/knockout-contrib/status.svg?path=packages/utils.once 11 | [david-dm-peer]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/utils.once&type=peer 12 | [david-dm-peer-shield]: https://david-dm.org/Profiscience/knockout-contrib/peer-status.svg?path=packages/utils.once 13 | [david-dm-dev]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/utils.once&type=dev 14 | [david-dm-dev-shield]: https://david-dm.org/Profiscience/knockout-contrib/dev-status.svg?path=packages/utils.once 15 | [npm]: https://www.npmjs.com/package/@profiscience/knockout-contrib-utils-once 16 | [npm-version-shield]: https://img.shields.io/npm/v/@profiscience/knockout-contrib-utils-once.svg 17 | [npm-stats]: http://npm-stat.com/charts.html?package=@profiscience/knockout-contrib-utils-once&author=&from=&to= 18 | [npm-stats-shield]: https://img.shields.io/npm/dt/@profiscience/knockout-contrib-utils-once.svg?maxAge=2592000 19 | 20 | > This package is intended for consumption via the [@profiscience/knockout-contrib] metapackage 21 | 22 | Creates a subscription that is called once and then disposed. Returns subscription for early disposal if necessary. 23 | 24 | ## Usage 25 | 26 | ```javascript 27 | import { once } from '@profiscience/knockout-contrib' 28 | 29 | const foo = ko.observable(0) 30 | 31 | const sub = once(foo, () => console.log('hit!')) 32 | 33 | foo(1) 34 | // hit! 35 | 36 | foo(2) 37 | // nothing... 38 | ``` 39 | -------------------------------------------------------------------------------- /packages/utils.once/index.ts: -------------------------------------------------------------------------------- 1 | export function once ( 2 | obs: ko.Observable | ko.Computed , 3 | fn: (v: T) => void 4 | ): ko.Subscription { 5 | const killMe = obs.subscribe((v) => { 6 | killMe.dispose() 7 | fn(v) 8 | }) 9 | return killMe 10 | } 11 | -------------------------------------------------------------------------------- /packages/utils.once/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-utils-once", 3 | "version": "2.0.1", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/utils.once", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.d.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "knockout": "3.5.1" 19 | }, 20 | "dependencies": { 21 | "babel-runtime": "^6.26.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/utils.once/test.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | import { once } from './index' 3 | 4 | test('subscribes once, then disposes', () => { 5 | expect.assertions(4) 6 | 7 | const observable = ko.observable('') 8 | const observableArray = ko.observableArray ([]) 9 | const computed = ko.computed(() => observable()) 10 | const pureComputed = ko.pureComputed(() => observable()) 11 | 12 | const hit = (v: any) => expect(v).toContain('foo') 13 | 14 | once(observable, hit) 15 | once(computed, hit) 16 | once(pureComputed, hit) 17 | once(observableArray, hit) 18 | 19 | observable('foo') 20 | observableArray(['foo']) 21 | 22 | // expect.assertions(4) would cause these to fail 23 | observable('bar') 24 | observableArray.push('bar') 25 | }) 26 | 27 | test('returns the subscription', (done) => { 28 | expect.assertions(1) 29 | 30 | const observable = ko.observable('') 31 | const hit = jest.fn() 32 | 33 | const sub = once(observable, hit) 34 | 35 | sub.dispose() 36 | 37 | observable('foo') 38 | 39 | once(observable, () => { 40 | expect(hit).not.toBeCalled() 41 | done() 42 | }) 43 | 44 | observable('bar') 45 | }) 46 | -------------------------------------------------------------------------------- /packages/utils.toggle/README.md: -------------------------------------------------------------------------------- 1 | # utils.toggle 2 | 3 | [![Version][npm-version-shield]][npm] 4 | [![Dependency Status][david-dm-shield]][david-dm] 5 | [![Peer Dependency Status][david-dm-peer-shield]][david-dm-peer] 6 | [![Dev Dependency Status][david-dm-dev-shield]][david-dm-dev] 7 | [![Downloads][npm-stats-shield]][npm-stats] 8 | 9 | [david-dm]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/utils.toggle 10 | [david-dm-shield]: https://david-dm.org/Profiscience/knockout-contrib/status.svg?path=packages/utils.toggle 11 | [david-dm-peer]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/utils.toggle&type=peer 12 | [david-dm-peer-shield]: https://david-dm.org/Profiscience/knockout-contrib/peer-status.svg?path=packages/utils.toggle 13 | [david-dm-dev]: https://david-dm.org/Profiscience/knockout-contrib?path=packages/utils.toggle&type=dev 14 | [david-dm-dev-shield]: https://david-dm.org/Profiscience/knockout-contrib/dev-status.svg?path=packages/utils.toggle 15 | [npm]: https://www.npmjs.com/package/@profiscience/knockout-contrib-utils-toggle 16 | [npm-version-shield]: https://img.shields.io/npm/v/@profiscience/knockout-contrib-utils-toggle.svg 17 | [npm-stats]: http://npm-stat.com/charts.html?package=@profiscience/knockout-contrib-utils-toggle&author=&from=&to= 18 | [npm-stats-shield]: https://img.shields.io/npm/dt/@profiscience/knockout-contrib-utils-toggle.svg?maxAge=2592000 19 | 20 | > This package is intended for consumption via the [@profiscience/knockout-contrib] metapackage 21 | 22 | Toggles a boolean observable 23 | 24 | ## Usage 25 | 26 | ```javascript 27 | import { toggle } from '@profiscience/knockout-contrib' 28 | 29 | const foo = ko.observable(true) 30 | 31 | toggle(foo) // false 32 | toggle(foo) // true 33 | ``` 34 | -------------------------------------------------------------------------------- /packages/utils.toggle/index.ts: -------------------------------------------------------------------------------- 1 | export function toggle( 2 | obs: ko.Observable | ko.Computed 3 | ): boolean { 4 | const orig = obs() 5 | const v = !orig 6 | if (typeof (orig as any) !== 'boolean') { 7 | throw new Error( 8 | `[@profiscience/knockout-contrib/utils.toggle]: Can only toggle boolean observables (type is ${typeof orig}` 9 | ) 10 | } 11 | obs(v) 12 | return v 13 | } 14 | -------------------------------------------------------------------------------- /packages/utils.toggle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@profiscience/knockout-contrib-utils-toggle", 3 | "version": "2.0.1", 4 | "license": "WTFPL", 5 | "author": "Casey Webb (https://caseyWebb.xyz)", 6 | "homepage": "https://profiscience.github.io/knockout-contrib/packages/utils.toggle", 7 | "bugs": "https://github.com/Profiscience/knockout-contrib/issues", 8 | "repository": "github:Profiscience/knockout-contrib", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "dist/node/index.js", 13 | "module": "dist/default/index.js", 14 | "esnext": "dist/esnext/index.js", 15 | "types": "index.d.ts", 16 | "sideEffects": false, 17 | "peerDependencies": { 18 | "knockout": "3.5.1" 19 | }, 20 | "dependencies": { 21 | "babel-runtime": "^6.26.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/utils.toggle/test.ts: -------------------------------------------------------------------------------- 1 | import * as ko from 'knockout' 2 | import { toggle } from './index' 3 | 4 | describe('utils.toggle', () => { 5 | test('toggles boolean observable', () => { 6 | const bool = ko.observable(true) 7 | toggle(bool) 8 | expect(bool()).toBe(false) 9 | toggle(bool) 10 | expect(bool()).toBe(true) 11 | }) 12 | 13 | test('toggles writable computeds', (done) => { 14 | const bool = ko.pureComputed({ 15 | read: () => true, 16 | write: (v) => { 17 | expect(v).toBe(false) 18 | done() 19 | }, 20 | }) 21 | toggle(bool) 22 | }) 23 | 24 | test('throws with non writable computeds', () => { 25 | const bool = ko.pureComputed(() => true) 26 | expect(() => toggle(bool)).toThrow() 27 | }) 28 | 29 | test('throws with non booleans', () => { 30 | const num = ko.pureComputed(() => 0) 31 | expect(() => toggle(num as any)).toThrow() 32 | }) 33 | 34 | test('returns the new value', () => { 35 | const bool = ko.observable(true) 36 | expect(toggle(bool)).toBe(false) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /support/xvfb_entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export DISPLAY=:99.0 4 | 5 | Xvfb :99 -screen 0 640x480x8 -nolisten tcp & 6 | 7 | exec "$@" -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "sourceMap": true, 5 | "jsx": "react", 6 | "jsxFactory": "h", 7 | "strict": true, 8 | "moduleResolution": "node", 9 | "allowSyntheticDefaultImports": true, 10 | "esModuleInterop": true, 11 | "noEmit": true, 12 | "noUnusedLocals": true, 13 | "lib": ["dom", "es5", "es2015", "esnext.asynciterable"] 14 | }, 15 | "include": ["**/*"], 16 | "exclude": ["**/dist", "**/examples", "templates", "node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"], 3 | "rules": { 4 | "array-type": [true, "array"], 5 | "await-promise": true, 6 | "curly": [true, "ignore-same-line"], 7 | "deprecation": true, 8 | "interface-over-type-literal": false, 9 | "max-classes-per-file": false, 10 | "member-ordering": [true, "instance-sandwich"], 11 | "no-boolean-literal-compare": true, 12 | "no-floating-promises": true, 13 | "no-for-in-array": true, 14 | "no-unnecessary-qualifier": true, 15 | "object-literal-sort-keys": false, 16 | "ordered-imports": [false], 17 | "prefer-readonly": true, 18 | "restrict-plus-operands": true, 19 | "strict-type-predicates": true, 20 | "use-default-type-parameter": true, 21 | "variable-name": [ 22 | true, 23 | "allow-leading-underscore", 24 | "allow-trailing-underscore" 25 | ] 26 | } 27 | } 28 | --------------------------------------------------------------------------------