diff --git a/types/mithril/hyperscript.d.ts b/types/mithril/hyperscript.d.ts new file mode 100644 index 0000000000..6f83f2244f --- /dev/null +++ b/types/mithril/hyperscript.d.ts @@ -0,0 +1,3 @@ +import { Hyperscript } from "mithril"; +declare const h: Hyperscript; +export = h; diff --git a/types/mithril/index.d.ts b/types/mithril/index.d.ts index d1c2ba78a6..adfc9cd117 100644 --- a/types/mithril/index.d.ts +++ b/types/mithril/index.d.ts @@ -1,863 +1,280 @@ -// Type definitions for Mithril +// Type definitions for Mithril 1.1 // Project: http://lhorie.github.io/mithril/ -// Definitions by: Leo Horie , Chris Bowdon +// Definitions by: Leo Horie , Chris Bowdon , Mike Linkovich , AndrĂ¡s Parditka // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.2 -// Mithril type definitions for Typescript - -/** -* This is the module containing all the types/declarations/etc. for Mithril -*/ declare namespace Mithril { - interface ChildArray extends Array {} - type Children = Child | ChildArray; - type Child = string | VirtualElement | Component; - interface Static { - /** - * Creates a virtual element for use with m.render, m.mount, etc. - * - * @param selector A simple CSS selector. May include SVG tags. Nested - * selectors are not supported. - * @param attributes Attributes to add. Any DOM attribute may be used - * as an attribute, although innerHTML and the like may be overwritten - * silently. - * @param children Child elements, components, and text to add. - * @return A virtual element. - * - * @see m.render - * @see m.mount - * @see m.component - */ - ( - selector: string, - ...children: Children[] - ): VirtualElement; - - /** - * Creates a virtual element for use with m.render, m.mount, etc. - * - * @param selector A simple CSS selector. May include SVG tags. Nested - * selectors are not supported. - * @param attributes Attributes to add. Any DOM attribute may be used - * as an attribute, although innerHTML and the like may be overwritten - * silently. - * @param children Child elements, components, and text to add. - * @return A virtual element. - * - * @see m.render - * @see m.mount - * @see m.component - */ - ( - selector: string, - attributes: Attributes, - ...children: Children[] - ): VirtualElement; - - /** - * Initializes a component for use with m.render, m.mount, etc. - * - * @param component A component. - * @param args Arguments to optionally pass to the component. - * @return A component. - * - * @see m.render - * @see m.mount - * @see m - */ - ( - component: Component, - ...args: any[] - ): Component; - - /** - * Creates a getter-setter function that wraps a Mithril promise. Useful - * for uniform data access, m.withAttr, etc. - * - * @param promise A thennable to initialize the property with. It may - * optionally be a Mithril promise. - * @return A getter-setter function wrapping the promise. - * - * @see m.withAttr - */ - prop(promise: Thennable) : Promise; - - /** - * Creates a getter-setter function that wraps a simple value. Useful - * for uniform data access, m.withAttr, etc. - * - * @param value A value to initialize the property with - * @return A getter-setter function wrapping the value. - * - * @see m.withAttr - */ - prop(value: T): BasicProperty; - - /** - * Creates a getter-setter function that wraps a simple value. Useful - * for uniform data access, m.withAttr, etc. - * - * @return A getter-setter function wrapping the value. - * - * @see m.withAttr - */ - prop(): BasicProperty; - - /** - * Returns a event handler that can be bound to an element, firing with - * the specified property. - * - * @param property The property to get from the event. - * @param callback The handler to use the value from the event. - * @return A function suitable for listening to an event. - */ - withAttr( - property: string, - callback: (value: any) => any, - callbackThis?: any - ): (e: Event) => void; - - /** - * @deprecated Use m.mount instead - */ - module( - rootElement: Node, - component: Component - ): T; - - /** - * Mounts a component to a base DOM node. - * - * @param rootElement The base node. - * @param component The component to mount. - * @return An instance of the top-level component's controller - */ - mount( - rootElement: Node, - component: Component - ): T; - - /** - * Initializes a component for use with m.render, m.mount, etc. - * - * @param selector A component. - * @param args Arguments to optionally pass to the component. - * @return A component. - * - * @see m.render - * @see m.mount - * @see m - */ - component( - component: Component, - ...args: any[] - ): Component; - - /** - * Trust this string of HTML. - * - * @param html The HTML to trust - * @return A String object instance with an added internal flag to mark - * it as trusted. - */ - trust(html: string): TrustedString; - - /** - * Render a virtual DOM tree. - * - * @param rootElement The base element/node to render the tree from. - * @param children One or more child nodes to add to the tree. - * @param forceRecreation If true, overwrite the entire tree without - * diffing against it. - */ - render( - rootElement: Element, - children: VirtualElement|VirtualElement[], - forceRecreation?: boolean - ): void; - - redraw: { - /** - * Force a redraw the active component. It redraws asynchronously by - * default to allow for simultaneous events to run before redrawing, - * such as the event combination keypress + input frequently used for - * input. - * - * @param force If true, redraw synchronously. - */ - (force?: boolean): void; - - /** - * Gets/sets the current redraw strategy, which returns one of the - * following: - * - * "all" - recreates the DOM tree from scratch - * "diff" - recreates the DOM tree from scratch - * "none" - leaves the DOM tree intact - * - * This is useful for event handlers, which may want to cancel - * the next redraw if the event doesn't update the UI. - * - * @return The current strategy - */ - strategy: BasicProperty<"all" | "diff" | "none">; - } - - route: { - /** - * Enable routing, mounting a controller based on the route. It - * automatically mounts the components for you, starting with the one - * specified by the default route. - * - * @param rootElement The element to mount the active controller to. - * @param defaultRoute The route to start with. - * @param routes A key-value mapping of pathname to controller. - */ - ( - rootElement: Element, - defaultRoute: string, - routes: Routes - ): void; - - /** - * This allows m.route to be used as the `config` attribute for a - * virtual element, particularly useful for cases like this: - * - * ```ts - * // Note that the '#' is not required in `href`, thanks to the - * `config` setting. - * m("a[href='/dashboard/alicesmith']", {config: m.route}); - * ``` - */ - ( - element: Element, - isInitialized: boolean, - context?: Context, - vdom?: VirtualElement - ): void; - - /** - * Programmatically redirect to another route. - * - * @param path The route to go to. - * @param params Parameters to pass as a query string. - * @param shouldReplaceHistory Whether to replace the current history - * instead of adding a new one. - */ - (path: string, params?: any, shouldReplaceHistory?: boolean): void; - - /** - * Gets the current route. - * - * @return The current route. - */ - (): string; - - /** - * Gets a route parameter. - * - * @param key The key to get. - * @return The value associated with the parameter key. - */ - param(key: string): string; - - /** - * The current routing mode. This may be changed before calling - * m.route to change the part of the URL used to perform the routing. - * - * The value can be set to one of the following, defaulting to - * "hash": - * - * "search" - Uses the query string. This allows for named anchors to - * work on the page, but changes cause IE8 and lower to refresh the - * page. - * - * "hash" - Uses the hash. This is the only routing mode that does - * not cause page refreshes on any browser, but it does not support - * named anchors. - * - * "pathname" - Uses the URL pathname. This requires server-side - * setup to support bookmarking and page refreshes. It always causes - * page refreshes on IE8 and lower. Note that this requires that the - * application to be run from the root of the URL. - */ - mode: "search" | "hash" | "pathname"; - - /** - * Serialize an object into a query string. - * - * @param data The data to serialize. - * @return The serialized string. - */ - buildQueryString(data: Object): string; - - /** - * Parse a query string into an object. - * - * @param data The data to parse. - * @return The parsed object data. - */ - parseQueryString(data: string): Object; - } - - /** - * Send an XHR request to a server. Note that the `url` option is - * required. - * - * @param options The options to use for the request. - * @return A promise to the returned data, or void if not applicable. - * - * @see XHROptions for the available options. - */ - request(options: XHROptions): Promise - - /** - * Send a JSONP request to a server. Note that the `url` option is - * required. - * - * @param options The options to use - * @return A promise to the returned data. - * - * @see JSONPOptions for the available options. - */ - request(options: JSONPOptions): Promise; - - deferred: { - /** - * Create a Mithril deferred object. It behaves synchronously if - * possible, an intentional deviation from Promises/A+. Note that - * deferreds are completely separate from the redrawing system, and - * never trigger a redraw on their own. - * - * @return A new Mithril deferred instance. - * - * @see m.deferred.onerror for the error callback called for Error - * subclasses - */ - (): Deferred; - - /** - * A callback for all uncaught native Error subclasses in deferreds. - * This defaults to synchronously rethrowing all errors, a deviation - * from Promises/A+, but the behavior is configurable. To restore - * Promises/A+-compatible behavior. simply set this to a no-op. - */ - onerror(e: Error): void; - } - - /** - * Takes a list of promises or thennables and returns a Mithril promise - * that resolves once all in the list are resolved, or rejects if any of - * them reject. - * - * @param promises A list of promises to try to resolve. - * @return A promise that resolves to all the promises if all resolve, or - * rejects with the error contained in the first rejection. - */ - sync(promises: Thennable[]): Promise; - - /** - * Use this and endComputation if your views aren't redrawing after - * calls to third-party libraries. For integrating asynchronous code, - * this should be called before any asynchronous work is done. For - * synchronous code, this should be called at the beginning of the - * problematic segment. Note that these calls must be balanced, much like - * braces and parentheses. This is mostly used internally. Prefer - * m.redraw where possible, especially when making repeated calls. - * - * @see endComputation - * @see m.render - */ - startComputation(): void; - - /** - * Use startComputation and this if your views aren't redrawing after - * calls to third-party libraries. For integrating asynchronous code, - * this should be called after all asynchronous work completes. For - * synchronous code, this should be called at the end of the problematic - * segment. Note that these calls must be balanced, much like braces and - * parentheses. This is mostly used internally. Prefer m.redraw where - * possible, especially when making repeated calls. - * - * @see startComputation - * @see m.render - */ - endComputation(): void; - - /** - * This overwrites the internal version of window used by Mithril. - * It's mostly useful for testing, and is also used internally by - * Mithril to test itself. By default Mithril uses `window` for the - * dependency. - * - * @param mockWindow The mock to use for the window. - * @return The mock that was passed in. - */ - deps(mockWindow: Window): Window; + export interface Lifecycle { + /** Any property attached to the component object is copied for every instance of the component. This allows simple state initialization. */ + [propName: string]: any; + /** The oninit hook is called before a vnode is touched by the virtual DOM engine. */ + oninit?: (this: State, vnode: Vnode) => any; + /** The oncreate hook is called after a DOM element is created and attached to the document. */ + oncreate?: (this: State, vnode: VnodeDOM) => any; + /** The onbeforeupdate hook is called before a vnode is diffed in a update. */ + onbeforeremove?: (this: State, vnode: VnodeDOM) => Promise | void; + /** The onupdate hook is called after a DOM element is updated, while attached to the document. */ + onremove?: (this: State, vnode: VnodeDOM) => any; + /** The onbeforeremove hook is called before a DOM element is detached from the document. If a Promise is returned, Mithril only detaches the DOM element after the promise completes. */ + onbeforeupdate?: (this: State, vnode: Vnode, old: VnodeDOM) => boolean | void; + /** The onremove hook is called before a DOM element is removed from the document. */ + onupdate?: (this: State, vnode: VnodeDOM) => any; } - interface TrustedString extends String { - /** @private Implementation detail. Don't depend on it. */ - $trusted: boolean; + export interface Hyperscript { + /** Creates a virtual element (Vnode). */ + (selector: string, ...children: Children[]): Vnode; + /** Creates a virtual element (Vnode). */ + (selector: string, attributes: Attributes, ...children: Children[]): Vnode; + /** Creates a virtual element (Vnode). */ + (component: ComponentTypes, attributes: Attrs & Lifecycle & { key?: string | number }, ...args: Children[]): Vnode; + /** Creates a virtual element (Vnode). */ + (component: ComponentTypes, ...args: Children[]): Vnode; + /** Creates a fragment virtual element (Vnode). */ + fragment(attrs: Lifecycle & { [key: string]: any }, children: ChildArrayOrPrimitive): Vnode; + /** Turns an HTML string into a virtual element (Vnode). Do not use trust on unsanitized user input. */ + trust(html: string): Vnode; } - /** - * The interface for a virtual element. It's best to consider this immutable - * for most use cases. - * - * @see m - */ - interface VirtualElement { - /** - * The tag name of this element. - */ - tag: string; - - /** - * The attributes of this element. - */ - attrs: Attributes; - - /** - * The children of this element. - */ - children: Children[]; + export interface RouteResolver { + /** The onmatch hook is called when the router needs to find a component to render. */ + render?: (this: State, vnode: Vnode) => Children; + /** The render method is called on every redraw for a matching route. */ + onmatch?: (args: Params, requestedPath: string) => Component | Promise | void; } - /** - * An event passed by Mithril to unload event handlers. - */ - interface Event { - /** - * Prevent the default behavior of scrolling the page and updating the - * URL on next route change. - */ - preventDefault(): void; + /** This represents a key-value mapping linking routes to components. */ + export interface RouteDefs { + /** The key represents the route. The value represents the corresponding component. */ + [url: string]: ComponentTypes | RouteResolver; } - /** - * A context object for configuration functions. - * - * @see ElementConfig - */ - interface Context { - /** - * A function to call when the node is unloaded. Useful for cleanup. - */ - onunload?(): any; - - /** - * Set true if the backing DOM node needs to be retained between route - * changes if possible. Set false if this node needs to be recreated - * every single time, regardless of how "different" it is. - */ - retain?: boolean; + export interface RouteOptions { + /** Routing parameters. If path has routing parameter slots, the properties of this object are interpolated into the path string. */ + replace?: boolean; + /** The state object to pass to the underlying history.pushState / history.replaceState call.*/ + state?: any; + /** The title string to pass to the underlying history.pushState / history.replaceState call. */ + title?: string; } - /** - * This represents a callback function for a virtual element's config - * attribute. It's a low-level function useful for extra cleanup after - * removal from the tree, storing instances of third-party classes that - * need to be associated with the DOM, etc. - * - * @see Attributes - * @see Context - */ - interface ElementConfig { - /** - * A callback function for a virtual element's config attribute. - * - * @param element The associated DOM element. - * @param isInitialized Whether this is the first call for the virtual - * element or not. - * @param context The associated context for this element. - * @param vdom The associated virtual element. - */ - ( - element: Element, - isInitialized: boolean, - context: Context, - vdom: VirtualElement - ): void; + export interface Route { + /** Creates application routes and mounts Components and/or RouteResolvers to a DOM element. */ + (element: Element, defaultRoute: string, routes: RouteDefs): void; + /** Returns the last fully resolved routing path, without the prefix. */ + get(): string; + /** Redirects to a matching route or to the default route if no matching routes can be found. */ + set(route: string, data?: any, options?: RouteOptions): void; + /** Defines a router prefix which is a fragment of the URL that dictates the underlying strategy used by the router. */ + prefix(urlFragment: string): void; + /** This method is meant to be used in conjunction with an Vnode's oncreate hook. */ + link(vnode: Vnode): (e?: Event) => any; + /** Returns the named parameter value from the current route. */ + param(name: string): string; + /** Gets all route parameters. */ + param(): any; } - /** - * This represents the attributes available for configuring virtual elements, - * beyond the applicable DOM attributes. - * - * @see m - */ - interface Attributes { - /** - * The class name(s) for this virtual element, as a space-separated list. - */ - className?: string; - - /** - * The class name(s) for this virtual element, as a space-separated list. - */ - class?: string; - - /** - * A custom, low-level configuration in case this element needs special - * cleanup after removal from the tree. - * - * @see ElementConfig - */ - config?: ElementConfig; - - /** - * A key to optionally associate with this element. - */ - key?: string | number; - - /** - * Any other virtual element properties, including attributes and event - * handlers. - */ - [property: string]: any; + export interface Mount { + /** Mounts a component to a DOM element, enabling it to autoredraw on user events. */ + (element: Element, component: ComponentTypes | null): void; } - /** - * The basis of a Mithril controller instance. - */ - interface Controller { - /** - * An optional handler to call when the associated virtual element is - * destroyed. - * - * @param evt An associated event. - */ - onunload?(evt: Event): any; + export interface WithAttr { + /** Creates an event handler which takes the value of the specified DOM element property and calls a function with it as the argument. */ + (name: string, callback: (value: any) => any, thisArg?: any): (e: { currentTarget: any, [p: string]: any }) => void; } - /** - * This represents a controller function. - * - * @see ControllerConstructor - */ - interface ControllerFunction { - (...args: any[]): T; + export interface ParseQueryString { + /** Returns an object with key/value pairs parsed from a string of the form: ?a=1&b=2 */ + (queryString: string): { [p: string]: any }; } - /** - * This represents a controller constructor. - * - * @see ControllerFunction - */ - interface ControllerConstructor { - new (...args: any[]): T; + export interface BuildQueryString { + /** Turns the key/value pairs of an object into a string of the form: a=1&b=2 */ + (values: { [p: string]: any }): string; } - /** - * This represents a Mithril component. - * - * @see m - * @see m.component - */ - interface Component { - /** - * The component's controller. - * - * @see m.component - */ - controller: ControllerFunction | ControllerConstructor; - - /** - * Creates a view out of virtual elements. - * - * @see m.component - */ - view(ctrl?: T, ...args: any[]): VirtualElement; - } - - /** - * This is the base interface for property getter-setters - * - * @see m.prop - */ - interface Property { - /** - * Gets the contained value. - * - * @return The contained value. - */ - (): T; - - /** - * Sets the contained value. - * - * @param value The new value to set. - * @return The newly set value. - */ - (value: T): T; - } - - /** - * This represents a non-promise getter-setter functions. - * - * @see m.prop which returns objects that implement this interface. - */ - interface BasicProperty extends Property { - /** - * Makes this serializable to JSON. - */ - toJSON(): T; - } - - /** - * This represents a key-value mapping linking routes to components. - */ - interface Routes { - /** - * The key represents the route. The value represents the corresponding - * component. - */ - [key: string]: Component; - } - - /** - * This represents a Mithril deferred object. - */ - interface Deferred { - /** - * Resolve this deferred's promise with a value. - * - * @param value The value to resolve the promise with. - */ - resolve(value?: T): void; - - /** - * Reject this deferred with an error. - * - * @param value The reason for rejecting the promise. - */ - reject(reason?: any): void; - - /** - * The backing promise. - * - * @see Promise - */ - promise: Promise; - } - - /** - * This represents a thennable success callback. - */ - interface SuccessCallback { - (value: T): U | Thennable; - } - - /** - * This represents a thennable error callback. - */ - interface ErrorCallback { - (value: Error): T | Thennable; - } - - /** - * This represents a thennable. - */ - interface Thennable { - then(success: SuccessCallback): Thennable; - then(success: SuccessCallback, error: ErrorCallback): Thennable; - catch?(error: ErrorCallback): Thennable; - catch?(error: ErrorCallback): Thennable; - } - - /** - * This represents a Mithril promise object. - */ - interface Promise extends Thennable, Property> { - /** - * Chain this promise with a simple success callback, propogating - * rejections. - * - * @param success The callback to call when the promise is resolved. - * @return The chained promise. - */ - then(success: SuccessCallback): Promise; - - /** - * Chain this promise with a success callback and error callback, without - * propogating rejections. - * - * @param success The callback to call when the promise is resolved. - * @param error The callback to call when the promise is rejected. - * @return The chained promise. - */ - then(success: SuccessCallback, error: ErrorCallback): Promise; - - /** - * Chain this promise with a single error callback, without propogating - * rejections. - * - * @param error The callback to call when the promise is rejected. - * @return The chained promise. - */ - catch(error: ErrorCallback): Promise; - } - - /** - * These are the common options shared across normal and JSONP requests. - * - * @see m.request - */ - interface RequestOptions { - /** - * The data to be sent. It's automatically serialized in the right format - * depending on the method (with exception of HTML5 FormData), and put in - * the appropriate section of the request. - */ + export interface RequestOptions { + /** The HTTP method to use. */ + method?: string; + /** The data to be interpolated into the URL and serialized into the querystring (for GET requests) or body (for other types of requests). */ data?: any; - - /** - * Whether to run it in the background, i.e. true if it doesn't affect - * template rendering. - */ + /** Whether the request should be asynchronous. Defaults to true. */ + async?: boolean; + /** A username for HTTP authorization. */ + user?: string; + /** A password for HTTP authorization. */ + password?: string; + /** Whether to send cookies to 3rd party domains. */ + withCredentials?: boolean; + /** Exposes the underlying XMLHttpRequest object for low-level configuration. */ + config?: (xhr: XMLHttpRequest) => any; + /** Headers to append to the request before sending it. */ + headers?: any; + /** A constructor to be applied to each object in the response. */ + type?: new (o: any) => any; + /** A serialization method to be applied to data. Defaults to JSON.stringify, or if options.data is an instance of FormData, defaults to the identity function. */ + serialize?: (data: any) => any; + /** A deserialization method to be applied to the response. Defaults to a small wrapper around JSON.parse that returns null for empty responses. */ + deserialize?: (data: string) => T; + /** A hook to specify how the XMLHttpRequest response should be read. Useful for reading response headers and cookies. Defaults to a function that returns xhr.responseText */ + extract?: (xhr: XMLHttpRequest, options: RequestOptions) => T; + /** Force the use of the HTTP body section for data in GET requests when set to true, or the use of querystring for other HTTP methods when set to false. Defaults to false for GET requests and true for other methods. */ + useBody?: boolean; + /** If false, redraws mounted components upon completion of the request. If true, it does not. */ background?: boolean; + } - /** - * Set an initial value while the request is working, to populate the - * promise getter-setter. - */ - initialValue?: any; - - /** - * An optional preprocessor function to unwrap a successful response, in - * case the response contains metadata wrapping the data. - * - * @param data The data to unwrap. - * @return The unwrapped result. - */ - unwrapSuccess?(data: any): any; - - /** - * An optional preprocessor function to unwrap an unsuccessful response, - * in case the response contains metadata wrapping the data. - * - * @param data The data to unwrap. - * @return The unwrapped result. - */ - unwrapError?(data: any): any; - - /** - * An optional function to serialize the data. This defaults to - * `JSON.stringify`. - * - * @param dataToSerialize The data to serialize. - * @return The serialized form as a string. - */ - serialize?(dataToSerialize: any): string; - - /** - * An optional function to deserialize the data. This defaults to - * `JSON.parse`. - * - * @param dataToSerialize The data to parse. - * @return The parsed form. - */ - deserialize?(dataToDeserialize: string): any; - - /** - * An optional function to extract the data from a raw XMLHttpRequest, - * useful if the relevant data is in a response header or the status - * field. - * - * @param xhr The associated XMLHttpRequest. - * @param options The options passed to this request. - * @return string The serialized format. - */ - extract?(xhr: XMLHttpRequest, options: this): string; - - /** - * The parsed data, or its children if it's an array, will be passed to - * this class constructor if it's given, to parse it into classes. - * - * @param data The data to parse. - * @return The new instance for the list. - */ - type?: new (data: any) => any; - - /** - * The URL to send the request to. - */ + export interface RequestOptionsAll extends RequestOptions { + /** The URL to send the request to. */ url: string; } - /** - * This represents the available options for configuring m.request for JSONP - * requests. - * - * @see m.request - */ - interface JSONPOptions extends RequestOptions { - /** - * For JSONP requests, this must be the string "jsonp". Otherwise, it's - * ignored. - */ - dataType: "jsonp"; + export interface Request { + /** Makes an XHR request and returns a promise. */ + (options: RequestOptionsAll): Promise; + /** Makes an XHR request and returns a promise. */ + (url: string, options?: RequestOptions): Promise; + } - /** - * The querystring key for the JSONP request callback. This is useful for - * APIs that don't use common conventions, such as - * `www.example.com/?jsonpCallback=doSomething`. It defaults to - * `callback`. - */ + export interface JsonpOptions { + /** The data to be interpolated into the URL and serialized into the querystring. */ + data?: any; + /** A constructor to be applied to each object in the response. */ + type?: new (o: any) => any; + /** The name of the function that will be called as the callback. */ + callbackName?: string; + /** The name of the querystring parameter name that specifies the callback name. */ callbackKey?: string; - - /** - * The data to send with the request. This is automatically serialized - * to a querystring. - */ - data?: Object; + /** If false, redraws mounted components upon completion of the request. If true, it does not. */ + background?: boolean; } - /** - * This represents the available options for configuring m.request for - * standard AJAX requests. - * - * @see m.request - */ - interface XHROptions extends RequestOptions { - /** - * This represents the HTTP method used, defaulting to "GET". - */ - method: "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS"; + export interface JsonpOptionsAll extends JsonpOptions { + /** The URL to send the request to. */ + url: string; + } - /** - * The username for HTTP authentication. - */ - user?: string; + export interface Jsonp { + /** Makes a JSON-P request and returns a promise. */ + (options: JsonpOptionsAll): Promise; + /** Makes a JSON-P request and returns a promise. */ + (url: string, options?: JsonpOptions): Promise; + } - /** - * The password for HTTP authentication. - */ - password?: string; + export interface RequestService { + request: Request; + jsonp: Jsonp; + } - /** - * An optional function to run between `open` and `send`, useful for - * adding request headers or using XHR2 features such as the `upload` - * property. It is even possible to override the XHR altogether with a - * similar object, such as an XDomainRequest instance. - * - * @param xhr The associated XMLHttpRequest. - * @param options The options passed to this request. - * @return The new XMLHttpRequest, or nothing if the same one is kept. - */ - config?(xhr: XMLHttpRequest, options: this): any; + export interface Render { + /** Renders a vnode structure into a DOM element. */ + (el: Element, vnodes: Children): void; + } - /** - * The data to send with the request. - */ - data?: Object; + export interface RenderService { + render: Render + } + + export interface Redraw { + /** Manually triggers a redraw of mounted components. */ + (): void; + } + + export interface RedrawService { + redraw: Redraw + render: Render + } + + export interface Static extends Hyperscript { + route: Route; + /** Activates a component, enabling it to autoredraw on user events. */ + mount: Mount; + /** Returns a event handler that can be bound to an element, firing with the specified property. */ + withAttr: WithAttr; + render: Render; + redraw: Redraw; + request: Request; + jsonp: Jsonp; + /** Parse a query string into an object. */ + parseQueryString: ParseQueryString; + /** Serialize an object into a query string. */ + buildQueryString: BuildQueryString; + /** A string containing the semver value for the current Mithril release. */ + version: string; + } + + // Vnode children types + export type Child = Vnode | string | number | boolean | null | undefined; + export interface ChildArray extends Array { } + export type Children = Child | ChildArray; + export type ChildArrayOrPrimitive = ChildArray | string | number | boolean; + + /** Virtual DOM nodes, or vnodes, are Javascript objects that represent an element (or parts of the DOM). */ + export interface Vnode> { + /** The nodeName of a DOM element. It may also be the string [ if a vnode is a fragment, # if it's a text vnode, or < if it's a trusted HTML vnode. Additionally, it may be a component. */ + tag: string | Component; + /** A hashmap of DOM attributes, events, properties and lifecycle methods. */ + attrs: Attrs; + /** An object that is persisted between redraws. In component vnodes, state is a shallow clone of the component object. */ + state: State; + /** The value used to map a DOM element to its respective item in an array of data. */ + key?: string | number; + /** In most vnode types, the children property is an array of vnodes. For text and trusted HTML vnodes, The children property is either a string, a number or a boolean. */ + children?: ChildArrayOrPrimitive; + /** This is used instead of children if a vnode contains a text node as its only child. This is done for performance reasons. Component vnodes never use the text property even if they have a text node as their only child. */ + text?: string | number | boolean; + } + + // In some lifecycle methods, Vnode will have a dom property + // and possibly a domSize property. + export interface VnodeDOM extends Vnode { + + /** Points to the element that corresponds to the vnode. */ + dom: Element; + + /** This defines the number of DOM elements that the vnode represents (starting from the element referenced by the dom property). */ + domSize?: number; + } + + export interface CVnode extends Vnode> { } + + export interface CVnodeDOM extends VnodeDOM> { } + + /** Components are a mechanism to encapsulate parts of a view to make code easier to organize and/or reuse. Any Javascript object that has a view method is a Mithril component. Components can be consumed via the m() utility. */ + export interface Component> extends Lifecycle { + + /** Creates a view out of virtual elements. */ + view(this: State, vnode: Vnode): Children | null | void; + } + + export interface ClassComponent extends Lifecycle> { + view(this: ClassComponent, vnode: CVnode): Children | null | void; + } + + // Factory component + export type FactoryComponent = (vnode: Vnode) => Component + + /** Components are a mechanism to encapsulate parts of a view to make code easier to organize and/or reuse. Any Javascript object that has a view method is a Mithril component. Components can be consumed via the m() utility. */ + export type Comp> = Component & State; + + export type ComponentTypes = Component | { new (vnode: CVnode): ClassComponent } | FactoryComponent + + /** This represents the attributes available for configuring virtual elements, beyond the applicable DOM attributes.*/ + export interface Attributes extends Lifecycle { + /** The class name(s) for this virtual element, as a space-separated list. */ + className?: string; + /** The class name(s) for this virtual element, as a space-separated list. */ + class?: string; + /** A key to optionally associate with this element. */ + key?: string | number; + /** Any other virtual element properties, including attributes and event handlers. */ + [property: string]: any; } } -declare const m: Mithril.Static; - -declare module "mithril" { - export = m; -} +declare const Mithril: Mithril.Static; +export = Mithril; diff --git a/types/mithril/mithril-tests.ts b/types/mithril/mithril-tests.ts deleted file mode 100644 index 8479074e07..0000000000 --- a/types/mithril/mithril-tests.ts +++ /dev/null @@ -1,55 +0,0 @@ - -// This is the todolist example from http://lhorie.github.io/mithril/getting-started.html - -var todo = { - - //the Todo class has two properties - Todo: function(data: any) { - this.description = m.prop(data.description); - this.done = m.prop(false); - }, - - //the TodoList class is a list of Todo's - TodoList: Array, - - //the controller uses three model-level entities, of which one is a custom defined class: - //`Todo` is the central class in this application - //`list` is merely a generic array, with standard array methods - //`description` is a temporary storage box that holds a string - // - //the `add` method simply adds a new todo to the list - controller: function() { - this.list = new todo.TodoList(); - this.description = m.prop(""); - - this.add = function() { - if (this.description()) { - this.list.push(new (todo.Todo)({description: this.description()})); - this.description(""); - } - }.bind(this); - }, - - //here's the view - view: function(ctrl: any) { - return m("html", [ - m("body", [ - m("input", {onchange: m.withAttr("value", ctrl.description), value: ctrl.description()} as any /* TODO remove `as any` */), - m("button", {onclick: ctrl.add} as any /* TODO remove `as any` */, "Add"), - m("table", [ - ctrl.list.map(function(task: any) { - return m("tr", [ - m("td", [ - m("input[type=checkbox]", {onclick: m.withAttr("checked", task.done), checked: task.done()} as any /* TODO remove `as any` */) - ]), - m("td", {style: {textDecoration: task.done() ? "line-through" : "none"}} as any /* TODO remove `as any` */, task.description()), - ]) - }) - ]) - ]) - ]); - }, -}; - -//initialize the application -m.mount(document, todo); diff --git a/types/mithril/mount.d.ts b/types/mithril/mount.d.ts new file mode 100644 index 0000000000..c177527f84 --- /dev/null +++ b/types/mithril/mount.d.ts @@ -0,0 +1,3 @@ +import { Mount } from "mithril"; +declare const mount: Mount; +export = mount; diff --git a/types/mithril/redraw.d.ts b/types/mithril/redraw.d.ts new file mode 100644 index 0000000000..8206db5c0e --- /dev/null +++ b/types/mithril/redraw.d.ts @@ -0,0 +1,3 @@ +import { Redraw } from "mithril"; +declare const redraw: Redraw; +export = redraw; diff --git a/types/mithril/render.d.ts b/types/mithril/render.d.ts new file mode 100644 index 0000000000..72651f42b4 --- /dev/null +++ b/types/mithril/render.d.ts @@ -0,0 +1,3 @@ +import { Render } from "mithril"; +declare const render: Render; +export = render; diff --git a/types/mithril/request.d.ts b/types/mithril/request.d.ts new file mode 100644 index 0000000000..0e737a04e9 --- /dev/null +++ b/types/mithril/request.d.ts @@ -0,0 +1,3 @@ +import { Request } from "mithril"; +declare const request: Request; +export = request; diff --git a/types/mithril/route.d.ts b/types/mithril/route.d.ts new file mode 100644 index 0000000000..df8a13181c --- /dev/null +++ b/types/mithril/route.d.ts @@ -0,0 +1,3 @@ +import { Route } from "mithril"; +declare const route: Route; +export = route; diff --git a/types/mithril/stream/index.d.ts b/types/mithril/stream/index.d.ts new file mode 100644 index 0000000000..7c5c1b3be0 --- /dev/null +++ b/types/mithril/stream/index.d.ts @@ -0,0 +1,44 @@ +declare namespace Stream { + export type Combiner = (...streams: any[]) => T; + + export interface Stream { + /** Returns the value of the stream. */ + (): T; + /** Sets the value of the stream. */ + (value: T): this; + /** Creates a dependent stream whose value is set to the result of the callback function. */ + map(f: (current: T) => Stream | T | void): Stream; + /** Creates a dependent stream whose value is set to the result of the callback function. */ + map(f: (current: T) => Stream | U): Stream; + /** This method is functionally identical to stream. It exists to conform to Fantasy Land's Applicative specification. */ + of(val?: T): Stream; + /** Apply. */ + ap(f: Stream<(value: T) => U>): Stream; + /** A co-dependent stream that unregisters dependent streams when set to true. */ + end: Stream; + /** When a stream is passed as the argument to JSON.stringify(), the value of the stream is serialized.*/ + toJSON(): string; + /** Returns the value of the stream. */ + valueOf(): T; + } + + export interface Static { + /** Creates a stream. */ + (value?: T): Stream; + /** Creates a computed stream that reactively updates if any of its upstreams are updated. */ + combine(combiner: Combiner, streams: Stream[]): Stream; + /** Creates a stream whose value is the array of values from an array of streams. */ + merge(streams: Stream[]): Stream; + /** Creates a new stream with the results of calling the function on every incoming stream with and accumulator and the incoming value. */ + scan(fn: (acc: U, value: T) => U, acc: U, stream: Stream): Stream; + /** Takes an array of pairs of streams and scan functions and merges all those streams using the given functions into a single stream. */ + scanMerge(pairs: [Stream, (acc: U, value: T) => U][], acc: U): Stream; + /** Takes an array of pairs of streams and scan functions and merges all those streams using the given functions into a single stream. */ + scanMerge(pairs: [Stream, (acc: U, value: any) => U][], acc: U): Stream; + /** A special value that can be returned to stream callbacks to halt execution of downstreams. */ + readonly HALT: any; + } +} + +declare const Stream: Stream.Static; +export = Stream; diff --git a/types/mithril/test/test-api.ts b/types/mithril/test/test-api.ts new file mode 100644 index 0000000000..49fb8ebefc --- /dev/null +++ b/types/mithril/test/test-api.ts @@ -0,0 +1,738 @@ +// Typescript adaptation of mithril's test suite. +// Not intended to be run; only to compile & check types. + +import * as m from 'mithril' +import * as stream from 'mithril/stream' + +const FRAME_BUDGET = 100 + +{ + let vnode = m("div") + console.assert(vnode.tag === "div") + console.assert(typeof m.version === "string") + console.assert(m.version.indexOf(".") > -1) +} + +{ + const vnode = m.trust("
") +} + +{ + const vnode = m.fragment({key: 123}, [m("div")]) + console.assert((vnode.children as m.Vnode[]).length === 1) + console.assert(vnode.children![0].tag === 'div') +} + +{ + const handler = m.withAttr("value", (value) => {}) + handler({currentTarget: {value: 10}}) +} + +{ + const params = m.parseQueryString("?a=1&b=2") + const query = m.buildQueryString({a: 1, b: 2}) +} + +{ + const root = window.document.createElement("div") + m.render(root, m("div")) + console.assert(root.childNodes.length === 1) +} + +{ + const root = window.document.createElement("div") + m.mount(root, {view: function() {return m("div")}}) + console.assert(root.childNodes.length === 1) + console.assert(root.firstChild!.nodeName === "DIV") +} + +{ + const root = window.document.createElement("div") + m.route(root, "/a", { + "/a": {view: function() {return m("div")}} + }) + + setTimeout(function() { + console.assert(root.childNodes.length === 1) + console.assert(root.firstChild!.nodeName === "DIV") + }, FRAME_BUDGET) +} + +{ + const root = window.document.createElement("div") + m.route.prefix("#") + m.route(root, "/a", { + "/a": {view: function() {return m("div")}} + }) + + setTimeout(function() { + console.assert(root.childNodes.length === 1) + console.assert(root.firstChild!.nodeName === "DIV") + }, FRAME_BUDGET) +} + +{ + const root = window.document.createElement("div") + m.route(root, "/a", { + "/a": {view: function() {return m("div")}} + }) + + setTimeout(function() { + console.assert(m.route.get() === "/a") + }, FRAME_BUDGET) +} + +{ + const root = window.document.createElement("div") + m.route(root, "/a", { + "/:id": {view: function() {return m("div")}} + }) + + setTimeout(function() { + m.route.set("/b") + setTimeout(function() { + console.assert(m.route.get() === "/b") + }, FRAME_BUDGET) + }, FRAME_BUDGET) +} + +{ + let count = 0 + const root = window.document.createElement("div") + m.mount(root, {view: function() {count++}}) + setTimeout(function() { + m.redraw() + console.assert(count === 2) + }, FRAME_BUDGET) +} + +// +// Additional tests by andraaspar +// + +//////////////////////////////////////////////////////////////////////////////// +// http://mithril.js.org/hyperscript.html#components +//////////////////////////////////////////////////////////////////////////////// + +;(function() { + + // define a component + let Greeter: m.Comp<{}, {}> = { + view: function(vnode) { + return m("div", vnode.attrs, ["Hello ", vnode.children]) + } + } + + // consume it + m(Greeter, { style: "color:red;" }, "world") + +}) + +//////////////////////////////////////////////////////////////////////////////// +// http://mithril.js.org/hyperscript.html#keys +//////////////////////////////////////////////////////////////////////////////// + +;(function() { + + let users = [ + { id: 1, name: "John" }, + { id: 2, name: "Mary" }, + ] + + function userInputs(users: { id: number, name: string }[]) { + return users.map(function(u) { + return m("input", { key: u.id }, u.name) + }) + } + + m.render(document.body, userInputs(users)) + +}) + +//////////////////////////////////////////////////////////////////////////////// +// http://mithril.js.org/components.html#state +//////////////////////////////////////////////////////////////////////////////// + +;(function() { + let ComponentWithInitialState: m.Comp<{}, {data: string}> = { + data: "Initial content", + view: function(vnode) { + return m("div", vnode.state.data) + } + } + + m(ComponentWithInitialState) +}) + +;(function() { + let ComponentWithDynamicState: m.Comp<{text: string}, {data?: string}> = { + oninit: function(vnode) { + vnode.state.data = vnode.attrs.text + }, + view: function(vnode) { + return m("div", vnode.state.data) + } + } + + m(ComponentWithDynamicState, { text: "Hello" }) +}) + +;(function() { + let ComponentUsingThis: m.Comp<{text: string}, {data?: string}> = { + oninit: function(vnode) { + this.data = vnode.attrs.text + }, + view: function(vnode) { + return m("div", this.data) + } + } + + m(ComponentUsingThis, { text: "Hello" }) +}) + +//////////////////////////////////////////////////////////////////////////////// +// http://mithril.js.org/lifecycle-methods.html +//////////////////////////////////////////////////////////////////////////////// + +;(function() { + let Fader: m.Comp<{}, {}> = { + onbeforeremove: function(vnode) { + vnode.dom.classList.add("fade-out") + return new Promise(function(resolve) { + setTimeout(resolve, 1000) + }) + }, + view: function() { + return m("div", "Bye") + }, + } +}) + +//////////////////////////////////////////////////////////////////////////////// +// http://mithril.js.org/route.html#wrapping-a-layout-component +//////////////////////////////////////////////////////////////////////////////// + +;(function() { + + let Home = { + view: function() { + return "Welcome" + } + } + + let state = { + term: "", + search: function() { + // save the state for this route + // this is equivalent to `history.replaceState({term: state.term}, null, location.href)` + m.route.set(m.route.get(), null, { replace: true, state: { term: state.term } }) + + // navigate away + location.href = "https://google.com/?q=" + state.term + } + } + + let Form: m.Comp<{term: string}, {}> = { + oninit: function(vnode) { + state.term = vnode.attrs.term || "" // populated from the `history.state` property if the user presses the back button + }, + view: function() { + return m("form", [ + m("input[placeholder='Search']", { oninput: m.withAttr("value", function(v) { state.term = v }), value: state.term }), + m("button", { onclick: state.search }, "Search") + ]) + } + } + + let Layout: m.Comp<{}, {}> = { + view: function(vnode) { + return m(".layout", vnode.children) + } + } + + // example 1 + m.route(document.body, "/", { + "/": { + view: function() { + return m(Layout, m(Home)) + }, + }, + "/form": { + view: function() { + return m(Layout, m(Form)) + }, + } + }) + + // example 2 + m.route(document.body, "/", { + "/": { + render: function() { + return m(Layout, m(Home)) + }, + }, + "/form": { + render: function() { + return m(Layout, m(Form)) + }, + } + }) + + // functionally equivalent to example 1 + let Anon1 = { + view: function() { + return m(Layout, m(Home)) + }, + } + let Anon2 = { + view: function() { + return m(Layout, m(Form)) + }, + } + + m.route(document.body, "/", { + "/": { + render: function() { + return m(Anon1) + } + }, + "/form": { + render: function() { + return m(Anon2) + } + }, + }) +}) + +//////////////////////////////////////////////////////////////////////////////// +// http://mithril.js.org/route.html#preloading-data +//////////////////////////////////////////////////////////////////////////////// + +;(function() { + let state = { + users: [], + loadUsers: function() { + return m.request("/api/v1/users").then(function(users) { + state.users = users + }) + } + } + + m.route(document.body, "/user/list", { + "/user/list": { + onmatch: state.loadUsers, + render: function() { + return state.users.map(function(user) { + return m("div", user.id) + }) + } + }, + }) +}) + +//////////////////////////////////////////////////////////////////////////////// +// http://mithril.js.org/request.html#monitoring-progress +//////////////////////////////////////////////////////////////////////////////// + +;(function() { + let progress = 0 + + m.mount(document.body, { + view: function() { + return [ + m("input[type=file]", { onchange: upload }), + progress + "% completed" + ] + } + }) + + function upload(e: Event) { + let file = ((e.target).files)[0] + + let data = new FormData() + data.append("myfile", file) + + m.request({ + method: "POST", + url: "/api/v1/upload", + data: data, + config: function(xhr) { + xhr.addEventListener("progress", function(e) { + progress = e.loaded / e.total + + m.redraw() // tell Mithril that data changed and a re-render is needed + }) + } + }) + } +}) + +//////////////////////////////////////////////////////////////////////////////// +// http://mithril.js.org/request.html#casting-response-to-a-type +//////////////////////////////////////////////////////////////////////////////// + +;(function() { + + // Start rewrite to TypeScript + class User { + name: string; + constructor(data: any) { + this.name = data.firstName + " " + data.lastName + } + } + // End rewrite to TypeScript + + // function User(data) { + // this.name = data.firstName + " " + data.lastName + // } + + m.request({ + method: "GET", + url: "/api/v1/users", + type: User + }) + .then(function(users) { + console.log(users[0].name) // logs a name + }) +}) + +//////////////////////////////////////////////////////////////////////////////// +// http://mithril.js.org/request.html#non-json-responses +//////////////////////////////////////////////////////////////////////////////// + +;(function() { + m.request({ + method: "GET", + url: "/files/icon.svg", + deserialize: function(value) { return value } + }) + .then(function(svg) { + m.render(document.body, m.trust(svg)) + }) +}) + +//////////////////////////////////////////////////////////////////////////////// +// http://mithril.js.org/jsonp.html +//////////////////////////////////////////////////////////////////////////////// + +;(function() { + m.jsonp({ + url: "/api/v1/users/:id", + data: { id: 1 }, + callbackKey: "callback", + }) + .then(function(result) { + console.log(result) + }) +}) + +//////////////////////////////////////////////////////////////////////////////// +// http://mithril.js.org/fragment.html +//////////////////////////////////////////////////////////////////////////////// + +;(function() { + let groupVisible = true + let log = function() { + console.log("group is now visible") + } + + m("ul", [ + m("li", "child 1"), + m("li", "child 2"), + groupVisible ? m.fragment({ oninit: log }, [ + // a fragment containing two elements + m("li", "child 3"), + m("li", "child 4"), + ]) : null + ]) +}) + +//////////////////////////////////////////////////////////////////////////////// +// http://mithril.js.org/stream.html#computed-properties +//////////////////////////////////////////////////////////////////////////////// + +;(function() { + let firstName = stream("John") + let lastName = stream("Doe") + let fullName = stream.merge([firstName, lastName]).map(function(values) { + return values.join(" ") + }) + + console.log(fullName()) // logs "John Doe" + + firstName("Mary") + + console.log(fullName()) // logs "Mary Doe" +}) + +//////////////////////////////////////////////////////////////////////////////// +// http://mithril.js.org/stream.html#chaining-streams +//////////////////////////////////////////////////////////////////////////////// + +;(function() { + let halted = stream(1).map(function(value) { + return stream.HALT + }) + + halted.map(function() { + // never runs + }) +}) + +//////////////////////////////////////////////////////////////////////////////// +// http://mithril.js.org/stream.html#combining-streams +//////////////////////////////////////////////////////////////////////////////// + +;(function() { + let a = stream(5) + let b = stream(7) + + let added = stream.combine(function(a: stream.Stream, b: stream.Stream) { + return a() + b() + }, [a, b]) + + console.log(added()) // logs 12 +}) + +//////////////////////////////////////////////////////////////////////////////// +// http://mithril.js.org/stream.html#ended-state +//////////////////////////////////////////////////////////////////////////////// + +;(function() { + let value = stream() + let doubled = value.map(function(value) { return value * 2 }) + + value.end(true) // set to ended state + + value(5) + + console.log(doubled()) +}) + +//////////////////////////////////////////////////////////////////////////////// +// https://github.com/lhorie/mithril.js/blob/master/examples/animation/mosaic.html +//////////////////////////////////////////////////////////////////////////////// + +// Excerpt + +;(function() { + + let root = (document.getElementById("root")) + + let empty: any[] = [] + let full: any[] = [] + for (let i = 0; i < 100; i++) full.push(i) + + let cells: any[] + + function view() { + return m(".container", cells.map(function(i) { + return m(".slice", { + style: {backgroundPosition: (i % 10 * 11) + "% " + (Math.floor(i / 10) * 11) + "%"}, + onbeforeremove: exit + }) + })) + } + + function exit(vnode: m.VnodeDOM) { + vnode.dom.classList.add("exit") + return new Promise(function(resolve) { + setTimeout(resolve, 1000) + }) + } + + function run() { + cells = cells === full ? empty : full + + m.render(root, [view()]) + + setTimeout(run, 2000) + } + + run() + +}) + +//////////////////////////////////////////////////////////////////////////////// +// https://github.com/lhorie/mithril.js/blob/master/examples/editor/index.html +//////////////////////////////////////////////////////////////////////////////// + +// Excerpt + +;(function() { + + // Start extra declarations + let marked = (v: string) => v + // End extra declarations + + //model + let state = { + text: "# Markdown Editor\n\nType on the left panel and see the result on the right panel", + update: function(value: string) { + state.text = value + } + } + + //view + let Editor = { + view: function() { + return [ + m("textarea.input", { + oninput: m.withAttr("value", state.update), + value: state.text + }), + m(".preview", m.trust(marked(state.text))), + ] + } + } + + m.mount(document.getElementById("editor"), Editor) + +}) + +//////////////////////////////////////////////////////////////////////////////// +// https://github.com/lhorie/mithril.js/blob/master/examples/todomvc/todomvc.js +//////////////////////////////////////////////////////////////////////////////// + +;(function() { + + //model + let state = { + dispatch: function(action: string, args?: any) { + (state)[action].apply(state, args || []) + requestAnimationFrame(function() { + localStorage["todos-mithril"] = JSON.stringify(state.todos) + }) + }, + + todos: JSON.parse(localStorage["todos-mithril"] || "[]"), + editing: null, + filter: "", + remaining: 0, + todosByStatus: [], + showing: undefined, + + createTodo: function(title: string) { + state.todos.push({title: title.trim(), completed: false}) + }, + setStatuses: function(completed: boolean) { + for (let i = 0; i < state.todos.length; i++) state.todos[i].completed = completed + }, + setStatus: function(todo: any, completed: boolean) { + todo.completed = completed + }, + destroy: function(todo: any) { + let index = state.todos.indexOf(todo) + if (index > -1) state.todos.splice(index, 1) + }, + clear: function() { + for (let i = 0; i < state.todos.length; i++) { + if (state.todos[i].completed) state.destroy(state.todos[i--]) + } + }, + + edit: function(todo: any) { + state.editing = todo + }, + update: function(title: string) { + if (state.editing != null) { + state.editing.title = title.trim() + if (state.editing.title === "") state.destroy(state.editing) + state.editing = null + } + }, + reset: function() { + state.editing = null + }, + + computed: function(vnode: m.Vnode) { + state.showing = vnode.attrs.status || "" + state.remaining = state.todos.filter(function(todo: any) {return !todo.completed}).length + state.todosByStatus = state.todos.filter(function(todo: any) { + switch (state.showing) { + case "": return true + case "active": return !todo.completed + case "completed": return todo.completed + } + }) + } + } + + //view + let Todos: m.Comp<{}, { + add(e: Event): void + toggleAll(): void + toggle(todo: any): void + focus(vnode: m.VnodeDOM, todo: any): void + save(e: KeyboardEvent): void + }> = { + add: function(e: KeyboardEvent) { + if (e.keyCode === 13) { + state.dispatch("createTodo", [(e.target).value]); + (e.target).value = "" + } + }, + toggleAll: function() { + state.dispatch("setStatuses", [(document.getElementById("toggle-all")).checked]) + }, + toggle: function(todo: any) { + state.dispatch("setStatus", [todo, !todo.completed]) + }, + focus: function(vnode: m.VnodeDOM, todo: any) { + if (todo === state.editing && vnode.dom !== document.activeElement) { + (vnode.dom).value = todo.title + (vnode.dom).focus() + (vnode.dom).selectionStart = (vnode.dom).selectionEnd = todo.title.length + } + }, + save: function(e: KeyboardEvent) { + if (e.keyCode === 13 || e.type === "blur") state.dispatch("update", [(e.target).value]) + else if (e.keyCode === 27) state.dispatch("reset") + }, + oninit: state.computed, + onbeforeupdate: state.computed, + view: function(vnode) { + let ui = vnode.state + return [ + m("header.header", [ + m("h1", "todos"), + m("input#new-todo[placeholder='What needs to be done?'][autofocus]", {onkeypress: ui.add}), + ]), + m("section#main", {style: {display: state.todos.length > 0 ? "" : "none"}}, [ + m("input#toggle-all[type='checkbox']", {checked: state.remaining === 0, onclick: ui.toggleAll}), + m("label[for='toggle-all']", {onclick: ui.toggleAll}, "Mark all as complete"), + m("ul#todo-list", [ + state.todosByStatus.map(function(todo: any) { + return m("li", {class: (todo.completed ? "completed" : "") + " " + (todo === state.editing ? "editing" : "")}, [ + m(".view", [ + m("input.toggle[type='checkbox']", {checked: todo.completed, onclick: function() {ui.toggle(todo)}}), + m("label", {ondblclick: function() {state.dispatch("edit", [todo])}}, todo.title), + m("button.destroy", {onclick: function() {state.dispatch("destroy", [todo])}}), + ]), + m("input.edit", {onupdate: function(vnode: m.VnodeDOM) {ui.focus(vnode, todo)}, onkeypress: ui.save, onblur: ui.save}) + ]) + }), + ]), + ]), + state.todos.length ? m("footer#footer", [ + m("span#todo-count", [ + m("strong", state.remaining), + state.remaining === 1 ? " item left" : " items left", + ]), + m("ul#filters", [ + m("li", m("a[href='/']", {oncreate: m.route.link, class: state.showing === "" ? "selected" : ""}, "All")), + m("li", m("a[href='/active']", {oncreate: m.route.link, class: state.showing === "active" ? "selected" : ""}, "Active")), + m("li", m("a[href='/completed']", {oncreate: m.route.link, class: state.showing === "completed" ? "selected" : ""}, "Completed")), + ]), + m("button#clear-completed", {onclick: function() {state.dispatch("clear")}}, "Clear completed"), + ]) : null, + ] + } + } + + m.route(document.getElementById("todoapp")!, "/", { + "/": Todos, + "/:status": Todos, + }) + +}) diff --git a/types/mithril/test/test-class-component.ts b/types/mithril/test/test-class-component.ts new file mode 100644 index 0000000000..3b99bb63ef --- /dev/null +++ b/types/mithril/test/test-class-component.ts @@ -0,0 +1,135 @@ +import * as m from 'mithril' +import {ClassComponent, CVnode, CVnodeDOM} from 'mithril' + +/////////////////////////////////////////////////////////// +// 0. +// Simplest component example - no attrs or state. +// +class Comp0 implements ClassComponent<{}> { + constructor (vnode: CVnode<{}>) { + } + view() { + return m('span', "Test") + } +} + +// Mount the component +m.mount(document.getElementById('comp0')!, Comp0) + +// Unmount the component +m.mount(document.getElementById('comp0')!, null) + +/////////////////////////////////////////////////////////// +// 1. +// Simple example with lifecycle methods. +// +class Comp1 implements ClassComponent<{}> { + oninit (vnode: CVnode<{}>) { + } + oncreate ({dom}: CVnodeDOM<{}>) { + } + view (vnode: CVnode<{}>) { + return m('span', "Test") + } +} + +/////////////////////////////////////////////////////////// +// 2. +// Component with attrs type +// +interface Comp2Attrs { + title: string + description: string +} + +class Comp2 implements ClassComponent { + view ({attrs: {title, description}}: CVnode) { + return [m('h2', title), m('p', description)] + } +} + +/////////////////////////////////////////////////////////// +// 3. +// Declares attrs type inline. +// Uses comp2 with typed attrs and makes use of `onremove` +// lifecycle method. +// +class Comp3 implements ClassComponent<{pageHead: string}> { + oncreate ({dom}: CVnodeDOM<{pageHead: string}>) { + // Can do stuff with dom + } + view ({attrs}: CVnode<{pageHead: string}>) { + return m('.page', + m('h1', attrs.pageHead), + m(Comp2, + { + // attrs is type checked - nice! + title: "A Title", + description: "Some descriptive text.", + onremove: (vnode) => { + // Vnode type is inferred + console.log("comp2 was removed") + }, + } + ), + // Test other hyperscript parameter variations + m(Comp1, m(Comp1)), + m('br') + ) + } +} + +/////////////////////////////////////////////////////////// +// 4. +// Typed attrs, component with state, methods +// +interface Comp4Attrs { + name: string +} + +class Comp4 implements ClassComponent { + count: number + constructor (vnode: CVnode) { + this.count = 0 + } + add (num: number) { + this.count += num + } + view ({attrs}: CVnode) { + return [ + m('h1', `This ${attrs.name} has been clicked ${this.count} times`), + m('button', + { + onclick: () => this.add(1) + }, + "Click me") + ] + } +} + +/////////////////////////////////////////////////////////// +// +// Test that all are mountable components +// +m.route(document.body, '/', { + '/comp0': Comp0, + '/comp1': Comp1, + '/comp2': Comp2, + '/comp3': Comp3, + '/comp4': Comp4 +}) + +/////////////////////////////////////////////////////////// +// +// Concise module example with default export +// +export interface Attrs { + name: string +} + +export default class MyComponent implements ClassComponent { + count = 0 + view ({attrs}: CVnode) { + return m('span', `name: ${attrs.name}, count: ${this.count}`) + } +} diff --git a/types/mithril/test/test-component.ts b/types/mithril/test/test-component.ts new file mode 100644 index 0000000000..060997b555 --- /dev/null +++ b/types/mithril/test/test-component.ts @@ -0,0 +1,160 @@ +import * as m from 'mithril' +import {Component, Comp} from 'mithril' + +/////////////////////////////////////////////////////////// +// 0. +// Simplest component example - no attrs or state. +// +const comp0 = { + view() { + return m('span', "Test") + } +} + +// Mount the component +m.mount(document.getElementById('comp0')!, comp0) + +// Unmount the component +m.mount(document.getElementById('comp0')!, null) + +/////////////////////////////////////////////////////////// +// 1. +// Simple example. Vnode type for component methods is inferred. +// +const comp1: Component<{},{}> = { + oncreate ({dom}) { + // vnode.dom type inferred + }, + view (vnode) { + return m('span', "Test") + } +} + +/////////////////////////////////////////////////////////// +// 2. +// Component with attrs +// +interface Comp2Attrs { + title: string + description: string +} + +const comp2: Component = { + view ({attrs: {title, description}}) { // Comp2Attrs type is inferred + return [m('h2', title), m('p', description)] + } +} + +/////////////////////////////////////////////////////////// +// 3. +// Declares attrs type inline. +// Uses comp2 with typed attrs and makes use of `onremove` +// lifecycle method. +// +const comp3: Component<{pageHead: string},{}> = { + oncreate ({dom}) { + // Can do stuff with dom + }, + view ({attrs}) { + return m('.page', + m('h1', attrs.pageHead), + m(comp2, + { + // attrs is type checked - nice! + title: "A Title", + description: "Some descriptive text.", + onremove: (vnode) => { + console.log("comp2 was removed") + }, + } + ), + // Test other hyperscript parameter variations + m(comp1, m(comp1)), + m('br') + ) + } +} + +/////////////////////////////////////////////////////////// +// 4. +// Typed attrs and state, and `this` type is inferred. +// +interface Comp4Attrs { + name: string +} + +interface Comp4State { + count: number + add: (this: Comp4State, num: number) => void +} + +// Either of these two Comp4 defs will work: +type Comp4 = Component & Comp4State +//interface Comp4 extends Component, Comp4State {} + +const comp4: Comp4 = { + count: 0, // <- Must be declared to satisfy Comp4 type which includes Comp4State type + add (num) { + // num and this types inferred + this.count += num + }, + oninit() { + this.count = 0 + }, + view ({attrs}) { + return [ + m('h1', `This ${attrs.name} has been clicked ${this.count} times`), + m('button', + { + // 'this' is typed! + onclick: () => this.add(1) + }, + "Click me") + ] + } +} + +/////////////////////////////////////////////////////////// +// 5. +// Stateful component (Equivalent to Comp4 example.) +// Avoids the use of `this` completely; state manipulated +// through vnode.state. +// +const comp5: Component = { + oninit ({state}) { + state.count = 0 + state.add = num => {state.count += num} + }, + view ({attrs, state}) { + return [ + m('h1', `This ${attrs.name} has been clicked ${state.count} times`), + m('button', + { + onclick: () => {state.add(1)} + }, + "Click me" + ) + ] + } +} + + +/////////////////////////////////////////////////////////// +// +// Concise module example with default export +// +interface Attrs { + name: string +} + +interface State { + count: number +} + +export default { + count: 0, + view ({attrs}) { + return m('span', `name: ${attrs.name}, count: ${this.count}`) + } +} as Comp +// Using the Comp type will apply the State intersection type for us. diff --git a/types/mithril/test/test-factory-component.ts b/types/mithril/test/test-factory-component.ts new file mode 100644 index 0000000000..1f8e10bb73 --- /dev/null +++ b/types/mithril/test/test-factory-component.ts @@ -0,0 +1,179 @@ +import * as m from 'mithril' +import {Component, FactoryComponent, Vnode} from 'mithril' + +/////////////////////////////////////////////////////////// +// 0. +// Simplest component example - no attrs or state. +// +function comp0() { + return { + view() { + return m('span', "Test") + } + } +} + +// Mount the component +m.mount(document.getElementById('comp0')!, comp0) + +// Unmount the component +m.mount(document.getElementById('comp0')!, null) + +/////////////////////////////////////////////////////////// +// 1. +// Simple example. Vnode type for component methods is inferred. +// +function comp1() { + return { + oncreate ({dom}) { + // vnode.dom type inferred + }, + view (vnode) { + return m('span', "Test") + } + } as Component<{},{}> +} + +/////////////////////////////////////////////////////////// +// 2. +// Component with attrs type. Different type annotation +// style to infer factory vnode type. +// +interface Comp2Attrs { + title: string + description: string +} + +const comp2 = function (vnode) { // vnode is inferred + return { + view ({attrs: {title, description}}) { // Comp2Attrs type is inferred + return [m('h2', title), m('p', description)] + } + } +} as FactoryComponent + +/////////////////////////////////////////////////////////// +// 3. +// Declares attrs type inline. +// Uses comp2 with typed attrs and makes use of `onremove` +// lifecycle method. +// +const comp3 = function() { + return { + oncreate ({dom}) { + // Can do stuff with dom + }, + view ({attrs}) { + return m('.page', + m('h1', attrs.pageHead), + m(comp2, + { + // attrs is type checked - nice! + title: "A Title", + description: "Some descriptive text.", + onremove: (vnode) => { + console.log("comp2 was removed") + }, + } + ), + // Test other hyperscript parameter variations + m(comp1, m(comp1)), + m('br') + ) + } + } +} as FactoryComponent<{pageHead: string}> + +/////////////////////////////////////////////////////////// +// 4. +// Stateful component using closure method & var +// to hold state. +// +interface Comp4Attrs { + name: string +} + +function comp4(): Component { + let count = 0 + + function add (num: number) { + count += num + } + + return { + oninit() { + count = 0 + }, + view ({attrs}) { + return [ + m('h1', `This ${attrs.name} has been clicked ${count} times`), + m('button', + { + onclick: () => add(1) + }, + "Click me" + ) + ] + } + } +} + +/////////////////////////////////////////////////////////// +// 5. +// Stateful component (Equivalent to Comp4 example.) +// Uses vnode.state instead of closure. +// +interface Comp5State { + count: number + add (num: number): void +} + +function comp5(): Component { + return { + oninit ({state}) { + state.count = 0 + state.add = num => {state.count += num} + }, + view ({attrs, state}) { + return [ + m('h1', `This ${attrs.name} has been clicked ${state.count} times`), + m('button', + { + onclick: () => {state.add(1)} + }, + "Click me" + ) + ] + } + } +} + +/////////////////////////////////////////////////////////// +// +// Test that all are mountable components +// +m.route(document.body, '/', { + '/comp0': comp0, + '/comp1': comp1, + '/comp2': comp2, + '/comp3': comp3, + '/comp4': comp4, + '/comp5': comp5 +}) + +/////////////////////////////////////////////////////////// +// +// Concise module example with default export +// +interface Attrs { + name: string +} + +export default (): Component => { + let count = 0 + return { + view ({attrs}) { + return m('span', `name: ${attrs.name}, count: ${count}`) + } + } +} diff --git a/types/mithril/test/test-fragment.ts b/types/mithril/test/test-fragment.ts new file mode 100644 index 0000000000..364680bbb2 --- /dev/null +++ b/types/mithril/test/test-fragment.ts @@ -0,0 +1,17 @@ +import * as m from 'mithril' +import {Vnode} from 'mithril/' +import * as h from 'mithril/hyperscript' + +const vnode = m.fragment({id: 'abc'}, ['test']) + +m.fragment({}, ['Test', 123]) + +m.fragment( + { + id: 'abc', + oninit: (vnode: Vnode) => { + console.log('oninit') + } + }, + [h('p', 'test1'), [123, h('p', 'abc'), ['abc']], 'Abc', h('p', 'test2')] +) diff --git a/types/mithril/test/test-jsonp.ts b/types/mithril/test/test-jsonp.ts new file mode 100644 index 0000000000..32f40ca4b6 --- /dev/null +++ b/types/mithril/test/test-jsonp.ts @@ -0,0 +1,27 @@ +import * as m from 'mithril' + +interface Result { + id: number +} + +m.jsonp('/item').then(data => { + console.log(data.id) +}) + +class User { + id: number + constructor (result: Result) { + this.id = result.id + } +} + +m.jsonp({ + url: '/user', + data: {test: 'abc'}, + type: User, + callbackName: 'getuser', + callbackKey: 'key', + background: true +}).then(user => { + console.log(user.id) +}) diff --git a/types/mithril/test/test-misc.ts b/types/mithril/test/test-misc.ts new file mode 100644 index 0000000000..75f1367de8 --- /dev/null +++ b/types/mithril/test/test-misc.ts @@ -0,0 +1,16 @@ +import * as m from 'mithril' + +const vnode = m.trust('Some bold text.') + +const params = m.parseQueryString('?id=123') + +const qstr = m.buildQueryString({id: 123}) + +m.render(document.body, 'Hello') +m.render(document.body, m('h1', 'Test')) +m.render(document.body, [ + m('h1', 'Test'), "abc", null, 123, false, m('p', 'Vnode array'), + ['a', 123, undefined, m('div', 'Nested')] +]) + +m.redraw() diff --git a/types/mithril/test/test-request.ts b/types/mithril/test/test-request.ts new file mode 100644 index 0000000000..8ea24fe091 --- /dev/null +++ b/types/mithril/test/test-request.ts @@ -0,0 +1,73 @@ +import * as request from 'mithril/request' + +interface Result { + id: number +} + +request({method: "GET", url: "/item"}).then(result => { + console.log(result.id) +}) + +request<{a: string}>("/item", {method: "POST"}).then(result => { + console.log(result.a) +}) + +request({ + method: "GET", + url: "/item", + data: {x: "y"} +}).then(result => { + console.log(result) +}) + +request({ + method: "GET", + url: "/item", + data: 5, + serialize: (data: number) => "id=" + data.toString() +}).then(result => { + console.log(result) +}) + +request('/item', { + method: "GET", + deserialize: str => JSON.parse(str) as Result +}).then(result => { + console.log(result.id) +}) + +request('/id', { + method: "GET", + extract: xhr => ({id: Number(xhr.responseText)}) +}).then(result => { + console.log(result.id) +}) + +request('/item', { + config: xhr => { + xhr.setRequestHeader('accept', '*') + }, + headers: {"Content-Type": "application/json"}, + background: true, +}).then(result => { + console.log(result.id) +}) + +class Item { + identifier: number + constructor(result: Result) { + this.identifier = result.id + } +} + +request('/item', { + method: 'GET', + async: true, + user: "Me", + password: "qwerty", + withCredentials: true, + type: Item, + useBody: false +}).then(item => { + console.log(item.identifier) +}) diff --git a/types/mithril/test/test-route.ts b/types/mithril/test/test-route.ts new file mode 100644 index 0000000000..900bb2f1f0 --- /dev/null +++ b/types/mithril/test/test-route.ts @@ -0,0 +1,60 @@ +import {Component} from 'mithril' +import * as h from 'mithril/hyperscript' +import * as route from 'mithril/route' + +const component1 = { + view() { + return h('h1', 'Test') + } +} + +const component2 = { + view ({attrs: {title}}) { + return h('h1', title) + } +} as Component<{title: string},{}> + +route(document.body, '/', { + '/': component1, + '/test1': { + onmatch (args, path) { + return component1 + } + }, + '/test2': { + render(vnode) { + return h(component1) + } + }, + 'test3': { + onmatch (args, path) { + return component2 + }, + render (vnode) { + return ['abc', 123, null, h(component2), ['nested', h('p', 123)]] + } + }, + 'test4': { + onmatch (args, path) { + // Must provide a Promise type if we want type checking + return new Promise>((resolve, reject) => { + resolve(component2) + }) + } + } +}) + +route.prefix('/app') +route.set('/test1') + +route.set('/test/:id', {id: 1}) + +route.set('/test2', undefined, { + replace: true, + state: {abc: 123}, + title: "Title" +}) + +const path: string = route.get() + +const fn = route.link(h('div', 'test')) diff --git a/types/mithril/test/test-stream.ts b/types/mithril/test/test-stream.ts new file mode 100644 index 0000000000..7e55d0273d --- /dev/null +++ b/types/mithril/test/test-stream.ts @@ -0,0 +1,391 @@ +import * as stream from 'mithril/stream' +import {Stream} from 'mithril/stream' + +{ + const s = stream(1) + const initialValue = s() + s(2) + const newValue = s() + console.assert(initialValue === 1) + console.assert(newValue === 2) +} + +{ + const s = stream() + console.assert(s() === undefined) +} + +{ + const s: Stream = stream(1) + s(undefined) + console.assert(s() === undefined) +} + +{ + const s = stream(stream(1)) + console.assert(s()() === 1) +} + +{ + const s = stream() + const doubled = stream.combine(function(s) {return s() * 2}, [s]) + s(2) + console.assert(doubled() === 4) +} + +{ + const s = stream(2) + const doubled = stream.combine(function(s) {return s() * 2}, [s]) + console.assert(doubled() === 4) +} + +{ + const s1 = stream() + const s2 = stream() + const added = stream.combine(function(s1, s2) {return s1() + s2()}, [s1, s2]) + s1(2) + s2(3) + console.assert(added() === 5) +} + +{ + const s1 = stream(2) + const s2 = stream(3) + const added = stream.combine(function(s1, s2) {return s1() + s2()}, [s1, s2]) + console.assert(added() === 5) +} + +{ + const s1 = stream(2) + const s2 = stream() + const added = stream.combine(function(s1, s2) {return s1() + s2()}, [s1, s2]) + s2(3) + console.assert(added() === 5) +} + +{ + let count = 0 + const a = stream() + const b = stream.combine(function(a) {return a() * 2}, [a]) + const c = stream.combine(function(a) {return a() * a()}, [a]) + const d = stream.combine(function(b, c) { + count++ + return b() + c() + }, [b, c]) + a(3) + console.assert(d() === 15) + console.assert(count === 1) +} + +{ + let count = 0 + const a = stream(3) + const b = stream.combine(function(a) {return a() * 2}, [a]) + const c = stream.combine(function(a) {return a() * a()}, [a]) + const d = stream.combine(function(b, c) { + count++ + return b() + c() + }, [b, c]) + console.assert(d() === 15) + console.assert(count === 1) +} + +{ + let streams: Stream[] = [] + const a = stream() + const b = stream() + const c = stream.combine(function(a, b, changed) { + streams = changed + }, [a, b]) + a(3) + b(5) + console.assert(streams.length === 1) + console.assert(streams[0] === b) +} + +{ + let streams: Stream[] = [] + const a = stream(3) + const b = stream(5) + const c = stream.combine(function(a, b, changed) { + streams = changed + }, [a, b]) + a(7) + console.assert(streams.length === 1) + console.assert(streams[0] === a) +} + +{ + const a = stream(1) + const b = stream.combine(function(a) { + return undefined + }, [a]) + + console.assert(b() === undefined) +} + +{ + const a = stream(1) + const b = stream.combine(function(a) { + return stream(2) + }, [a]) + console.assert(b()() === 2) +} + +{ + const a = stream(1) + const b = stream.combine(function(a) { + return stream() + }, [a]) + console.assert(b()() === undefined) +} + +{ + let count = 0 + const a = stream(1) + const b = stream.combine(function(a) { + return stream.HALT + }, [a]) + ["fantasy-land/map"](function() { + count++ + return 1 + }) + console.assert(b() === undefined) +} + +{ + const all = stream.merge([ + stream(10), + stream("20"), + stream({value: 30}), + ]) +} + +{ + const straggler = stream() + const all = stream.merge([ + stream(10), + stream("20"), + straggler, + ]) + console.assert(all() === undefined) + straggler(30) +} + +{ + let value = 0 + const id = function(value: number) {return value} + const a = stream() + const b = stream() + + const all = stream.merge([a.map(id), b.map(id)]).map(function(data) { + value = data[0] + data[1] + }) + + a(1) + b(2) + console.assert(value === 3) + + a(3) + b(4) + console.assert(value === 7) +} + +{ + const s = stream() + const doubled = stream.combine(function(stream) {return stream() * 2}, [s]) + s.end(true) + s(3) + console.assert(doubled() === undefined) +} + +{ + const s = stream(2) + const doubled = stream.combine(function(stream) {return stream() * 2}, [s]) + s.end(true) + s(3) + console.assert(doubled() === 4) +} + +{ + const s = stream(2) + s.end(true) + const doubled = stream.combine(function(stream) {return stream() * 2}, [s]) + s(3) + console.assert(doubled() === undefined) +} + +{ + const s = stream(2) + const doubled = stream.combine(function(stream) {return stream() * 2}, [s]) + doubled.end(true) + s(4) + console.assert(doubled() === 4) +} + +{ + const s = stream() + const doubled = s["fantasy-land/map"](function(value: number) {return value * 2}) + s(3) + console.assert(doubled() === 6) +} + +{ + const s = stream(3) + const doubled = s["fantasy-land/map"](function(value: number) {return value * 2}) + console.assert(doubled() === 6) +} + +{ + const s = stream() + const mapped = s["fantasy-land/map"](function(value: undefined) {return String(value)}) + s(undefined) + console.assert(mapped() === "undefined") +} + +{ + const s = stream(undefined) + const mapped = s["fantasy-land/map"](function(value: undefined) {return String(value)}) + console.assert(mapped() === "undefined") +} + +{ + const s = stream(undefined) + const mapped = s["fantasy-land/map"](function(value: undefined) {return stream()}) + console.assert(mapped()() === undefined) +} + +{ + const s = stream(undefined) + console.assert(s["fantasy-land/map"] === s.map) +} + +{ + const apply = stream(function(value: number) {return value * 2}) + const s = stream(3) + const applied = s["fantasy-land/ap"](apply) + console.assert(applied() === 6) + apply(function(value) {return value / 3}) + console.assert(applied() === 1) + s(9) + console.assert(applied() === 3) +} + +{ + const apply = stream(function(value: undefined) {return String(value)}) + const s = stream(undefined) + const applied = s["fantasy-land/ap"](apply) + console.assert(applied() === "undefined") + apply(function(value) {return String(value) + "a"}) + console.assert(applied() === "undefineda") +} + +{ + const s = stream(3) + const mapped = s["fantasy-land/map"](function(value: number) {return value}) + console.assert(s() === mapped()) +} + +{ + const f = function f(x: number) {return x * 2} + const g = function g(x: number) {return x * x} + const s = stream(3) + const mapped = s["fantasy-land/map"](function(value: any) {return f(g(value))}) + const composed = s["fantasy-land/map"](g)["fantasy-land/map"](f) + console.assert(mapped() === 18) + console.assert(mapped() === composed()) +} + +{ + const a = stream(function(value: number) {return value * 2}) + const u = stream(function(value: number) {return value * 3}) + const v = stream(5) + const mapped = v["fantasy-land/ap"](u["fantasy-land/ap"](a["fantasy-land/map"](function(f: any) { + return function(g: any) { + return function(x: any) { + return f(g(x)) + } + } + }))) + const composed = v["fantasy-land/ap"](u)["fantasy-land/ap"](a) + console.assert(mapped() === 30) + console.assert(mapped() === composed()) +} + +{ + const a = stream()["fantasy-land/of"](function(value: number) {return value}) + const v = stream(5) + console.assert(v["fantasy-land/ap"](a)() === 5) + console.assert(v["fantasy-land/ap"](a)() === v()) +} + +{ + const a = stream(0) + const f = function(value: number) {return value * 2} + const x = 3 + console.assert(a["fantasy-land/of"](x)["fantasy-land/ap"](a["fantasy-land/of"](f))() === 6) + console.assert(a["fantasy-land/of"](x)["fantasy-land/ap"](a["fantasy-land/of"](f))() === a["fantasy-land/of"](f(x))()) +} + +{ + const u = stream(function(value: number) {return value * 2}) + const a = stream() + const y = 3 + console.assert(a["fantasy-land/of"](y)["fantasy-land/ap"](u)() === 6) + console.assert(a["fantasy-land/of"](y)["fantasy-land/ap"](u)() === u["fantasy-land/ap"](a["fantasy-land/of"](function(f: any) {return f(y)}))()) +} + +// scan + +{ + const parent = stream() + const child = stream.scan((out, p) => out - p, 123, parent) +} + +{ + const parent = stream() + const child = stream.scan((arr, p) => arr.concat(p), [] as number[], parent) + parent(7) +} + +// scanMerge + +{ + const parent1 = stream() + const parent2 = stream() + + const child = stream.scanMerge([ + [parent1, (out, p1) => out + p1], + [parent2, (out, p2) => out + p2] + ], -10) +} + +{ + const parent1 = stream() + const parent2 = stream() + + const child = stream.scanMerge([ + [parent1, (out, p1) => out + p1], + [parent2, (out, p2) => out + p2 + p2] + ], "a") + + parent1("b") + parent2("c") + parent1("b") + + console.assert(child() === 'abccb') +} + +{ + const parent1 = stream() + const parent2 = stream() + const child = stream.scanMerge([ + [parent1, (out, p1) => out + p1], + [parent2, (out, p2) => out + p2 + p2] + ], "a") + + parent1("a") + parent2(1) + + console.assert(child() === 'aa11') +} diff --git a/types/mithril/tsconfig.json b/types/mithril/tsconfig.json index a80a6ce9f2..8189032c62 100644 --- a/types/mithril/tsconfig.json +++ b/types/mithril/tsconfig.json @@ -1,23 +1,39 @@ { - "compilerOptions": { - "module": "commonjs", - "lib": [ - "es6", - "dom" - ], - "noImplicitAny": true, - "noImplicitThis": false, - "strictNullChecks": false, - "baseUrl": "../", - "typeRoots": [ - "../" - ], - "types": [], - "noEmit": true, - "forceConsistentCasingInFileNames": true - }, - "files": [ - "index.d.ts", - "mithril-tests.ts" - ] -} \ No newline at end of file + "compilerOptions": { + "module": "commonjs", + "lib": ["es2015", "dom"], + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "suppressImplicitAnyIndexErrors": true, + "noEmit": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": "../", + "typeRoots": ["../"], + "types": [] + }, + "files": [ + "test/test-api.ts", + "test/test-class-component.ts", + "test/test-component.ts", + "test/test-factory-component.ts", + "test/test-fragment.ts", + "test/test-jsonp.ts", + "test/test-misc.ts", + "test/test-request.ts", + "test/test-route.ts", + "test/test-stream.ts", + "index.d.ts", + "hyperscript.d.ts", + "mount.d.ts", + "redraw.d.ts", + "render.d.ts", + "request.d.ts", + "route.d.ts", + "withAttr.d.ts", + "stream/index.d.ts" + ], + "atom": { + "rewriteTsconfig": false + } +} diff --git a/types/mithril/tslint.json b/types/mithril/tslint.json new file mode 100644 index 0000000000..2c473fb8ea --- /dev/null +++ b/types/mithril/tslint.json @@ -0,0 +1,54 @@ +{ + "rules": { + "class-name": true, + "comment-format": [ + false, + "check-space" + ], + "indent": [ + true, + "tabs" + ], + "no-duplicate-variable": true, + "no-eval": true, + "no-internal-module": false, + "no-trailing-whitespace": true, + "no-var-keyword": true, + "one-line": [ + true, + "check-open-brace", + "check-whitespace" + ], + "quotemark": [ + false, + "double" + ], + "semicolon": [false, "always"], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + false, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "variable-name": [ + true, + "ban-keywords" + ], + "whitespace": [ + false, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ] + } +} \ No newline at end of file diff --git a/types/mithril/withAttr.d.ts b/types/mithril/withAttr.d.ts new file mode 100644 index 0000000000..14623971a6 --- /dev/null +++ b/types/mithril/withAttr.d.ts @@ -0,0 +1,3 @@ +import { WithAttr } from "mithril"; +declare const withAttr: WithAttr; +export = withAttr;