diff --git a/resources/js/Components/Component.js b/resources/js/Components/Component.js new file mode 100644 index 000000000..f061d9e15 --- /dev/null +++ b/resources/js/Components/Component.js @@ -0,0 +1,156 @@ +/* eslint-disable no-unused-vars */ +import * as Mithril from 'mithril'; + +export interface ComponentAttrs extends Mithril.Attributes {} + +/** + * @abstract + * + * The `Component` class defines a user interface 'building block'. A component + * generates a virtual DOM to be rendered on each redraw. + * + * Essentially, this is a wrapper for Mithril's components that adds several useful features: + * + * - In the `oninit` and `onbeforeupdate` lifecycle hooks, we store vnode attrs in `this.attrs. + * This allows us to use attrs across components without having to pass the vnode to every single + * method. + * - The static `initAttrs` method allows a convenient way to provide defaults (or to otherwise + * modify) the attrs that have been passed into a component. + * - When the component is created in the DOM, we store its DOM element under `this.element`; + * this lets us use jQuery to modify child DOM state from internal methods via the `this.$()` + * method. + * - A convenience `component` method, which serves as an alternative to hyperscript and JSX. + * + * As with other Mithril components, components extending Component can be initialized + * and nested using JSX, hyperscript, or a combination of both. The `component` method can also + * be used. + * + * @example + * return m('div',

Hello World

); + * + * @example + * return m('div', MyComponent.component({foo: 'bar'), m('p', 'Hello World!')); + * + * @see https://mithril.js.org/components.html + */ +export default class Component implements ComponentAttrs { + /** + * The root DOM element for the component. + * + * @protected + */ + element: Element; + + /** + * The attributes passed into the component. + * + * @see https://mithril.js.org/components.html#passing-data-to-components + * @protected + */ + attrs; + + /** + * @inheritdoc + * @abstract + */ + view(vnode: Mithril.vnode) {} + + /** + * @inheritdoc + */ + oninit(vnode: Mithril.vnode) { + this.setAttrs(vnode.attrs); + } + + /** + * @inheritdoc + */ + oncreate(vnode: Mithril.vnode) { + this.element = vnode.dom; + } + + /** + * @inheritdoc + */ + onbeforeupdate(vnode: Mithril.vnode) { + this.setAttrs(vnode.attrs); + } + + /** + * @inheritdoc + */ + onupdate(vnode: Mithril.vnode) { + } + + /** + * @inheritdoc + */ + onbeforeremove(vnode: Mithril.vnode) { + } + + /** + * @inheritdoc + */ + onremove(vnode: Mithril.vnode) { + } + + /** + * Returns a jQuery object for this component's element. If you pass in a + * selector string, this method will return a jQuery object, using the current + * element as its buffer. + * + * For example, calling `component.$('li')` will return a jQuery object + * containing all of the `li` elements inside the DOM element of this + * component. + * + * @param [selector] a jQuery-compatible selector string + * @returns the jQuery object for the DOM node + * @final + * @protected + */ + /* $(selector?: string): JQuery { + const $element: JQuery = $(this.element); + return selector ? $element.find(selector) : $element; + }; */ + + + /** + * Convenience method to attach a component without JSX. + * Has the same effect as calling `m(THIS_CLASS, attrs, children)`. + * + * @see https://mithril.js.org/hyperscript.html#mselector,-attributes,-children + */ + static component(attrs = {}, children = null): Mithril.vnode { + const componentAttrs: Record = { ...attrs}; + + return Mithril.m(this, componentAttrs, children); + } + + /** + * Saves a reference to the vnode attrs after running them through initAttrs, + * and checking for common issues. + * + * @private + */ + setAttrs(attrs = {}): void { + this.initAttrs(attrs); + if (attrs) { + if ('children' in attrs) { + throw new Error(`[${this.constructor.name}] The "children" attribute of attrs should never be used. Either pass children in as the vnode children or rename the attribute`); + } + if ('tag' in attrs) { + throw new Error(`[${this.constructor.name}] You cannot use the "tag" attribute name with Mithril 2.`); + } + } + this.attrs = attrs; + } + + /** + * Initialize the component's attrs. + * + * This can be used to assign default values for missing, optional attrs. + * + * @protected + */ + initAttrs(attrs): void {} +} diff --git a/resources/js/Components/Page.js b/resources/js/Components/Page.js new file mode 100644 index 000000000..58d213a66 --- /dev/null +++ b/resources/js/Components/Page.js @@ -0,0 +1,8 @@ +import Component from './Component'; + +/** + * The `Page` component + * + * @abstract + */ +export default class Page extends Component {} diff --git a/resources/js/Views/SetupPage.js b/resources/js/Views/SetupPage.js index 1dcd6a743..8e8a2afd9 100644 --- a/resources/js/Views/SetupPage.js +++ b/resources/js/Views/SetupPage.js @@ -1,5 +1,12 @@ -import m from 'mithril'; +import Page from '../Components/Page'; -export default { - view: () =>

Hello World!

-}; +export default class SetupPage extends Page { + // eslint-disable-next-line no-unused-vars + view(vnode) { + return ( + <> +

Hello World!

+ + ); + } +} diff --git a/resources/js/utils.js b/resources/js/utils.js new file mode 100644 index 000000000..7b4ac2bd0 --- /dev/null +++ b/resources/js/utils.js @@ -0,0 +1,6 @@ +/** + * Check if class A is the same as or a subclass of class B. + */ +export default function subclassOf(A, B) { + return A && (A === B || A.prototype instanceof B); +}