Skip to content

Commit

Permalink
feat(core): add extensionDecorator
Browse files Browse the repository at this point in the history
Add new `extensionDecorator` function which augments the static properties of an `Extension` constructor when used as a decorator.
  • Loading branch information
ifiokjr committed Jul 28, 2020
1 parent 84655f0 commit e45706e
Show file tree
Hide file tree
Showing 9 changed files with 407 additions and 13 deletions.
38 changes: 38 additions & 0 deletions .changeset/happy-llamas-reflect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
'@remirror/core': minor
'@remirror/core-types': patch
---

Add new `extensionDecorator` function which augments the static properties of an `Extension` constructor when used as a decorator.

The following code will add a decorator to the extension.

```ts
import { PlainExtension, ExtensionPriority, extensionDecorator } from 'remirror/core';

interface ExampleOptions {
color?: string;

/**
* This option is annotated as a handler and needs a static property.
**/
onChange?: Handler<() => void>;
}

@extensionDecorator<ExampleOptions>({
defaultOptions: { color: 'red' },
defaultPriority: ExtensionPriority.Lowest,
handlerKeys: ['onChange'],
})
class ExampleExtension extends PlainExtension<ExampleOptions> {
get name() {
return 'example' as const;
}
}
```

The extension decorator updates the static properties of the extension. If you prefer not to use decorators it can also be called as a function. The `Extension` constructor is mutated by the function call and does not need to be returned.

```ts
extensionDecorator({ defaultSettings: { color: 'red' } })(ExampleExtension);
```
163 changes: 163 additions & 0 deletions docs/concepts/extension.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,166 @@ onDestroy(extensions: readonly AnyExtension[]): void
```

This is called when the `RemirrorManager` is being destroyed.

## Options

Options are used to configure the extension at runtime. They come in four different flavours via the
option annotations.

```ts
import {
Static,
Dynamic,
Handler,
CustomHandler,
extensionDecoration,
PlainExtension,
ExtensionPriority,
} from 'remirror/core';

interface ExampleOptions {
// `Static` types can only be provided at instantiation.
type: Static<'awesome' | 'not-awesome'>;

// Options are `Dynamic` by default.
color?: string;

// `Dynamic` properties can also be set with the annotation, although it's unnecessary.
backgroundColor?: Dynamic<string>;

// `Handlers` are used to represent event handlers.
onChange?: Handler<() => void>;

// `CustomHandler` options are for customised handlers and it's completely up
// to you to integrate them properly.
keyBindings: CustomHandler<Record<string, () => boolean>>;
}

@extensionDecorator<ExampleOptions>({
defaultOptions: { color: 'red', backgroundColor: 'green' },
defaultPriority: ExtensionPriority.High,

// Let's the extension know that these are the static keys
staticKeys: ['type'],

// Provides the keys which should be converted into handlers.
handlerKeys: ['onChange'],

// Provides the keys which should be created treated as custom handlers.
customHandlerKeys: ['keyBindings'],
})
class ExampleExtension extends PlainExtension<ExampleOptions> {
get name() {
return 'example' as const;
}
}
```

These annotations can be used to provide better intelli-sense support for the end user.

### `extensionDecorator`

The extension decorator updates the static properties of the extension. If you prefer not to use
decorators it can also be called as a function. The `Extension` constructor is mutated by the
function call.

```ts
extensionDecorator({ defaultSettings: { color: 'red' } })(ExampleExtension);
```

### `Dynamic` options

`Dynamic` options can be passed in at instantiation and also during runtime. When no annotation
exists the option is assumed to be dynamic.

```ts
const exampleExtension = new ExampleExtension({
type: 'awesome',
color: 'blue',
backgroundColor: 'yellow',
});

// Runtime update
exampleExtension.setOptions({ color: 'pink', backgroundColor: 'purple' });
```

### `Static` options

`Static` options should be used when it is not possible to update an option during runtime.
Typically this is reserved for options that affect the schema, since the schema is created at
initialization. They will throw an error if an error if updated during runtime.

```ts
const exampleExtension = new ExampleExtension({
type: 'awesome',
});

// Will throw an error.
exampleExtension.setOptions({ type: 'not-awesome' });
```

### `Handler` options

`Handler` options are a pseudo option in that they are completely handled by the underlying remirror
extension.

To get them to work we would change the above example extension implentation to look like the
following.

```ts
import { StateUpdateLifecycleParameter, hasTransactionChanged } from 'remirror/core';
import { hasStateChanged } from 'remirror/extension-positioner';

@extensionDecorator<ExampleOptions>({
defaultOptions: { color: 'red', backgroundColor: 'green' },
defaultPriority: ExtensionPriority.High,
staticKeys: ['type'],
handlerKeys: ['onChange'],
customHandlerKeys: ['keyBindings'],
})
class ExampleExtension extends PlainExtension<ExampleOptions> {
get name() {
return 'example' as const;
}

onTransaction(parameter: StateUpdateLifecycleParameter) {
const { state } = parameter;
const { tr, state, previousState } = parameter;

const hasChanged = tr
? hasTransactionChanged(tr)
: !state.doc.eq(previousState.doc) || !state.selection.eq(previousState.selection);

if (!hasChanged) {
return;
}

if (state.doc.textContent.includes('example')) {
// Call the handler when certain text is matched
this.options.onChange();
}
}
}
```

Now that the extension is wired to respond to `onChange` handlers we can add a new handler.

```ts
const exampleExtension = new ExampleExtension({
type: 'awesome',
});

