Skip to content

Commit

Permalink
feat: ItemList component
Browse files Browse the repository at this point in the history
  • Loading branch information
SychO9 committed Nov 10, 2023
1 parent d01c0e5 commit 13f997d
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 70 deletions.
55 changes: 55 additions & 0 deletions framework/core/js/src/common/components/ItemList.tsx
Original file line number Diff line number Diff line change
@@ -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>[];
}
}
89 changes: 89 additions & 0 deletions framework/core/js/src/common/extenders/ItemList.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
}
}
2 changes: 2 additions & 0 deletions framework/core/js/src/common/extenders/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
1 change: 0 additions & 1 deletion framework/core/js/src/forum/components/DiscussionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
109 changes: 40 additions & 69 deletions framework/core/js/src/forum/components/PageStructure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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>;
}

rootItems(): ItemList<Mithril.Children> {
const items = new ItemList<Mithril.Children>();

items.add('pane', this.providedPane(), 100);
items.add('main', this.main(), 10);

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>;
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>

<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>

<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>

<div key="content" className="Page-content">
{this.content}
</div>
</div>
</ItemList>
)}
</div>
</ItemList>
</div>
);
}
}
4 changes: 4 additions & 0 deletions framework/core/less/forum/DiscussionPage.less
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@

&-sidebar {
margin-top: 0;

&-main {
height: 100%;
}
}
}
}
Expand Down

0 comments on commit 13f997d

Please sign in to comment.