Skip to content

Commit

Permalink
mount and nav refactoring, retil-link
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesknelson committed May 30, 2021
1 parent 352d9d2 commit f32198d
Show file tree
Hide file tree
Showing 77 changed files with 1,246 additions and 818 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
"@testing-library/react-hooks": "^5.1.0",
"@types/jest": "^24.0.19",
"@types/node": "^12.0.0",
"@types/react": "^17.0.4",
"@types/react-dom": "^17.0.3",
"@types/react": "^17.0.6",
"@types/react-dom": "^17.0.5",
"@types/use-subscription": "^1.0.0",
"@typescript-eslint/eslint-plugin": "^4.0.0",
"@typescript-eslint/parser": "^4.0.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/retil-interaction/src/surface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export function splitSurfaceProps<P extends SurfaceProps>(
// should not be passed through.
export interface ConnectSurfaceProps<
SurfaceElement extends HTMLElement,
MergeProps extends ConnectSurfaceMergeableProps<SurfaceElement>
MergeProps extends ConnectSurfaceMergeableProps<SurfaceElement>,
> extends SurfaceProps {
mergeProps?: MergeProps

Expand Down Expand Up @@ -110,7 +110,7 @@ export const SurfaceDepthContext = createContext<number>(0)

export function ConnectSurface<
SurfaceElement extends HTMLElement,
MergeProps extends ConnectSurfaceMergeableProps<SurfaceElement>
MergeProps extends ConnectSurfaceMergeableProps<SurfaceElement>,
>(
props: ConnectSurfaceProps<SurfaceElement, MergeProps> & {
mergeProps?: {
Expand Down
15 changes: 15 additions & 0 deletions packages/retil-link/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const { pathsToModuleNameMapper } = require('ts-jest/utils')
// In the following statement, replace `./tsconfig` with the path to your `tsconfig` file
// which contains the path mapping (ie the `compilerOptions.paths` option):
const { compilerOptions } = require('../../tsconfig')

module.exports = {
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
prefix: '<rootDir>/../',
}),
modulePaths: ['<rootDir>/src/'],
modulePathIgnorePatterns: ['<rootDir>/demo/'],
preset: 'ts-jest',
testEnvironment: 'jsdom',
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
}
37 changes: 37 additions & 0 deletions packages/retil-link/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "retil-link",
"version": "0.20.1",
"description": "A composable link component for React apps.",
"author": "James K Nelson <[email protected]>",
"license": "MIT",
"main": "dist/commonjs/index.js",
"module": "dist/es/index.js",
"types": "dist/types/index.d.ts",
"scripts": {
"clean": "rimraf dist",
"build:commonjs": "tsc -p tsconfig.build.json --module commonjs --outDir dist/commonjs",
"build:es": "tsc -p tsconfig.build.json --module es2015 --outDir dist/es",
"build:types": "tsc -p tsconfig.build.json --declaration --emitDeclarationOnly --outDir dist/types --isolatedModules false",
"build": "yarn run clean && yarn build:es && yarn build:commonjs && yarn build:types",
"build:watch": "yarn run clean && yarn build:es -- --types --watch",
"lint": "eslint --ext ts,tsx src",
"prepare": "yarn test && yarn build",
"test": "jest",
"test:watch": "jest --watch"
},
"dependencies": {
"retil-interaction": "^0.20.1",
"retil-nav": "^0.20.1",
"tslib": "^2.2.0"
},
"devDependencies": {
"typescript": "4.2.4"
},
"files": [
"dist"
],
"keywords": [
"react",
"loader"
]
}
1 change: 1 addition & 0 deletions packages/retil-link/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './link'
81 changes: 81 additions & 0 deletions packages/retil-link/src/link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, { forwardRef } from 'react'
import {
ConnectSurface,
SurfaceProps,
splitSurfaceProps,
} from 'retil-interaction'
import {
NavAction,
UseNavLinkOptions,
useNavLink,
useNavMatch,
useNavResolve,
} from 'retil-nav'

export interface LinkProps
extends SurfaceProps,
UseNavLinkOptions,
Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
active?: boolean
activeClassName?: string
activeStyle?: object
children: React.ReactNode
exact?: boolean
ref?: React.Ref<HTMLAnchorElement>
to: NavAction
}

