-
Notifications
You must be signed in to change notification settings - Fork 356
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add new JS component API and improve the Redux API
- Loading branch information
1 parent
d030901
commit 2aeaddb
Showing
13 changed files
with
504 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
import { Unsubscribe } from 'redux'; | ||
|
||
/** | ||
* Component definition, serving as a blueprint to create new instances, as well | ||
* as providing access to existing instances. | ||
* | ||
* Use the `create` method to create new instance and (optionally) mount it to | ||
* the given DOM element, depending on whether the component requires DOM context. | ||
* | ||
* Note that the `create` method itself is optional - components may expose their | ||
* existing instances without allowing further instantiation. | ||
*/ | ||
export interface ComponentDefinition { | ||
|
||
/** | ||
* Name that reflects the component type (must be unique). | ||
*/ | ||
name: string; | ||
|
||
/** | ||
* Create new instance and (optionally) mount it to the given DOM element. | ||
* | ||
* @param props Used to initialize the component. | ||
* @param mountTo DOM element to mount the component to. | ||
* @param callback Callback for providing component instance. | ||
* | ||
* @returns Function to destroy (and unmount) the component. | ||
*/ | ||
create?(props?: any, mountTo?: HTMLElement, callback?: ComponentInstanceCallback): Unsubscribe; | ||
|
||
/** | ||
* Get instance of this component, as indicated by its `id`. | ||
* | ||
* @param id Instance `id` to lookup. | ||
* | ||
* @returns Component instance matching the `id` or `undefined` if there is | ||
* no such instance. | ||
*/ | ||
getInstance(id: string): ComponentInstance | undefined; | ||
|
||
/** | ||
* Get all component instances. | ||
* | ||
* @returns Current snapshot of instances associated with this definition. | ||
*/ | ||
getAllInstances(): ComponentInstance[]; | ||
|
||
} | ||
|
||
/** | ||
* Component instance, identified (and lookup-able) by its `id` property. | ||
* | ||
* Each instance may provide an interface for interaction. For example, | ||
* a navigational component might expose `addMenuItem` or similar methods. | ||
* By convention, such interface should be represented by the `interact` | ||
* property. | ||
*/ | ||
export interface ComponentInstance { | ||
|
||
/** | ||
* Instance `id` is merely a way to distinguish individual component instances | ||
* for better lookup - it's not guaranteed to be unique or follow any specific | ||
* format. | ||
* | ||
* If not defined or not a string, its value will be auto-generated. | ||
*/ | ||
id: string; | ||
|
||
/** | ||
* Interface for component interaction (optional). | ||
* | ||
* The value is entirely component specific. | ||
*/ | ||
interact?: any; | ||
|
||
} | ||
|
||
/** | ||
* Blueprint used to create and destroy component instances. | ||
*/ | ||
export interface ComponentBlueprint { | ||
|
||
/** | ||
* Just like the `ComponentDefinition.create` method, minus the cleanup part. | ||
* | ||
* This method *must* return an object that satisfies the `ComponentInstance` | ||
* interface. Otherwise, the `ComponentDefinition.create` method will throw | ||
* an error that indicates a problem with the blueprint. | ||
* | ||
* Note that the returned instance will be sanitized (e.g. ensure proper `id` | ||
* value) prior to adding it to the instance collection. | ||
*/ | ||
create?(props?: any, mountTo?: HTMLElement): ComponentInstance; | ||
|
||
/** | ||
* Destroy `instance` and unmount it from the given DOM element as necessary. | ||
*/ | ||
destroy?(instance?: ComponentInstance, mountFrom?: HTMLElement): void; | ||
|
||
} | ||
|
||
/** | ||
* Callback used to provide the component instance. | ||
*/ | ||
export type ComponentInstanceCallback = (instance: ComponentInstance) => void; | ||
|
||
/** | ||
* `ManageIQ.component` API. | ||
*/ | ||
export interface ComponentApi { | ||
|
||
/** | ||
* Define new component. | ||
* | ||
* Each component has a unique `name`. Attempts to define new component with | ||
* an already taken `name` will have no effect. | ||
* | ||
* @param name Component name (must be unique). | ||
* @param blueprint Blueprint used to create and destroy component instances. | ||
* @param instances Existing instances to associate with this definition. | ||
* | ||
* @returns Definition of the new component or `undefined` if the provided | ||
* `name` is already taken. | ||
*/ | ||
define(name: string, blueprint: ComponentBlueprint, instances?: ComponentInstance[]): ComponentDefinition | undefined; | ||
|
||
/** | ||
* Get definition of a component, as indicated by its `name`. | ||
* | ||
* @param name Component `name` to lookup. | ||
* | ||
* @returns Component definition matching the `name` or `undefined` if there | ||
* is no such definition. | ||
*/ | ||
getDefinition(name: string): ComponentDefinition | undefined; | ||
|
||
/** | ||
* Get all component definitions. | ||
* | ||
* @returns Current snapshot of all component definitions. | ||
*/ | ||
getAllDefinitions(): ComponentDefinition[]; | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { IModule } from 'angular'; | ||
|
||
import { | ||
sanitizeInstance, instanceCreatorFactory, | ||
define, getDefinition, getAllDefinitions, | ||
clearRegistry | ||
} from './registry'; | ||
|
||
export default (app: IModule): void => { | ||
// allow unit-testing specific module exports | ||
if (window['jasmine']) { | ||
app.constant('_component_units', { | ||
sanitizeInstance, | ||
instanceCreatorFactory, | ||
define, | ||
getDefinition, | ||
getAllDefinitions, | ||
clearRegistry | ||
}); | ||
} | ||
|
||
// initialize the namespace (don't wait for application startup) | ||
ManageIQ.component = { | ||
define, | ||
getDefinition, | ||
getAllDefinitions | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import { Unsubscribe } from 'redux'; | ||
|
||
import { ComponentDefinition, ComponentInstance, ComponentBlueprint, ComponentInstanceCallback } from './component-typings'; | ||
|
||
const registry: Map<ComponentDefinition, Set<ComponentInstance>> = new Map(); | ||
|
||
/** | ||
* Sanitize `instance` properties, as defined by the `ComponentInstance` interface. | ||
*/ | ||
export function sanitizeInstance(instance: ComponentInstance, definition: ComponentDefinition): void { | ||
if (typeof instance.id !== 'string') { | ||
instance.id = `${definition.name}-${definition.getAllInstances().length}`; | ||
} | ||
} | ||
|
||
/** | ||
* Provides `ComponentDefinition.create` method implementations. | ||
*/ | ||
export function instanceCreatorFactory(definition: ComponentDefinition, blueprint: ComponentBlueprint): | ||
(props: any, mountTo: HTMLElement, callback: ComponentInstanceCallback) => Unsubscribe { | ||
return (props, mountTo, callback) => { | ||
// construct | ||
const newInstance = blueprint.create(props, mountTo); | ||
|
||
// validate | ||
if (!newInstance) { | ||
throw new Error(`blueprint.create returned falsy value during ${definition.name} instance creation`); | ||
} | ||
|
||
// sanitize | ||
sanitizeInstance(newInstance, definition); | ||
|
||
// add instance to the registry | ||
registry.get(definition).add(newInstance); | ||
|
||
// provide instance through callback | ||
typeof callback === 'function' && callback(newInstance); | ||
|
||
return () => { | ||
// destroy | ||
typeof blueprint.destroy === 'function' && blueprint.destroy(newInstance, mountTo); | ||
|
||
// remove instance from the registry | ||
registry.get(definition).delete(newInstance); | ||
}; | ||
} | ||
} | ||
|
||
/** | ||
* Implements `ComponentApi.define` method. | ||
*/ | ||
export function define(name: string, blueprint: ComponentBlueprint, instances?: ComponentInstance[]): ComponentDefinition | undefined { | ||
if (typeof name !== 'string' || !blueprint || getDefinition(name) !== undefined) { | ||
return; | ||
} | ||
|
||
const getAllInstances = () => Array.from(registry.get(newDefinition)); | ||
|
||
// create component definition | ||
const newDefinition: ComponentDefinition = { | ||
name, | ||
getInstance: (id: string) => getAllInstances().find(instance => instance.id === id), | ||
getAllInstances | ||
}; | ||
|
||
// add instance creator, if supported by the blueprint | ||
if (typeof blueprint.create === 'function') { | ||
newDefinition.create = instanceCreatorFactory(newDefinition, blueprint); | ||
} | ||
|
||
// add definition to the registry | ||
registry.set(newDefinition, new Set()); | ||
|
||
// add existing instances to the registry | ||
if (Array.isArray(instances)) { | ||
instances.forEach(instance => { | ||
if (instance) { | ||
sanitizeInstance(instance, newDefinition); | ||
registry.get(newDefinition).add(instance); | ||
} | ||
}); | ||
} | ||
|
||
return newDefinition; | ||
} | ||
|
||
/** | ||
* Implements `ComponentApi.getDefinition` method. | ||
*/ | ||
export function getDefinition(name: string): ComponentDefinition | undefined { | ||
return getAllDefinitions().find(definition => definition.name === name); | ||
} | ||
|
||
/** | ||
* Implements `ComponentApi.getAllDefinitions` method. | ||
*/ | ||
export function getAllDefinitions(): ComponentDefinition[] { | ||
return Array.from(registry.keys()); | ||
} | ||
|
||
/** | ||
* Remove all component data. | ||
* | ||
* Does not call `blueprint.destroy` for existing instances. | ||
* | ||
* *For testing purposes only.* | ||
*/ | ||
export function clearRegistry(): void { | ||
registry.clear(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,26 +1,28 @@ | ||
import { IModule } from 'angular'; | ||
|
||
import { configureNgReduxStore } from './store'; | ||
import { rootReducer, addReducer, clearReducers, applyReducerHash } from './reducer'; | ||
|
||
const app = ManageIQ.angular.app; | ||
|
||
const initialState = {}; | ||
|
||
// configure Angular application to use ng-redux | ||
configureNgReduxStore(app, initialState); | ||
export default (app: IModule): void => { | ||
// allow unit-testing specific module exports | ||
if (window['jasmine']) { | ||
app.constant('_redux_units', { | ||
rootReducer, | ||
addReducer, | ||
clearReducers, | ||
applyReducerHash | ||
}); | ||
} | ||
|
||
// allow unit-testing specific module exports | ||
if (window['jasmine']) { | ||
app.constant('_rootReducer', rootReducer); | ||
app.constant('_addReducer', addReducer); | ||
app.constant('_clearReducers', clearReducers); | ||
app.constant('_applyReducerHash', applyReducerHash); | ||
} | ||
// configure Angular application to use ng-redux | ||
configureNgReduxStore(app, {}); | ||
|
||
// initialize Redux namespace upon application startup | ||
app.run(['$ngRedux', ($ngRedux) => { | ||
ManageIQ.redux = { | ||
store: $ngRedux, | ||
addReducer, | ||
applyReducerHash | ||
}; | ||
}]); | ||
// initialize the namespace upon application startup | ||
app.run(['$ngRedux', ($ngRedux) => { | ||
ManageIQ.redux = { | ||
store: $ngRedux, | ||
addReducer, | ||
applyReducerHash | ||
}; | ||
}]); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.