feat: ItemList component

This commit is contained in:
Sami Mazouz 2023-09-14 18:29:35 +01:00
parent d01c0e5210
commit 13f997d784
No known key found for this signature in database
6 changed files with 187 additions and 67 deletions

View File

@ -0,0 +1,55 @@
import ItemListUtil from '../utils/ItemList';
import Component from '../Component';
import type Mithril from 'mithril';
import listItems from '../helpers/listItems';
export interface IItemListAttrs {
/** Unique key for the list. Use the convention of `componentName.listName` */
key: string;
/** The context of the list. Usually the component instance. Will be automatically set if not provided. */
context?: any;
/** Optionally, the element tag to wrap each item in. Defaults to none. */
wrapper?: string;
}
export default class ItemList<CustomAttrs extends IItemListAttrs = IItemListAttrs> extends Component<CustomAttrs> {
view(vnode: Mithril.Vnode<CustomAttrs>) {
const items = this.items(vnode.children).toArray();
return vnode.attrs.wrapper ? listItems(items, vnode.attrs.wrapper) : items;
}
items(children: Mithril.ChildArrayOrPrimitive | undefined): ItemListUtil<Mithril.Children> {
const items = new ItemListUtil<Mithril.Children>();
let priority = 10;
this.validateChildren(children)
.reverse()
.forEach((child: Mithril.Vnode<any, any>) => {
items.add(child.key!.toString(), child, (priority += 10));
});
return items;
}
private validateChildren(children: Mithril.ChildArrayOrPrimitive | undefined): Mithril.Vnode<any, any>[] {
if (!children) return [];
children = Array.isArray(children) ? children : [children];
children = children.filter((child: Mithril.Children) => child !== null && child !== undefined);
// It must be a Vnode array
children.forEach((child: Mithril.Children) => {
if (typeof child !== 'object' || !('tag' in child!)) {
throw new Error(`[${this.attrs.key}] The ItemList component requires a valid mithril Vnode array. Found: ${typeof child}.`);
}
if (!child.key) {
throw new Error('The ItemList component requires a unique key for each child in the list.');
}
});
return children as Mithril.Vnode<any, any>[];
}
}

View File

@ -0,0 +1,89 @@
import type IExtender from './IExtender';
import type { IExtensionModule } from './IExtender';
import type Application from '../Application';
import type Mithril from 'mithril';
import type { IItemObject } from '../utils/ItemList';
import { extend } from '../extend';
import ItemListComponent from '../components/ItemList';
type LazyContent<T> = (context: T) => Mithril.Children;
/**
* The `ItemList` extender allows you to add, remove, and replace items in an
* `ItemList` component. Each ItemList has a unique key, which is used to
* identify it.
*
* @example
* ```tsx
* import Extend from 'flarum/common/extenders';
*
* export default [
* new Extend.ItemList<PageStructure>('PageStructure.mainItems')
* .add('test', (context) => app.forum.attribute('baseUrl'), 400)
* .setContent('hero', (context) => <div>My new content</div>)
* .setPriority('hero', 0)
* .remove('hero')
* ]
* ```
*/
export default class ItemList<T = Component<any>> implements IExtender {
protected key: string;
protected additions: Array<IItemObject<LazyContent<T>>> = [];
protected removals: string[] = [];
protected contentReplacements: Record<string, LazyContent<T>> = {};
protected priorityReplacements: Record<string, number> = {};
constructor(key: string) {
this.key = key;
}
add(itemName: string, content: LazyContent<T>, priority: number = 0) {
this.additions.push({ itemName, content, priority });
return this;
}
remove(itemName: string) {
this.removals.push(itemName);
return this;
}
setContent(itemName: string, content: LazyContent<T>) {
this.contentReplacements[itemName] = content;
return this;
}
setPriority(itemName: string, priority: number) {
this.priorityReplacements[itemName] = priority;
return this;
}
extend(app: Application, extension: IExtensionModule) {
const { key, additions, removals, contentReplacements, priorityReplacements } = this;
extend(ItemListComponent.prototype, 'items', function (this: ItemListComponent, items) {
if (key !== this.attrs.key) return;
const safeContent = (content: Mithril.Children) => (typeof content === 'string' ? [content] : content);
for (const itemName of removals) {
items.remove(itemName);
}
for (const { itemName, content, priority } of additions) {
items.add(itemName, safeContent(content(this.attrs.context)), priority);
}
for (const [itemName, content] of Object.entries(contentReplacements)) {
items.setContent(itemName, safeContent(content(this.attrs.context)));
}
for (const [itemName, priority] of Object.entries(priorityReplacements)) {
items.setPriority(itemName, priority);
}
});
}
}

