From 8388b3d60ed0269bcbca93707147cc6b49cc3d8e Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Mon, 7 Feb 2022 14:19:41 -0800 Subject: [PATCH 1/6] Refactor a bunch of component internals for TS --- .../glimmer/lib/component-managers/curly.ts | 3 +- .../glimmer/lib/component-managers/root.ts | 3 +- .../-internals/glimmer/lib/component.ts | 768 +++++++++--------- .../-internals/glimmer/lib/environment.ts | 2 +- .../@ember/-internals/glimmer/lib/renderer.ts | 21 +- .../-internals/glimmer/lib/utils/bindings.ts | 2 +- .../lib/utils/curly-component-state-bucket.ts | 22 +- .../application/debug-render-tree-test.ts | 11 +- .../runtime/lib/mixins/evented.d.ts | 7 + .../lib/mixins/target_action_support.d.ts | 12 + packages/@ember/-internals/views/index.d.ts | 44 - .../-internals/views/{index.js => index.ts} | 0 .../views/lib/mixins/action_support.d.ts | 8 + .../views/lib/mixins/child_views_support.d.ts | 10 + .../views/lib/mixins/class_names_support.d.ts | 10 + .../views/lib/mixins/view_state_support.d.ts | 8 + .../views/lib/mixins/view_support.d.ts | 20 + .../views/lib/system/event_dispatcher.d.ts | 8 + .../-internals/views/lib/system/utils.ts | 32 +- .../-internals/views/lib/views/core_view.d.ts | 12 + .../@ember/component/type-tests/index.test.ts | 10 + 21 files changed, 545 insertions(+), 468 deletions(-) create mode 100644 packages/@ember/-internals/runtime/lib/mixins/target_action_support.d.ts delete mode 100644 packages/@ember/-internals/views/index.d.ts rename packages/@ember/-internals/views/{index.js => index.ts} (100%) create mode 100644 packages/@ember/-internals/views/lib/mixins/action_support.d.ts create mode 100644 packages/@ember/-internals/views/lib/mixins/child_views_support.d.ts create mode 100644 packages/@ember/-internals/views/lib/mixins/class_names_support.d.ts create mode 100644 packages/@ember/-internals/views/lib/mixins/view_state_support.d.ts create mode 100644 packages/@ember/-internals/views/lib/mixins/view_support.d.ts create mode 100644 packages/@ember/-internals/views/lib/system/event_dispatcher.d.ts create mode 100644 packages/@ember/-internals/views/lib/views/core_view.d.ts create mode 100644 packages/@ember/component/type-tests/index.test.ts diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts b/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts index 2cc64543431..771bcc80f18 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/curly.ts @@ -39,6 +39,7 @@ import { valueForTag, } from '@glimmer/validator'; import { SimpleElement } from '@simple-dom/interface'; +import Component from '../component'; import { DynamicScope } from '../renderer'; import RuntimeResolver from '../resolver'; import { isTemplateFactory } from '../template'; @@ -49,7 +50,7 @@ import { parseAttributeBinding, } from '../utils/bindings'; -import ComponentStateBucket, { Component } from '../utils/curly-component-state-bucket'; +import ComponentStateBucket from '../utils/curly-component-state-bucket'; import { processComponentArgs } from '../utils/process-args'; export const ARGS = enumerableSymbol('ARGS'); diff --git a/packages/@ember/-internals/glimmer/lib/component-managers/root.ts b/packages/@ember/-internals/glimmer/lib/component-managers/root.ts index afbfe4c0ca8..2b964149c17 100644 --- a/packages/@ember/-internals/glimmer/lib/component-managers/root.ts +++ b/packages/@ember/-internals/glimmer/lib/component-managers/root.ts @@ -11,8 +11,9 @@ import { } from '@glimmer/interfaces'; import { capabilityFlagsFrom } from '@glimmer/manager'; import { CONSTANT_TAG, consumeTag } from '@glimmer/validator'; +import Component from '../component'; import { DynamicScope } from '../renderer'; -import ComponentStateBucket, { Component } from '../utils/curly-component-state-bucket'; +import ComponentStateBucket from '../utils/curly-component-state-bucket'; import CurlyComponentManager, { DIRTY_TAG, initialRenderInstrumentDetails, diff --git a/packages/@ember/-internals/glimmer/lib/component.ts b/packages/@ember/-internals/glimmer/lib/component.ts index d7a12fb0ec4..916a1032c1c 100644 --- a/packages/@ember/-internals/glimmer/lib/component.ts +++ b/packages/@ember/-internals/glimmer/lib/component.ts @@ -1,5 +1,5 @@ import { get, PROPERTY_DID_CHANGE } from '@ember/-internals/metal'; -import { getOwner } from '@ember/-internals/owner'; +import { getOwner, Owner } from '@ember/-internals/owner'; import { TargetActionSupport } from '@ember/-internals/runtime'; import { ActionSupport, @@ -13,7 +13,7 @@ import { } from '@ember/-internals/views'; import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; -import { Environment } from '@glimmer/interfaces'; +import { Environment, Template, TemplateFactory } from '@glimmer/interfaces'; import { setInternalComponentManager } from '@glimmer/manager'; import { isUpdatableRef, updateRef } from '@glimmer/reference'; import { normalizeProperty } from '@glimmer/runtime'; @@ -26,6 +26,7 @@ import { DIRTY_TAG, IS_DISPATCHING_ATTRS, } from './component-managers/curly'; +import { View } from './renderer'; // Keep track of which component classes have already been processed for lazy event setup. let lazyEventsProcessed = new WeakMap>(); @@ -648,402 +649,431 @@ let lazyEventsProcessed = new WeakMap>(); @uses Ember.ViewStateSupport @public */ -const Component = CoreView.extend( - ChildViewsSupport, - ViewStateSupport, - ClassNamesSupport, - TargetActionSupport, - ActionSupport, - ViewMixin, - { - isComponent: true, - - init() { - this._super(...arguments); - this[IS_DISPATCHING_ATTRS] = false; - this[DIRTY_TAG] = createTag(); - this[BOUNDS] = null; - - let eventDispatcher = this._dispatcher; - if (eventDispatcher) { - let lazyEventsProcessedForComponentClass = lazyEventsProcessed.get(eventDispatcher); - if (!lazyEventsProcessedForComponentClass) { - lazyEventsProcessedForComponentClass = new WeakSet(); - lazyEventsProcessed.set(eventDispatcher, lazyEventsProcessedForComponentClass); - } +interface Component + extends CoreView, + ChildViewsSupport, + ViewStateSupport, + ClassNamesSupport, + TargetActionSupport, + ActionSupport, + ViewMixin { + parentView: Component | null; + + /** + Layout can be used to wrap content in a component. + @property layout + @type Function + @public + */ + layout?: TemplateFactory | Template; + + /** + The name of the layout to lookup if no layout is provided. + By default `Component` will lookup a template with this name in + `Ember.TEMPLATES` (a shared global object). + @property layoutName + @type String + @default undefined + @private + */ + layoutName?: string; + + attributeBindings?: Array; +} +class Component + extends CoreView.extend( + ChildViewsSupport, + ViewStateSupport, + ClassNamesSupport, + TargetActionSupport, + ActionSupport, + ViewMixin + ) + implements View { + static isComponentFactory = true; + static positionalParams = []; + + static toString(): string { + return '@ember/component'; + } - let proto = Object.getPrototypeOf(this); - if (!lazyEventsProcessedForComponentClass.has(proto)) { - let lazyEvents = eventDispatcher.lazyEvents; + isComponent = true; - lazyEvents.forEach((mappedEventName: string, event: string) => { - if (mappedEventName !== null && typeof this[mappedEventName] === 'function') { - eventDispatcher.setupHandlerForBrowserEvent(event); - } - }); + private __dispatcher: EventDispatcher | null | undefined; - lazyEventsProcessedForComponentClass.add(proto); - } - } + constructor(owner: Owner) { + super(owner); + this[IS_DISPATCHING_ATTRS] = false; + this[DIRTY_TAG] = createTag(); + this[BOUNDS] = null; - if (DEBUG && eventDispatcher && this.renderer._isInteractive && this.tagName === '') { - let eventNames = []; - let events = eventDispatcher.finalEventNameMapping; + const eventDispatcher = this._dispatcher; + if (eventDispatcher) { + let lazyEventsProcessedForComponentClass = lazyEventsProcessed.get(eventDispatcher); + if (!lazyEventsProcessedForComponentClass) { + lazyEventsProcessedForComponentClass = new WeakSet(); + lazyEventsProcessed.set(eventDispatcher, lazyEventsProcessedForComponentClass); + } - for (let key in events) { - let methodName = events[key]; + let proto = Object.getPrototypeOf(this); + if (!lazyEventsProcessedForComponentClass.has(proto)) { + let lazyEvents = eventDispatcher.lazyEvents; - if (typeof this[methodName] === 'function') { - eventNames.push(methodName); + lazyEvents.forEach((mappedEventName: string, event: string) => { + if (mappedEventName !== null && typeof this[mappedEventName] === 'function') { + eventDispatcher.setupHandlerForBrowserEvent(event); } + }); + + lazyEventsProcessedForComponentClass.add(proto); + } + } + + // @ts-expect-error _isInteractive is private + if (DEBUG && eventDispatcher && this.renderer._isInteractive && this.tagName === '') { + let eventNames = []; + let events = eventDispatcher.finalEventNameMapping; + + for (let key in events) { + let methodName = events[key]; + + if (methodName && typeof this[methodName] === 'function') { + eventNames.push(methodName); } - // If in a tagless component, assert that no event handlers are defined + } + // If in a tagless component, assert that no event handlers are defined + assert( + `You can not define \`${eventNames}\` function(s) to handle DOM event in the \`${this}\` tagless component since it doesn't have any DOM element.`, + !eventNames.length + ); + } + } + + get _dispatcher(): EventDispatcher | null { + if (this.__dispatcher === undefined) { + let owner = getOwner(this); + assert('Component is unexpectedly missing an owner', owner); + + if ((owner.lookup('-environment:main') as Environment)!.isInteractive) { + let dispatcher = owner.lookup('event_dispatcher:main'); assert( - `You can not define \`${eventNames}\` function(s) to handle DOM event in the \`${this}\` tagless component since it doesn't have any DOM element.`, - !eventNames.length + 'Expected dispatcher to be an EventDispatcher', + dispatcher instanceof EventDispatcher ); + this.__dispatcher = dispatcher; + } else { + // In FastBoot we have no EventDispatcher. Set to null to not try again to look it up. + this.__dispatcher = null; } - }, - - get _dispatcher(): EventDispatcher | null { - if (this.__dispatcher === undefined) { - let owner = getOwner(this); - assert('Component is unexpectedly missing an owner', owner); - - if ((owner.lookup('-environment:main') as Environment)!.isInteractive) { - this.__dispatcher = owner.lookup('event_dispatcher:main'); - assert( - 'Expected dispatcher to be an EventDispatcher', - this.__dispatcher instanceof EventDispatcher - ); - } else { - // In FastBoot we have no EventDispatcher. Set to null to not try again to look it up. - this.__dispatcher = null; - } - } + } - return this.__dispatcher; - }, + return this.__dispatcher; + } - on(eventName: string) { - this._dispatcher?.setupHandlerForEmberEvent(eventName); - return this._super(...arguments); - }, + on( + name: string, + target: Target, + method: string | ((this: Target, ...args: any[]) => void) + ): this; + on(name: string, method: ((...args: any[]) => void) | string): this; + on( + name: string, + ...args: + | [Target, string | ((this: Target, ...args: any[]) => void)] + | [((...args: any[]) => void) | string] + ): this { + this._dispatcher?.setupHandlerForEmberEvent(name); + return super.on(name, ...args); + } - rerender() { - dirtyTag(this[DIRTY_TAG]); - this._super(); - }, + rerender() { + dirtyTag(this[DIRTY_TAG]); + } - [PROPERTY_DID_CHANGE](key: string, value?: unknown) { - if (this[IS_DISPATCHING_ATTRS]) { - return; - } + [PROPERTY_DID_CHANGE](key: string, value?: unknown) { + if (this[IS_DISPATCHING_ATTRS]) { + return; + } - let args = this[ARGS]; - let reference = args !== undefined ? args[key] : undefined; + let args = this[ARGS]; + let reference = args !== undefined ? args[key] : undefined; - if (reference !== undefined && isUpdatableRef(reference)) { - updateRef(reference, arguments.length === 2 ? value : get(this, key)); - } - }, - - getAttr(key: string) { - // TODO Intimate API should be deprecated - return this.get(key); - }, - - /** - Normally, Ember's component model is "write-only". The component takes a - bunch of attributes that it got passed in, and uses them to render its - template. - - One nice thing about this model is that if you try to set a value to the - same thing as last time, Ember (through HTMLBars) will avoid doing any - work on the DOM. - - This is not just a performance optimization. If an attribute has not - changed, it is important not to clobber the element's "hidden state". - For example, if you set an input's `value` to the same value as before, - it will clobber selection state and cursor position. In other words, - setting an attribute is not **always** idempotent. - - This method provides a way to read an element's attribute and also - update the last value Ember knows about at the same time. This makes - setting an attribute idempotent. - - In particular, what this means is that if you get an `` element's - `value` attribute and then re-render the template with the same value, - it will avoid clobbering the cursor and selection position. - Since most attribute sets are idempotent in the browser, you typically - can get away with reading attributes using jQuery, but the most reliable - way to do so is through this method. - @method readDOMAttr - - @param {String} name the name of the attribute - @return String - @public - */ - readDOMAttr(name: string) { - // TODO revisit this - let _element = getViewElement(this); + if (reference !== undefined && isUpdatableRef(reference)) { + updateRef(reference, arguments.length === 2 ? value : get(this, key)); + } + } - assert( - `Cannot call \`readDOMAttr\` on ${this} which does not have an element`, - _element !== null - ); + getAttr(key: string) { + // TODO Intimate API should be deprecated + return this.get(key); + } - let element = _element; - let isSVG = element.namespaceURI === Namespace.SVG; - let { type, normalized } = normalizeProperty(element, name); + /** + Normally, Ember's component model is "write-only". The component takes a + bunch of attributes that it got passed in, and uses them to render its + template. + + One nice thing about this model is that if you try to set a value to the + same thing as last time, Ember (through HTMLBars) will avoid doing any + work on the DOM. + + This is not just a performance optimization. If an attribute has not + changed, it is important not to clobber the element's "hidden state". + For example, if you set an input's `value` to the same value as before, + it will clobber selection state and cursor position. In other words, + setting an attribute is not **always** idempotent. + + This method provides a way to read an element's attribute and also + update the last value Ember knows about at the same time. This makes + setting an attribute idempotent. + + In particular, what this means is that if you get an `` element's + `value` attribute and then re-render the template with the same value, + it will avoid clobbering the cursor and selection position. + Since most attribute sets are idempotent in the browser, you typically + can get away with reading attributes using jQuery, but the most reliable + way to do so is through this method. + @method readDOMAttr + + @param {String} name the name of the attribute + @return String + @public + */ + readDOMAttr(name: string) { + // TODO revisit this + let _element = getViewElement(this); - if (isSVG || type === 'attr') { - return element.getAttribute(normalized); - } + assert( + `Cannot call \`readDOMAttr\` on ${this} which does not have an element`, + _element !== null + ); + + let element = _element; + let isSVG = element.namespaceURI === Namespace.SVG; + let { type, normalized } = normalizeProperty(element, name); + + if (isSVG || type === 'attr') { + return element.getAttribute(normalized); + } + + return element[normalized]; + } + + /** + The WAI-ARIA role of the control represented by this view. For example, a + button may have a role of type 'button', or a pane may have a role of + type 'alertdialog'. This property is used by assistive software to help + visually challenged users navigate rich web applications. - return element[normalized]; - }, - - /** - The WAI-ARIA role of the control represented by this view. For example, a - button may have a role of type 'button', or a pane may have a role of - type 'alertdialog'. This property is used by assistive software to help - visually challenged users navigate rich web applications. - - The full list of valid WAI-ARIA roles is available at: - [https://www.w3.org/TR/wai-aria/#roles_categorization](https://www.w3.org/TR/wai-aria/#roles_categorization) - - @property ariaRole - @type String - @default null - @public - */ - - /** - Enables components to take a list of parameters as arguments. - For example, a component that takes two parameters with the names - `name` and `age`: - - ```app/components/my-component.js - import Component from '@ember/component'; - - let MyComponent = Component.extend(); - - MyComponent.reopenClass({ - positionalParams: ['name', 'age'] - }); - - export default MyComponent; - ``` - - It can then be invoked like this: - - ```hbs - {{my-component "John" 38}} - ``` - - The parameters can be referred to just like named parameters: - - ```hbs - Name: {{name}}, Age: {{age}}. - ``` - - Using a string instead of an array allows for an arbitrary number of - parameters: - - ```app/components/my-component.js - import Component from '@ember/component'; - - let MyComponent = Component.extend(); - - MyComponent.reopenClass({ - positionalParams: 'names' - }); - - export default MyComponent; - ``` - - It can then be invoked like this: - - ```hbs - {{my-component "John" "Michael" "Scott"}} - ``` - The parameters can then be referred to by enumerating over the list: - - ```hbs - {{#each names as |name|}}{{name}}{{/each}} - ``` - - @static - @public - @property positionalParams - @since 1.13.0 - */ - - /** - Called when the attributes passed into the component have been updated. - Called both during the initial render of a container and during a rerender. - Can be used in place of an observer; code placed here will be executed - every time any attribute updates. - @method didReceiveAttrs - @public - @since 1.13.0 - */ - didReceiveAttrs() {}, - - /** - Called when the attributes passed into the component have been updated. - Called both during the initial render of a container and during a rerender. - Can be used in place of an observer; code placed here will be executed - every time any attribute updates. - @event didReceiveAttrs - @public - @since 1.13.0 - */ - - /** - Called after a component has been rendered, both on initial render and - in subsequent rerenders. - @method didRender - @public - @since 1.13.0 - */ - didRender() {}, - - /** - Called after a component has been rendered, both on initial render and - in subsequent rerenders. - @event didRender - @public - @since 1.13.0 - */ - - /** - Called before a component has been rendered, both on initial render and - in subsequent rerenders. - @method willRender - @public - @since 1.13.0 - */ - willRender() {}, - - /** - Called before a component has been rendered, both on initial render and - in subsequent rerenders. - @event willRender - @public - @since 1.13.0 - */ - - /** - Called when the attributes passed into the component have been changed. - Called only during a rerender, not during an initial render. - @method didUpdateAttrs - @public - @since 1.13.0 - */ - didUpdateAttrs() {}, - - /** - Called when the attributes passed into the component have been changed. - Called only during a rerender, not during an initial render. - @event didUpdateAttrs - @public - @since 1.13.0 - */ - - /** - Called when the component is about to update and rerender itself. - Called only during a rerender, not during an initial render. - @method willUpdate - @public - @since 1.13.0 - */ - willUpdate() {}, - - /** - Called when the component is about to update and rerender itself. - Called only during a rerender, not during an initial render. - @event willUpdate - @public - @since 1.13.0 - */ - - /** - Called when the component has updated and rerendered itself. - Called only during a rerender, not during an initial render. - @method didUpdate - @public - @since 1.13.0 - */ - didUpdate() {}, - - /** - Called when the component has updated and rerendered itself. - Called only during a rerender, not during an initial render. - @event didUpdate - @public - @since 1.13.0 - */ - - /** - Layout can be used to wrap content in a component. - @property layout - @type Function - @public + The full list of valid WAI-ARIA roles is available at: + [https://www.w3.org/TR/wai-aria/#roles_categorization](https://www.w3.org/TR/wai-aria/#roles_categorization) + + @property ariaRole + @type String + @default null + @public */ - /** - The name of the layout to lookup if no layout is provided. - By default `Component` will lookup a template with this name in - `Ember.TEMPLATES` (a shared global object). - @property layoutName - @type String - @default null - @private + /** + Enables components to take a list of parameters as arguments. + For example, a component that takes two parameters with the names + `name` and `age`: + + ```app/components/my-component.js + import Component from '@ember/component'; + + let MyComponent = Component.extend(); + + MyComponent.reopenClass({ + positionalParams: ['name', 'age'] + }); + + export default MyComponent; + ``` + + It can then be invoked like this: + + ```hbs + {{my-component "John" 38}} + ``` + + The parameters can be referred to just like named parameters: + + ```hbs + Name: {{name}}, Age: {{age}}. + ``` + + Using a string instead of an array allows for an arbitrary number of + parameters: + + ```app/components/my-component.js + import Component from '@ember/component'; + + let MyComponent = Component.extend(); + + MyComponent.reopenClass({ + positionalParams: 'names' + }); + + export default MyComponent; + ``` + + It can then be invoked like this: + + ```hbs + {{my-component "John" "Michael" "Scott"}} + ``` + The parameters can then be referred to by enumerating over the list: + + ```hbs + {{#each names as |name|}}{{name}}{{/each}} + ``` + + @static + @public + @property positionalParams + @since 1.13.0 */ - /** - The HTML `id` of the component's element in the DOM. You can provide this - value yourself but it must be unique (just as in HTML): - - ```handlebars - {{my-component elementId="a-really-cool-id"}} - ``` - - ```handlebars - - ``` - If not manually set a default value will be provided by the framework. - Once rendered an element's `elementId` is considered immutable and you - should never change it. If you need to compute a dynamic value for the - `elementId`, you should do this when the component or element is being - instantiated: - - ```javascript - export default Component.extend({ - init() { - this._super(...arguments); - - var index = this.get('index'); - this.set('elementId', `component-id${index}`); - } - }); - ``` + /** + Called when the attributes passed into the component have been updated. + Called both during the initial render of a container and during a rerender. + Can be used in place of an observer; code placed here will be executed + every time any attribute updates. + @method didReceiveAttrs + @public + @since 1.13.0 + */ + didReceiveAttrs() {} + + /** + Called when the attributes passed into the component have been updated. + Called both during the initial render of a container and during a rerender. + Can be used in place of an observer; code placed here will be executed + every time any attribute updates. + @event didReceiveAttrs + @public + @since 1.13.0 + */ - @property elementId - @type String - @public + /** + Called after a component has been rendered, both on initial render and + in subsequent rerenders. + @method didRender + @public + @since 1.13.0 + */ + didRender() {} + + /** + Called after a component has been rendered, both on initial render and + in subsequent rerenders. + @event didRender + @public + @since 1.13.0 + */ + + /** + Called before a component has been rendered, both on initial render and + in subsequent rerenders. + @method willRender + @public + @since 1.13.0 + */ + willRender() {} + + /** + Called before a component has been rendered, both on initial render and + in subsequent rerenders. + @event willRender + @public + @since 1.13.0 + */ + + /** + Called when the attributes passed into the component have been changed. + Called only during a rerender, not during an initial render. + @method didUpdateAttrs + @public + @since 1.13.0 + */ + didUpdateAttrs() {} + + /** + Called when the attributes passed into the component have been changed. + Called only during a rerender, not during an initial render. + @event didUpdateAttrs + @public + @since 1.13.0 */ - } -); -Component.toString = () => '@ember/component'; + /** + Called when the component is about to update and rerender itself. + Called only during a rerender, not during an initial render. + @method willUpdate + @public + @since 1.13.0 + */ + willUpdate() {} + + /** + Called when the component is about to update and rerender itself. + Called only during a rerender, not during an initial render. + @event willUpdate + @public + @since 1.13.0 + */ + + /** + Called when the component has updated and rerendered itself. + Called only during a rerender, not during an initial render. + @method didUpdate + @public + @since 1.13.0 + */ + didUpdate() {} + + /** + Called when the component has updated and rerendered itself. + Called only during a rerender, not during an initial render. + @event didUpdate + @public + @since 1.13.0 + */ + + /** + The HTML `id` of the component's element in the DOM. You can provide this + value yourself but it must be unique (just as in HTML): + + ```handlebars + {{my-component elementId="a-really-cool-id"}} + ``` + + ```handlebars + + ``` + If not manually set a default value will be provided by the framework. + Once rendered an element's `elementId` is considered immutable and you + should never change it. If you need to compute a dynamic value for the + `elementId`, you should do this when the component or element is being + instantiated: + + ```javascript + export default Component.extend({ + init() { + this._super(...arguments); + + var index = this.get('index'); + this.set('elementId', `component-id${index}`); + } + }); + ``` -Component.reopenClass({ - isComponentFactory: true, - positionalParams: [], -}); + @property elementId + @type String + @public + */ +} setInternalComponentManager(CURLY_COMPONENT_MANAGER, Component); diff --git a/packages/@ember/-internals/glimmer/lib/environment.ts b/packages/@ember/-internals/glimmer/lib/environment.ts index b69f58b505f..fa1bfb51794 100644 --- a/packages/@ember/-internals/glimmer/lib/environment.ts +++ b/packages/@ember/-internals/glimmer/lib/environment.ts @@ -41,7 +41,7 @@ setGlobalContext({ warnIfStyleNotTrusted(value: unknown) { warn( - constructStyleDeprecationMessage(value), + constructStyleDeprecationMessage(String(value)), (() => { if (value === null || value === undefined || isHTMLSafe(value)) { return true; diff --git a/packages/@ember/-internals/glimmer/lib/renderer.ts b/packages/@ember/-internals/glimmer/lib/renderer.ts index a3c6016589c..77dd4ba6748 100644 --- a/packages/@ember/-internals/glimmer/lib/renderer.ts +++ b/packages/@ember/-internals/glimmer/lib/renderer.ts @@ -1,6 +1,7 @@ import { privatize as P } from '@ember/-internals/container'; import { ENV } from '@ember/-internals/environment'; import { getOwner, Owner } from '@ember/-internals/owner'; +import { guidFor } from '@ember/-internals/utils'; import { getViewElement, getViewId } from '@ember/-internals/views'; import { assert } from '@ember/debug'; import { _backburner, _getCurrentRunLoop } from '@ember/runloop'; @@ -38,23 +39,29 @@ import { unwrapTemplate } from '@glimmer/util'; import { CURRENT_TAG, validateTag, valueForTag } from '@glimmer/validator'; import { SimpleDocument, SimpleElement, SimpleNode } from '@simple-dom/interface'; import RSVP from 'rsvp'; +import Component from './component'; import { BOUNDS } from './component-managers/curly'; import { createRootOutlet } from './component-managers/outlet'; import { RootComponentDefinition } from './component-managers/root'; import { NodeDOMTreeConstruction } from './dom'; import { EmberEnvironmentDelegate } from './environment'; import ResolverImpl from './resolver'; -import { Component } from './utils/curly-component-state-bucket'; import { OutletState } from './utils/outlet'; import OutletView from './views/outlet'; export type IBuilder = (env: Environment, cursor: Cursor) => ElementBuilder; +export interface View { + parentView: Option; + renderer: Renderer; + tagName: string | null; + elementId: string | null; + isDestroying: boolean; + isDestroyed: boolean; +} + export class DynamicScope implements GlimmerDynamicScope { - constructor( - public view: Component | {} | null, - public outletState: Reference - ) {} + constructor(public view: View | null, public outletState: Reference) {} child() { return new DynamicScope(this.view, this.outletState); @@ -129,7 +136,7 @@ class RootState { template !== undefined ); - this.id = getViewId(root); + this.id = root instanceof OutletView ? guidFor(root) : getViewId(root); this.result = undefined; this.destroyed = false; @@ -449,7 +456,7 @@ export class Renderer { this._clearAllRoots(); } - getElement(view: unknown): Option { + getElement(view: View): Option { if (this._isInteractive) { return getViewElement(view); } else { diff --git a/packages/@ember/-internals/glimmer/lib/utils/bindings.ts b/packages/@ember/-internals/glimmer/lib/utils/bindings.ts index 1c87da38c85..780e5ee2594 100644 --- a/packages/@ember/-internals/glimmer/lib/utils/bindings.ts +++ b/packages/@ember/-internals/glimmer/lib/utils/bindings.ts @@ -10,7 +10,7 @@ import { Reference, valueForRef, } from '@glimmer/reference'; -import { Component } from './curly-component-state-bucket'; +import Component from '../component'; function referenceForParts(rootRef: Reference, parts: string[]): Reference { let isAttrs = parts[0] === 'attrs'; diff --git a/packages/@ember/-internals/glimmer/lib/utils/curly-component-state-bucket.ts b/packages/@ember/-internals/glimmer/lib/utils/curly-component-state-bucket.ts index 117c4a41a30..5ad9fe9aa18 100644 --- a/packages/@ember/-internals/glimmer/lib/utils/curly-component-state-bucket.ts +++ b/packages/@ember/-internals/glimmer/lib/utils/curly-component-state-bucket.ts @@ -1,27 +1,9 @@ import { clearElementView, clearViewElement, getViewElement } from '@ember/-internals/views'; import { registerDestructor } from '@glimmer/destroyable'; -import { CapturedNamedArguments, Template, TemplateFactory } from '@glimmer/interfaces'; +import { CapturedNamedArguments } from '@glimmer/interfaces'; import { createConstRef, Reference } from '@glimmer/reference'; import { beginUntrackFrame, endUntrackFrame, Revision, Tag, valueForTag } from '@glimmer/validator'; -import { Renderer } from '../renderer'; - -export interface Component { - _debugContainerKey: string; - _transitionTo(name: string): void; - layout?: TemplateFactory | Template; - layoutName?: string; - attributeBindings: Array; - classNames: Array; - classNameBindings: Array; - elementId: string; - tagName: string; - isDestroying: boolean; - appendChild(view: {}): void; - trigger(event: string): void; - destroy(): void; - setProperties(props: { [key: string]: any }): void; - renderer: Renderer; -} +import Component from '../component'; type Finalizer = () => void; function NOOP() {} diff --git a/packages/@ember/-internals/glimmer/tests/integration/application/debug-render-tree-test.ts b/packages/@ember/-internals/glimmer/tests/integration/application/debug-render-tree-test.ts index 1494f62c0b8..3cc9f927a84 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/application/debug-render-tree-test.ts +++ b/packages/@ember/-internals/glimmer/tests/integration/application/debug-render-tree-test.ts @@ -1003,7 +1003,7 @@ if (ENV._DEBUG_RENDER_TREE) { ); this.addComponent('hello-world', { - ComponentClass: Component.extend(), + ComponentClass: class extends Component {}, template: 'Hello World', }); @@ -1498,11 +1498,12 @@ if (ENV._DEBUG_RENDER_TREE) { ); this.addComponent('hello-world', { - ComponentClass: Component.extend({ - init() { + ComponentClass: class extends Component { + constructor(owner: Owner) { + super(owner); throw new Error('oops!'); - }, - }), + } + }, template: '{{@name}}', }); diff --git a/packages/@ember/-internals/runtime/lib/mixins/evented.d.ts b/packages/@ember/-internals/runtime/lib/mixins/evented.d.ts index 5d0bb160322..5b053513747 100644 --- a/packages/@ember/-internals/runtime/lib/mixins/evented.d.ts +++ b/packages/@ember/-internals/runtime/lib/mixins/evented.d.ts @@ -13,6 +13,13 @@ interface Evented { method: string | ((this: Target, ...args: any[]) => void) ): this; on(name: string, method: ((...args: any[]) => void) | string): this; + // Allow for easier super calls + on( + name: string, + ...args: + | [Target, string | ((this: Target, ...args: any[]) => void)] + | [((...args: any[]) => void) | string] + ): this; /** * Subscribes a function to a named event and then cancels the subscription * after the first time the event is triggered. It is good to use ``one`` when diff --git a/packages/@ember/-internals/runtime/lib/mixins/target_action_support.d.ts b/packages/@ember/-internals/runtime/lib/mixins/target_action_support.d.ts new file mode 100644 index 00000000000..fc7195ce6b4 --- /dev/null +++ b/packages/@ember/-internals/runtime/lib/mixins/target_action_support.d.ts @@ -0,0 +1,12 @@ +import Mixin from '@ember/object/mixin'; + +interface TargetActionSupport { + target: unknown; + action: unknown; + actionContext: unknown; + actionContextObject: unknown; + triggerAction(opts?: object): unknown; +} +declare const TargetActionSupport: Mixin; + +export default TargetActionSupport; diff --git a/packages/@ember/-internals/views/index.d.ts b/packages/@ember/-internals/views/index.d.ts deleted file mode 100644 index 811551560d9..00000000000 --- a/packages/@ember/-internals/views/index.d.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Option } from '@glimmer/interfaces'; -import { SimpleElement } from '@simple-dom/interface'; -import { Object as EmberObject } from '@ember/-internals/runtime'; - -export const ActionSupport: any; -export const ChildViewsSupport: any; -export const ClassNamesSupport: any; -export const CoreView: any; -export const ViewMixin: any; -export const ViewStateSupport: any; - -export function getElementView(element: SimpleElement): unknown; -export function getViewElement(view: unknown): Option; -export function setElementView(element: SimpleElement, view: unknown): void; -export function setViewElement(view: unknown, element: SimpleElement): void; -export function clearElementView(element: SimpleElement): void; -export function clearViewElement(view: unknown): void; - -export function addChildView(parent: unknown, child: unknown): void; - -export function isSimpleClick(event: Event): boolean; - -export function constructStyleDeprecationMessage(affectedStyle: any): string; - -export function getViewId(view: any): string; - -export const MUTABLE_CELL: string; - -export const ActionManager: { - registeredActions: { - [id: string]: any | undefined; - }; -}; - -export declare class EventDispatcher extends EmberObject { - events: Record; - finalEventNameMapping: Record; - rootElement: string | HTMLElement; - lazyEvents: Map; - - setup(addedEvents: object, rootElement?: string | HTMLElement): void; - setupHandlerForBrowserEvent(event: string): void; - setupHandlerForEmberEvent(event: string): void; -} diff --git a/packages/@ember/-internals/views/index.js b/packages/@ember/-internals/views/index.ts similarity index 100% rename from packages/@ember/-internals/views/index.js rename to packages/@ember/-internals/views/index.ts diff --git a/packages/@ember/-internals/views/lib/mixins/action_support.d.ts b/packages/@ember/-internals/views/lib/mixins/action_support.d.ts new file mode 100644 index 00000000000..70ba2cbfaf0 --- /dev/null +++ b/packages/@ember/-internals/views/lib/mixins/action_support.d.ts @@ -0,0 +1,8 @@ +import Mixin from '@ember/object/mixin'; + +interface ActionSupport { + send(actionName: string, ...args: unknown[]): void; +} +declare const ActionSupport: Mixin; + +export default ActionSupport; diff --git a/packages/@ember/-internals/views/lib/mixins/child_views_support.d.ts b/packages/@ember/-internals/views/lib/mixins/child_views_support.d.ts new file mode 100644 index 00000000000..5b60b8703d2 --- /dev/null +++ b/packages/@ember/-internals/views/lib/mixins/child_views_support.d.ts @@ -0,0 +1,10 @@ +import { View } from '@ember/-internals/glimmer/lib/renderer'; +import Mixin from '@ember/object/mixin'; + +interface ChildViewsSupport { + readonly childViews: View[]; + appendChild(view: View): void; +} +declare const ChildViewsSupport: Mixin; + +export default ChildViewsSupport; diff --git a/packages/@ember/-internals/views/lib/mixins/class_names_support.d.ts b/packages/@ember/-internals/views/lib/mixins/class_names_support.d.ts new file mode 100644 index 00000000000..508e1c04baa --- /dev/null +++ b/packages/@ember/-internals/views/lib/mixins/class_names_support.d.ts @@ -0,0 +1,10 @@ +import Mixin from '@ember/object/mixin'; + +interface ClassNamesSupport { + concatenatedProperties: string[]; + classNames: string[]; + classNameBindings: string[]; +} +declare const ClassNamesSupport: Mixin; + +export default ClassNamesSupport; diff --git a/packages/@ember/-internals/views/lib/mixins/view_state_support.d.ts b/packages/@ember/-internals/views/lib/mixins/view_state_support.d.ts new file mode 100644 index 00000000000..0c92c9b7fa5 --- /dev/null +++ b/packages/@ember/-internals/views/lib/mixins/view_state_support.d.ts @@ -0,0 +1,8 @@ +import Mixin from '@ember/object/mixin'; + +interface ViewStateSupport { + _transitionTo(state: unknown): void; +} +declare const ViewStateSupport: Mixin; + +export default ViewStateSupport; diff --git a/packages/@ember/-internals/views/lib/mixins/view_support.d.ts b/packages/@ember/-internals/views/lib/mixins/view_support.d.ts new file mode 100644 index 00000000000..a26059e51cb --- /dev/null +++ b/packages/@ember/-internals/views/lib/mixins/view_support.d.ts @@ -0,0 +1,20 @@ +import Mixin from '@ember/object/mixin'; + +interface ViewSupport { + concatenatedProperties: string[]; + rerender(): unknown; + element: Element; + appendTo(selector: string | Element): this; + append(): this; + elementId: string | null; + willInsertElement(): void; + didInsertElement(): void; + willClearRender(): void; + willDestroyElement(): void; + parentViewDidChange(): void; + tagName: string | null; + handleEvent(eventName: string, evt: unknown): unknown; +} +declare const ViewSupport: Mixin; + +export default ViewSupport; diff --git a/packages/@ember/-internals/views/lib/system/event_dispatcher.d.ts b/packages/@ember/-internals/views/lib/system/event_dispatcher.d.ts new file mode 100644 index 00000000000..5113dc50e67 --- /dev/null +++ b/packages/@ember/-internals/views/lib/system/event_dispatcher.d.ts @@ -0,0 +1,8 @@ +import EmberObject from '@ember/object'; + +export default class EventDispatcher extends EmberObject { + finalEventNameMapping: Record; + lazyEvents: Map; + setupHandlerForBrowserEvent(event: string): void; + setupHandlerForEmberEvent(event: string): void; +} diff --git a/packages/@ember/-internals/views/lib/system/utils.ts b/packages/@ember/-internals/views/lib/system/utils.ts index 975af3d1f81..633c76459b4 100644 --- a/packages/@ember/-internals/views/lib/system/utils.ts +++ b/packages/@ember/-internals/views/lib/system/utils.ts @@ -1,15 +1,18 @@ -import { Renderer } from '@ember/-internals/glimmer'; +import { View } from '@ember/-internals/glimmer/lib/renderer'; import { getOwner, Owner } from '@ember/-internals/owner'; -/* globals Element */ import { guidFor } from '@ember/-internals/utils'; import { assert } from '@ember/debug'; import { Dict, Option } from '@glimmer/interfaces'; +import { SimpleElement } from '@simple-dom/interface'; /** @module ember */ -export function isSimpleClick(event: MouseEvent): boolean { +export function isSimpleClick(event: Event): boolean { + if (!(event instanceof MouseEvent)) { + return false; + } let modifier = event.shiftKey || event.metaKey || event.altKey || event.ctrlKey; let secondaryClick = event.which > 1; // IE9 may return undefined @@ -29,15 +32,6 @@ export function constructStyleDeprecationMessage(affectedStyle: string): string ); } -interface View { - parentView: Option; - renderer: Renderer; - tagName?: string; - elementId?: string; - isDestroying: boolean; - isDestroyed: boolean; -} - /** @private @method getRootViews @@ -73,10 +67,10 @@ export function getViewId(view: View): string { } } -const ELEMENT_VIEW: WeakMap = new WeakMap(); -const VIEW_ELEMENT: WeakMap = new WeakMap(); +const ELEMENT_VIEW: WeakMap = new WeakMap(); +const VIEW_ELEMENT: WeakMap = new WeakMap(); -export function getElementView(element: Element): Option { +export function getElementView(element: SimpleElement): Option { return ELEMENT_VIEW.get(element) || null; } @@ -85,15 +79,15 @@ export function getElementView(element: Element): Option { @method getViewElement @param {Ember.View} view */ -export function getViewElement(view: View): Option { +export function getViewElement(view: View): Option { return VIEW_ELEMENT.get(view) || null; } -export function setElementView(element: Element, view: View): void { +export function setElementView(element: SimpleElement, view: View): void { ELEMENT_VIEW.set(element, view); } -export function setViewElement(view: View, element: Element): void { +export function setViewElement(view: View, element: SimpleElement): void { VIEW_ELEMENT.set(view, element); } @@ -102,7 +96,7 @@ export function setViewElement(view: View, element: Element): void { // this case, we want to prevent access to the element (and vice verse) during // destruction. -export function clearElementView(element: Element): void { +export function clearElementView(element: SimpleElement): void { ELEMENT_VIEW.delete(element); } diff --git a/packages/@ember/-internals/views/lib/views/core_view.d.ts b/packages/@ember/-internals/views/lib/views/core_view.d.ts new file mode 100644 index 00000000000..a3c23eaa4e5 --- /dev/null +++ b/packages/@ember/-internals/views/lib/views/core_view.d.ts @@ -0,0 +1,12 @@ +import { Renderer } from '@ember/-internals/glimmer'; +import { ActionHandler } from '@ember/-internals/runtime'; +import EmberObject from '@ember/object'; +import Evented from '@ember/object/evented'; + +interface CoreView extends Evented, ActionHandler {} +declare class CoreView extends EmberObject { + parentView: CoreView | null; + renderer: Renderer; +} + +export { CoreView as default }; diff --git a/packages/@ember/component/type-tests/index.test.ts b/packages/@ember/component/type-tests/index.test.ts new file mode 100644 index 00000000000..bccc1576357 --- /dev/null +++ b/packages/@ember/component/type-tests/index.test.ts @@ -0,0 +1,10 @@ +import Component from '@ember/component'; +import { CoreView } from '@ember/-internals/views'; +import { expectTypeOf } from 'expect-type'; +import { Owner } from '@ember/-internals/owner'; + +// NOTE: This is invalid, but acceptable for type tests +let owner = {} as Owner; +let component = new Component(owner); + +expectTypeOf(component).toMatchTypeOf(); From 4b1ccc2be4dd8f7d7a7bb1d1a3ff96eb76d6e5c3 Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Mon, 7 Feb 2022 15:40:11 -0800 Subject: [PATCH 2/6] Future public types for @ember/component --- .../@ember/-internals/glimmer/lib/helper.ts | 27 ++++++++---- .../-internals/glimmer/lib/utils/managers.ts | 6 +-- .../component/type-tests/capabilities.test.ts | 16 +++++++ .../type-tests/get-component-template.test.ts | 8 ++++ .../type-tests/helper/helper.test.ts | 18 ++++++++ .../component/type-tests/helper/index.test.ts | 42 +++++++++++++++++++ .../@ember/component/type-tests/index.test.ts | 41 ++++++++++++++++++ .../type-tests/set-component-manager.test.ts | 17 ++++++++ .../type-tests/set-component-template.test.ts | 8 ++++ .../type-tests/template-only.test.ts | 11 +++++ .../helper/type-tests/invoke-helper.test.ts | 8 +++- 11 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 packages/@ember/component/type-tests/capabilities.test.ts create mode 100644 packages/@ember/component/type-tests/get-component-template.test.ts create mode 100644 packages/@ember/component/type-tests/helper/helper.test.ts create mode 100644 packages/@ember/component/type-tests/helper/index.test.ts create mode 100644 packages/@ember/component/type-tests/set-component-manager.test.ts create mode 100644 packages/@ember/component/type-tests/set-component-template.test.ts create mode 100644 packages/@ember/component/type-tests/template-only.test.ts diff --git a/packages/@ember/-internals/glimmer/lib/helper.ts b/packages/@ember/-internals/glimmer/lib/helper.ts index 2370ffe92e8..e9932fac16d 100644 --- a/packages/@ember/-internals/glimmer/lib/helper.ts +++ b/packages/@ember/-internals/glimmer/lib/helper.ts @@ -13,7 +13,11 @@ import { consumeTag, createTag, dirtyTag } from '@glimmer/validator'; export const RECOMPUTE_TAG = symbol('RECOMPUTE_TAG'); -export type HelperFunction = (positional: unknown[], named: Dict) => T; +export type HelperFunction< + T = unknown, + P extends unknown[] = unknown[], + N extends Dict = Dict +> = (positional: P, named: N) => T; export type SimpleHelperFactory = Factory>; export type ClassHelperFactory = Factory>; @@ -30,8 +34,12 @@ export interface HelperInstance { const IS_CLASSIC_HELPER: unique symbol = Symbol('IS_CLASSIC_HELPER'); -export interface SimpleHelper { - compute: HelperFunction; +export interface SimpleHelper< + T = unknown, + P extends unknown[] = unknown[], + N extends Dict = Dict +> { + compute: HelperFunction; } /** @@ -89,7 +97,7 @@ interface Helper { @public @since 1.13.0 */ - compute(params: unknown[], hash: object): unknown; + compute(params: unknown[], hash: Dict): unknown; } class Helper extends FrameworkObject { static isHelperFactory = true; @@ -199,10 +207,11 @@ export const CLASSIC_HELPER_MANAGER = getInternalHelperManager(Helper); /////////// -class Wrapper implements HelperFactory { +class Wrapper = Dict> + implements HelperFactory> { isHelperFactory: true = true; - constructor(public compute: HelperFunction) {} + constructor(public compute: HelperFunction) {} create() { // needs new instance or will leak containers @@ -256,7 +265,11 @@ setHelperManager(() => SIMPLE_CLASSIC_HELPER_MANAGER, Wrapper.prototype); @public @since 1.13.0 */ -export function helper(helperFn: HelperFunction): HelperFactory { +export function helper< + T = unknown, + P extends unknown[] = unknown[], + N extends Dict = Dict +>(helperFn: HelperFunction): HelperFactory> { return new Wrapper(helperFn); } diff --git a/packages/@ember/-internals/glimmer/lib/utils/managers.ts b/packages/@ember/-internals/glimmer/lib/utils/managers.ts index 1c7a8674292..a3b88671ccb 100644 --- a/packages/@ember/-internals/glimmer/lib/utils/managers.ts +++ b/packages/@ember/-internals/glimmer/lib/utils/managers.ts @@ -17,10 +17,10 @@ import { @return {Object} the same object passed in @public */ -export function setComponentManager( +export function setComponentManager( manager: (owner: Owner) => ComponentManager, - obj: object -): object { + obj: T +): T { return glimmerSetComponentManager(manager, obj); } diff --git a/packages/@ember/component/type-tests/capabilities.test.ts b/packages/@ember/component/type-tests/capabilities.test.ts new file mode 100644 index 00000000000..0014713e0ae --- /dev/null +++ b/packages/@ember/component/type-tests/capabilities.test.ts @@ -0,0 +1,16 @@ +import { capabilities } from '@ember/component'; +import { expectTypeOf } from 'expect-type'; + +expectTypeOf(capabilities('3.13')).toMatchTypeOf<{ + asyncLifecycleCallbacks?: boolean | undefined; + destructor?: boolean | undefined; + updateHook?: boolean | undefined; +}>(); + +capabilities('3.13', { asyncLifecycleCallbacks: true }); +capabilities('3.4', { asyncLifecycleCallbacks: true }); + +// @ts-expect-error invalid capabilities +capabilities('3.13', { asyncLifecycleCallbacks: 1 }); +// @ts-expect-error invalid verison +capabilities('3.12'); diff --git a/packages/@ember/component/type-tests/get-component-template.test.ts b/packages/@ember/component/type-tests/get-component-template.test.ts new file mode 100644 index 00000000000..6a50cf49cb5 --- /dev/null +++ b/packages/@ember/component/type-tests/get-component-template.test.ts @@ -0,0 +1,8 @@ +import { getComponentTemplate } from '@ember/component'; +import { TemplateFactory } from '@glimmer/interfaces'; +import { expectTypeOf } from 'expect-type'; + +expectTypeOf(getComponentTemplate({})).toEqualTypeOf(); + +// @ts-expect-error requires param +getComponentTemplate(); diff --git a/packages/@ember/component/type-tests/helper/helper.test.ts b/packages/@ember/component/type-tests/helper/helper.test.ts new file mode 100644 index 00000000000..e1e02d26e2a --- /dev/null +++ b/packages/@ember/component/type-tests/helper/helper.test.ts @@ -0,0 +1,18 @@ +import { HelperFactory, SimpleHelper } from '@ember/-internals/glimmer/lib/helper'; +import { helper } from '@ember/component/helper'; +import { expectTypeOf } from 'expect-type'; + +// NOTE: The types for `helper` are not actually safe. Glint helps with this. + +let myHelper = helper(function ([cents]: [number], { currency }: { currency: string }) { + return `${currency}${cents * 0.01}`; +}); +expectTypeOf(myHelper).toEqualTypeOf< + HelperFactory> +>(); + +// @ts-expect-error invalid named params +helper(function ([cents]: [number], named: number) {}); + +// @ts-expect-error invalid params +helper(function (params: number) {}); diff --git a/packages/@ember/component/type-tests/helper/index.test.ts b/packages/@ember/component/type-tests/helper/index.test.ts new file mode 100644 index 00000000000..e3d3a8e075f --- /dev/null +++ b/packages/@ember/component/type-tests/helper/index.test.ts @@ -0,0 +1,42 @@ +import { Owner } from '@ember/-internals/owner'; +import { FrameworkObject } from '@ember/-internals/runtime'; +import Helper from '@ember/component/helper'; +import { expectTypeOf } from 'expect-type'; + +// Good enough for tests +let owner = {} as Owner; + +// NOTE: The types for `compute` are not actually safe. Glint helps with this. + +let helper = new Helper(owner); + +expectTypeOf(helper).toMatchTypeOf(); + +class MyHelper extends Helper { + compute([cents]: [number], { currency }: { currency: string }) { + return `${currency}${cents * 0.01}`; + } +} +new MyHelper(owner); + +class NoHash extends Helper { + compute([cents]: [number]): string { + return `${cents * 0.01}`; + } +} +new NoHash(owner); + +class NoParams extends Helper { + compute(): string { + return 'hello'; + } +} +new NoParams(owner); + +class InvalidHelper extends Helper { + // @ts-expect-error Invalid params + compute(value: boolean): string { + return String(value); + } +} +new InvalidHelper(owner); diff --git a/packages/@ember/component/type-tests/index.test.ts b/packages/@ember/component/type-tests/index.test.ts index bccc1576357..8538178abcc 100644 --- a/packages/@ember/component/type-tests/index.test.ts +++ b/packages/@ember/component/type-tests/index.test.ts @@ -2,9 +2,50 @@ import Component from '@ember/component'; import { CoreView } from '@ember/-internals/views'; import { expectTypeOf } from 'expect-type'; import { Owner } from '@ember/-internals/owner'; +import { View } from '@ember/-internals/glimmer/lib/renderer'; +import { action } from '@ember/object'; +import { tracked } from '@ember/-internals/metal'; // NOTE: This is invalid, but acceptable for type tests let owner = {} as Owner; let component = new Component(owner); expectTypeOf(component).toMatchTypeOf(); +expectTypeOf(component).toMatchTypeOf(); + +class MyComponent extends Component { + tagName = 'em'; + classNames = ['my-class', 'my-other-class']; + classNameBindings = ['propertyA', 'propertyB']; + attributeBindings = ['href']; + + @tracked propertyA = 'from-a'; + + get propertyB(): string | void { + if (this.propertyA === 'from-a') { + return 'from-b'; + } + } + + @tracked href = 'https://tilde.io'; + + @action click(_event: Event): void { + // Clicked! + } +} +new MyComponent(owner); + +class BadComponent extends Component { + // @ts-expect-error invalid tag name + tagName = 1; + + // @ts-expect-error invalid classname + classNames = 'foo'; + + // @ts-expect-error invalid classNameBindings + classNameBindings = [1]; + + // @ts-expect-error invalid attributeBindings + attributeBindings = [true]; +} +new BadComponent(owner); diff --git a/packages/@ember/component/type-tests/set-component-manager.test.ts b/packages/@ember/component/type-tests/set-component-manager.test.ts new file mode 100644 index 00000000000..6e29af3c11f --- /dev/null +++ b/packages/@ember/component/type-tests/set-component-manager.test.ts @@ -0,0 +1,17 @@ +import { Owner } from '@ember/-internals/owner'; +import { setComponentManager } from '@ember/component'; +import { ComponentManager } from '@glimmer/interfaces'; +import { expectTypeOf } from 'expect-type'; + +// Obviously this is invalid, but it works for our purposes. +let manager = {} as ComponentManager; + +class Foo {} +let foo = new Foo(); + +expectTypeOf(setComponentManager((_owner: Owner) => manager, foo)).toEqualTypeOf(); + +// @ts-expect-error invalid callback +setComponentManager(() => { + return {}; +}, foo); diff --git a/packages/@ember/component/type-tests/set-component-template.test.ts b/packages/@ember/component/type-tests/set-component-template.test.ts new file mode 100644 index 00000000000..a99a3d3608e --- /dev/null +++ b/packages/@ember/component/type-tests/set-component-template.test.ts @@ -0,0 +1,8 @@ +import { setComponentTemplate } from '@ember/component'; +import { TemplateFactory } from '@glimmer/interfaces'; +import { expectTypeOf } from 'expect-type'; + +// Good enough for testing +let factory = {} as TemplateFactory; + +expectTypeOf(setComponentTemplate(factory, {})).toEqualTypeOf(); diff --git a/packages/@ember/component/type-tests/template-only.test.ts b/packages/@ember/component/type-tests/template-only.test.ts new file mode 100644 index 00000000000..a47ba4153e2 --- /dev/null +++ b/packages/@ember/component/type-tests/template-only.test.ts @@ -0,0 +1,11 @@ +import templateOnlyComponent from '@ember/component/template-only'; +import { TemplateOnlyComponentDefinition } from '@glimmer/runtime/dist/types/lib/component/template-only'; +import { expectTypeOf } from 'expect-type'; + +expectTypeOf(templateOnlyComponent()).toEqualTypeOf(); + +templateOnlyComponent('myModule'); +templateOnlyComponent('myModule', 'myName'); + +// @ts-expect-error invalid params +templateOnlyComponent(1); diff --git a/packages/@ember/helper/type-tests/invoke-helper.test.ts b/packages/@ember/helper/type-tests/invoke-helper.test.ts index 596b856bc8c..65300ce5519 100644 --- a/packages/@ember/helper/type-tests/invoke-helper.test.ts +++ b/packages/@ember/helper/type-tests/invoke-helper.test.ts @@ -1,5 +1,6 @@ import Component from '@ember/-internals/glimmer/lib/component'; import { getValue } from '@ember/-internals/metal'; +import { Owner } from '@ember/-internals/owner'; import Helper from '@ember/component/helper'; import { invokeHelper } from '@ember/helper'; import { Cache } from '@glimmer/validator'; @@ -14,6 +15,9 @@ class PlusOne extends Helper { } export default class PlusOneComponent extends Component { + // Glint would help with this + declare args: { number: number }; + plusOne = invokeHelper(this, PlusOne, () => { return { positional: [this.args.number], @@ -25,6 +29,8 @@ export default class PlusOneComponent extends Component { } } -let component = new PlusOneComponent(); +// Good enough for tests! +let owner = {} as Owner; +let component = new PlusOneComponent(owner); expectTypeOf(component.plusOne).toEqualTypeOf>(); From 182a0345703e1456013b2206c414beaf7fb81caa Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Tue, 22 Feb 2022 15:45:10 -0800 Subject: [PATCH 3/6] Avoid zebra classes for Component --- .../-internals/glimmer/lib/component.ts | 732 +++++++++--------- .../runtime/lib/mixins/action_handler.d.ts | 5 +- .../runtime/lib/system/core_object.d.ts | 27 +- .../-internals/views/lib/views/core_view.d.ts | 12 - .../lib/views/{core_view.js => core_view.ts} | 15 +- .../@ember/component/type-tests/index.test.ts | 49 +- .../helper/type-tests/invoke-helper.test.ts | 7 +- 7 files changed, 406 insertions(+), 441 deletions(-) delete mode 100644 packages/@ember/-internals/views/lib/views/core_view.d.ts rename packages/@ember/-internals/views/lib/views/{core_view.js => core_view.ts} (75%) diff --git a/packages/@ember/-internals/glimmer/lib/component.ts b/packages/@ember/-internals/glimmer/lib/component.ts index 916a1032c1c..b1f7c791ba4 100644 --- a/packages/@ember/-internals/glimmer/lib/component.ts +++ b/packages/@ember/-internals/glimmer/lib/component.ts @@ -1,6 +1,7 @@ import { get, PROPERTY_DID_CHANGE } from '@ember/-internals/metal'; -import { getOwner, Owner } from '@ember/-internals/owner'; +import { getOwner } from '@ember/-internals/owner'; import { TargetActionSupport } from '@ember/-internals/runtime'; +import { CoreObjectClass } from '@ember/-internals/runtime/lib/system/core_object'; import { ActionSupport, ChildViewsSupport, @@ -26,7 +27,6 @@ import { DIRTY_TAG, IS_DISPATCHING_ATTRS, } from './component-managers/curly'; -import { View } from './renderer'; // Keep track of which component classes have already been processed for lazy event setup. let lazyEventsProcessed = new WeakMap>(); @@ -649,6 +649,8 @@ let lazyEventsProcessed = new WeakMap>(); @uses Ember.ViewStateSupport @public */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface ComponentClass extends CoreObjectClass {} interface Component extends CoreView, ChildViewsSupport, @@ -657,7 +659,7 @@ interface Component TargetActionSupport, ActionSupport, ViewMixin { - parentView: Component | null; + attributeBindings?: string[]; /** Layout can be used to wrap content in a component. @@ -677,403 +679,387 @@ interface Component @private */ layoutName?: string; - - attributeBindings?: Array; } -class Component - extends CoreView.extend( - ChildViewsSupport, - ViewStateSupport, - ClassNamesSupport, - TargetActionSupport, - ActionSupport, - ViewMixin - ) - implements View { - static isComponentFactory = true; - static positionalParams = []; - - static toString(): string { - return '@ember/component'; - } - - isComponent = true; - - private __dispatcher: EventDispatcher | null | undefined; - constructor(owner: Owner) { - super(owner); - this[IS_DISPATCHING_ATTRS] = false; - this[DIRTY_TAG] = createTag(); - this[BOUNDS] = null; - - const eventDispatcher = this._dispatcher; - if (eventDispatcher) { - let lazyEventsProcessedForComponentClass = lazyEventsProcessed.get(eventDispatcher); - if (!lazyEventsProcessedForComponentClass) { - lazyEventsProcessedForComponentClass = new WeakSet(); - lazyEventsProcessed.set(eventDispatcher, lazyEventsProcessedForComponentClass); - } +const Component = (CoreView.extend( + ChildViewsSupport, + ViewStateSupport, + ClassNamesSupport, + TargetActionSupport, + ActionSupport, + ViewMixin, + { + isComponent: true, + + init() { + this._super(...arguments); + this[IS_DISPATCHING_ATTRS] = false; + this[DIRTY_TAG] = createTag(); + this[BOUNDS] = null; + + let eventDispatcher = this._dispatcher; + if (eventDispatcher) { + let lazyEventsProcessedForComponentClass = lazyEventsProcessed.get(eventDispatcher); + if (!lazyEventsProcessedForComponentClass) { + lazyEventsProcessedForComponentClass = new WeakSet(); + lazyEventsProcessed.set(eventDispatcher, lazyEventsProcessedForComponentClass); + } - let proto = Object.getPrototypeOf(this); - if (!lazyEventsProcessedForComponentClass.has(proto)) { - let lazyEvents = eventDispatcher.lazyEvents; + let proto = Object.getPrototypeOf(this); + if (!lazyEventsProcessedForComponentClass.has(proto)) { + let lazyEvents = eventDispatcher.lazyEvents; - lazyEvents.forEach((mappedEventName: string, event: string) => { - if (mappedEventName !== null && typeof this[mappedEventName] === 'function') { - eventDispatcher.setupHandlerForBrowserEvent(event); - } - }); + lazyEvents.forEach((mappedEventName: string, event: string) => { + if (mappedEventName !== null && typeof this[mappedEventName] === 'function') { + eventDispatcher.setupHandlerForBrowserEvent(event); + } + }); - lazyEventsProcessedForComponentClass.add(proto); + lazyEventsProcessedForComponentClass.add(proto); + } } - } - // @ts-expect-error _isInteractive is private - if (DEBUG && eventDispatcher && this.renderer._isInteractive && this.tagName === '') { - let eventNames = []; - let events = eventDispatcher.finalEventNameMapping; + if (DEBUG && eventDispatcher && this.renderer._isInteractive && this.tagName === '') { + let eventNames = []; + let events = eventDispatcher.finalEventNameMapping; - for (let key in events) { - let methodName = events[key]; + for (let key in events) { + let methodName = events[key]; - if (methodName && typeof this[methodName] === 'function') { - eventNames.push(methodName); + if (typeof this[methodName] === 'function') { + eventNames.push(methodName); + } } - } - // If in a tagless component, assert that no event handlers are defined - assert( - `You can not define \`${eventNames}\` function(s) to handle DOM event in the \`${this}\` tagless component since it doesn't have any DOM element.`, - !eventNames.length - ); - } - } - - get _dispatcher(): EventDispatcher | null { - if (this.__dispatcher === undefined) { - let owner = getOwner(this); - assert('Component is unexpectedly missing an owner', owner); - - if ((owner.lookup('-environment:main') as Environment)!.isInteractive) { - let dispatcher = owner.lookup('event_dispatcher:main'); + // If in a tagless component, assert that no event handlers are defined assert( - 'Expected dispatcher to be an EventDispatcher', - dispatcher instanceof EventDispatcher + `You can not define \`${eventNames}\` function(s) to handle DOM event in the \`${this}\` tagless component since it doesn't have any DOM element.`, + !eventNames.length ); - this.__dispatcher = dispatcher; - } else { - // In FastBoot we have no EventDispatcher. Set to null to not try again to look it up. - this.__dispatcher = null; } - } - - return this.__dispatcher; - } - - on( - name: string, - target: Target, - method: string | ((this: Target, ...args: any[]) => void) - ): this; - on(name: string, method: ((...args: any[]) => void) | string): this; - on( - name: string, - ...args: - | [Target, string | ((this: Target, ...args: any[]) => void)] - | [((...args: any[]) => void) | string] - ): this { - this._dispatcher?.setupHandlerForEmberEvent(name); - return super.on(name, ...args); - } - - rerender() { - dirtyTag(this[DIRTY_TAG]); - } - - [PROPERTY_DID_CHANGE](key: string, value?: unknown) { - if (this[IS_DISPATCHING_ATTRS]) { - return; - } - - let args = this[ARGS]; - let reference = args !== undefined ? args[key] : undefined; - - if (reference !== undefined && isUpdatableRef(reference)) { - updateRef(reference, arguments.length === 2 ? value : get(this, key)); - } - } - - getAttr(key: string) { - // TODO Intimate API should be deprecated - return this.get(key); - } - - /** - Normally, Ember's component model is "write-only". The component takes a - bunch of attributes that it got passed in, and uses them to render its - template. - - One nice thing about this model is that if you try to set a value to the - same thing as last time, Ember (through HTMLBars) will avoid doing any - work on the DOM. - - This is not just a performance optimization. If an attribute has not - changed, it is important not to clobber the element's "hidden state". - For example, if you set an input's `value` to the same value as before, - it will clobber selection state and cursor position. In other words, - setting an attribute is not **always** idempotent. - - This method provides a way to read an element's attribute and also - update the last value Ember knows about at the same time. This makes - setting an attribute idempotent. - - In particular, what this means is that if you get an `` element's - `value` attribute and then re-render the template with the same value, - it will avoid clobbering the cursor and selection position. - Since most attribute sets are idempotent in the browser, you typically - can get away with reading attributes using jQuery, but the most reliable - way to do so is through this method. - @method readDOMAttr - - @param {String} name the name of the attribute - @return String - @public - */ - readDOMAttr(name: string) { - // TODO revisit this - let _element = getViewElement(this); - - assert( - `Cannot call \`readDOMAttr\` on ${this} which does not have an element`, - _element !== null - ); - - let element = _element; - let isSVG = element.namespaceURI === Namespace.SVG; - let { type, normalized } = normalizeProperty(element, name); - - if (isSVG || type === 'attr') { - return element.getAttribute(normalized); - } - - return element[normalized]; - } - - /** - The WAI-ARIA role of the control represented by this view. For example, a - button may have a role of type 'button', or a pane may have a role of - type 'alertdialog'. This property is used by assistive software to help - visually challenged users navigate rich web applications. - - The full list of valid WAI-ARIA roles is available at: - [https://www.w3.org/TR/wai-aria/#roles_categorization](https://www.w3.org/TR/wai-aria/#roles_categorization) - - @property ariaRole - @type String - @default null - @public - */ - - /** - Enables components to take a list of parameters as arguments. - For example, a component that takes two parameters with the names - `name` and `age`: - - ```app/components/my-component.js - import Component from '@ember/component'; - - let MyComponent = Component.extend(); - - MyComponent.reopenClass({ - positionalParams: ['name', 'age'] - }); - - export default MyComponent; - ``` - - It can then be invoked like this: - - ```hbs - {{my-component "John" 38}} - ``` - - The parameters can be referred to just like named parameters: - - ```hbs - Name: {{name}}, Age: {{age}}. - ``` - - Using a string instead of an array allows for an arbitrary number of - parameters: - - ```app/components/my-component.js - import Component from '@ember/component'; - - let MyComponent = Component.extend(); - - MyComponent.reopenClass({ - positionalParams: 'names' - }); - - export default MyComponent; - ``` - - It can then be invoked like this: - - ```hbs - {{my-component "John" "Michael" "Scott"}} - ``` - The parameters can then be referred to by enumerating over the list: - - ```hbs - {{#each names as |name|}}{{name}}{{/each}} - ``` - - @static - @public - @property positionalParams - @since 1.13.0 - */ - - /** - Called when the attributes passed into the component have been updated. - Called both during the initial render of a container and during a rerender. - Can be used in place of an observer; code placed here will be executed - every time any attribute updates. - @method didReceiveAttrs - @public - @since 1.13.0 - */ - didReceiveAttrs() {} + }, + + get _dispatcher(): EventDispatcher | null { + if (this.__dispatcher === undefined) { + let owner = getOwner(this); + assert('Component is unexpectedly missing an owner', owner); + + if ((owner.lookup('-environment:main') as Environment)!.isInteractive) { + this.__dispatcher = owner.lookup('event_dispatcher:main'); + assert( + 'Expected dispatcher to be an EventDispatcher', + this.__dispatcher instanceof EventDispatcher + ); + } else { + // In FastBoot we have no EventDispatcher. Set to null to not try again to look it up. + this.__dispatcher = null; + } + } - /** - Called when the attributes passed into the component have been updated. - Called both during the initial render of a container and during a rerender. - Can be used in place of an observer; code placed here will be executed - every time any attribute updates. - @event didReceiveAttrs - @public - @since 1.13.0 - */ + return this.__dispatcher; + }, - /** - Called after a component has been rendered, both on initial render and - in subsequent rerenders. - @method didRender - @public - @since 1.13.0 - */ - didRender() {} + on(eventName: string) { + this._dispatcher?.setupHandlerForEmberEvent(eventName); + return this._super(...arguments); + }, - /** - Called after a component has been rendered, both on initial render and - in subsequent rerenders. - @event didRender - @public - @since 1.13.0 - */ + rerender() { + dirtyTag(this[DIRTY_TAG]); + this._super(); + }, - /** - Called before a component has been rendered, both on initial render and - in subsequent rerenders. - @method willRender - @public - @since 1.13.0 - */ - willRender() {} + [PROPERTY_DID_CHANGE](key: string, value?: unknown) { + if (this[IS_DISPATCHING_ATTRS]) { + return; + } - /** - Called before a component has been rendered, both on initial render and - in subsequent rerenders. - @event willRender - @public - @since 1.13.0 - */ + let args = this[ARGS]; + let reference = args !== undefined ? args[key] : undefined; - /** - Called when the attributes passed into the component have been changed. - Called only during a rerender, not during an initial render. - @method didUpdateAttrs - @public - @since 1.13.0 - */ - didUpdateAttrs() {} + if (reference !== undefined && isUpdatableRef(reference)) { + updateRef(reference, arguments.length === 2 ? value : get(this, key)); + } + }, + + getAttr(key: string) { + // TODO Intimate API should be deprecated + return this.get(key); + }, + + /** + Normally, Ember's component model is "write-only". The component takes a + bunch of attributes that it got passed in, and uses them to render its + template. + + One nice thing about this model is that if you try to set a value to the + same thing as last time, Ember (through HTMLBars) will avoid doing any + work on the DOM. + + This is not just a performance optimization. If an attribute has not + changed, it is important not to clobber the element's "hidden state". + For example, if you set an input's `value` to the same value as before, + it will clobber selection state and cursor position. In other words, + setting an attribute is not **always** idempotent. + + This method provides a way to read an element's attribute and also + update the last value Ember knows about at the same time. This makes + setting an attribute idempotent. + + In particular, what this means is that if you get an `` element's + `value` attribute and then re-render the template with the same value, + it will avoid clobbering the cursor and selection position. + Since most attribute sets are idempotent in the browser, you typically + can get away with reading attributes using jQuery, but the most reliable + way to do so is through this method. + @method readDOMAttr + + @param {String} name the name of the attribute + @return String + @public + */ + readDOMAttr(name: string) { + // TODO revisit this + let _element = getViewElement(this); - /** - Called when the attributes passed into the component have been changed. - Called only during a rerender, not during an initial render. - @event didUpdateAttrs - @public - @since 1.13.0 - */ + assert( + `Cannot call \`readDOMAttr\` on ${this} which does not have an element`, + _element !== null + ); - /** - Called when the component is about to update and rerender itself. - Called only during a rerender, not during an initial render. - @method willUpdate - @public - @since 1.13.0 - */ - willUpdate() {} + let element = _element; + let isSVG = element.namespaceURI === Namespace.SVG; + let { type, normalized } = normalizeProperty(element, name); - /** - Called when the component is about to update and rerender itself. - Called only during a rerender, not during an initial render. - @event willUpdate - @public - @since 1.13.0 - */ + if (isSVG || type === 'attr') { + return element.getAttribute(normalized); + } - /** - Called when the component has updated and rerendered itself. - Called only during a rerender, not during an initial render. - @method didUpdate - @public - @since 1.13.0 - */ - didUpdate() {} + return element[normalized]; + }, + + /** + The WAI-ARIA role of the control represented by this view. For example, a + button may have a role of type 'button', or a pane may have a role of + type 'alertdialog'. This property is used by assistive software to help + visually challenged users navigate rich web applications. + + The full list of valid WAI-ARIA roles is available at: + [https://www.w3.org/TR/wai-aria/#roles_categorization](https://www.w3.org/TR/wai-aria/#roles_categorization) + + @property ariaRole + @type String + @default null + @public + */ + + /** + Enables components to take a list of parameters as arguments. + For example, a component that takes two parameters with the names + `name` and `age`: + + ```app/components/my-component.js + import Component from '@ember/component'; + + let MyComponent = Component.extend(); + + MyComponent.reopenClass({ + positionalParams: ['name', 'age'] + }); + + export default MyComponent; + ``` + + It can then be invoked like this: + + ```hbs + {{my-component "John" 38}} + ``` + + The parameters can be referred to just like named parameters: + + ```hbs + Name: {{name}}, Age: {{age}}. + ``` + + Using a string instead of an array allows for an arbitrary number of + parameters: + + ```app/components/my-component.js + import Component from '@ember/component'; + + let MyComponent = Component.extend(); + + MyComponent.reopenClass({ + positionalParams: 'names' + }); + + export default MyComponent; + ``` + + It can then be invoked like this: + + ```hbs + {{my-component "John" "Michael" "Scott"}} + ``` + The parameters can then be referred to by enumerating over the list: + + ```hbs + {{#each names as |name|}}{{name}}{{/each}} + ``` + + @static + @public + @property positionalParams + @since 1.13.0 + */ + + /** + Called when the attributes passed into the component have been updated. + Called both during the initial render of a container and during a rerender. + Can be used in place of an observer; code placed here will be executed + every time any attribute updates. + @method didReceiveAttrs + @public + @since 1.13.0 + */ + didReceiveAttrs() {}, + + /** + Called when the attributes passed into the component have been updated. + Called both during the initial render of a container and during a rerender. + Can be used in place of an observer; code placed here will be executed + every time any attribute updates. + @event didReceiveAttrs + @public + @since 1.13.0 + */ + + /** + Called after a component has been rendered, both on initial render and + in subsequent rerenders. + @method didRender + @public + @since 1.13.0 + */ + didRender() {}, + + /** + Called after a component has been rendered, both on initial render and + in subsequent rerenders. + @event didRender + @public + @since 1.13.0 + */ + + /** + Called before a component has been rendered, both on initial render and + in subsequent rerenders. + @method willRender + @public + @since 1.13.0 + */ + willRender() {}, + + /** + Called before a component has been rendered, both on initial render and + in subsequent rerenders. + @event willRender + @public + @since 1.13.0 + */ + + /** + Called when the attributes passed into the component have been changed. + Called only during a rerender, not during an initial render. + @method didUpdateAttrs + @public + @since 1.13.0 + */ + didUpdateAttrs() {}, + + /** + Called when the attributes passed into the component have been changed. + Called only during a rerender, not during an initial render. + @event didUpdateAttrs + @public + @since 1.13.0 + */ + + /** + Called when the component is about to update and rerender itself. + Called only during a rerender, not during an initial render. + @method willUpdate + @public + @since 1.13.0 + */ + willUpdate() {}, + + /** + Called when the component is about to update and rerender itself. + Called only during a rerender, not during an initial render. + @event willUpdate + @public + @since 1.13.0 + */ + + /** + Called when the component has updated and rerendered itself. + Called only during a rerender, not during an initial render. + @method didUpdate + @public + @since 1.13.0 + */ + didUpdate() {}, + + /** + Called when the component has updated and rerendered itself. + Called only during a rerender, not during an initial render. + @event didUpdate + @public + @since 1.13.0 + */ + + /** + The HTML `id` of the component's element in the DOM. You can provide this + value yourself but it must be unique (just as in HTML): + + ```handlebars + {{my-component elementId="a-really-cool-id"}} + ``` + + ```handlebars + + ``` + If not manually set a default value will be provided by the framework. + Once rendered an element's `elementId` is considered immutable and you + should never change it. If you need to compute a dynamic value for the + `elementId`, you should do this when the component or element is being + instantiated: + + ```javascript + export default Component.extend({ + init() { + this._super(...arguments); + + var index = this.get('index'); + this.set('elementId', `component-id${index}`); + } + }); + ``` - /** - Called when the component has updated and rerendered itself. - Called only during a rerender, not during an initial render. - @event didUpdate - @public - @since 1.13.0 + @property elementId + @type String + @public */ + } +) as unknown) as ComponentClass; - /** - The HTML `id` of the component's element in the DOM. You can provide this - value yourself but it must be unique (just as in HTML): - - ```handlebars - {{my-component elementId="a-really-cool-id"}} - ``` - - ```handlebars - - ``` - If not manually set a default value will be provided by the framework. - Once rendered an element's `elementId` is considered immutable and you - should never change it. If you need to compute a dynamic value for the - `elementId`, you should do this when the component or element is being - instantiated: - - ```javascript - export default Component.extend({ - init() { - this._super(...arguments); - - var index = this.get('index'); - this.set('elementId', `component-id${index}`); - } - }); - ``` +Component.toString = () => '@ember/component'; - @property elementId - @type String - @public - */ -} +Component.reopenClass({ + isComponentFactory: true, + positionalParams: [], +}); setInternalComponentManager(CURLY_COMPONENT_MANAGER, Component); diff --git a/packages/@ember/-internals/runtime/lib/mixins/action_handler.d.ts b/packages/@ember/-internals/runtime/lib/mixins/action_handler.d.ts index 07e4a62c7d0..dd0e93c132d 100644 --- a/packages/@ember/-internals/runtime/lib/mixins/action_handler.d.ts +++ b/packages/@ember/-internals/runtime/lib/mixins/action_handler.d.ts @@ -1,3 +1,6 @@ import { Mixin } from '@ember/-internals/metal'; -export default class ActionHandler extends Mixin {} +export default class ActionHandler extends Mixin { + actions?: Record void>; + send(actionName: string, ...args: unknown[]): void; +} diff --git a/packages/@ember/-internals/runtime/lib/system/core_object.d.ts b/packages/@ember/-internals/runtime/lib/system/core_object.d.ts index 45d0d5b1344..d62d796b081 100644 --- a/packages/@ember/-internals/runtime/lib/system/core_object.d.ts +++ b/packages/@ember/-internals/runtime/lib/system/core_object.d.ts @@ -11,18 +11,39 @@ import { Owner } from '@ember/-internals/owner'; * Implementation is carefully chosen for the reasons described in * https://github.com/typed-ember/ember-typings/pull/29 */ -type EmberClassConstructor = new (owner: Owner) => T; +export type EmberClassConstructor = new (owner: Owner) => T; -type MergeArray = Arr extends [infer T, ...infer Rest] +export type MergeArray = Arr extends [infer T, ...infer Rest] ? T & MergeArray : unknown; // TODO: Is this correct? +/** @internal */ +export interface CoreObjectClass extends EmberClassConstructor { + /** @internal */ + extend( + this: EmberClassConstructor, + ...args: any[] + ): CoreObjectClass; + + /** @internal */ + reopen(...args: any[]): any; + + /** @internal */ + reopenClass(...args: any[]): any; + + /** @internal */ + create, Args extends Array>>( + this: Class, + ...args: Args + ): InstanceType & MergeArray; +} + export default class CoreObject { /** @internal */ static extend( this: Statics & EmberClassConstructor, ...args: any[] - ): Readonly & EmberClassConstructor; + ): Readonly & CoreObjectClass; /** @internal */ static reopen(...args: any[]): any; diff --git a/packages/@ember/-internals/views/lib/views/core_view.d.ts b/packages/@ember/-internals/views/lib/views/core_view.d.ts deleted file mode 100644 index a3c23eaa4e5..00000000000 --- a/packages/@ember/-internals/views/lib/views/core_view.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Renderer } from '@ember/-internals/glimmer'; -import { ActionHandler } from '@ember/-internals/runtime'; -import EmberObject from '@ember/object'; -import Evented from '@ember/object/evented'; - -interface CoreView extends Evented, ActionHandler {} -declare class CoreView extends EmberObject { - parentView: CoreView | null; - renderer: Renderer; -} - -export { CoreView as default }; diff --git a/packages/@ember/-internals/views/lib/views/core_view.js b/packages/@ember/-internals/views/lib/views/core_view.ts similarity index 75% rename from packages/@ember/-internals/views/lib/views/core_view.js rename to packages/@ember/-internals/views/lib/views/core_view.ts index 7ba99b29f21..8d5963c5ee1 100644 --- a/packages/@ember/-internals/views/lib/views/core_view.js +++ b/packages/@ember/-internals/views/lib/views/core_view.ts @@ -1,5 +1,7 @@ +import { View } from '@ember/-internals/glimmer/lib/renderer'; import { inject } from '@ember/-internals/metal'; import { ActionHandler, Evented, FrameworkObject } from '@ember/-internals/runtime'; +import { CoreObjectClass } from '@ember/-internals/runtime/lib/system/core_object'; import states from './states'; /** @@ -18,7 +20,10 @@ import states from './states'; @uses Ember.ActionHandler @private */ -const CoreView = FrameworkObject.extend(Evented, ActionHandler, { +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface CoreViewClass extends CoreObjectClass {} +interface CoreView extends FrameworkObject, Evented, ActionHandler, View {} +const CoreView = (FrameworkObject.extend(Evented, ActionHandler, { isView: true, _states: states, @@ -42,7 +47,7 @@ const CoreView = FrameworkObject.extend(Evented, ActionHandler, { */ parentView: null, - instrumentDetails(hash) { + instrumentDetails(hash: any) { hash.object = this.toString(); hash.containerKey = this._debugContainerKey; hash.view = this; @@ -57,7 +62,7 @@ const CoreView = FrameworkObject.extend(Evented, ActionHandler, { @param name {String} @private */ - trigger(name, ...args) { + trigger(name: string, ...args: any[]) { this._super(...arguments); let method = this[name]; if (typeof method === 'function') { @@ -65,10 +70,10 @@ const CoreView = FrameworkObject.extend(Evented, ActionHandler, { } }, - has(name) { + has(name: string) { return typeof this[name] === 'function' || this._super(name); }, -}); +}) as unknown) as CoreViewClass; CoreView.reopenClass({ isViewFactory: true, diff --git a/packages/@ember/component/type-tests/index.test.ts b/packages/@ember/component/type-tests/index.test.ts index 8538178abcc..31d080ac528 100644 --- a/packages/@ember/component/type-tests/index.test.ts +++ b/packages/@ember/component/type-tests/index.test.ts @@ -1,51 +1,16 @@ import Component from '@ember/component'; import { CoreView } from '@ember/-internals/views'; import { expectTypeOf } from 'expect-type'; -import { Owner } from '@ember/-internals/owner'; import { View } from '@ember/-internals/glimmer/lib/renderer'; -import { action } from '@ember/object'; -import { tracked } from '@ember/-internals/metal'; -// NOTE: This is invalid, but acceptable for type tests -let owner = {} as Owner; -let component = new Component(owner); +const MyComponent = Component.extend(); + +let component = MyComponent.create(); expectTypeOf(component).toMatchTypeOf(); expectTypeOf(component).toMatchTypeOf(); -class MyComponent extends Component { - tagName = 'em'; - classNames = ['my-class', 'my-other-class']; - classNameBindings = ['propertyA', 'propertyB']; - attributeBindings = ['href']; - - @tracked propertyA = 'from-a'; - - get propertyB(): string | void { - if (this.propertyA === 'from-a') { - return 'from-b'; - } - } - - @tracked href = 'https://tilde.io'; - - @action click(_event: Event): void { - // Clicked! - } -} -new MyComponent(owner); - -class BadComponent extends Component { - // @ts-expect-error invalid tag name - tagName = 1; - - // @ts-expect-error invalid classname - classNames = 'foo'; - - // @ts-expect-error invalid classNameBindings - classNameBindings = [1]; - - // @ts-expect-error invalid attributeBindings - attributeBindings = [true]; -} -new BadComponent(owner); +expectTypeOf(component.tagName).toEqualTypeOf(); +expectTypeOf(component.classNames).toEqualTypeOf(); +expectTypeOf(component.classNameBindings).toEqualTypeOf(); +expectTypeOf(component.attributeBindings).toEqualTypeOf(); diff --git a/packages/@ember/helper/type-tests/invoke-helper.test.ts b/packages/@ember/helper/type-tests/invoke-helper.test.ts index 65300ce5519..626a00bedd8 100644 --- a/packages/@ember/helper/type-tests/invoke-helper.test.ts +++ b/packages/@ember/helper/type-tests/invoke-helper.test.ts @@ -1,6 +1,5 @@ import Component from '@ember/-internals/glimmer/lib/component'; import { getValue } from '@ember/-internals/metal'; -import { Owner } from '@ember/-internals/owner'; import Helper from '@ember/component/helper'; import { invokeHelper } from '@ember/helper'; import { Cache } from '@glimmer/validator'; @@ -14,7 +13,7 @@ class PlusOne extends Helper { } } -export default class PlusOneComponent extends Component { +class PlusOneComponent extends Component { // Glint would help with this declare args: { number: number }; @@ -29,8 +28,6 @@ export default class PlusOneComponent extends Component { } } -// Good enough for tests! -let owner = {} as Owner; -let component = new PlusOneComponent(owner); +let component = PlusOneComponent.create(); expectTypeOf(component.plusOne).toEqualTypeOf>(); From befe02254bcd408b47f8603b1afd6221130d4189 Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Tue, 22 Feb 2022 15:58:20 -0800 Subject: [PATCH 4/6] Improve helper type generics, fix confusing variable name --- .../@ember/-internals/glimmer/lib/helper.ts | 22 +++++++++---------- .../@ember/-internals/glimmer/lib/resolver.ts | 5 ++++- .../helper/type-tests/invoke-helper.test.ts | 5 ++--- .../type-tests/set-helper-manager.test.ts | 9 ++++---- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/packages/@ember/-internals/glimmer/lib/helper.ts b/packages/@ember/-internals/glimmer/lib/helper.ts index e9932fac16d..537851153b3 100644 --- a/packages/@ember/-internals/glimmer/lib/helper.ts +++ b/packages/@ember/-internals/glimmer/lib/helper.ts @@ -13,13 +13,15 @@ import { consumeTag, createTag, dirtyTag } from '@glimmer/validator'; export const RECOMPUTE_TAG = symbol('RECOMPUTE_TAG'); -export type HelperFunction< - T = unknown, - P extends unknown[] = unknown[], - N extends Dict = Dict -> = (positional: P, named: N) => T; - -export type SimpleHelperFactory = Factory>; +export type HelperFunction> = ( + positional: P, + named: N +) => T; + +export type SimpleHelperFactory> = Factory< + SimpleHelper, + HelperFactory> +>; export type ClassHelperFactory = Factory>; export interface HelperFactory { @@ -34,11 +36,7 @@ export interface HelperInstance { const IS_CLASSIC_HELPER: unique symbol = Symbol('IS_CLASSIC_HELPER'); -export interface SimpleHelper< - T = unknown, - P extends unknown[] = unknown[], - N extends Dict = Dict -> { +export interface SimpleHelper> { compute: HelperFunction; } diff --git a/packages/@ember/-internals/glimmer/lib/resolver.ts b/packages/@ember/-internals/glimmer/lib/resolver.ts index ffd80103b21..cebdb9a84b6 100644 --- a/packages/@ember/-internals/glimmer/lib/resolver.ts +++ b/packages/@ember/-internals/glimmer/lib/resolver.ts @@ -174,7 +174,10 @@ export default class ResolverImpl implements RuntimeResolver, CompileTime } const factory = owner.factoryFor(`helper:${name}`) as - | Factory> + | Factory< + SimpleHelper>, + HelperFactory>> + > | Factory>; if (factory === undefined) { diff --git a/packages/@ember/helper/type-tests/invoke-helper.test.ts b/packages/@ember/helper/type-tests/invoke-helper.test.ts index 626a00bedd8..dc11df20609 100644 --- a/packages/@ember/helper/type-tests/invoke-helper.test.ts +++ b/packages/@ember/helper/type-tests/invoke-helper.test.ts @@ -14,12 +14,11 @@ class PlusOne extends Helper { } class PlusOneComponent extends Component { - // Glint would help with this - declare args: { number: number }; + declare number: number; plusOne = invokeHelper(this, PlusOne, () => { return { - positional: [this.args.number], + positional: [this.number], }; }); diff --git a/packages/@ember/helper/type-tests/set-helper-manager.test.ts b/packages/@ember/helper/type-tests/set-helper-manager.test.ts index 9ad7937b270..9b11f65029f 100644 --- a/packages/@ember/helper/type-tests/set-helper-manager.test.ts +++ b/packages/@ember/helper/type-tests/set-helper-manager.test.ts @@ -1,13 +1,14 @@ import { HelperFactory, HelperFunction, SimpleHelper } from '@ember/-internals/glimmer/lib/helper'; import { getDebugName } from '@ember/-internals/utils'; import { capabilities, setHelperManager } from '@ember/helper'; -import { Arguments, HelperManager } from '@glimmer/interfaces'; +import { Arguments, Dict, HelperManager } from '@glimmer/interfaces'; import { expectTypeOf } from 'expect-type'; -class Wrapper implements HelperFactory { +class Wrapper = Dict> + implements HelperFactory> { isHelperFactory: true = true; - constructor(public compute: HelperFunction) {} + constructor(public compute: HelperFunction) {} create() { // needs new instance or will leak containers @@ -41,7 +42,7 @@ export const SIMPLE_CLASSIC_HELPER_MANAGER = new SimpleClassicHelperManager(); expectTypeOf( setHelperManager(() => SIMPLE_CLASSIC_HELPER_MANAGER, Wrapper.prototype) -).toEqualTypeOf(); +).toEqualTypeOf>(); // @ts-expect-error invalid factory setHelperManager(1, Wrapper.prototype); From aacca3ba348e3e2d2d4fbb0243564108ebc0e97c Mon Sep 17 00:00:00 2001 From: Chris Krycho Date: Wed, 23 Feb 2022 16:53:39 -0700 Subject: [PATCH 5/6] Do not use defaults for helper type params --- packages/@ember/-internals/glimmer/lib/helper.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/@ember/-internals/glimmer/lib/helper.ts b/packages/@ember/-internals/glimmer/lib/helper.ts index 537851153b3..e2cbbc34a27 100644 --- a/packages/@ember/-internals/glimmer/lib/helper.ts +++ b/packages/@ember/-internals/glimmer/lib/helper.ts @@ -263,11 +263,9 @@ setHelperManager(() => SIMPLE_CLASSIC_HELPER_MANAGER, Wrapper.prototype); @public @since 1.13.0 */ -export function helper< - T = unknown, - P extends unknown[] = unknown[], - N extends Dict = Dict ->(helperFn: HelperFunction): HelperFactory> { +export function helper>( + helperFn: HelperFunction +): HelperFactory> { return new Wrapper(helperFn); } From e36de34391ab527615db9d7b4c13e449d50fb5cf Mon Sep 17 00:00:00 2001 From: Peter Wagenet Date: Wed, 23 Feb 2022 16:15:42 -0800 Subject: [PATCH 6/6] Resolve a few more issues in component types --- .../@ember/-internals/runtime/lib/mixins/action_handler.d.ts | 2 +- packages/@ember/-internals/runtime/lib/system/core_object.d.ts | 2 ++ packages/@ember/-internals/views/lib/views/core_view.ts | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/@ember/-internals/runtime/lib/mixins/action_handler.d.ts b/packages/@ember/-internals/runtime/lib/mixins/action_handler.d.ts index dd0e93c132d..a4aac4e466a 100644 --- a/packages/@ember/-internals/runtime/lib/mixins/action_handler.d.ts +++ b/packages/@ember/-internals/runtime/lib/mixins/action_handler.d.ts @@ -1,6 +1,6 @@ import { Mixin } from '@ember/-internals/metal'; export default class ActionHandler extends Mixin { - actions?: Record void>; + actions?: Record unknown>; send(actionName: string, ...args: unknown[]): void; } diff --git a/packages/@ember/-internals/runtime/lib/system/core_object.d.ts b/packages/@ember/-internals/runtime/lib/system/core_object.d.ts index d62d796b081..6b302f2b597 100644 --- a/packages/@ember/-internals/runtime/lib/system/core_object.d.ts +++ b/packages/@ember/-internals/runtime/lib/system/core_object.d.ts @@ -11,8 +11,10 @@ import { Owner } from '@ember/-internals/owner'; * Implementation is carefully chosen for the reasons described in * https://github.com/typed-ember/ember-typings/pull/29 */ +/** @internal */ export type EmberClassConstructor = new (owner: Owner) => T; +/** @internal */ export type MergeArray = Arr extends [infer T, ...infer Rest] ? T & MergeArray : unknown; // TODO: Is this correct? diff --git a/packages/@ember/-internals/views/lib/views/core_view.ts b/packages/@ember/-internals/views/lib/views/core_view.ts index 8d5963c5ee1..81754bdc670 100644 --- a/packages/@ember/-internals/views/lib/views/core_view.ts +++ b/packages/@ember/-internals/views/lib/views/core_view.ts @@ -47,7 +47,7 @@ const CoreView = (FrameworkObject.extend(Evented, ActionHandler, { */ parentView: null, - instrumentDetails(hash: any) { + instrumentDetails(hash: Record) { hash.object = this.toString(); hash.containerKey = this._debugContainerKey; hash.view = this;