Skip to content

Commit

Permalink
feat: doc on slots
Browse files Browse the repository at this point in the history
  • Loading branch information
quentinderoubaix committed Jan 29, 2024
1 parent 627ae87 commit 8c000c6
Show file tree
Hide file tree
Showing 29 changed files with 898 additions and 32 deletions.
19 changes: 19 additions & 0 deletions angular/demo/src/app/samples/slots/context.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {PaginationComponent, PaginationNumberDirective} from '@agnos-ui/angular';
import {Component} from '@angular/core';

@Component({
standalone: true,
imports: [PaginationComponent, PaginationNumberDirective],
template: `
<p>The default look of the pagination:</p>
<nav auPagination auCollectionSize="60"></nav>
<p>Changing the slot displaying the page number to use letters instead:</p>
<nav auPagination auCollectionSize="60">
<ng-template auPaginationNumber let-displayedPage="displayedPage">
{{ ['A', 'B', 'C', 'D', 'E', 'F'][displayedPage - 1] }}
</ng-template>
</nav>
`,
})
export default class SlotsContextComponent {}
21 changes: 21 additions & 0 deletions angular/demo/src/app/samples/slots/headless.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {Component, ViewEncapsulation} from '@angular/core';
import {RatingReadonlyComponent, RatingReadonlyStarDirective} from './rating-readonly.component';

@Component({
standalone: true,
imports: [RatingReadonlyComponent, RatingReadonlyStarDirective],
encapsulation: ViewEncapsulation.None,
template: `
<div>The readonly rating without slot:</div>
<app-rating-readonly [rating]="7" [maxRating]="10" />
<div class="mt-2">Using a slot to customize the display:</div>
<app-rating-readonly className="rating-custom" [rating]="7" [maxRating]="10">
<ng-template appRatingStar let-fill="fill" let-index="index">
<span class="star" [class.filled]="fill === 100" [class.bad]="index < 3">&#9733;</span>
</ng-template>
</app-rating-readonly>
`,
styles: "@import '@agnos-ui/common/samples/rating/custom.scss';",
})
export default class SlotsHeadlessComponent {}
65 changes: 65 additions & 0 deletions angular/demo/src/app/samples/slots/rating-readonly.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type {AdaptSlotContentProps, RatingWidget, SlotContent, StarContext} from '@agnos-ui/angular-headless';
import {BaseWidgetDirective, SlotDirective, auNumberAttribute, callWidgetFactory, createRating} from '@agnos-ui/angular-headless';
import type {AfterContentChecked} from '@angular/core';
import {ChangeDetectionStrategy, Component, ContentChild, Directive, Input, TemplateRef, inject} from '@angular/core';

/**
* This directive allows the component to retrieve the slot template.
*/
@Directive({selector: 'ng-template[appRatingStar]', standalone: true})
export class RatingReadonlyStarDirective {
public templateRef = inject(TemplateRef<AdaptSlotContentProps<StarContext>>);
static ngTemplateContextGuard(_dir: RatingReadonlyStarDirective, context: unknown): context is StarContext {
return true;
}
}

