Sitelet https://github.com/limitless-angular/limitless-angular/pull/44/files
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/sanity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ We welcome contributions! Check our [Contributing Guide](CONTRIBUTING.md) for de

## Credits

Adapted from [@portabletext/react v3.1.0](https://github.com/portabletext/react-portabletext/blob/main/CHANGELOG.md#310-2024-05-28) which provided the
Adapted from [@portabletext/react v6.2.0](https://github.com/portabletext/react-portabletext/releases/tag/v6.2.0) which provided the
vast majority of node rendering logic.

## License
Expand Down
40 changes: 39 additions & 1 deletion packages/sanity/portabletext/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

Render [Portable Text](https://portabletext.org/) with Angular.

Based on [@portabletext/react v3.1.0](https://github.com/portabletext/react-portabletext/blob/main/CHANGELOG.md#310-2024-05-28).
Based on [@portabletext/react v6.2.0](https://github.com/portabletext/react-portabletext/releases/tag/v6.2.0).

## Demo

Expand All @@ -17,6 +17,7 @@ You can also see the example project in the monorepo: [`apps/sanity-example`](/a

- [Installation](#installation)
- [Basic usage](#basic-usage)
- [Inputs](#inputs)
- [Styling](#styling-the-output)
- [Customizing components](#customizing-components)
- [Available components](#available-components)
Expand All @@ -26,6 +27,7 @@ You can also see the example project in the monorepo: [`apps/sanity-example`](/a
- [list](#list)
- [listItem](#listitem)
- [hardBreak](#hardbreak)
- [Fallback components](#fallback-components)
- [Architecture Documentation](./ARCHITECTURE.md)

## Installation
Expand Down Expand Up @@ -57,6 +59,15 @@ export class YourComponent {
}
```

## Inputs

The Angular component follows the `@portabletext/react` prop contract with Angular inputs:

- `value`: a single Portable Text node, an array of nodes, `null`, or `undefined`. `null` and `undefined` render nothing.
- `components`: optional Angular component overrides.
- `onMissingComponent`: warning handler, or `false` to disable warnings.
- `listNestingMode`: optional `ListNestMode` (`html` by default, or `direct`).

## Styling the output

The rendered HTML does not have any styling applied, so you will either render a parent container with a class name you can target in your CSS, or pass [custom components](#customizing-components) if you want to control the direct markup and CSS of each element.
Expand Down Expand Up @@ -715,6 +726,33 @@ Component to use for rendering "hard breaks", eg `\n` inside of text spans.

Will by default render a `<br />`. Pass `false` to render as-is (`\n`)

### Fallback components

You can also override the fallback components used when no renderer is registered:

- `unknownType`
- `unknownMark`
- `unknownBlockStyle`
- `unknownList`
- `unknownListItem`

For example:

```typescript
import { Component } from '@angular/core';
import { PortableTextComponents, PortableTextTypeComponent } from '@limitless-angular/sanity/portabletext';

@Component({
selector: 'div',
template: `Unknown type: {{ value()._type }}`,
})
export class UnknownTypeComponent extends PortableTextTypeComponent {}

export const components: PortableTextComponents = {
unknownType: UnknownTypeComponent,
};
```

## Rendering Plain Text

This module also exports a function (toPlainText()) that will render one or more Portable Text blocks as plain text. This is helpful in cases where formatted text is not supported, or you need to process the raw text value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { RenderNode } from '../directives/render-node.directive';
<ng-container
[renderNode]="child"
[isInline]="child.isInline ?? isInline ?? true"
[index]="index"
[index]="child.index ?? index"
/>
}
</ng-template>`,
Expand All @@ -30,7 +30,7 @@ import { RenderNode } from '../directives/render-node.directive';
export class ChildrenComponent {
template = viewChild.required<
TemplateRef<{
children: (TypedObject & { isInline?: boolean })[];
children: (TypedObject & { index?: number; isInline?: boolean })[];
isInline?: boolean;
}>
>(TemplateRef);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
import { PortableTextComponents } from '../../types';
/* eslint-disable @angular-eslint/component-selector */
/* eslint-disable @angular-eslint/component-class-suffix */
import { Component } from '@angular/core';
import { PortableTextAngularComponents } from '../../types';
import { defaultBlockStyles } from './blocks';
import { defaultLists } from './list';
import { DefaultListItem } from './list';
import { defaultMarks } from './marks';
import {
DefaultUnknownType,
DefaultUnknownMark,
DefaultUnknownBlockStyle,
DefaultUnknownList,
DefaultUnknownListItem,
} from './unknown';
export const defaultComponents: PortableTextComponents = {

@Component({
selector: 'br',
template: '',
})
export class DefaultHardBreak {}

export const defaultComponents: PortableTextAngularComponents = {
types: {},

block: defaultBlockStyles,
marks: defaultMarks,
list: defaultLists,
listItem: DefaultListItem,
hardBreak: DefaultHardBreak,

unknownType: DefaultUnknownType,
unknownMark: DefaultUnknownMark,
unknownList: DefaultUnknownList,
unknownListItem: DefaultUnknownListItem,
Expand Down
15 changes: 14 additions & 1 deletion packages/sanity/portabletext/src/components/defaults/unknown.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
/* eslint-disable @angular-eslint/component-selector */
/* eslint-disable @angular-eslint/component-class-suffix */
import { Component } from '@angular/core';
import { Component, computed } from '@angular/core';
import {
PortableTextBlockComponent,
PortableTextListComponent,
PortableTextListItemComponent,
PortableTextMarkComponent,
PortableTextTypeComponent,
} from '../../directives/portable-text-directives';
import { unknownTypeWarning } from '../../warnings';

const template = '<ng-container #children />';

@Component({
selector: 'span',
template: `{{ warning() }}`,
host: { '[style.display]': '"none"' },
})
export class DefaultUnknownType extends PortableTextTypeComponent {
protected readonly warning = computed(() =>
unknownTypeWarning(this.value()._type),
);
}

@Component({
selector: 'span,span[data-unknown="true"]',
template,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import {
} from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';

import { LIST_NEST_MODE_HTML, nestLists } from '@portabletext/toolkit';
import {
LIST_NEST_MODE_HTML,
nestLists,
type ToolkitListNestMode,
} from '@portabletext/toolkit';
import {
ArbitraryTypedObject,
PortableTextBlock,
Expand All @@ -22,6 +26,7 @@ import {

import {
MissingComponentHandler,
PortableTextAngularComponents,
PortableTextComponents,
RenderNodeContext,
} from '../types';
Expand Down Expand Up @@ -66,16 +71,16 @@ import { TextComponent } from './text.component';
}
</ng-template>

<ng-template #unknownTmpl let-node="node" let-isInline="isInline">
<ng-template
#unknownTmpl
let-node="node"
let-isInline="isInline"
let-warning="warning"
>
@if (isInline) {
<span style="display: none" aria-hidden="true">{{
'Unknown block type: ' + node._type
}}</span>
<span style="display: none">{{ warning }}</span>
} @else {
<!-- display: inline -->
<div style="display: none" aria-hidden="true">{{
'Unknown block type: ' + node._type
}}</div>
<div style="display: none">{{ warning }}</div>
}
</ng-template>
`,
Expand All @@ -95,20 +100,26 @@ export class PortableTextComponent<
* The Portable Text content to render.
* Can be a single block or an array of blocks.
*/
value = input.required<B | B[]>();
value = input.required<B | B[] | null | undefined>();

/**
* Custom components to override the default rendering.
* @see PortableTextComponents
*/
componentOverrides = input<Partial<PortableTextComponents>>(
{},
componentOverrides = input<PortableTextComponents<B> | undefined>(
undefined,
// eslint-disable-next-line @angular-eslint/no-input-rename
{ alias: 'components' },
);

/**
* Template reference for rendering nodes
* Determines whether lists are nested inside list items (`html`) or as a
* direct child of another list (`direct`).
*/
listNestingMode = input<ToolkitListNestMode>();

/**
* @internal Template reference for rendering nodes.
*/
renderNode =
viewChild.required<TemplateRef<RenderNodeContext<B>>>('renderNode');
Expand All @@ -126,17 +137,20 @@ export class PortableTextComponent<
/**
* Merged components from default and overrides
*/
availableComponents = computed(() =>
mergeComponents(defaultComponents, this.componentOverrides()),
);
availableComponents = computed<PortableTextAngularComponents<B>>(() => {
const overrides = this.componentOverrides();
return overrides
? mergeComponents(defaultComponents, overrides)
: (defaultComponents as PortableTextAngularComponents<B>);
});

/**
* Template reference for rendering unknown block types
*/
unknownTmpl =
viewChild.required<TemplateRef<{ node: TypedObject; isInline: boolean }>>(
'unknownTmpl',
);
viewChild.required<
TemplateRef<{ node: TypedObject; isInline: boolean; warning: string }>
>('unknownTmpl');

/**
* Creates a template for rendering children
Expand All @@ -160,8 +174,21 @@ export class PortableTextComponent<
* Handles both single blocks and arrays of blocks
*/
nestedBlocks = computed(() => {
const blocks = Array.isArray(this.value()) ? this.value() : [this.value()];
return nestLists(blocks as TypedObject[], LIST_NEST_MODE_HTML);
const input = this.value();
let blocks: B[];

if (Array.isArray(input)) {
blocks = input;
} else if (input == null) {
blocks = [];
} else {
blocks = [input];
}

return nestLists(
blocks as TypedObject[],
this.listNestingMode() || LIST_NEST_MODE_HTML,
);
});

/**
Expand Down
5 changes: 2 additions & 3 deletions packages/sanity/portabletext/src/components/text.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ import { PortableTextComponents } from '../types';
// prettier-ignore
template: `<ng-template let-node let-components="components">
@if (node.text === '\\n') {
@if (components.hardBreak === undefined) {<br />}
@else if (components.hardBreak === false) {{{ '\\n' }}}
@else {<ng-container *ngComponentOutlet="components.hardBreak" />}
@if (components.hardBreak) {<ng-container *ngComponentOutlet="components.hardBreak" />}
@else {{{ '\\n' }}}
} @else {{{ node.text }}}
</ng-template>`,
changeDetection: ChangeDetectionStrategy.OnPush,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import { PortableTextComponent } from '../components/portable-text.component';
export class DynamicPortableTextContent<Node extends TypedObject = TypedObject>
implements AfterViewInit
{
children = input.required<(TypedObject & { isInline?: boolean })[]>();
children =
input.required<(TypedObject & { index?: number; isInline?: boolean })[]>();
container = viewChild<ViewContainerRef, ViewContainerRef>('children', {
read: ViewContainerRef,
});
Expand Down Expand Up @@ -93,6 +94,11 @@ export class PortableTextTypeComponent<
> extends DynamicPortableTextContent<T> {
value = input.required<T>();

/**
* Index within its parent.
*/
index = input(0);

/**
* Whether this node is "inline" - ie as a child of a text block,
* alongside text spans, or a block in and of itself.
Expand All @@ -103,16 +109,20 @@ export class PortableTextTypeComponent<
// eslint-disable-next-line @angular-eslint/directive-selector
@Directive({ selector: '[portableTextBlock]' })
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export class PortableTextBlockComponent extends PortableTextTypeComponent<PortableTextBlock> {}
export class PortableTextBlockComponent<
T extends TypedObject = PortableTextBlock,
> extends PortableTextTypeComponent<T> {}

// eslint-disable-next-line @angular-eslint/directive-selector
@Directive({ selector: '[portableTextListItem]' })
@Directive({ selector: '[portableTextList]' })
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export class PortableTextListComponent extends PortableTextTypeComponent<PortableTextListBlock> {}
export class PortableTextListComponent<
T extends TypedObject = PortableTextListBlock,
> extends PortableTextTypeComponent<T> {}

// eslint-disable-next-line @angular-eslint/directive-selector
@Directive({ selector: '[portableTextListItem]' })
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export class PortableTextListItemComponent extends PortableTextTypeComponent<PortableTextListItemBlock> {
index = input.required<number>();
}
export class PortableTextListItemComponent<
T extends TypedObject = PortableTextListItemBlock,
> extends PortableTextTypeComponent<T> {}
31 changes: 29 additions & 2 deletions packages/sanity/portabletext/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,38 @@
export { PortableTextComponent } from './components/portable-text.component';
export {
PortableTextComponent,
PortableTextComponent as PortableText,
} from './components/portable-text.component';
export {
PortableTextTypeComponent,
PortableTextMarkComponent,
PortableTextBlockComponent,
PortableTextListItemComponent,
PortableTextListComponent,
} from './directives/portable-text-directives';
export { PortableTextComponents } from './types';
export { defaultComponents } from './components/defaults/default-components';
export { mergeComponents } from './utils/merge';
export type {
AngularPortableTextList,
AngularPortableTextListItem,
DefaultPortableTextBlockStyle,
DefaultPortableTextListItem,
DefaultPortableTextMark,
InferComponents,
InferStrictComponents,
InferValue,
MissingComponentHandler,
NodeType,
PortableTextAngularComponent,
PortableTextAngularComponents,
PortableTextBlockComponentType,
PortableTextComponents,
PortableTextListBlock,
PortableTextListComponentType,
PortableTextListItemComponentType,
PortableTextMarkComponentType,
PortableTextProps,
UnknownNodeType,
} from './types';
export type { ToolkitListNestMode as ListNestMode } from '@portabletext/toolkit';
export { toPlainText } from '@portabletext/toolkit';
export type { PortableTextBlock } from '@portabletext/types';
Loading
Loading