Becklyn / mojave

A library of commonly used JavaScript tools and helpers by Becklyn
BSD 3-Clause "New" or "Revised" License
5 stars 3 forks source link

[v5] Mount improvements: automatic parameter inference/checking #225

Closed keichinger closed 5 years ago

keichinger commented 5 years ago
Q A
Bug fix? no
New feature? yes
Improvement? no
BC breaks? yes
Deprecations? no
Docs PR missing

This PR splits the mount method up (again) into three distinct methods:

This change has sadly been necessary, as TypeScripts generic type inference couldn't support a new feature: automatic parameter inference/checking for the component you were trying to mount. In case there is no additional component parameters, the corresponding mojave.*MountOptions::params type will be never, to enforce this as well.

There is one caveat to this feature, which I'm not yet sure how to prevent/implement it right now, which is TypeScript isn't throwing any errors when I don't define the options.params property. However, this is a known limitation (for now) which I greatly accept as the new feature is better for overall DX and reduces the amount of accidental errors. Other than that, we're currently not throwing any errors as well, so this is not even a regression and thus should be fine.

Here are a few examples of what's working and what is (currently) still not yet possible to support:

mount with functions

Example component

function someMountableFunction(el: HTMLElement, a: boolean): void
{
}

mount now is aware of the second, boolean parameter and correctly throws errors when too few or too many parameters have been passed. It extracts any additional function from the passed-in function and uses these as type for mojave.FunctionMountOptions::params:

mount(".foo", someMountableFunction);                           // no errors -> false positive & known limitation!
mount(".foo", someMountableFunction, { type: "func" });         // no errors -> false positive & known limitation!
mount(".foo", someMountableFunction, { type: "func", params: [  // no errors -> correct!
    true,
]});
mount(".foo", someMountableFunction, { type: "func", params: [  // errors, too many parameters -> correct!
    true,
    "bla", // unknown additional parameter
    42,    // unknown additional parameter
]});

mountClass with StandaloneComponent

Example component

class SomeMountableComponent extends StandaloneComponent
{
    private el: HTMLElement;
    private a: boolean;

    public constructor(el: HTMLElement, a: boolean)
    {
        this.el = el;
        this.a = a;
    }

    /**
     * @inheritDoc
     */
    public init(): void
    {
    }
}

Here, mounting SomeMountableComponent using mountClass works the same way as mount does: It automatically extracts additional constructor parameters from the passed-in StandaloneComponent, which is then used as type for mojave.ClassMountOptions::params to check against.

mountClass(".foo", SomeMountableComponent);                             // no errors -> false positive & known limitation!
mountClass(".foo", SomeMountableComponent, { type: "class" });          // no errors -> false positive & known limitation!
mountClass(".foo", SomeMountableComponent, { type: "class", params: [   // no errors -> correct!
    true,
]});
mountClass(".foo", SomeMountableComponent, { type: "class", params: [   // errors, too many parameters -> correct!
    true,
    "bla", // unknown additional parameter
    42,    // unknown additional parameter
]});

mountJsx with Preact components

Last but not least:

//
// Functional component
//
type PreactFunctionalProps = {
    a: string;
    b: number;
}

function SomePreactFunctionalComponent (props: PreactFunctionalProps) : JSX.Element
{
    return null;
}

//
// Class component
//
type SomePreactClassComponentProps = {
    a: string;
    b: number;
}

class SomePreactClassComponent extends Component<SomePreactClassComponentProps, {}>
{
    /**
     * @inheritDoc
     */
    public render(props: Readonly<SomePreactClassComponentProps>, state: Readonly<{}>): preact.ComponentChild
    {
        return null;
    }
}

The story here is very similar. The only difference, as previously, is that mojave.ComponentMountOptions::params's type is now the corresponding props-type of the given component. So in the case of SomePreactFunctionalComponent the type is PreactFunctionalProps, for SomePreactClassComponent it's SomePreactClassComponentProps.

//
// Functional component
//
mountJsx(".foo", SomePreactFunctionalComponent);                            // no errors -> false positive & known limitation!
mountJsx(".foo", SomePreactFunctionalComponent, { type: "jsx" });           // no errors -> false positive & known limitation!
mountJsx(".foo", SomePreactFunctionalComponent, { type: "jsx", params: {    // no errors -> correct!
    a: "foo",
    b: 42,
}});
mountJsx(".foo", SomePreactFunctionalComponent, { type: "jsx", params: {    // errors, too many parameters -> correct!
    a: "foo",
    b: 42,
    asdf: "bla", // unknown additional parameter
}});

//
// Class component
//
mountJsx(".foo", SomePreactClassComponent);                                 // no errors -> false positive & known limitation!
mountJsx(".foo", SomePreactClassComponent, { type: "jsx" });                // no errors -> false positive & known limitation!
mountJsx(".foo", SomePreactClassComponent, { type: "jsx", params: {         // no errors -> correct!
    a: "foo",
    b: 42,
}});
mountJsx(".foo", SomePreactClassComponent, { type: "jsx", params: {         // errors, too many parameters -> correct!
    a: "foo",
    b: 42,
    asdf: "bla", // unknown additional parameter
}});
apfelbox commented 5 years ago

With this change we must get rid of the type parameter and ideally with the if-parts in doMount as well.

Also please target master and branch of a master-v4. master is always the latest version.

keichinger commented 5 years ago

Apparently GitHub has automatically closed my PR because I've renamed the master branches. Well, I've opened a new PR #226 with the requested changes.