// Need to include this type definition, as the automatically generated one
// is incompatible with some versions of the react typings.
export const Link: React.FunctionComponent<LinkProps> = forwardRef(
(props: LinkProps, anchorRef: React.Ref<HTMLAnchorElement>) => {
const [surfaceProps, linkProps] = splitSurfaceProps(props)

const {
exact,
onClick: onClickProp,
onMouseEnter: onMouseEnterProp,
precacheOn,
replace,
state,
to,
...rest
} = linkProps

const action = useNavResolve(to, state)
const activeMatch = useNavMatch(action.pathname + (exact ? '' : '*'))
const activated = props.activated ?? activeMatch

const navLinkProps = useNavLink(action, {
disabled: surfaceProps.disabled,
onClick: onClickProp,
onMouseEnter: onMouseEnterProp,
precacheOn,
replace,
})

// Don't handle events on links with a `target` prop.
const onClick = props.target ? onClickProp : navLinkProps.onClick
const onMouseEnter = props.target
? onMouseEnterProp
: navLinkProps.onMouseEnter

return (
<ConnectSurface
{...surfaceProps}
activated={activated}
mergeProps={{
...rest,
...navLinkProps,
ref: anchorRef,
onClick,
onMouseEnter,
}}>
{(props) => (
// eslint-disable-next-line jsx-a11y/anchor-has-content
<a {...props} />
)}
</ConnectSurface>
)
},
)
6 changes: 6 additions & 0 deletions packages/retil-link/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.build.json",
"include": [
"src/**/*"
]
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,36 @@
import { Source, fuse, hasSnapshot } from 'retil-source'
import { ArrayKeyedMap, Maybe } from 'retil-support'

import { fuse } from './fuse'
import { Source, hasSnapshot } from './source'

type UseInvocation = [
snapshot: unknown,
source: Source<unknown>,
maybeDefaultValue: Maybe<unknown>,
inject?: unknown,
]
const VectorSymbol = Symbol('FuseVector')
const EnvVectorHead = Symbol('EnvVector')
const ValuelessSymbol = Symbol('valueless')
type Valueless = typeof ValuelessSymbol

export type Vector<T> = [typeof VectorSymbol, ...T[]]
export type EnvVector<T> = [typeof EnvVectorHead, ...T[]]

export type VectorSource<T> = Source<Vector<T>>
export type EnvSource<T> = Source<EnvVector<T>>

