mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Support editable props, state, and context values
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
.App {
|
||||
/* GitHub.com frontend fonts */
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
|
||||
sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.Header {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// @flow
|
||||
|
||||
import React, { createContext, Component, Fragment } from 'react';
|
||||
import styles from './EditableProps.css';
|
||||
|
||||
type StatefulFunctionProps = {| count: number |};
|
||||
|
||||
function StatefulFunction({ count }: StatefulFunctionProps) {
|
||||
return <li>Count: {count}</li>;
|
||||
}
|
||||
|
||||
const BoolContext = createContext(true);
|
||||
// $FlowFixMe Flow does not yet know about Context.displayName
|
||||
BoolContext.displayName = 'BoolContext';
|
||||
|
||||
type Props = {| name: string, toggle: boolean |};
|
||||
type State = {| cities: Array<string>, state: string |};
|
||||
|
||||
class StatefulClass extends Component<Props, State> {
|
||||
static contextType = BoolContext;
|
||||
|
||||
state: State = {
|
||||
cities: ['San Francisco', 'San Jose'],
|
||||
state: 'California',
|
||||
};
|
||||
|
||||
handleChange = ({ target }) =>
|
||||
this.setState({
|
||||
state: target.value,
|
||||
});
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<li>Name: {this.props.name}</li>
|
||||
<li>Toggle: {this.props.toggle ? 'true' : 'false'}</li>
|
||||
<li>
|
||||
State: <input value={this.state.state} onChange={this.handleChange} />
|
||||
</li>
|
||||
<li>Cities: {this.state.cities.join(', ')}</li>
|
||||
<li>Context: {this.context ? 'true' : 'false'}</li>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default function EditableProps() {
|
||||
return (
|
||||
<div className={styles.App}>
|
||||
<div className={styles.Header}>Editable props</div>
|
||||
<ul>
|
||||
<StatefulClass name="Brian" toggle={true} />
|
||||
<StatefulFunction count={1} />
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// @flow
|
||||
|
||||
import EditableProps from './EditableProps';
|
||||
|
||||
export default EditableProps;
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import { createElement } from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import EditableProps from './EditableProps';
|
||||
import ElementTypes from './ElementTypes';
|
||||
import InspectableElements from './InspectableElements';
|
||||
import ToDoList from './ToDoList';
|
||||
@@ -24,6 +25,7 @@ function mountTestApp() {
|
||||
mountHelper(ToDoList);
|
||||
mountHelper(InspectableElements);
|
||||
mountHelper(ElementTypes);
|
||||
mountHelper(EditableProps);
|
||||
}
|
||||
|
||||
function unmountTestApp() {
|
||||
|
||||
+44
-2
@@ -18,6 +18,18 @@ const debug = (methodName, ...args) => {
|
||||
}
|
||||
};
|
||||
|
||||
type InspectSelectParams = {|
|
||||
id: number,
|
||||
rendererID: number,
|
||||
|};
|
||||
|
||||
type SetInParams = {|
|
||||
id: number,
|
||||
path: Array<string | number>,
|
||||
rendererID: number,
|
||||
value: any,
|
||||
|};
|
||||
|
||||
export default class Agent extends EventEmitter {
|
||||
_bridge: Bridge = ((null: any): Bridge);
|
||||
_rendererInterfaces: { [key: RendererID]: RendererInterface } = {};
|
||||
@@ -27,6 +39,9 @@ export default class Agent extends EventEmitter {
|
||||
|
||||
bridge.addListener('highlightElementInDOM', this.highlightElementInDOM);
|
||||
bridge.addListener('inspectElement', this.inspectElement);
|
||||
bridge.addListener('overrideContext', this.overrideContext);
|
||||
bridge.addListener('overrideProps', this.overrideProps);
|
||||
bridge.addListener('overrideState', this.overrideState);
|
||||
bridge.addListener('selectElement', this.selectElement);
|
||||
bridge.addListener('startInspectingDOM', this.startInspectingDOM);
|
||||
bridge.addListener('stopInspectingDOM', this.stopInspectingDOM);
|
||||
@@ -80,7 +95,7 @@ export default class Agent extends EventEmitter {
|
||||
}
|
||||
};
|
||||
|
||||
inspectElement = ({ id, rendererID }: { id: number, rendererID: number }) => {
|
||||
inspectElement = ({ id, rendererID }: InspectSelectParams) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
@@ -89,7 +104,7 @@ export default class Agent extends EventEmitter {
|
||||
}
|
||||
};
|
||||
|
||||
selectElement = ({ id, rendererID }: { id: number, rendererID: number }) => {
|
||||
selectElement = ({ id, rendererID }: InspectSelectParams) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
@@ -98,6 +113,33 @@ export default class Agent extends EventEmitter {
|
||||
}
|
||||
};
|
||||
|
||||
overrideContext = ({ id, path, rendererID, value }: SetInParams) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
} else {
|
||||
renderer.setInContext(id, path, value);
|
||||
}
|
||||
};
|
||||
|
||||
overrideProps = ({ id, path, rendererID, value }: SetInParams) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
} else {
|
||||
renderer.setInProps(id, path, value);
|
||||
}
|
||||
};
|
||||
|
||||
overrideState = ({ id, path, rendererID, value }: SetInParams) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
} else {
|
||||
renderer.setInState(id, path, value);
|
||||
}
|
||||
};
|
||||
|
||||
setRendererInterface(
|
||||
rendererID: RendererID,
|
||||
rendererInterface: RendererInterface
|
||||
|
||||
+62
-7
@@ -2,7 +2,8 @@
|
||||
|
||||
import { gte } from 'semver';
|
||||
import {
|
||||
ElementTypeClassOrFunction,
|
||||
ElementTypeClass,
|
||||
ElementTypeFunction,
|
||||
ElementTypeContext,
|
||||
ElementTypeForwardRef,
|
||||
ElementTypeMemo,
|
||||
@@ -12,7 +13,7 @@ import {
|
||||
ElementTypeSuspense,
|
||||
} from 'src/devtools/types';
|
||||
import { getDisplayName, utfEncodeString } from '../utils';
|
||||
import { cleanForBridge } from './utils';
|
||||
import { cleanForBridge, copyWithSet, setInObject } from './utils';
|
||||
import {
|
||||
__DEBUG__,
|
||||
TREE_OPERATION_ADD,
|
||||
@@ -193,6 +194,8 @@ export function attach(
|
||||
DEPRECATED_PLACEHOLDER_SYMBOL_STRING,
|
||||
} = ReactSymbols;
|
||||
|
||||
const { overrideProps } = renderer;
|
||||
|
||||
const debug = (name: string, fiber: Fiber, parentFiber: ?Fiber): void => {
|
||||
if (__DEBUG__) {
|
||||
const fiberData = getDataForFiber(fiber);
|
||||
@@ -295,13 +298,19 @@ export function attach(
|
||||
|
||||
switch (tag) {
|
||||
case ClassComponent:
|
||||
case FunctionComponent:
|
||||
case IncompleteClassComponent:
|
||||
fiberData = {
|
||||
displayName: getDisplayName(resolvedType),
|
||||
key,
|
||||
type: ElementTypeClass,
|
||||
};
|
||||
break;
|
||||
case FunctionComponent:
|
||||
case IndeterminateComponent:
|
||||
fiberData = {
|
||||
displayName: getDisplayName(resolvedType),
|
||||
key,
|
||||
type: ElementTypeClassOrFunction,
|
||||
type: ElementTypeFunction,
|
||||
};
|
||||
break;
|
||||
case ForwardRef:
|
||||
@@ -1072,7 +1081,7 @@ export function attach(
|
||||
tag === IncompleteClassComponent ||
|
||||
tag === IndeterminateComponent
|
||||
) {
|
||||
if (stateNode && Object.keys(stateNode.context).length > 0) {
|
||||
if (stateNode && stateNode.context != null) {
|
||||
context = stateNode.context;
|
||||
}
|
||||
} else if (
|
||||
@@ -1135,7 +1144,7 @@ export function attach(
|
||||
id,
|
||||
|
||||
// Does the current renderer support editable props/state/hooks?
|
||||
canEditValues: false, // TODO
|
||||
canEditFunctionProps: typeof overrideProps === 'function',
|
||||
|
||||
// Inspectable properties.
|
||||
// TODO Review sanitization approach for the below inspectable values.
|
||||
@@ -1156,6 +1165,49 @@ export function attach(
|
||||
};
|
||||
}
|
||||
|
||||
function setInProps(id: number, path: Array<string | number>, value: any) {
|
||||
const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id));
|
||||
if (fiber !== null) {
|
||||
const instance = fiber.stateNode;
|
||||
if (instance === null) {
|
||||
if (typeof overrideProps === 'function') {
|
||||
overrideProps(fiber, path, value);
|
||||
}
|
||||
} else {
|
||||
fiber.pendingProps = copyWithSet(instance.props, path, value);
|
||||
instance.forceUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setInState(id: number, path: Array<string | number>, value: any) {
|
||||
const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id));
|
||||
if (fiber !== null) {
|
||||
const instance = fiber.stateNode;
|
||||
setInObject(instance.state, path, value);
|
||||
instance.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
function setInContext(id: number, path: Array<string | number>, value: any) {
|
||||
// To simplify hydration and display of primative context values (e.g. number, string)
|
||||
// the inspectElement() method wraps context in a {value: ...} object.
|
||||
// We need to remove the first part of the path (the "value") before continuing.
|
||||
path = path.slice(1);
|
||||
|
||||
const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id));
|
||||
if (fiber !== null) {
|
||||
const instance = fiber.stateNode;
|
||||
if (path.length === 0) {
|
||||
// Simple context value
|
||||
instance.context = value;
|
||||
} else {
|
||||
setInObject(instance.context, path, value);
|
||||
}
|
||||
instance.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
getFiberIDFromNative,
|
||||
getNativeFromReactElement,
|
||||
@@ -1164,7 +1216,10 @@ export function attach(
|
||||
inspectElement,
|
||||
selectElement,
|
||||
cleanup,
|
||||
walkTree,
|
||||
renderer,
|
||||
setInContext,
|
||||
setInProps,
|
||||
setInState,
|
||||
walkTree,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,6 +55,9 @@ export type RendererInterface = {
|
||||
inspectElement: (id: number) => InspectedElement | null,
|
||||
renderer: ReactRenderer | null,
|
||||
selectElement: (id: number) => void,
|
||||
setInProps: (id: number, path: Array<string | number>, value: any) => void,
|
||||
setInState: (id: number, path: Array<string | number>, value: any) => void,
|
||||
setInContext: (id: number, path: Array<string | number>, value: any) => void,
|
||||
walkTree: () => void,
|
||||
};
|
||||
|
||||
|
||||
@@ -16,3 +16,37 @@ export function cleanForBridge(data: Object | null): DehydratedData | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function copyWithSet(
|
||||
obj: Object | Array<any>,
|
||||
path: Array<string | number>,
|
||||
value: any,
|
||||
index: number = 0
|
||||
): Object | Array<any> {
|
||||
if (index >= path.length) {
|
||||
return value;
|
||||
}
|
||||
const key = path[index];
|
||||
const updated = Array.isArray(obj) ? obj.slice() : { ...obj };
|
||||
// $FlowFixMe number or string is fine here
|
||||
updated[key] = copyWithSet(obj[key], path, value, index + 1);
|
||||
return updated;
|
||||
}
|
||||
|
||||
export function setInObject(
|
||||
object: Object,
|
||||
path: Array<string | number>,
|
||||
value: any
|
||||
) {
|
||||
const last = path.pop();
|
||||
if (object != null) {
|
||||
const parent: Object = path.reduce(
|
||||
// $FlowFixMe
|
||||
(reduced, attribute) => reduced[attribute],
|
||||
object
|
||||
);
|
||||
if (parent) {
|
||||
parent[last] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-11
@@ -1,18 +1,19 @@
|
||||
// @flow
|
||||
|
||||
export const ElementTypeClassOrFunction = 1;
|
||||
export const ElementTypeContext = 2;
|
||||
export const ElementTypeForwardRef = 3;
|
||||
export const ElementTypeMemo = 4;
|
||||
export const ElementTypeOtherOrUnknown = 5;
|
||||
export const ElementTypeProfiler = 6;
|
||||
export const ElementTypeRoot = 7;
|
||||
export const ElementTypeSuspense = 8;
|
||||
export const ElementTypeClass = 1;
|
||||
export const ElementTypeFunction = 2;
|
||||
export const ElementTypeContext = 3;
|
||||
export const ElementTypeForwardRef = 4;
|
||||
export const ElementTypeMemo = 5;
|
||||
export const ElementTypeOtherOrUnknown = 6;
|
||||
export const ElementTypeProfiler = 7;
|
||||
export const ElementTypeRoot = 8;
|
||||
export const ElementTypeSuspense = 9;
|
||||
|
||||
// Different types of elements displayed in the Elements tree.
|
||||
// These types may be used to visually distinguish types,
|
||||
// or to enable/disable certain functionality.
|
||||
export type ElementType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
||||
export type ElementType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
||||
|
||||
// Each element on the frontend corresponds to a Fiber on the backend.
|
||||
// Some of its information (e.g. id, type, displayName) come from the backend.
|
||||
@@ -47,8 +48,8 @@ export type Owner = {|
|
||||
export type InspectedElement = {|
|
||||
id: number,
|
||||
|
||||
// Does the current renderer support editable props/state/hooks?
|
||||
canEditValues: boolean,
|
||||
// Does the current renderer support editable function props?
|
||||
canEditFunctionProps: boolean,
|
||||
|
||||
// Inspectable properties.
|
||||
context: Object | null,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import React, { Fragment, useCallback, useContext, useMemo } from 'react';
|
||||
import { ElementTypeClassOrFunction } from 'src/devtools/types';
|
||||
import { ElementTypeClass, ElementTypeFunction } from 'src/devtools/types';
|
||||
import { createRegExp } from './utils';
|
||||
import { TreeContext } from './TreeContext';
|
||||
|
||||
@@ -42,7 +42,8 @@ export default function ElementView({ index, style }: Props) {
|
||||
);
|
||||
|
||||
const isSelected = selectedElementID === id;
|
||||
const showDollarR = isSelected && type === ElementTypeClassOrFunction;
|
||||
const showDollarR =
|
||||
isSelected && (type === ElementTypeClass || type === ElementTypeFunction);
|
||||
|
||||
// TODO styles.SelectedElement is 100% width but it doesn't take horizontal overflow into account.
|
||||
|
||||
|
||||
@@ -7,16 +7,44 @@
|
||||
}
|
||||
|
||||
.Item {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.Name {
|
||||
color: var(--color-attribute-name);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.Name:after {
|
||||
content: ': ';
|
||||
color: var(--color-text-color);
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.Value {
|
||||
color: var(--color-attribute-value);
|
||||
}
|
||||
|
||||
.ValueInputLabel {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
.ValueInputLabel:focus-within {
|
||||
background-color: var(--color-button-background-focus);
|
||||
}
|
||||
|
||||
.ValueInput {
|
||||
background: none;
|
||||
border: 1px solid transparent;
|
||||
color: var(--color-attribute-editable-value);
|
||||
border-radius: 0.125rem;
|
||||
width: 100%;
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-normal);
|
||||
}
|
||||
.ValueInput:focus {
|
||||
background-color: var(--color-button-background-focus);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.None {
|
||||
color: var(--color-dimmer);
|
||||
font-style: italic;
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { getMetaValueLabel } from './utils';
|
||||
import { meta } from '../../hydration';
|
||||
import styles from './InspectedElementTree.css';
|
||||
|
||||
type OverrideValueFn = (path: Array<string | number>, value: any) => void;
|
||||
|
||||
type Props = {|
|
||||
data: Object | null,
|
||||
label: string,
|
||||
overrideValueFn?: ?OverrideValueFn,
|
||||
showWhenEmpty?: boolean,
|
||||
|};
|
||||
|
||||
export default function InspectedElementTree({
|
||||
data,
|
||||
label,
|
||||
overrideValueFn,
|
||||
showWhenEmpty = false,
|
||||
}: Props) {
|
||||
const isEmpty = data === null || Object.keys(data).length === 0;
|
||||
@@ -33,6 +37,8 @@ export default function InspectedElementTree({
|
||||
key={name}
|
||||
depth={1}
|
||||
name={name}
|
||||
overrideValueFn={overrideValueFn}
|
||||
path={[name]}
|
||||
value={(data: any)[name]}
|
||||
/>
|
||||
))}
|
||||
@@ -44,10 +50,18 @@ export default function InspectedElementTree({
|
||||
type KeyValueProps = {|
|
||||
depth: number,
|
||||
name: string,
|
||||
overrideValueFn?: ?OverrideValueFn,
|
||||
path?: Array<any>,
|
||||
value: any,
|
||||
|};
|
||||
|
||||
export function KeyValue({ depth, name, value }: KeyValueProps) {
|
||||
export function KeyValue({
|
||||
depth,
|
||||
name,
|
||||
overrideValueFn,
|
||||
path = [],
|
||||
value,
|
||||
}: KeyValueProps) {
|
||||
const dataType = typeof value;
|
||||
const isSimpleType =
|
||||
dataType === 'number' ||
|
||||
@@ -72,14 +86,24 @@ export function KeyValue({ depth, name, value }: KeyValueProps) {
|
||||
|
||||
children = (
|
||||
<div key="root" className={styles.Item} style={{ paddingLeft }}>
|
||||
<span className={styles.Name}>{name}</span>:{' '}
|
||||
<span className={styles.Value}>{displayValue}</span>
|
||||
<span className={styles.Name}>{name}</span>
|
||||
{typeof overrideValueFn === 'function' ? (
|
||||
<EditableValue
|
||||
dataType={dataType}
|
||||
overrideValueFn={overrideValueFn}
|
||||
path={path}
|
||||
value={value}
|
||||
/>
|
||||
) : (
|
||||
<span className={styles.Value}>{displayValue}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (value.hasOwnProperty(meta.type)) {
|
||||
// TODO Is this type even necessary? Can we just drop it?
|
||||
children = (
|
||||
<div key="root" className={styles.Item} style={{ paddingLeft }}>
|
||||
<span className={styles.Name}>{name}</span>:{' '}
|
||||
<span className={styles.Name}>{name}</span>
|
||||
<span className={styles.Value}>{getMetaValueLabel(value)}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -90,6 +114,8 @@ export function KeyValue({ depth, name, value }: KeyValueProps) {
|
||||
key={index}
|
||||
depth={depth + 1}
|
||||
name={index}
|
||||
overrideValueFn={overrideValueFn}
|
||||
path={path.concat(index)}
|
||||
value={value[index]}
|
||||
/>
|
||||
));
|
||||
@@ -99,13 +125,21 @@ export function KeyValue({ depth, name, value }: KeyValueProps) {
|
||||
className={styles.Item}
|
||||
style={{ paddingLeft }}
|
||||
>
|
||||
<span className={styles.Name}>{name}</span>: <span>Array</span>
|
||||
<span className={styles.Name}>{name}</span>
|
||||
<span>Array</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// $FlowFixMe
|
||||
children = Object.entries(value).map(([name, value]) => (
|
||||
<KeyValue key={name} depth={depth + 1} name={name} value={value} />
|
||||
<KeyValue
|
||||
key={name}
|
||||
depth={depth + 1}
|
||||
name={name}
|
||||
overrideValueFn={overrideValueFn}
|
||||
path={path.concat(name)}
|
||||
value={value}
|
||||
/>
|
||||
));
|
||||
children.unshift(
|
||||
<div
|
||||
@@ -113,7 +147,8 @@ export function KeyValue({ depth, name, value }: KeyValueProps) {
|
||||
className={styles.Item}
|
||||
style={{ paddingLeft }}
|
||||
>
|
||||
<span className={styles.Name}>{name}</span>: <span>Object</span>
|
||||
<span className={styles.Name}>{name}</span>
|
||||
<span>Object</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -121,3 +156,66 @@ export function KeyValue({ depth, name, value }: KeyValueProps) {
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
type EditableValueProps = {|
|
||||
dataType: string,
|
||||
overrideValueFn: OverrideValueFn,
|
||||
path: Array<string | number>,
|
||||
value: any,
|
||||
|};
|
||||
|
||||
function EditableValue({
|
||||
dataType,
|
||||
overrideValueFn,
|
||||
path,
|
||||
value,
|
||||
}: EditableValueProps) {
|
||||
const [editableValue, setEditableValue] = useState(value);
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ target }) => {
|
||||
if (dataType === 'boolean') {
|
||||
setEditableValue(target.checked);
|
||||
overrideValueFn(path, target.checked);
|
||||
} else if (dataType === 'number') {
|
||||
setEditableValue(parseFloat(target.value));
|
||||
} else {
|
||||
setEditableValue(target.value);
|
||||
}
|
||||
},
|
||||
[dataType, setEditableValue]
|
||||
);
|
||||
|
||||
const handleKeyPress = useCallback(
|
||||
({ key }) => {
|
||||
if (key === 'Enter') {
|
||||
overrideValueFn(path, editableValue);
|
||||
}
|
||||
},
|
||||
[path, editableValue, overrideValueFn]
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(event => event.stopPropagation(), []);
|
||||
|
||||
// Render different input types based on the dataType
|
||||
let type = 'text';
|
||||
if (dataType === 'boolean') {
|
||||
type = 'checkbox';
|
||||
} else if (dataType === 'number') {
|
||||
type = 'number';
|
||||
}
|
||||
|
||||
return (
|
||||
<label className={styles.ValueInputLabel}>
|
||||
<input
|
||||
checked={dataType === 'boolean' ? editableValue : undefined}
|
||||
className={styles.ValueInput}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyPress={handleKeyPress}
|
||||
type={type}
|
||||
value={dataType === 'boolean' ? undefined : editableValue || ''}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,9 +15,10 @@ import HooksTree from './HooksTree';
|
||||
import InspectedElementTree from './InspectedElementTree';
|
||||
import { hydrate } from 'src/hydration';
|
||||
import styles from './SelectedElement.css';
|
||||
import { ElementTypeClass, ElementTypeFunction } from '../types';
|
||||
|
||||
import type { InspectedElement } from '../types';
|
||||
import type { DehydratedData } from 'src/devtools/types';
|
||||
import type { DehydratedData, Element } from 'src/devtools/types';
|
||||
|
||||
export type Props = {||};
|
||||
|
||||
@@ -87,26 +88,74 @@ export default function SelectedElement(_: Props) {
|
||||
)}
|
||||
|
||||
{inspectedElement !== null && (
|
||||
<InspectedElementView inspectedElement={inspectedElement} />
|
||||
<InspectedElementView
|
||||
element={element}
|
||||
inspectedElement={inspectedElement}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type InspectedElementViewProps = {|
|
||||
element: Element,
|
||||
inspectedElement: InspectedElement,
|
||||
|};
|
||||
|
||||
function InspectedElementView({ inspectedElement }: InspectedElementViewProps) {
|
||||
let { context, hooks, owners, props, state } = inspectedElement;
|
||||
function InspectedElementView({
|
||||
element,
|
||||
inspectedElement,
|
||||
}: InspectedElementViewProps) {
|
||||
const { id, type } = element;
|
||||
const { context, hooks, owners, props, state } = inspectedElement;
|
||||
|
||||
const { ownerStack } = useContext(TreeContext);
|
||||
const bridge = useContext(BridgeContext);
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
let overrideContextFn = null;
|
||||
let overridePropsFn = null;
|
||||
let overrideStateFn = null;
|
||||
if (type === ElementTypeClass) {
|
||||
overrideContextFn = (path: Array<string | number>, value: any) => {
|
||||
const rendererID = store.getRendererIDForElement(id);
|
||||
bridge.send('overrideContext', { id, path, rendererID, value });
|
||||
};
|
||||
overridePropsFn = (path: Array<string | number>, value: any) => {
|
||||
const rendererID = store.getRendererIDForElement(id);
|
||||
bridge.send('overrideProps', { id, path, rendererID, value });
|
||||
};
|
||||
overrideStateFn = (path: Array<string | number>, value: any) => {
|
||||
const rendererID = store.getRendererIDForElement(id);
|
||||
bridge.send('overrideState', { id, path, rendererID, value });
|
||||
};
|
||||
} else if (type === ElementTypeFunction) {
|
||||
// TODO Only enable this if renderer.canEditFunctionProps is true!
|
||||
overridePropsFn = (path: Array<string | number>, value: any) => {
|
||||
const rendererID = store.getRendererIDForElement(id);
|
||||
bridge.send('overrideProps', { id, path, rendererID, value });
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.InspectedElement}>
|
||||
<InspectedElementTree label="props" data={props} showWhenEmpty />
|
||||
<InspectedElementTree label="state" data={state} />
|
||||
<InspectedElementTree
|
||||
label="props"
|
||||
data={props}
|
||||
overrideValueFn={overridePropsFn}
|
||||
showWhenEmpty
|
||||
/>
|
||||
<InspectedElementTree
|
||||
label="state"
|
||||
data={state}
|
||||
overrideValueFn={overrideStateFn}
|
||||
/>
|
||||
<HooksTree hooksTree={hooks} />
|
||||
<InspectedElementTree label="context" data={context} />
|
||||
<InspectedElementTree
|
||||
label="context"
|
||||
data={context}
|
||||
overrideValueFn={overrideContextFn}
|
||||
/>
|
||||
|
||||
{ownerStack.length === 0 && owners !== null && owners.length > 0 && (
|
||||
<div className={styles.Owners}>
|
||||
|
||||
@@ -118,6 +118,7 @@ function updateDisplayDensity(displayDensity: DisplayDensity): void {
|
||||
function updateThemeVariables(theme: Theme): void {
|
||||
updateStyleHelper(theme, 'color-attribute-name');
|
||||
updateStyleHelper(theme, 'color-attribute-value');
|
||||
updateStyleHelper(theme, 'color-attribute-editable-value');
|
||||
updateStyleHelper(theme, 'color-background');
|
||||
updateStyleHelper(theme, 'color-border');
|
||||
updateStyleHelper(theme, 'color-button-background');
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
/* Light theme */
|
||||
--light-color-attribute-name: #ef6632;
|
||||
--light-color-attribute-value: #1a1aa6;
|
||||
--light-color-attribute-editable-value: #1a1aa6;
|
||||
--light-color-background: #ffffff;
|
||||
--light-color-button-background: #ffffff;
|
||||
--light-color-button-background-focus: #ebf1fb;
|
||||
@@ -31,6 +32,7 @@
|
||||
/* Dark theme */
|
||||
--dark-color-attribute-name: #9d87d2;
|
||||
--dark-color-attribute-value: #cedae0;
|
||||
--dark-color-attribute-editable-value: yellow;
|
||||
--dark-color-background: #282c34;
|
||||
--dark-color-button-background: #282c34;
|
||||
--dark-color-button-background-focus: #3d424a;
|
||||
|
||||
Reference in New Issue
Block a user