Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ItemList component #3928

Draft
wants to merge 1 commit into
base: 2.x
Choose a base branch
from
Draft
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
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;
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%;
}
Comment on lines +43 to +46
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these changes necessary in this pr? Seems unrelated?

}
}
}
Expand Down
Loading