const disposeChangeHandler = exampleExtension.addHandler('onChange', () => {
console.log('example was found');
});

// Later
disposeChangeHandler();
```

The onChange handler is automatically managed for you.

### `CustomHandler` options

`CustomHandler` options are like `Handler` options except it's up to you to wire up the handler.
More examples will be added later.
9 changes: 5 additions & 4 deletions packages/@remirror/core-types/src/annotation-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
PickPartial,
RemoveFlavoring,
Shape,
StringKey,
} from './base-types';

type StaticAnnotation = Flavoring<'StaticAnnotation'>;
Expand Down Expand Up @@ -216,10 +217,10 @@ export type GetConstructorParameter<Options extends ValidOptions> = GetStatic<Op
*/
export type Dispose = () => void;

export type HandlerKey<Options extends ValidOptions> = keyof GetHandler<Options>;
export type StaticKey<Options extends ValidOptions> = keyof GetStatic<Options>;
export type DynamicKey<Options extends ValidOptions> = keyof GetDynamic<Options>;
export type CustomHandlerKey<Options extends ValidOptions> = keyof GetCustomHandler<Options>;
export type HandlerKey<Options extends ValidOptions> = StringKey<GetHandler<Options>>;
export type StaticKey<Options extends ValidOptions> = StringKey<GetStatic<Options>>;
export type DynamicKey<Options extends ValidOptions> = StringKey<GetDynamic<Options>>;
export type CustomHandlerKey<Options extends ValidOptions> = StringKey<GetCustomHandler<Options>>;
export type HandlerKeyList<Options extends ValidOptions> = Array<HandlerKey<Options>>;
export type StaticKeyList<Options extends ValidOptions> = Array<StaticKey<Options>>;
export type DynamicKeyList<Options extends ValidOptions> = Array<DynamicKey<Options>>;
Expand Down
5 changes: 5 additions & 0 deletions packages/@remirror/core-types/src/base-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ export type PartialWithRequiredKeys<Type extends object, Keys extends keyof Type
> &
Required<Pick<Type, Keys>>;

/**
* Remove all readonly modifiers from the provided type.
*/
export type Writeable<Type> = { -readonly [Key in keyof Type]: Type[Key] };

/**
* Makes specified keys of an interface optional while the rest stay the same.
*/
Expand Down
53 changes: 53 additions & 0 deletions packages/@remirror/core/src/__tests__/decorators.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { CustomHandler, Dynamic, Handler, Static } from '@remirror/core-types';

import { extensionDecorator } from '..';
import { PlainExtension } from '../extension';

interface GeneralOptions {
type: Static<'awesome' | 'not-awesome'>;
color?: string;
backgroundColor?: Dynamic<string>;
onChange?: Handler<() => void>;
keyBindings: CustomHandler<Record<string, () => boolean>>;
}

describe('@extensionDecorator', () => {
it('can decorate an extension', () => {
@extensionDecorator<GeneralOptions>({
defaultOptions: { backgroundColor: 'red', color: 'pink' },
staticKeys: ['type'],
handlerKeys: ['onChange'],
customHandlerKeys: ['keyBindings'],
})
class TestExtension extends PlainExtension<GeneralOptions> {
get name() {
return 'test' as const;
}
}

expect(TestExtension.staticKeys).toEqual(['type']);
expect(TestExtension.handlerKeys).toEqual(['onChange']);
expect(TestExtension.customHandlerKeys).toEqual(['keyBindings']);
expect(TestExtension.defaultOptions).toEqual({ backgroundColor: 'red', color: 'pink' });
});

it('can decorate an extension as a function call', () => {
const TestExtension = extensionDecorator<GeneralOptions>({
defaultOptions: { backgroundColor: 'red', color: 'pink' },
staticKeys: ['type'],
handlerKeys: ['onChange'],
customHandlerKeys: ['keyBindings'],
})(
class TestExtension extends PlainExtension<GeneralOptions> {
get name() {
return 'test' as const;
}
},
);

expect(TestExtension.staticKeys).toEqual(['type']);
expect(TestExtension.handlerKeys).toEqual(['onChange']);
expect(TestExtension.customHandlerKeys).toEqual(['keyBindings']);
expect(TestExtension.defaultOptions).toEqual({ backgroundColor: 'red', color: 'pink' });
});
});
9 changes: 9 additions & 0 deletions packages/@remirror/core/src/builtins/schema-extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,5 +518,14 @@ declare global {
*/
schema: EditorSchema;
}

interface StaticExtensionOptions {
/**
* When true will disable extra attributes for all instances of this extension.
*
* @defaultValue `false`
*/
readonly disableExtraAttributes?: boolean;
}
}
}

0 comments on commit e45706e

Please sign in to comment.