Skip to content

Commit 3c6714e

Browse files
committed
Add new JS component API
1 parent 3fc329c commit 3c6714e

File tree

16 files changed

+1083
-149
lines changed

16 files changed

+1083
-149
lines changed

app/assets/javascripts/miq_global.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ if (! window.ManageIQ) {
8484
debounced: {}, // running debounces
8585
debounce_counter: 1,
8686
},
87-
redux: null // Redux API
87+
redux: null, // Redux API
88+
component: null // Component API
8889
};
8990
}

app/helpers/application_helper.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ module ApplicationHelper
1111
include NumberHelper
1212
include PlanningHelper
1313
include Title
14-
include ReactjsHelper
1514

1615
VALID_PERF_PARENTS = {
1716
"EmsCluster" => :ems_cluster,

app/helpers/reactjs_helper.rb

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React from 'react';
2+
import ReactDOM from 'react-dom';
3+
import { Provider } from 'react-redux';
4+
5+
import {
6+
ComponentProps,
7+
BasicComponentInstance,
8+
ManagedComponentInstance,
9+
ComponentBlueprint
10+
} from '../miq-component/component-typings'
11+
12+
// TODO(vs) importing from @types/react and @types/react-dom causes TypeScript
13+
// compiler to generate tons of errors, updating TypeScript related dependencies
14+
// should fix the problem
15+
16+
export default (
17+
reactElementCreator: (props: ComponentProps) => any /* ReactElement */,
18+
mapPropsToInteract: (props: ComponentProps) => any = () => undefined
19+
): ComponentBlueprint => {
20+
function render(props: ComponentProps, container: HTMLElement) {
21+
ReactDOM.render(
22+
<Provider store={ManageIQ.redux.store}>
23+
{reactElementCreator(props)}
24+
</Provider>,
25+
container
26+
);
27+
}
28+
29+
return {
30+
31+
create(props, mountTo) {
32+
render(props, mountTo);
33+
return { interact: mapPropsToInteract(props) };
34+
},
35+
36+
update(newProps, mountedTo) {
37+
render(newProps, mountedTo);
38+
},
39+
40+
destroy(instance, unmountFrom) {
41+
ReactDOM.unmountComponentAtNode(unmountFrom);
42+
}
43+
44+
};
45+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import React from 'react';
2+
import blueprint from './blueprint';
3+
4+
export default () => {
5+
// add common component definitions
6+
};
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/**
2+
* Props are component's inputs, containing data and/or callbacks.
3+
*
4+
* Props should be treated as immutable if possible, which greatly simplifies
5+
* the data flow in your application, making it easier to understand and change.
6+
*/
7+
export interface ComponentProps {
8+
[key: string]: any;
9+
}
10+
11+
/**
12+
* Represents a single instance of the underlying technology specific component.
13+
*/
14+
export interface BasicComponentInstance {
15+
16+
/**
17+
* Instance `id` is used to distinguish between individual component instances.
18+
*
19+
* Instance `id` must be unique across all instances of the given component.
20+
* Attempts to create new instances with an already taken `id` will throw an
21+
* error.
22+
*
23+
* If not defined or not a string, the value will be auto-generated as part of
24+
* the `newInstance` method.
25+
*/
26+
id?: string;
27+
28+
/**
29+
* Interface for component interaction.
30+
*
31+
* The value is entirely component specific and optional.
32+
*/
33+
interact?: any;
34+
35+
}
36+
37+
/**
38+
* Component instance created through (and managed by) the component API.
39+
*/
40+
export interface ManagedComponentInstance extends BasicComponentInstance {
41+
42+
/**
43+
* Current component props.
44+
*
45+
* The value is initialized based on the `initialProps` parameter. Every time
46+
* `instance.update` is called, the `props` value gets updated.
47+
*/
48+
props: ComponentProps;
49+
50+
/**
51+
* Update the component instance.
52+
*
53+
* This method creates new props by merging current props with `newProps` and
54+
* then calls the `blueprint.update` method; any properties present in current
55+
* props but missing in `newProps` will be retained.
56+
*
57+
* @param newProps New props to use.
58+
*/
59+
update(
60+
newProps: ComponentProps
61+
): void;
62+
63+
/**
64+
* Destroy and unmount the component instance.
65+
*
66+
* Once destroyed, attempts to access properties of the instance other than `id`
67+
* will throw an error.
68+
*/
69+
destroy(
70+
): void;
71+
72+
}
73+
74+
export type AnyComponentInstance = BasicComponentInstance | ManagedComponentInstance;
75+
76+
/**
77+
* Blueprint used to manage component instances.
78+
*/
79+
export interface ComponentBlueprint {
80+
81+
/**
82+
* Recipe for creating new instances.
83+
*
84+
* If not defined, the `newInstance` method will have no effect for the given
85+
* component.
86+
*
87+
* Returning a reference to an existing component instance will cause the
88+
* `newInstance` method to throw an error.
89+
*
90+
* @param props Props to use when creating the component instance.
91+
* @param mountTo DOM element to mount the component instance to.
92+
*
93+
* @returns Object that represents the actual component instance.
94+
*/
95+
create?(
96+
props: ComponentProps,
97+
mountTo?: HTMLElement
98+
): BasicComponentInstance;
99+
100+
/**
101+
* Recipe for updating instances.
102+
*
103+
* If not defined, the `instance.update` method will have no effect.
104+
*
105+
* @param newProps New props to use.
106+
* @param mountedTo DOM element to which the component instance is mounted.
107+
*/
108+
update?(
109+
newProps: ComponentProps,
110+
mountedTo?: HTMLElement
111+
): void;
112+
113+
/**
114+
* Recipe for destroying instances.
115+
*
116+
* Component instance that was previously mounted to a DOM element should be
117+
* unmounted as part of this method.
118+
*
119+
* _To prevent memory leaks, components that require DOM context (have their
120+
* instances mounted to a DOM element upon creation) must have their blueprint
121+
* implement both `create` and `destroy` methods. Avoid leaky blueprints!_
122+
*
123+
* @param instance Component instance to destroy.
124+
* @param unmountFrom DOM element from which to unmount the component instance.
125+
*/
126+
destroy?(
127+
instance: ManagedComponentInstance,
128+
unmountFrom?: HTMLElement
129+
): void;
130+
131+
}
132+
133+
/**
134+
* `ManageIQ.component` API.
135+
*/
136+
export interface ComponentApi {
137+
138+
/**
139+
* Define new component.
140+
*
141+
* Each component has a unique `name`. Attempts to define new component with
142+
* an already taken `name` will have no effect.
143+
*
144+
* @param name Component name.
145+
* @param blueprint Blueprint used to manage component instances.
146+
* @param instances Existing instances to associate with this component.
147+
*/
148+
define(
149+
name: string,
150+
blueprint: ComponentBlueprint,
151+
instances?: BasicComponentInstance[]
152+
): void;
153+
154+
/**
155+
* Create new component instance and mount it to the given DOM element as
156+
* necessary.
157+
*
158+
* Each component must be defined before creating its instances. Attempts to
159+
* instantiate a component that isn't already defined will have no effect.
160+
*
161+
* This method delegates to component's blueprint. If the blueprint doesn't
162+
* support creating new instances (`blueprint.create`), this method will have
163+
* no effect.
164+
*
165+
* The `initialProps` object will be proxied in order to allow intercepting
166+
* writes to its properties. Calling `props.foo = newValue` on the resulting
167+
* props will trigger an update of the component instance. With that in mind,
168+
* always treat props as immutable if possible, i.e. always prefer calling
169+
* `instance.update` over current props modification.
170+
*
171+
* Note that the `mountTo` parameter is optional. This allows for definition
172+
* of components that don't require DOM context.
173+
*
174+
* _Make sure to destroy component instances once they're no longer needed to
175+
* prevent memory leaks._
176+
*
177+
* @param name Component name.
178+
* @param initialProps Initial props to use when creating the instance.
179+
* @param mountTo DOM element to mount the component instance to.
180+
*
181+
* @returns New component instance or `undefined` if it couldn't be created.
182+
*/
183+
newInstance(
184+
name: string,
185+
initialProps: ComponentProps,
186+
mountTo?: HTMLElement
187+
): ManagedComponentInstance | undefined;
188+
189+
/**
190+
* Get existing component instance by its `id`.
191+
*
192+
* @param name Component name.
193+
* @param id Component instance `id`.
194+
*
195+
* @returns Matching component instance or `undefined` if not found.
196+
*/
197+
getInstance(
198+
name: string,
199+
id: string
200+
): AnyComponentInstance | undefined;
201+
202+
/**
203+
* Check if a component with the given `name` is defined.
204+
*
205+
* @param name Component name.
206+
*
207+
* @returns `true` if the component is defined, `false` otherwise.
208+
*/
209+
isDefined(
210+
name: string
211+
): boolean;
212+
213+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {
2+
define,
3+
newInstance,
4+
getInstance,
5+
isDefined,
6+
} from './registry';
7+
8+
export default () => {
9+
// initialize the namespace, don't wait for application startup
10+
ManageIQ.component = {
11+
define,
12+
newInstance,
13+
getInstance,
14+
isDefined
15+
};
16+
};

0 commit comments

Comments
 (0)