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

Making Mithril more component-driven and less component-oriented #2278

Closed
dead-claudia opened this issue Nov 4, 2018 · 8 comments
Closed
Labels
Type: Breaking Change For any feature request or suggestion that could reasonably break existing code Type: Enhancement For any feature request or suggestion that isn't a bug fix Type: Meta/Feedback For high-level discussion around the project and/or community itself

Comments

@dead-claudia
Copy link
Member

dead-claudia commented Nov 4, 2018

Updates
  • Remove m.async from this bug
  • Add Simplify our component model #2295 to this bug
  • Add a philosophical summary
  • Fix a missed word
  • Add a TypeScript definition summary of everything

This evolved out of a long discussion between @pygy, @barneycarroll, and I on Gitter.

So I've got this concept of how Mithril could evolve, at a high level, and I feel it could really leverage components and m better and be less of a kingdom of nouns. This would all aim for better usability and consistency first, but incidental perf wins could also arise out of this. This is a general synthesis of what all this is and how it'd fit together.

There are three general types of changes I'm suggesting here:

I plan to first kick this off in my own fork in a dedicated branch, so I can work on nailing exacts down. It'll also let me independently experiment on how it should be modularized.

If this gets accepted, it'd be a semver-major breaking change with non-trivial migration. It won't be as bad as v0.2 to v1, but it certainly won't be as simple as v1 to v2.

If you're curious what this looks like from 10,000 feet away, here's a rough (and probably broken) TypeScript definition file.
type StringCoercibleChild =
    string | number | boolean |
    null | undefined;

type Child<T extends Vnode["tag"]> =
    Vnode & {tag: T} | StringCoercibleChild | ChildArray<string>;
interface ChildArray<T extends Vnode["tag"]> extends Array<Child<T>> {}

type KeyedChild<K extends PropertyKey, T extends Vnode["tag"]> =
    Vnode & {tag: T, key: K} | KeyedChildArray<T>;
interface KeyedChildArray<K extends PropertyKey, DOM>
    extends Array<KeyedChild<K, DOM>> {}

// Definitions omitted for brevity - it involves a lot of type-level hacking.
type NormalizeChildren<T extends string, C extends Child<any>[]> = unknown;

type OnUpdateReturn<View extends AttrsV<any> | undefined> =
    View | OnUpdateHooks<View>;

interface OnUpdateHooks<View extends AttrsV<any> | undefined> {
    view: View;
    onremove?(): void | Promise<void>;
}

interface AttrsV<A extends Attributes<T>, T extends Vnode["tag"]> extends Vnode {
    tag: T;
    attrs: A;
}

interface Attributes<T extends Vnode["tag"]> {
    key?: any;
    onupdate?(vnode: AttrsV<this, T>, old: AttrsV<this, T> | undefined):
        OnUpdateReturn<void | typeof old>;
}

interface Events {
    [name: string]: EventListenerOrEventListenerObject;
    handleEvent(ev: Event): void;
}

type InferChildren<V extends Vnode> = (
    V["tag"] extends Component1<V["attrs"], infer C> ? C :
    NormalizeChildren<Elements<any>[V["tag"]]["children"]>
);

interface Vnode {
    tag: string | Component<ComponentType>;
    key: this["attrs"]["key"];
    attrs: Attributes<this["tag"]>;
    children: InferChildren<this>;
    state: unknown | undefined;
    dom: Node;
    domSize: number;
    update: undefined | (next: State) => void;

    // Private: don't use these!
    // Component and `onupdate` return value, if it's an object.
    _hooks: ComponentHooks<any, any>;
    // Next state for components, event handler for elements
    _next: State | Events | undefined;
    // Component's `view` instance + copy of element vnodes' `children`
    _rendered: Children | Child<unknown>;
}

interface ComponentType {
    Attrs?: {};
    Children?: Vnode[];
    State?: any;
    DOM?: string | undefined;
}

