├── .npmignore ├── .eslintignore ├── .uiharness.yml ├── .travis.yml ├── .gitignore ├── test └── test.js ├── ui-harness.js ├── src ├── index.js ├── specs │ ├── util.js │ ├── index.js │ ├── Text.spec.jsx │ ├── Date.spec.jsx │ ├── Function.spec.jsx │ ├── Primitive.spec.jsx │ ├── Complex.spec.jsx │ ├── ValueList.spec.jsx │ ├── Value.spec.jsx │ └── sections.js └── components │ ├── util.js │ ├── Ellipsis.jsx │ ├── Function.jsx │ ├── Date.jsx │ ├── Primitive.jsx │ ├── Text.jsx │ ├── ValueList.jsx │ ├── Value.jsx │ └── Complex.jsx ├── .babelrc ├── .eslintrc ├── CHANGELOG.md ├── gulpfile.js ├── LICENSE ├── README.md └── package.json /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/specs 2 | -------------------------------------------------------------------------------- /.uiharness.yml: -------------------------------------------------------------------------------- 1 | entry: ./src/specs 2 | port: 3030 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | lib 3 | node_modules 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { expect } from 'chai'; 3 | -------------------------------------------------------------------------------- /ui-harness.js: -------------------------------------------------------------------------------- 1 | require("ui-harness/server").start({ babel: 1 }); 2 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export Value from './components/Value'; 2 | export ValueList from './components/ValueList'; 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "plugins": ["transform-class-properties"] 4 | } 5 | -------------------------------------------------------------------------------- /src/specs/util.js: -------------------------------------------------------------------------------- 1 | export const lorem = require("js-util/lib/lorem").default; 2 | export const css = require("js-util/lib/react-css").default; 3 | export const PropTypes = require("react-schema").PropTypes; 4 | -------------------------------------------------------------------------------- /src/specs/index.js: -------------------------------------------------------------------------------- 1 | describe("react-object", function() { 2 | require("./Value.spec"); 3 | require("./ValueList.spec"); 4 | 5 | describe("internal", function() { 6 | require("./Primitive.spec"); 7 | require("./Complex.spec"); 8 | require("./Function.spec"); 9 | require("./Date.spec"); 10 | require("./Text.spec"); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/components/util.js: -------------------------------------------------------------------------------- 1 | import R from 'ramda'; 2 | 3 | export css from 'js-util/lib/react-css'; 4 | export { PropTypes } from 'react-schema'; 5 | 6 | 7 | 8 | export const isEmptyObjectOrArray = (value) => { 9 | if (R.is(Object, value)) { return R.keys(value).length === 0; } 10 | if (R.is(Array, value)) { return value.length === 0; } 11 | return false; 12 | }; 13 | -------------------------------------------------------------------------------- /src/components/Ellipsis.jsx: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0 */ 2 | 3 | import React from "react"; 4 | import Text from "./Text"; 5 | 6 | 7 | /** 8 | * An ellipsis element. 9 | */ 10 | export default class Ellipsis extends React.Component { 11 | render() { 12 | return ( 13 | ... 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "rules": { 5 | "no-multiple-empty-lines": 0, 6 | "no-param-reassign": 0, 7 | "padded-blocks": 0, 8 | "new-cap": [0, { "capIsNewExceptions": ["React.Component"] }], 9 | "react/jsx-closing-bracket-location": [2, { "location": "after-props" }], 10 | "template-curly-spacing": ["error", "always"], 11 | "react/jsx-curly-spacing": ["error", "always"], 12 | "no-confusing-arrow": ["error", {"allowParens": true}] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | 6 | ## [Unreleased] - YYYY-MM-DD 7 | #### Added 8 | #### Changed 9 | - Updated component `PropTypes` to use static properties. 10 | 11 | static propTypes = {}; 12 | static defaultProps = {}; 13 | 14 | - Updated to `react@0.14.6`. 15 | 16 | 17 | #### Deprecated 18 | #### Removed 19 | #### Fixed 20 | #### Security 21 | 22 | 23 | 24 | ## [2.0.0] - 2016-01-20 25 | #### Changed 26 | - Updated to `babel-6`. 27 | 28 | 29 | 30 | ## [0.0.1] - YYYY-MM-DD 31 | #### Added 32 | Initial creation and publish. 33 | -------------------------------------------------------------------------------- /src/specs/Text.spec.jsx: -------------------------------------------------------------------------------- 1 | "use strict" 2 | import React from "react"; 3 | import Text from "../components/Text"; 4 | import { COLORS } from "../components/Text"; 5 | import { inlineSection, italicSection, sizeSection } from "./sections"; 6 | 7 | 8 | 9 | 10 | describe("Text", function() { 11 | this.header(`## Text display with commonly used style properties.`) 12 | before(() => { 13 | this 14 | .align("top left") 15 | .load(My Text); 16 | }); 17 | 18 | inlineSection.call(this); 19 | italicSection.call(this); 20 | sizeSection.call(this); 21 | 22 | section("color", () => { 23 | Object.keys(COLORS).forEach(color => { 24 | it(`\`${ color }\``, () => this.props({ color })); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | var gulp = require("gulp"); 3 | var plumber = require('gulp-plumber'); 4 | var eslint = require("gulp-eslint"); 5 | var babel = require("gulp-babel"); 6 | 7 | var SOURCE_PATH = ["./src/**/*.js", "./src/**/*.jsx"]; 8 | 9 | 10 | gulp.task("build", function() { 11 | return gulp.src(SOURCE_PATH) 12 | .pipe(plumber()) // Keep task alive on build errors. 13 | .pipe(babel()) 14 | .pipe(gulp.dest("lib")); 15 | }); 16 | gulp.task("watch", function(callback) { gulp.watch(SOURCE_PATH, ["build"]) }); 17 | 18 | 19 | gulp.task("lint", function() { 20 | return gulp.src(["./src/components/**/*.js", "./src/components/**/*.jsx"]) 21 | .pipe(eslint()) 22 | .pipe(eslint.format()); 23 | }); 24 | 25 | 26 | 27 | gulp.task("default", ["build", "watch"]); 28 | -------------------------------------------------------------------------------- /src/specs/Date.spec.jsx: -------------------------------------------------------------------------------- 1 | "use strict" 2 | import React from "react"; 3 | import DateComponent from "../components/Date"; 4 | import { italicSection, sizeSection } from "./sections"; 5 | 6 | 7 | const getDate = (minsOffset = 0) => { 8 | return new Date(new Date().getTime() + minsOffset * 60000); 9 | }; 10 | 11 | 12 | describe("Date", function() { 13 | this.header(`## A Date object.`); 14 | before(() => { 15 | this 16 | .align("top left") 17 | .load( ); 18 | }); 19 | 20 | section("Value", () => { 21 | it("`now`", () => this.load( )); 22 | it("`5 minutes ago`", () => this.load( )); 23 | it("`5 minutes from now`", () => this.load( )); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/specs/Function.spec.jsx: -------------------------------------------------------------------------------- 1 | "use strict" 2 | import React from "react"; 3 | import Function from "../components/Function"; 4 | import { sizeSection } from "./sections"; 5 | 6 | const fn1 = function myFunc(param1, param2) {}; 7 | const fn2 = function myFunc() {}; 8 | const fn3 = function value() {}; 9 | 10 | 11 | 12 | describe("Function", function() { 13 | this.header(`## A Function with parameters details.`); 14 | 15 | before(() => { 16 | this 17 | .align("top left") 18 | .load( ); 19 | }); 20 | 21 | section("Values", () => { 22 | it("`named with 2 params`", () => this.load( )); 23 | it("`named with no params`", () => this.load( )); 24 | it("`named 'value'`", () => this.load( )); 25 | it("`unnamed with 1 param`", () => this.load( 0 }/> )); 26 | it("`unnamed with no params`", () => this.load( true }/> )); 27 | }); 28 | 29 | 30 | sizeSection.call(this); 31 | }); 32 | -------------------------------------------------------------------------------- /src/specs/Primitive.spec.jsx: -------------------------------------------------------------------------------- 1 | "use strict" 2 | import React from "react"; 3 | import Primitive from "../components/Primitive"; 4 | import { inlineSection, italicSection, sizeSection } from "./sections"; 5 | 6 | 7 | describe("Primitive", function() { 8 | this.header(`## A simple primitive value.`); 9 | before(() => { 10 | this 11 | .align("top left") 12 | .load( ); 13 | }); 14 | 15 | section("load", () => { 16 | it("`string`", () => this.load( )); 17 | it("`number`", () => this.load( )); 18 | it("`true`", () => this.load( )); 19 | it("`false`", () => this.load( )); 20 | it("`null`", () => this.load( )); 21 | it("`undefined`", () => this.load( )); 22 | it("`object` (error)", () => this.load( )); 23 | }); 24 | 25 | italicSection.call(this); 26 | sizeSection.call(this); 27 | }); 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Phil Cockfield (https://github.com/philcockfield) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/Function.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { PropTypes } from "./util"; 3 | import Text from "./Text"; 4 | import { functionParameters } from "js-util"; 5 | 6 | 7 | /** 8 | * A Function with parameters details. 9 | */ 10 | export default class Function extends React.Component { 11 | static propTypes = { 12 | value: PropTypes.func.isRequired, 13 | size: Text.propTypes.size 14 | }; 15 | static defaultProps = { 16 | size: Text.defaultProps.size 17 | }; 18 | 19 | render() { 20 | const { value, size } = this.props; 21 | const textProps = { italic: true, size }; 22 | let { name } = value; 23 | name = name === "value" ? "function" : name; 24 | const elName = name && { name }; 25 | const params = []; 26 | const paramNames = functionParameters(value); 27 | paramNames.forEach((paramName, i) => { 28 | const isLast = i === paramNames.length - 1; 29 | params.push( { paramName } ); 30 | if (!isLast) { 31 | params.push( , ); 32 | } 33 | }); 34 | 35 | return ( 36 | 37 | { elName } 38 | ( 39 | { params } 40 | ) 41 | 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/specs/Complex.spec.jsx: -------------------------------------------------------------------------------- 1 | "use strict" 2 | import React from "react"; 3 | import Complex from "../components/Complex"; 4 | import { 5 | inlineSection, 6 | italicSection, 7 | sizeSection, 8 | objectValueSection, 9 | arrayValueSection, 10 | collapsedTotalSection 11 | } from "./sections"; 12 | 13 | 14 | describe("Complex", function() { 15 | this.header(`## A complex value (object, array).`); 16 | before(() => { 17 | let value = { 18 | foo: 123, 19 | bar: "hello", 20 | baz: { number: -1 }, 21 | // fn: () => true 22 | }; 23 | 24 | // value = [] 25 | // value = [1,2,3]; 26 | // value.foo = "hello"; 27 | 28 | this 29 | .align("top left") 30 | .scroll(true) 31 | .load( 32 | 35 | ); 36 | }); 37 | 38 | section("label", () => { 39 | it("`true`", () => this.props({ label: true })); 40 | it("`false`", () => this.props({ label: false })); 41 | it("`'My Label'`", () => this.props({ label: "My Label" })); 42 | }); 43 | 44 | section("isExpanded", () => { 45 | it("`true`", () => this.props({ isExpanded: true })); 46 | it("`false`", () => this.props({ isExpanded: false })); 47 | }); 48 | 49 | objectValueSection.call(this); 50 | arrayValueSection.call(this); 51 | 52 | italicSection.call(this); 53 | sizeSection.call(this); 54 | collapsedTotalSection.call(this); 55 | }); 56 | -------------------------------------------------------------------------------- /src/components/Date.jsx: -------------------------------------------------------------------------------- 1 | import R from "ramda"; 2 | import React from "react"; 3 | import { PropTypes } from "./util"; 4 | import Text from "./Text"; 5 | 6 | 7 | const doubleDigits = (number) => { 8 | return R.takeLast(2, `0${ number }`); 9 | }; 10 | 11 | 12 | /** 13 | * A Date object. 14 | */ 15 | export default class DateComponent extends React.Component { 16 | static propTypes = { 17 | value: PropTypes.instanceOf(Date).isRequired, 18 | italic: Text.propTypes.italic, 19 | size: Text.propTypes.size 20 | }; 21 | static defaultProps = { 22 | italic: true, 23 | size: Text.defaultProps.size 24 | }; 25 | 26 | render() { 27 | const { value, italic, size } = this.props; 28 | const textProps = { italic, size }; 29 | 30 | // Date. 31 | const year = value.getUTCFullYear(); 32 | const month = doubleDigits(value.getUTCMonth() + 1); 33 | const day = doubleDigits(value.getUTCDate()); 34 | 35 | // Time. 36 | let hours = value.getHours(); 37 | const minutes = doubleDigits(value.getMinutes()); 38 | const seconds = doubleDigits(value.getSeconds()); 39 | const period = hours > 11 ? "pm" : "am"; 40 | if (hours > 12) { 41 | hours = hours - 12; 42 | } 43 | 44 | return ( 45 | 46 | 47 | { `${ year }-${ month }-${ day },` } 48 | { `${ hours }:${ minutes }:${ seconds }${ period }` } 49 | 50 | 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-object 2 | [![Build Status](https://travis-ci.org/philcockfield/react-object.svg?branch=master)](https://travis-ci.org/philcockfield/react-object) 3 | 4 | Rich visual representation and editor of javascript objects and values. 5 | 6 | ![Example](https://cloud.githubusercontent.com/assets/185555/10372121/4667a0b2-6d9b-11e5-952f-7bc119b1b040.png) 7 | 8 | 9 | #### Features 10 | - Color highlighted pretty-printing of objects and primitive values. 11 | - Expandable to drill into object and array hierarchy. 12 | - Abbreviates large objects/arrays to prevent render slow-down. 13 | - Rich date notation. 14 | - Rich function notation (with parameter names). 15 | 16 | 17 | ## Getting Started 18 | 19 | npm install react-object 20 | 21 | Render a complex object: 22 | 23 | ```js 24 | import { Value } from "react-object"; 25 | 26 | const obj = { foo: 123, bar: { baz: "hello" }}; 27 | 28 | 31 | ``` 32 | 33 | Will yield: 34 | 35 | ![Value](https://cloud.githubusercontent.com/assets/185555/10420913/2ec14578-70fa-11e5-92be-3f38a07e8e27.png) 36 | 37 | 38 | 39 | ## TODO 40 | - editable 41 | 42 | 43 | ## Explore the API in the [UIHarness](http://uiharness.com/) 44 | git clone https://github.com/philcockfield/react-object.git 45 | cd react-object 46 | npm install 47 | npm start 48 | 49 | ![ui-harness](https://cloud.githubusercontent.com/assets/185555/10324272/3254e10c-6c3d-11e5-9ce6-6f9598461313.png) 50 | 51 | 52 | --- 53 | ### License: MIT 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-object", 3 | "version": "2.1.1", 4 | "description": "Rich visual representation and editor of javascript objects and values.", 5 | "main": "./lib/index.js", 6 | "scripts": { 7 | "start": "node ./node_modules/ui-harness/start", 8 | "test": "./node_modules/mocha/bin/mocha --recursive --compilers js:babel-register", 9 | "tdd": "./node_modules/mocha/bin/mocha --recursive --compilers js:babel-register --watch", 10 | "lint": "./node_modules/eslint/bin/eslint.js ./src", 11 | "build": "./node_modules/babel-cli/bin/babel.js src --out-dir lib --source-maps", 12 | "build:watch": "npm run build -- --watch", 13 | "prepublish": "npm test && npm run lint && npm run build" 14 | }, 15 | "dependencies": { 16 | "radium": "^0.18.1", 17 | "ramda": "^0.22.1", 18 | "react-atoms": "^2.0.10" 19 | }, 20 | "devDependencies": { 21 | "babel-cli": "^6.10.1", 22 | "chai": "^3.5.0", 23 | "js-babel-dev": "^6.0.6", 24 | "mocha": "^2.5.3", 25 | "ui-harness": "^3.9.5" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/philcockfield/react-object" 30 | }, 31 | "keywords": [ 32 | "react-component", 33 | "pretty-print", 34 | "log", 35 | "debug" 36 | ], 37 | "author": { 38 | "name": "Phil Cockfield", 39 | "email": "phil@cockfield.net", 40 | "url": "https://github.com/philcockfield" 41 | }, 42 | "homepage": "https://github.com/philcockfield/react-object", 43 | "license": "MIT" 44 | } 45 | -------------------------------------------------------------------------------- /src/components/Primitive.jsx: -------------------------------------------------------------------------------- 1 | import R from "ramda"; 2 | import React from "react"; 3 | import { PropTypes } from "./util"; 4 | import Text from "./Text"; 5 | 6 | const COLORS = { 7 | "Null": "lightGrey", 8 | "Undefined": "lightGrey", 9 | "String": "red", 10 | "Number": "blue", 11 | "Boolean": "blue" 12 | }; 13 | 14 | /** 15 | * Determines whether the given value is primitive. 16 | */ 17 | export const isPrimitive = (value) => { 18 | if (R.isNil(value)) { 19 | return true; 20 | } else { 21 | const isType = (type) => R.is(type, value); 22 | return R.any(isType, [String, Number, Boolean]); 23 | } 24 | }; 25 | 26 | 27 | 28 | /** 29 | * A primitive/simple value. 30 | */ 31 | export default class Primitive extends React.Component { 32 | static propTypes = { 33 | value: PropTypes.oneOfType([ 34 | PropTypes.string, 35 | PropTypes.number, 36 | PropTypes.bool 37 | ]), 38 | italic: Text.propTypes.italic, 39 | size: Text.propTypes.size 40 | }; 41 | static defaultProps = { 42 | italic: Text.defaultProps.italic, 43 | size: Text.defaultProps.size 44 | }; 45 | 46 | render() { 47 | let { value } = this.props; 48 | const type = R.type(value); 49 | switch (type) { 50 | case "Undefined": value = ""; break; 51 | case "Null": value = ""; break; 52 | case "Boolean": value = value.toString(); break; 53 | case "String": value = `“${ value }”`; break; 54 | } 55 | return ( 56 | { value } 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/specs/ValueList.spec.jsx: -------------------------------------------------------------------------------- 1 | "use strict" 2 | import React from "react"; 3 | import ValueList from "../components/ValueList"; 4 | import { ELLIPSIS } from "../components/ValueList"; 5 | import { 6 | inlineSection, 7 | italicSection, 8 | sizeSection, 9 | collapsedTotalSection 10 | } from "./sections"; 11 | 12 | const DEFAULT_ITEMS = [ 13 | { label: "one", value: 1 }, 14 | { 15 | label: "two", 16 | value: { 17 | foo: ["foo", 2, new Date()], 18 | bar: () => true 19 | } 20 | }, 21 | { label: "three", value: "toru" }, 22 | { label: "four" }, 23 | { label: "five", value: { simple: 1 } }, 24 | { label: "six", value: [] }, 25 | { label: "seven", value: {} }, 26 | ]; 27 | 28 | 29 | describe("ValueList", function() { 30 | this.header(`## A list of values.`); 31 | before(() => { 32 | this 33 | .align("top left") 34 | .load( ); 35 | }); 36 | 37 | section("items", () => { 38 | it("`default`", () => this.load( )); 39 | it("`ellipsis` penultimate", () => { 40 | const items = [ 41 | { label: "one", value: 1 }, 42 | { label: "two", value: 2 }, 43 | ELLIPSIS, 44 | { label: "three", value: 3 } 45 | ] 46 | this.load( ) 47 | }); 48 | it("`ellipsis` last", () => { 49 | const items = [ 50 | { label: "one", value: 1 }, 51 | { label: "two", value: 2 }, 52 | ELLIPSIS 53 | ] 54 | this.load( ) 55 | }); 56 | }); 57 | 58 | inlineSection.call(this); 59 | italicSection.call(this); 60 | sizeSection.call(this); 61 | collapsedTotalSection.call(this); 62 | }); 63 | -------------------------------------------------------------------------------- /src/specs/Value.spec.jsx: -------------------------------------------------------------------------------- 1 | "use strict" 2 | import React from "react"; 3 | import Value from "../components/Value"; 4 | import { lorem } from "./util"; 5 | import { 6 | italicSection, 7 | sizeSection, 8 | objectValueSection, 9 | arrayValueSection, 10 | functionValueSection, 11 | collapsedTotalSection 12 | } from "./sections"; 13 | 14 | 15 | describe("Value", function() { 16 | this.header("## A single value of any type."); 17 | before(() => { 18 | const value = { foo: 123, bar: { baz: "hello" }}; 19 | this 20 | .align("top left") 21 | .scroll(true) 22 | .load( 25 | ); 26 | }); 27 | 28 | 29 | section("label", () => { 30 | it("`null`", () => this.props({ label: null })); 31 | it("`'foo'`", () => this.props({ label: "foo" })); 32 | }); 33 | 34 | 35 | section("showTwisty", () => { 36 | it("`true`", () => this.props({ showTwisty:true })); 37 | it("`false`", () => this.props({ showTwisty:false })); 38 | it("`undefined` (auto)", () => this.props({ showTwisty:undefined })); 39 | }); 40 | 41 | 42 | section("Primitive", () => { 43 | it("`string` short", () => this.props({ value: "My String" })); 44 | it("`string` long", () => this.props({ value: lorem() })); 45 | it("`number: 123456`", () => this.props({ value: 123456 })); 46 | it("`number: -1`", () => this.props({ value: -1 })); 47 | it("`bool: true`", () => this.props({ value: true })); 48 | it("`bool: false`", () => this.props({ value: false })); 49 | it("`null`", () => this.props({ value: null })); 50 | it("`undefined`", () => this.props({ value: undefined })); 51 | }); 52 | 53 | objectValueSection.call(this); 54 | arrayValueSection.call(this); 55 | 56 | section("Date", () => { 57 | it("`date: now`", () => this.props({ value: new Date() })); 58 | }); 59 | 60 | functionValueSection.call(this); 61 | italicSection.call(this); 62 | sizeSection.call(this); 63 | collapsedTotalSection.call(this); 64 | }); 65 | -------------------------------------------------------------------------------- /src/components/Text.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Radium from "radium"; 3 | import { css, PropTypes } from "./util"; 4 | 5 | export const FONT_MONO = "Menlo, monospace"; 6 | export const COLORS = { 7 | black: "#000", 8 | darkGrey: "#303942", 9 | grey: "#999999", 10 | lightGrey: "#C8C8C8", 11 | red: "#C9214C", 12 | brown: "#9B4500", 13 | green: "#007500", 14 | blue: "#180AA9", 15 | purple: "#AC0093" 16 | }; 17 | 18 | 19 | /** 20 | * Text display with commonly used style properties. 21 | */ 22 | class Text extends React.Component { 23 | static propTypes = { 24 | inline: PropTypes.bool, 25 | italic: PropTypes.bool, 26 | size: PropTypes.numberOrString, 27 | color: PropTypes.oneOf(Object.keys(COLORS)), 28 | marginLeft: PropTypes.numberOrString, 29 | marginRight: PropTypes.numberOrString, 30 | letterSpacing: PropTypes.numberOrString, 31 | lineHeight: PropTypes.numberOrString, 32 | onClick: PropTypes.func 33 | }; 34 | static defaultProps = { 35 | inline: true, 36 | italic: false, 37 | size: 12, 38 | color: "darkGrey", 39 | marginLeft: 0, 40 | marginRight: 0, 41 | letterSpacing: "normal", 42 | lineHeight: "1.4em" 43 | }; 44 | 45 | styles() { 46 | return css({ 47 | base: { 48 | fontFamily: FONT_MONO, 49 | fontSize: this.props.size, 50 | fontWeight: "normal", 51 | fontStyle: this.props.italic === true ? "italic" : "normal", 52 | lineHeight: this.props.lineHeight, 53 | color: COLORS[this.props.color], 54 | marginLeft: this.props.marginLeft, 55 | marginRight: this.props.marginRight, 56 | cursor: this.props.onClick ? "pointer" : null, 57 | letterSpacing: this.props.letterSpacing 58 | } 59 | }); 60 | } 61 | 62 | handleClick(e) { 63 | const { onClick } = this.props; 64 | if (onClick) { 65 | onClick({ args: e }); 66 | } 67 | } 68 | 69 | render() { 70 | const styles = this.styles(); 71 | const handleClick = this.handleClick.bind(this); 72 | return this.props.inline 73 | ? { this.props.children } 74 | :
{ this.props.children }
; 75 | } 76 | } 77 | 78 | export default Radium(Text); 79 | -------------------------------------------------------------------------------- /src/components/ValueList.jsx: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0 */ 2 | import React from "react"; 3 | import Radium from "radium"; 4 | import Ellipsis from "./Ellipsis"; 5 | import { css, PropTypes } from "./util"; 6 | import Text from "./Text"; 7 | let Value; // NB: Lazily required to prevent circular-reference. 8 | 9 | export const ELLIPSIS = Symbol("ellipsis"); 10 | 11 | 12 | /** 13 | * A list of 's. 14 | */ 15 | class ValueList extends React.Component { 16 | static propTypes = { 17 | inline: PropTypes.bool, 18 | italic: Text.propTypes.italic, 19 | size: Text.propTypes.size, 20 | items: PropTypes.array, 21 | level: PropTypes.number, 22 | collapsedTotal: PropTypes.number // The number of {object} properties to show when not expanded. 23 | }; 24 | static defaultProps = { 25 | inline: false, 26 | italic: Text.defaultProps.italic, 27 | size: Text.defaultProps.size, 28 | items: [], 29 | level: 0 30 | }; 31 | 32 | constructor(props) { 33 | super(props); 34 | if (!Value) { 35 | Value = require("./Value").default; // NB: Lazily required to prevent circular-reference. 36 | } 37 | } 38 | 39 | styles() { 40 | const { inline } = this.props; 41 | return css({ 42 | base: { 43 | display: inline ? "inline-block" : null, 44 | listStyleType: "none", 45 | margin: 0, 46 | paddingTop: 0, 47 | paddingRight: 0, 48 | paddingBottom: 0, 49 | paddingLeft: inline ? 0 : "0.2em", 50 | fontSize: this.props.size, 51 | fontStyle: this.props.italic ? "italic" : "normal" 52 | }, 53 | li: { 54 | display: this.props.inline ? "inline" : null 55 | } 56 | }); 57 | } 58 | 59 | render() { 60 | const styles = this.styles(); 61 | const { inline, size, italic } = this.props; 62 | const textStyles = { italic, size }; 63 | const total = this.props.items.length; 64 | const items = []; 65 | this.props.items.forEach((item, i) => { 66 | // Insert the . 67 | const isLast = i === total - 1; 68 | const isEllipsis = item === ELLIPSIS; 69 | const isNextEllipsis = this.props.items[i + 1] === ELLIPSIS; 70 | const el = (isEllipsis) 71 | ? 75 | : ; 84 | items.push(
  • { el }
  • ); 85 | 86 | // Inert dividing comma. 87 | if (inline && !isLast && !isEllipsis && !isNextEllipsis) { 88 | items.push( 89 | , 94 | ); 95 | } 96 | }); 97 | 98 | return
      { items }
    ; 99 | } 100 | } 101 | 102 | export default Radium(ValueList); 103 | -------------------------------------------------------------------------------- /src/specs/sections.js: -------------------------------------------------------------------------------- 1 | import R from "ramda"; 2 | 3 | 4 | const array = (total) => R.repeat(null, total).map((item, i) => i); 5 | 6 | 7 | export const inlineSection = function() { 8 | section("inline", () => { 9 | it("`inline: true`", () => this.props({ inline: true })); 10 | it("`inline: false`", () => this.props({ inline: false })); 11 | }); 12 | }; 13 | 14 | 15 | export const italicSection = function() { 16 | section("italic", () => { 17 | it("`italic: true`", () => this.props({ italic: true })); 18 | it("`italic: false`", () => this.props({ italic: false })); 19 | }); 20 | }; 21 | 22 | 23 | export const sizeSection = function() { 24 | section("size", () => { 25 | it("`size: 12 (default)`", () => this.props({ size: 12 })); 26 | it("`size: 14`", () => this.props({ size: 14 })); 27 | it("`size: 16`", () => this.props({ size: 16 })); 28 | it("`size: 22`", () => this.props({ size: 22 })); 29 | }); 30 | }; 31 | 32 | 33 | 34 | export const objectValueSection = function () { 35 | class MyClass { 36 | constructor(props) { 37 | this.foo = "abc"; 38 | } 39 | bar() { return "hello"; } 40 | }; 41 | MyClass.number = 123; 42 | 43 | section("Object", () => { 44 | it("`{}`", () => this.props({ value: {}})); 45 | it("`{ foo, bar }`", () => this.props({ value: { foo: 123, bar: "hello" }})); 46 | it("`{ foo, bar ... }`", () => this.props({ value: { foo: 123, bar: "hello", baz: { number: -1 }}})); 47 | it("`{ 100 }`", () => { 48 | const value = {}; 49 | array(100).forEach(i => value[`prop${i}`] = `value-${ i }`); 50 | this.props({ value }); 51 | }); 52 | it("`MyClass{}`", () => this.props({ value: new MyClass() })); 53 | it("`complex`", () => { 54 | 55 | const value = { 56 | "1": 1, 57 | yes: true, 58 | no: false, 59 | text: "hello", 60 | "multi-part name": "value", 61 | number: -9999, 62 | date: new Date(), 63 | fn: (p1, p2) => true, 64 | obj: { 65 | text: "foo", 66 | number: 123, 67 | child: { 68 | array: array(50) 69 | } 70 | }, 71 | array: [1, "two", { three:3 }] 72 | }; 73 | 74 | this.props({ value }); 75 | }); 76 | }); 77 | }; 78 | 79 | 80 | export const arrayValueSection = function () { 81 | section("Array", () => { 82 | it("`[]`", () => this.props({ value: [] })); 83 | it("`[1, 2]`", () => this.props({ value: [1, 2] })); 84 | it("`['one', 2, { label:'three' }]`", () => { 85 | const value = ["one", 2, { label: "three" }]; 86 | value.myProp = "foo"; 87 | this.props({ value }); 88 | }); 89 | it("`[0..5]`", () => this.props({ value: array(5) })); 90 | it("`[0..100]`", () => this.props({ value: array(100) })); 91 | }); 92 | }; 93 | 94 | 95 | 96 | export const functionValueSection = function() { 97 | const fn1 = function myFunc(param1, param2) {}; 98 | const fn2 = function myFunc() {}; 99 | section("Function", () => { 100 | it("`named with 2 params`", () => this.props({ value: fn1 })); 101 | it("`named with no params`", () => this.props({ value: fn2 })); 102 | it("`unnamed with 1 param`", () => this.props({ value: (p1) => 0 })); 103 | it("`unnamed with no params`", () => this.props({ value: () => true })); 104 | }); 105 | }; 106 | 107 | 108 | 109 | export const collapsedTotalSection = function() { 110 | section("collapsedTotal", () => { 111 | it("`0`", () => this.props({ collapsedTotal: 0 })); 112 | it("`1`", () => this.props({ collapsedTotal: 1 })); 113 | it("`3`", () => this.props({ collapsedTotal: 3 })); 114 | }); 115 | }; 116 | -------------------------------------------------------------------------------- /src/components/Value.jsx: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0 */ 2 | 3 | import R from "ramda"; 4 | import React from "react"; 5 | import Radium from "radium"; 6 | import { css, PropTypes } from "./util"; 7 | import Text from "./Text"; 8 | import Twisty from "react-atoms/components/Twisty"; 9 | import Primitive, { isPrimitive } from "./Primitive"; 10 | import Complex from "./Complex"; 11 | import FunctionComponent from "./Function"; 12 | import DateComponent from "./Date"; 13 | import { isEmptyObjectOrArray } from "./util"; 14 | 15 | 16 | 17 | /** 18 | * A single value of any type (with optional key and expansion toggle). 19 | */ 20 | class Value extends React.Component { 21 | static propTypes = { 22 | value: PropTypes.any, 23 | label: PropTypes.string, 24 | italic: Text.propTypes.italic, 25 | size: Text.propTypes.size, 26 | inline: PropTypes.bool, 27 | level: PropTypes.number, 28 | isExpanded: PropTypes.bool, 29 | showTwisty: PropTypes.bool, 30 | marginLeft: PropTypes.numberOrString, 31 | marginRight: PropTypes.numberOrString, 32 | collapsedTotal: PropTypes.number // The number of {object} properties to show when not expanded. 33 | }; 34 | static defaultProps = { 35 | italic: Text.defaultProps.italic, 36 | size: Text.defaultProps.size, 37 | inline: false, 38 | level: 0, 39 | isExpanded: false, 40 | marginLeft: 0, 41 | marginRight: 0 42 | }; 43 | 44 | componentWillMount() { 45 | this.setState({ isExpanded: this.props.isExpanded }); 46 | } 47 | 48 | styles() { 49 | const { inline, value } = this.props; 50 | const { isExpanded } = this.state; 51 | const showTwisty = this.showTwisty(); 52 | const twistyWidth = 10; 53 | const indent = showTwisty === true ? twistyWidth + 2 : 0; 54 | const canExpand = showTwisty && !isExpanded && !this.isPrimitive() && !isEmptyObjectOrArray(value); 55 | 56 | return css({ 57 | base: { 58 | position: "relative", 59 | paddingLeft: indent, 60 | display: inline ? "inline-block" : null, 61 | marginLeft: this.props.marginLeft, 62 | marginRight: this.props.marginRight, 63 | cursor: canExpand ? "pointer" : null 64 | }, 65 | twistyOuter: { 66 | Absolute: [0, null, null, 0], 67 | width: twistyWidth, 68 | fontSize: this.props.size, 69 | lineHeight: Text.defaultProps.lineHeight 70 | }, 71 | twistyAlign: { 72 | AbsoluteCenter: "y", 73 | width: twistyWidth 74 | } 75 | }); 76 | } 77 | 78 | 79 | isPrimitive() { return isPrimitive(this.props.value); } 80 | 81 | 82 | showTwisty() { 83 | let result = this.props.showTwisty; 84 | if (result === undefined) { 85 | const { value } = this.props; 86 | if (!isPrimitive(value) && !isEmptyObjectOrArray(value)) { 87 | result = true; 88 | } 89 | } 90 | return result; 91 | } 92 | 93 | 94 | handleToggleClick(e) { 95 | this.setState({ isExpanded: !this.state.isExpanded }); 96 | } 97 | 98 | 99 | render() { 100 | const styles = this.styles(); 101 | const { label, italic, size, value, level } = this.props; 102 | const { isExpanded } = this.state; 103 | const textStyles = { italic, size }; 104 | const IS_PRIMITIVE = this.isPrimitive(); 105 | let showTwisty = this.showTwisty(); 106 | 107 | if (isEmptyObjectOrArray(value)) { 108 | showTwisty = false; 109 | } 110 | 111 | let elTwisty, handleToggleClick; 112 | if (showTwisty === true && !IS_PRIMITIVE) { 113 | handleToggleClick = this.handleToggleClick.bind(this); 114 | // NB: Add the "zero width non-joiner" (\u200C) character to force the 115 | // height of the twisty container to the height of the label. 116 | elTwisty =
    117 |
    118 | 122 |
    123 | { "\u200C" } 124 |
    ; 125 | } 126 | 127 | const elLabel = label && { label }; 131 | 132 | let elValue; 133 | if (IS_PRIMITIVE) { 134 | // Simple value (string, number, bool). 135 | elValue = ; 136 | } else { 137 | // Complex value (object, array). 138 | if (R.is(Function, value)) { 139 | elValue = ; 140 | } else if (R.is(Date, value)) { 141 | elValue = ; 142 | } else { 143 | elValue = ; 151 | } 152 | } 153 | 154 | return ( 155 |
    156 | { elTwisty } 157 | { elLabel } 158 | { elLabel && : } 159 | { elValue } 160 |
    161 | ); 162 | } 163 | } 164 | 165 | export default Radium(Value); 166 | -------------------------------------------------------------------------------- /src/components/Complex.jsx: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0 */ 2 | 3 | import R from "ramda"; 4 | import React from "react"; 5 | import Radium from "radium"; 6 | import { css, PropTypes } from "./util"; 7 | import Text from "./Text"; 8 | import ValueList, { ELLIPSIS } from "./ValueList"; 9 | import Ellipsis from "./Ellipsis"; 10 | import { isPrimitive } from "./Primitive"; 11 | import { isEmptyObjectOrArray } from "./util"; 12 | 13 | 14 | const toProp = (label, value) => ({ label, value }); 15 | 16 | 17 | const toObjectProps = (obj, max) => { 18 | const toObjectProp = (key) => toProp(key, obj[key]); 19 | let props = R.pipe(R.keys, R.map(toObjectProp))(obj); 20 | if (max !== undefined) { 21 | props = withinBounds(props, max); 22 | } 23 | return props; 24 | 25 | }; 26 | 27 | 28 | 29 | const withinBounds = (array, max) => { 30 | if (max === 0) { return []; } 31 | max = max === undefined ? array.length : max; 32 | const takeTotal = array.length === max ? array.length : max - 1; 33 | const items = R.take(takeTotal, array); 34 | if (array.length > max) { 35 | if (max === 1) { 36 | items.push(R.last(array)); 37 | items.push(ELLIPSIS); 38 | } else { 39 | items.push(ELLIPSIS); 40 | items.push(R.last(array)); 41 | } 42 | } 43 | return items; 44 | }; 45 | 46 | 47 | 48 | const toPrimitiveProps = (obj, max) => { 49 | const isPrimitiveProp = (prop) => isPrimitive(prop.value); 50 | let props = R.filter(isPrimitiveProp, toObjectProps(obj)); 51 | props = withinBounds(props, max); 52 | if (R.keys(obj).length > props.length && !R.any(R.equals(ELLIPSIS), props)) { 53 | props.push(ELLIPSIS); 54 | } 55 | return props; 56 | }; 57 | 58 | 59 | 60 | const toArrayProps = (array, max) => { 61 | // Add array items. 62 | let items = array.map((item, i) => { 63 | return item === ELLIPSIS 64 | ? item 65 | : toProp(i.toString(), item); 66 | }); 67 | items = withinBounds(items, max); 68 | 69 | // Add any properties on the array. 70 | const keys = R.keys(array); 71 | const propKeys = R.takeLast(keys.length - array.length, keys); 72 | propKeys.forEach(key => items.push(toProp(key, array[key]))); 73 | 74 | // Finish up. 75 | return items; 76 | }; 77 | 78 | 79 | 80 | /** 81 | * A complex value (Object, Array). 82 | */ 83 | class Complex extends React.Component { 84 | static propTypes = { 85 | label: PropTypes.boolOrString, 86 | value: PropTypes.oneOfType([ 87 | PropTypes.object, 88 | PropTypes.array 89 | ]).isRequired, 90 | level: PropTypes.number, 91 | isExpanded: PropTypes.bool, 92 | collapsedStyle: PropTypes.shape({ italic: Text.propTypes.italic }), 93 | italic: Text.propTypes.italic, 94 | size: Text.propTypes.size, 95 | collapsedTotal: PropTypes.number, // The number of {object} properties to show when not expanded. 96 | onClick: PropTypes.func 97 | }; 98 | static defaultProps = { 99 | label: true, 100 | isExpanded: false, 101 | level: 0, 102 | italic: Text.defaultProps.italic, 103 | size: Text.defaultProps.size, 104 | collapsedTotal: 3 105 | }; 106 | 107 | 108 | styles() { 109 | return css({ 110 | base: { 111 | cursor: this.props.onClick ? "pointer" : null 112 | } 113 | }); 114 | } 115 | 116 | 117 | render() { 118 | const styles = this.styles(); 119 | let { label, value, isExpanded, italic, size, level } = this.props; 120 | const textStyles = { italic, size }; 121 | const isArray = R.is(Array, value); 122 | let braceMargin = 0; 123 | if (isExpanded && isEmptyObjectOrArray(value)) { isExpanded = false; } 124 | 125 | // Prepare the label. 126 | if (label === true) { 127 | // Only show labels for custom object names (eg. Classes). 128 | label = value.constructor.name; 129 | if (R.any(R.equals(label), ["Object", "Array"])) { label = null; } 130 | } 131 | const elLabel = label && { label }; 132 | const openChar = isArray ? "[" : "{"; 133 | const closeChar = isArray ? "]" : "}"; 134 | 135 | // Prepare the value content. 136 | let elContent; 137 | if (isExpanded) { 138 | // -- Expanded --. 139 | const items = isArray ? toArrayProps(value, 5) : toObjectProps(value, 50); 140 | elContent = ; 145 | 146 | } else { 147 | 148 | // -- Collapsed --. 149 | if (isArray && value.length > 0) { 150 | // Array: Show length, eg: "[2]". 151 | elContent = { value.length }; 152 | } else { 153 | // Object: Show flat list of primitive props, eg: { foo:123 }. 154 | elContent = ; 155 | // const totalProps = R.keys(value).length; 156 | // if (totalProps > 0) { 157 | // const primitiveProps = toPrimitiveProps(value, this.props.collapsedTotal); 158 | // const hasProps = primitiveProps.length > 0; 159 | // braceMargin = hasProps ? 3 : 0; 160 | // elContent = hasProps 161 | // ? 167 | // : ; 168 | // } 169 | } 170 | } 171 | 172 | return ( 173 | 174 | { elLabel } 175 | { openChar } 176 | { elContent } 177 | { closeChar } 178 | 179 | ); 180 | } 181 | } 182 | 183 | 184 | export default Radium(Complex); 185 | --------------------------------------------------------------------------------