View File

@ -2,12 +2,14 @@ import Model from './Model';
import PostTypes from './PostTypes';
import Routes from './Routes';
import Store from './Store';
import ItemList from './ItemList';
const extenders = {
Model,
PostTypes,
Routes,
Store,
ItemList,
};
export default extenders;

View File

@ -5,7 +5,6 @@ import Page, { IPageAttrs } from '../../common/components/Page';
import ItemList from '../../common/utils/ItemList';
import DiscussionHero from './DiscussionHero';
import DiscussionListPane from './DiscussionListPane';
import LoadingIndicator from '../../common/components/LoadingIndicator';
import SplitDropdown from '../../common/components/SplitDropdown';
import listItems from '../../common/helpers/listItems';
import DiscussionControls from '../utils/DiscussionControls';

View File

@ -2,8 +2,8 @@ import Component from '../../common/Component';
import type { ComponentAttrs } from '../../common/Component';
import type Mithril from 'mithril';
import classList from '../../common/utils/classList';
import ItemList from '../../common/utils/ItemList';
import LoadingIndicator from '../../common/components/LoadingIndicator';
import ItemList from '../../common/components/ItemList';
export interface PageStructureAttrs extends ComponentAttrs {
hero?: () => Mithril.Children;
@ -21,73 +21,44 @@ export default class PageStructure<CustomAttrs extends PageStructureAttrs = Page
this.content = vnode.children;
return <div className={classList('Page', className)}>{this.rootItems().toArray()}</div>;
}
return (
<div className={classList('Page', className)}>
<ItemList key="PageStructure.rootItems" context={this}>
<div key="pane" className="Page-pane">
{(this.attrs.pane && this.attrs.pane()) || null}
</div>
rootItems(): ItemList<Mithril.Children> {
const items = new ItemList<Mithril.Children>();
<div key="main" className="Page-main">
{this.attrs.loading ? (
<ItemList key="PageStructure.loadingItems" context={this}>
<LoadingIndicator key="spinner" display="block" />
</ItemList>
) : (
<ItemList key="PageStructure.mainItems" context={this}>
<div key="hero" className="Page-hero">
{(this.attrs.hero && this.attrs.hero()) || null}
</div>
items.add('pane', this.providedPane(), 100);
items.add('main', this.main(), 10);
<div key="container" className="Page-container container">
<div key="sidebar" className="Page-sidebar">
<ItemList key="PageStructure.sidebarItems" context={this}>
{this.attrs.sidebar && (
<div key="provided" className="Page-sidebar-main">
{this.attrs.sidebar()}
</div>
)}
</ItemList>
</div>
return items;
}
mainItems(): ItemList<Mithril.Children> {
const items = new ItemList<Mithril.Children>();
items.add('hero', this.providedHero(), 100);
items.add('container', this.container(), 10);
return items;
}
loadingItems(): ItemList<Mithril.Children> {
const items = new ItemList<Mithril.Children>();
items.add('spinner', <LoadingIndicator display="block" />, 100);
return items;
}
main(): Mithril.Children {
return <div className="Page-main">{this.attrs.loading ? this.loadingItems().toArray() : this.mainItems().toArray()}</div>;
}
containerItems(): ItemList<Mithril.Children> {
const items = new ItemList<Mithril.Children>();
items.add('sidebar', this.sidebar(), 100);
items.add('content', this.providedContent(), 10);
return items;
}
container(): Mithril.Children {
return <div className="Page-container container">{this.containerItems().toArray()}</div>;
}
sidebarItems(): ItemList<Mithril.Children> {
const items = new ItemList<Mithril.Children>();
items.add('sidebar', (this.attrs.sidebar && this.attrs.sidebar()) || null, 100);
return items;
}
sidebar(): Mithril.Children {
return <div className="Page-sidebar">{this.sidebarItems().toArray()}</div>;
}
providedPane(): Mithril.Children {
return <div className="Page-pane">{(this.attrs.pane && this.attrs.pane()) || null}</div>;
}
providedHero(): Mithril.Children {
return <div className="Page-hero">{(this.attrs.hero && this.attrs.hero()) || null}</div>;
}
providedContent(): Mithril.Children {
return <div className="Page-content">{this.content}</div>;
<div key="content" className="Page-content">
{this.content}
</div>
</div>
</ItemList>
)}
</div>
</ItemList>
</div>
);
}
}

View File

@ -40,6 +40,10 @@
&-sidebar {
margin-top: 0;
&-main {
height: 100%;
}
}
}
}