interface V<T extends ComponentType> extends Vnode {
    tag: Component<T["Attrs"], T["Children"]>;
    attrs: T["Attrs"];
    children: T["Children"],
    state: T["State"] | undefined;
    update: (next: State) => void;
}

type Update<T extends ComponentType> = (value: T["State"]) => void;

type ComponentReturn<View extends Child<any> | V<any> | undefined> =
    View | ComponentHooks<T, View>;

interface ComponentHooks<View extends Child<any> | V<any> | undefined> {
    next: State;
    view: View;
    onremove?(): void | Promise<void>;
}

type Component<T extends ComponentType> = (
    vnode: V<T>, old: V<T> | undefined, update: Update<T>
) => ComponentReturn<Child<T["DOM"]> | typeof old>;

interface RouteSetOptions {
    data?: any;
    params?: any;
    replace?: boolean;
    state?: any;
    title?: boolean;
}

interface Router {
    prefix: string;
    current: string;
    readonly params: {[key: string]: unknown};
    set(path: string, options?: RouteSetOptions): Promise<void>;
    init: Component<{
        Attrs: {
        	default: string,
            onmatch?(
                render: (...children: Child<unknown>[]) => void
            ): void | Promise<void>;
            routes: {
                [key: string](childRoute: Router): Child<unknown>;
            };
        };
    }>;
    link: Component<{
        // An existential would be nice right about here...
        Attrs: RouteSetOptions & {
            tag: string | Component<ComponentType>;
            attrs: Attributes<this["tag"]> & {[key: string]: any};
        };
        Children: Vnode;
    }>;
}

const m: {
    (
        tag: string, attrs: Elements[typeof tag]["attrs"],
        ...children: Elements[typeof tag]["children"]
    ): Vnode & {
        tag: typeof tag; attrs: typeof attrs;
        children: NormalizeChildren<typeof tag, typeof children>
    };
    (
        tag: string, ...children: Elements<undefined>[typeof tag]["children"]
    ): Vnode & {
        tag: typeof tag; attrs: undefined;
        children: NormalizeChildren<typeof tag, typeof children>
    };

    (
        tag: Component<{Attrs: typeof attrs, Children: NormalizeChildren<typeof children>}>,
        attrs: Attributes<typeof tag>,
        ...children: Child<unknown>[]
    ): Vnode & {
        tag: typeof tag; attrs: typeof attrs;
        children: NormalizeChildren<typeof tag, typeof children>
    };
    (
        tag: Component<{Attrs?: undefined, Children: NormalizeChildren<typeof children>}>,
        ...children: Child<unknown>[]
    ): Vnode & {
        tag: typeof tag; attrs: undefined;
        children: NormalizeChildren<typeof tag, typeof children>
    };

    readonly fragment: "[";
    readonly trust: "<";
    readonly text: "#";
    readonly keyed: ".";

    vnode: {
        (
            tag: string,
            key: (typeof attrs)["key"],
            attrs: Elements[typeof tag]["attrs"],
            children: NormalizeChildren<Elements[typeof tag]["children"]>,
            dom: Node,
        ): Vnode & {
            tag: typeof tag; attrs: typeof attrs; children: typeof children;
        };
        (
            tag: Component<typeof attrs, typeof children>,
            key: (typeof attrs)["key"],
            attrs: Attributes<typeof tag>,
            children: Vnode[],
            dom: undefined,
        ): Vnode & {
            tag: typeof tag; attrs: typeof attrs; children: typeof children;
        };

        normalize(this: any, child: Child<unknown>): NormalizeChildren<[typeof child]>;
    };

    // Invoke an async redraw
    redraw(this: any): void;
    // Invoke a sync redraw
    redrawSync(this: any): void;

    // Mount and subscribe a render function for an element
    mount(this: any, elem: ParentNode, render: () => Child<unknown>): void;
    // Unmount an element's corresponding render function
    mount(this: any, elem: ParentNode, render?: null | undefined): void;

    // Render a vnode
    render(this: any, elem: ParentNode, child: Child<unknown>, redraw: () => void): void;

    route: Router;
}
export default m;