export function createVector<T>(xs: [T, ...T[]]): Vector<T> {
return [VectorSymbol, ...xs]
export function createEnvVector<T>(xs: [T, ...T[]]): EnvVector<T> {
return [EnvVectorHead, ...xs]
}

export function isVector<T>(x: Vector<T> | unknown): x is Vector<T> {
return Array.isArray(x) && x[0] === VectorSymbol
export function isEnvVector<T>(x: EnvVector<T> | unknown): x is EnvVector<T> {
return Array.isArray(x) && x[0] === EnvVectorHead
}

export type VectorFusorUse = <U, V = U>(
source: Source<Vector<U> | U>,
export type EnvFusorUse = <U, V = U>(
source: Source<EnvVector<U> | U>,
...defaultValues: [V] | []
) => U | V

export type VectorFusor<T> = (use: VectorFusorUse) => T
export type EnvFusor<T> = (use: EnvFusorUse) => T

export function vectorFuse<T>(fusor: VectorFusor<T>): Source<Vector<T>> {
export function fuseEnvSource<T>(fusor: EnvFusor<T>): Source<EnvVector<T>> {
let previousNextQueue = [] as UseInvocation[][]
let previousResults = new ArrayKeyedMap<unknown[], T>()

Expand All @@ -41,9 +39,9 @@ export function vectorFuse<T>(fusor: VectorFusor<T>): Source<Vector<T>> {
previousResults.clear()
}

return fuse<Vector<T>>((use) => {
return fuse<EnvVector<T>>((use) => {
const results = new ArrayKeyedMap<unknown[], T>()
const resultVector = [VectorSymbol] as Vector<T>
const resultVector = [EnvVectorHead] as EnvVector<T>

// Cache calls to use, as `fuse` isn't designed to deal with the many
// invocations of `use` that can result from combining vector sources.
Expand Down Expand Up @@ -81,7 +79,7 @@ export function vectorFuse<T>(fusor: VectorFusor<T>): Source<Vector<T>> {

const [snapshot, source, maybeDefaultValue] = useList[i]
if (invocation.length === 4) {
if (isVector(snapshot)) {
if (isEnvVector(snapshot)) {
replaceUseInvocationsBySource.set(source, invocation)
}
} else if (replaceUseInvocationsBySource.has(source)) {
Expand All @@ -91,7 +89,7 @@ export function vectorFuse<T>(fusor: VectorFusor<T>): Source<Vector<T>> {
useList[i][2] = maybeDefaultValue
} else {
const currentSnapshot = cachedUse(source)
if (isVector(currentSnapshot)) {
if (isEnvVector(currentSnapshot)) {
// Replace this item in the queue with an expansion for the
// vector
expandVectorFrom(
Expand Down Expand Up @@ -139,11 +137,11 @@ export function vectorFuse<T>(fusor: VectorFusor<T>): Source<Vector<T>> {
// precached value, we can keep track of the inputs that correspond
// to it.
const wrappedUse = <T, U>(
source: Source<Vector<T> | T>,
source: Source<EnvVector<T> | T>,
...defaultValues: [U] | []
): T | U => {
if (process.env.NODE_ENV !== 'production') {
if (defaultValues.length && isVector(defaultValues[0])) {
if (defaultValues.length && isEnvVector(defaultValues[0])) {
throw new Error(
"You can't use a vector as a default value for use() within vectorFuse()",
)
Expand Down Expand Up @@ -174,7 +172,7 @@ export function vectorFuse<T>(fusor: VectorFusor<T>): Source<Vector<T>> {
use(source)
}
inject = defaultValues[0] as U
} else if (isVector(snapshot)) {
} else if (isEnvVector(snapshot)) {
expandVectorFrom(
queue,
useList,
Expand Down Expand Up @@ -213,7 +211,7 @@ function expandVectorFrom(
queue: UseInvocation[][],
useList: UseInvocation[],
vectorIndex: number,
vector: Vector<any>,
vector: EnvVector<any>,
source: Source<any>,
defaultValues: Maybe<any>,
startIndex: number,
Expand Down
8 changes: 4 additions & 4 deletions packages/retil-mount/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
export { vectorFuse as createEnvSource } from 'retil-source'

export * from './dependencyList'
export * from './lazy'
export * from './lazyImport'
export * from './envSource'
export * from './loadAsync'
export * from './loadLazy'
export * from './mount'
export * from './mountComponents'
export * from './mountContext'
export * from './mountOnce'
export * from './mountTypes'
export * from './serverMount'
export * from './useMount'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { ReactElement, ReactNode } from 'react'

import { MountEnv, Loader } from './mountTypes'
import { Loader, LoaderProps } from './mountTypes'

interface OutcomeRef {
current:
Expand All @@ -22,9 +22,9 @@ export interface AsyncContentProps {

export const AsyncContent: React.FunctionComponent<AsyncContentProps> = ({
promisedContent,
outcomeRef: resultRef,
outcomeRef,
}) => {
const result = resultRef.current
const result = outcomeRef.current
if (!result) {
throw Promise.resolve(promisedContent)
} else if (result.type === 'error') {
Expand All @@ -33,10 +33,10 @@ export const AsyncContent: React.FunctionComponent<AsyncContentProps> = ({
return <>{result.content}</>
}

export function lazy<Env extends object>(
asyncLoader: (env: Env & MountEnv) => PromiseLike<ReactNode>,
export function loadAsync<Env extends object>(
asyncLoader: (env: LoaderProps<Env>) => PromiseLike<ReactNode>,
): Loader<Env, ReactElement> {
return (env: Env & MountEnv) => {
return (props: LoaderProps<Env>) => {
const outcomeRef: OutcomeRef = {
current: null,
}
Expand All @@ -47,7 +47,7 @@ export function lazy<Env extends object>(
const lazyPromisedContent: PromiseLike<ReactNode> = {
then: (...args) => {
if (!promisedContent) {
promisedContent = asyncLoader(env).then(
promisedContent = asyncLoader(props).then(
(content) => {
outcomeRef.current = {
type: 'content',
Expand All @@ -69,7 +69,7 @@ export function lazy<Env extends object>(
},
}

env.dependencies.add(lazyPromisedContent)
props.mount.dependencies.add(lazyPromisedContent)

return (
<AsyncContent
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { lazy } from './lazy'
import { loadAsync } from './loadAsync'
import { Loader } from './mountTypes'

export function lazyImport<Env extends object>(
export function loadLazy<Env extends object>(
load: () => PromiseLike<{ default: Loader<Env> }>,
): Loader<Env> {
let loader: Loader<Env> | undefined

return lazy(async (env) => {
return loadAsync(async (env) => {
if (!loader) {
loader = (await load()).default
}
Expand Down
Loading

0 comments on commit f32198d

Please sign in to comment.