/**
* To use the defined slotStar, we simply need to use the {@link SlotDirective} and give it the prop as input.
* The auSlotProps is used to provide context to the slot.
*/
@Component({
selector: 'app-rating-readonly',
standalone: true,
imports: [SlotDirective],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="d-inline-flex au-rating" [class]="state().className">
@for (item of state().stars; track item) {
<span class="au-rating-star">
<ng-template [auSlot]="state().slotStar" [auSlotProps]="item"></ng-template>
</span>
}
</div>
`,
})
export class RatingReadonlyComponent extends BaseWidgetDirective<RatingWidget> implements AfterContentChecked {
readonly _widget = callWidgetFactory({
factory: createRating,
widgetName: 'rating',
defaultConfig: {
readonly: true,
},
events: {
onHover: () => {},
onLeave: () => {},
onRatingChange: () => {},
},
});

@Input()
slotStar: SlotContent<AdaptSlotContentProps<StarContext>>;
@ContentChild(RatingReadonlyStarDirective, {static: false}) slotStarFromContent: RatingReadonlyStarDirective | undefined;

@Input({transform: auNumberAttribute}) rating: number | undefined;

@Input({transform: auNumberAttribute}) maxRating: number | undefined;

@Input() className: string;

ngAfterContentChecked(): void {
this._widget.patchSlots({
slotStar: this.slotStarFromContent?.templateRef,
});
}
}
20 changes: 20 additions & 0 deletions angular/demo/src/app/samples/slots/usage.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {AlertComponent} from '@agnos-ui/angular';
import {Component} from '@angular/core';

@Component({
standalone: true,
imports: [AlertComponent],
template: `
<au-component auAlert auType="primary" [auDismissible]="false"> Label provided by slot </au-component>
<au-component auAlert auType="secondary" [auDismissible]="false" auSlotDefault="Label provided by property" />
<au-component
auAlert
auType="info"
[auDismissible]="false"
auSlotDefault="When both prop and slot are provided, the prop's content will take precedence."
>
This content is ignored.
</au-component>
`,
})
export default class SlotsUsageComponent {}
12 changes: 12 additions & 0 deletions angular/docs/Slots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## Slots in Angular

Angular applications usually handle slots using [content projection](https://angular.dev/guide/components/content-projection).
We support this behavior, while going further.

The AgnosUI Angular slots can be set using:

- a simple `string`
- a function `(props: Props) => string`
- a [TemplateRef](https://angular.io/api/core/TemplateRef)
- an Angular component
- a `ComponentTemplate`, an AgnosUI utility allowing to use an Angular component without the host element
3 changes: 2 additions & 1 deletion demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
"static/**",
"svelte.config.js",
"vite.config.ts",
"vite-env.d.ts"
"vite-env.d.ts",
"scripts/*"
],
"output": [
"dist/**",
Expand Down
3 changes: 3 additions & 0 deletions demo/src/lib/layout/Code.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
}
pre {
> code {
white-space: pre-wrap;
}
// color: black;
tab-size: 1rem;
}
Expand Down
48 changes: 25 additions & 23 deletions demo/src/lib/layout/Sample.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@
/**
* Whether the code button must be displayed
*/
export let showCodeButton = true;
export let showButtons = true;
let showCode = false;
export let showCode = false;
let code = '';
$: path = `${sample.componentName}/${sample.sampleName}`.toLowerCase();
Expand Down Expand Up @@ -88,8 +88,8 @@
{/if}
<iframe class="demo-sample d-block" use:iframeSrc={sampleUrl} {title} use:handler={sampleBaseUrl} />
</div>
<div class="btn-toolbar border border-top-0 d-flex align-items-center p-1" role="toolbar" aria-label="Toolbar with button groups">
{#if showCodeButton}
{#if showButtons}
<div class="btn-toolbar border border-top-0 d-flex align-items-center p-1" role="toolbar" aria-label="Toolbar with button groups">
<button
class="btn btn-sm btn-link m-1 p-0"
aria-label="Show or hide the code"
Expand All @@ -103,25 +103,27 @@
on:click={async () => (await import('../stackblitz')).openInStackblitz(sample, $selectedFramework$)}
><Svg className="icon-24 align-middle" svg={stackblitz} /></button
>
{/if}
<a
href={sampleUrl}
class="action m-1 p-0"
target="_blank"
rel="noreferrer nofollow external"
aria-label="View sample in new tab"
use:tooltip={{content: 'Open example in a new tab'}}
><Svg className="icon-20 align-middle" svg={openLink} />
</a>
</div>
<a
href={sampleUrl}
class="action m-1 p-0"
target="_blank"
rel="noreferrer nofollow external"
aria-label="View sample in new tab"
use:tooltip={{content: 'Open example in a new tab'}}
><Svg className="icon-20 align-middle" svg={openLink} />
</a>
</div>
{/if}
{#if showCode}
<ul class="nav nav-underline p-3 border-start border-end">
{#each files as file}
<li class="nav-item">
<button class="nav-link" class:active={selectedFileName === file} on:click={() => (selectedFileName = file)}>{file}</button>
</li>
{/each}
</ul>
{#if files.length > 1}
<ul class="nav nav-underline p-3 border-start border-end">
{#each files as file}
<li class="nav-item">
<button class="nav-link" class:active={selectedFileName === file} on:click={() => (selectedFileName = file)}>{file}</button>
</li>
{/each}
</ul>
{/if}
<div class="border border-top-0">
<Lazy component={() => import('./Code.svelte')} {code} fileName={selectedFileName}>
<div class="spinner-border text-primary" role="status">
Expand All @@ -138,7 +140,7 @@
}
.action {
display: inline-block;
display: inline-flex;
> :global(svg) {
width: 20px;
Expand Down
2 changes: 1 addition & 1 deletion demo/src/lib/layout/playground/Playground.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

<div class="row">
<div class="col">
<Sample title="Configuration" {sample} urlParameters={$sampleParameters$} showCodeButton={false} {height} {noresize} />
<Sample title="Configuration" {sample} urlParameters={$sampleParameters$} showButtons={false} {height} {noresize} />
</div>
</div>
<div class="row">
Expand Down
2 changes: 1 addition & 1 deletion demo/src/lib/markdown/renderers/MdCode.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@

{#if lang === 'sample'}
{#if sample}
<Sample {title} {sample} {height} />
<Sample {title} {sample} {height} showCode showButtons={false} />
{:else}
Sample not found, make sure to fill the samples.ts file.
{/if}
Expand Down
8 changes: 8 additions & 0 deletions demo/src/lib/markdown/samples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@ import type {SampleInfo} from '$lib/layout/sample';

import focustrack from '@agnos-ui/samples/focustrack/focustrack';
import floatingUI from '@agnos-ui/samples/floatingUI/floatingUI';
import slotsUsage from '@agnos-ui/samples/slots/usage';
import slotsContext from '@agnos-ui/samples/slots/context';
import alertsIcon from '@agnos-ui/samples/alert/icon';
import slotsHeadless from '@agnos-ui/samples/slots/headless';

const samples: Map<string, SampleInfo> = new Map();
samples.set('focustrack/focustrack', focustrack);
samples.set('floatingUI/floatingUI', floatingUI);
samples.set('slots/usage', slotsUsage);
samples.set('slots/context', slotsContext);
samples.set('alert/icon', alertsIcon);
samples.set('slots/headless', slotsHeadless);

export default samples;
19 changes: 17 additions & 2 deletions demo/src/lib/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,22 @@ export async function listPages() {
return categories;
}

export async function retrieveMarkdown(slug: string) {
const regexFrameworkSpecific = /<!--\s+<framework-specific\s+src="([^"]*)">\s+-->[\s\S]*?<!--\s+<\/framework-specific>\s+-->/;

async function preparseMarkdown(path: string, framework: string): Promise<string> {
let markdown = await readFile(path, 'utf-8');
let match;
do {
match = markdown.match(regexFrameworkSpecific);
if (match) {
markdown =
markdown.slice(0, match.index) + (await readFile(`../${framework}/docs/${match[1]}`)) + markdown.substring(match.index! + match[0].length);
}
} while (match);
return markdown;
}

export async function retrieveMarkdown(slug: string, framework: string) {
const categories = await listPages();
let prev;
let next;
Expand All @@ -66,5 +81,5 @@ export async function retrieveMarkdown(slug: string) {
}
}
}
return file ? {prev, next, content: await readFile(file.mdpath!, 'utf-8')} : undefined;
return file ? {prev, next, content: await preparseMarkdown(file.mdpath!, framework)} : undefined;
}
2 changes: 1 addition & 1 deletion demo/src/routes/docs/[framework]/[...slug]/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {retrieveMarkdown} from '$lib/server';
import {error} from '@sveltejs/kit';

export const load = async ({params}) => {
const file = await retrieveMarkdown(params.slug);
const file = await retrieveMarkdown(params.slug, params.framework);
if (!file) error(404);
else {
return {
Expand Down
63 changes: 61 additions & 2 deletions docs/01-Headless/02-Slots.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,64 @@
# Slots

## TODO
## What are Slots ?

To be done, issue #376
Slots are essentially placeholders within a component that can be filled with custom content.
They provide a way to dynamically inject content into a component without affecting its original template.
This makes it possible to create components that can be easily adapted to different scenarios without having to duplicate code or create a new component from scratch.

## Why use them ?

Slots facilitate the creation of more versatile and reusable components.
They allow developers to design components with predefined structures while leaving room for variation in content.
This separation of structure and content enhances code reusability and promotes a cleaner, more modular codebase.

## AgnosUI Slot

AgnosUI core widgets include slots as **properties** prefixed by _slot_ in their states.
This allows to specifiy the projected content in multiple manners, like simple `string`, context-aware functions, standard slots or even fully-fledged components.

To illustrate the basic usage, let's see in action how we can use a simple slot in the Bootstrap flavour of the **Alert** component:

```sample
{Slot Standard Usage:slots/usage:278}
```

## Context

Slots have access to a context, which for most cases is the widget state.
It is possible however to extend the context, which enables powerful customization. Here is an example with the Bootstrap flavour of the **Pagination** component:

```sample
{Slot Context:slots/context:220}
```

## Integration with Configuration

As explained above, AgnosUI slots are inherently properties, thus benefit from the [Configuration](01-Configuration.md).
For instance, we may configure the _slotStructure_ of the **Alert** to use a custom component, allowing to fully customize the widget.

```sample
{Slot Configuration:alert/icon:402}
```

<!-- <framework-specific src="Slots.md"> -->

## Headless Usage

AgnosUI provides utilities to manage slots for each framework, as frameworks have differences in their implementations of slots / templates / snippets.
To learn more about the specificies of each framework, go here:

<p align="center">
<a href="../../angular/docs/Slots.md">Slots in Angular</a>&nbsp;&nbsp;
<a href="../../react/docs/Slots.md">Slots in React</a>&nbsp;&nbsp;
<a href="../../svelte/docs/Slots.md">Slots in Svelte</a>
</p>
<!-- </framework-specific> -->

## Headless example

You can check out the following example, re-writing the Bootstrap flavour of the **Rating** component as readonly:

```sample
{Slot Headless:slots/headless:148}
```
Loading

0 comments on commit 8c000c6

Please sign in to comment.