interface HTMLAttributes<Tag extends string> extends Attributes<Tag> {
    // Omitted for brevity
}

interface Elements {
    "#": {attrs: Attributes<"#">, children: StringCoercibleChild[]};
    "<": {attrs: Attributes<"<">, children: StringCoercibleChild[]};
    "[": {attrs: Attributes<"[">, children: Child<unknown>[]};
    // Keyed fragments require children with valid property keys and keys of the
    // same type.
    ".": (
        K extends PropertyKey ?
        {attrs: Attributes<".">, children: KeyedChild[]} :
        never
    );

    // HTML elements - a few are shown for demonstration, but of course this
    // would include all of them.
    "a": {attrs: HTMLAttributes<"a">, children: Child<any>[]};
    "details": {attrs: HTMLAttributes<"details">, children: [
        Vnode & {tag: "summary"},
        ...Child<any>[]
    ]};
}

In this comment, I wrote this paragraph. This is a great summary for why I'm pursuing this.

I think Mithril v0.2 was on to something by keeping it simple - its config attribute at its core was fundamentally pretty sound, just its design was really ad-hoc. The v1 rewrite really felt like both a step forward (better decomposition) and a step back (more complicated API), and I'd like for us to recover that lost step by reigning in the API complexity. I miss that beautiful simplicity it had.

@dead-claudia dead-claudia added Type: Breaking Change For any feature request or suggestion that could reasonably break existing code Type: Enhancement For any feature request or suggestion that isn't a bug fix Type: Meta/Feedback For high-level discussion around the project and/or community itself labels Nov 4, 2018
@dead-claudia dead-claudia added this to the post-v2 milestone Nov 4, 2018
@dead-claudia dead-claudia added this to Under consideration in Feature requests/Suggestions via automation Nov 4, 2018
@project-bot project-bot bot added this to Needs triage in Triage/bugs Nov 4, 2018
@dead-claudia dead-claudia removed this from Needs triage in Triage/bugs Nov 4, 2018
@dead-claudia
Copy link
Member Author

@MithrilJS/collaborators Could I get some feedback on this?

I won't likely be able to respond to any of it today, but I'll be able to tomorrow.

@StephanHoyer
Copy link
Member

Some great proposals. Thanks @isiahmeadows.

The m.route changes are pretty much. I have to take a closer look to give you some feedback.

@spacejack
Copy link
Contributor

Yes, definitely, view functions for mount/route would be appreciated. It sounds like they would no longer accept components which is, I think, bit unfortinate for really simple stuff.

Would it be easier to use m.render with routing?

I'm not 100% sold on adding m.async to core rather than leaving that to userland solutions.

@fuzetsu
Copy link
Contributor

fuzetsu commented Nov 4, 2018

I like the proposals, and this statement really resonates with me:

My goal here would be to further encourage component use over definition

The async component might not need to be in core, but I think having more upfront/semi official pre made components for mithril would be great thing. Even if it was a sub repo, or part of the mithril org.

@dead-claudia
Copy link
Member Author

@fuzetsu Just filed #2284.

@dead-claudia
Copy link
Member Author

BTW, I'm going to close this + its sub-bugs, and I'll file a single RFC-style issue once I have a branch set up on my local fork with a recast, better-structured proposal. I need to have this better locked down and organized first, and spreading it out across 5 issues just isn't cutting it. It's also easier to explain if it's all a bunch of Markdown files I can link to and within.

Feature requests/Suggestions automation moved this from In discussion to Completed/Declined Nov 27, 2018
@dead-claudia
Copy link
Member Author

If you want to follow developments in the meantime (before I get to filing the issues), follow this branch.

@dead-claudia
Copy link
Member Author

Edit: before I get to filing the issue (singular).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Type: Breaking Change For any feature request or suggestion that could reasonably break existing code Type: Enhancement For any feature request or suggestion that isn't a bug fix Type: Meta/Feedback For high-level discussion around the project and/or community itself
Projects
Development

No branches or pull requests

4 participants