From 6834f09d731cc5413c9d701a0a254984b08c9337 Mon Sep 17 00:00:00 2001 From: Mauricio Palma Date: Mon, 25 Jun 2018 10:52:54 +0200 Subject: [PATCH] feat(editable-title): make page title editable (#553) * feat(chrome-name): component prep for the new editable title * feat(chrome-title): include interaction handlers * feat(editable-title): extract component from preview tile * feat(editable-title-component): implementation and integration * feat(editable-title): implementation of the editable title container * chore(general): fix lint errors from origin * fix(editable-title): rename the state enum : * fix(editable-title): include data-attr directly during rendering * chore(editable-tigle): rename the editable-title-state enum * fix(editable-title): remove fontSize props * feat(editable title): include title type enumerable * chore(editable-title): extract dinamic styled into own functions * chore(page-tile): remove unnecessary HTML and styling * fix(editable-title): remove text truncation on input (edit mode) * fix(editable-container): move global state into an internal state * fix: allow primary and secondary type * fix(editable-title): include required category property * chore(types): include editable title type in the model types --- src/components/chrome/demo.tsx | 11 +- src/components/editable-title/demo.tsx | 31 +++ src/components/editable-title/index.tsx | 176 ++++++++++++++++++ src/components/editable-title/pattern.json | 7 + src/components/index.ts | 1 + src/components/page-tile/demo.tsx | 32 +--- src/components/page-tile/index.tsx | 117 +----------- src/components/view-switch/demo.tsx | 5 +- src/components/view-switch/index.tsx | 73 +++++++- src/container/chrome/chrome-container.tsx | 20 +- .../editable-title-container.tsx | 129 +++++++++++++ .../page-list/page-tile-container.tsx | 93 ++------- src/electron/renderer.tsx | 2 +- src/model/page/page.ts | 17 +- src/types/types.ts | 7 +- 15 files changed, 474 insertions(+), 247 deletions(-) create mode 100644 src/components/editable-title/demo.tsx create mode 100644 src/components/editable-title/index.tsx create mode 100644 src/components/editable-title/pattern.json create mode 100644 src/container/editable-title/editable-title-container.tsx diff --git a/src/components/chrome/demo.tsx b/src/components/chrome/demo.tsx index dafcc9064..3742088a4 100644 --- a/src/components/chrome/demo.tsx +++ b/src/components/chrome/demo.tsx @@ -9,20 +9,23 @@ const DemoChrome: React.StatelessComponent = () => ( null} onRightClick={() => null} leftVisible={true} rightVisible={true} - title="Page Title" - /> + > + Page Name + null} onRightClick={() => null} leftVisible={true} rightVisible={true} - title="Page Title" - /> + > + Page Name + diff --git a/src/components/editable-title/demo.tsx b/src/components/editable-title/demo.tsx new file mode 100644 index 000000000..6a8f066fc --- /dev/null +++ b/src/components/editable-title/demo.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; + +import DemoContainer from '../demo-container'; +import { Headline } from '../headline'; +import { Space, SpaceSize } from '../space'; +import { EditableTitle, EditableTitleState, EditableTitleType } from '.'; + +export default (): JSX.Element => ( + + + Editable Title + + + + Editable Title + + + +); diff --git a/src/components/editable-title/index.tsx b/src/components/editable-title/index.tsx new file mode 100644 index 000000000..5108a6eb2 --- /dev/null +++ b/src/components/editable-title/index.tsx @@ -0,0 +1,176 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import styled from 'styled-components'; + +import { Color } from '../colors'; +import { CopySize } from '../copy'; +import { getSpace, SpaceSize } from '../space'; + +export enum EditableTitleState { + Editable = 'Editable', + Editing = 'Editing' +} + +export enum EditableTitleType { + Primary, + Secondary +} + +export interface EditableTitleProps { + category: EditableTitleType; + focused: boolean; + name: string; + nameState: EditableTitleState; + value: string; + onBlur?: React.FocusEventHandler; + onChange?: React.ChangeEventHandler; + onClick?: React.MouseEventHandler; + onFocus?: React.FocusEventHandler; + onKeyDown?: React.KeyboardEventHandler; +} + +interface EditableInputProps { + category: EditableTitleType; + autoFocus: boolean; + autoSelect: boolean; + value: string; + onBlur?: React.FocusEventHandler; + onChange?: React.ChangeEventHandler; + onClick?: React.MouseEventHandler; + onFocus?: React.FocusEventHandler; + onKeyDown?: React.KeyboardEventHandler; +} + +interface StyledEditableTitleProps { + children: React.ReactNode; + editable: boolean; + focused?: boolean; + category: EditableTitleType; +} + +interface StyledInputProps { + category: EditableTitleType; +} + +const categorizedTitleStyles = (props: StyledEditableTitleProps) => { + switch (props.category) { + case EditableTitleType.Secondary: + return ` + width: 200px; + margin: 0 ${getSpace(SpaceSize.XS)}px ${getSpace(SpaceSize.XXS)}px; + font-size: ${CopySize.M}px; + color: ${Color.Grey36}; + `; + case EditableTitleType.Primary: + default: + return ` + width: 100%; + margin: 0; + font-size: ${CopySize.S}px; + color: ${Color.Black}; + `; + } +}; + +const StyledTitle = styled.strong` + box-sizing: border-box; + display: inline-block; + padding: 0; + overflow: hidden; + font-weight: normal; + text-align: center; + cursor: ${(props: StyledEditableTitleProps) => (props.editable ? 'text' : 'default')}; + white-space: nowrap; + text-overflow: ellipsis; + + ${categorizedTitleStyles}; +`; + +const categorizedEditableTitleStyles = (props: StyledInputProps) => { + switch (props.category) { + case EditableTitleType.Secondary: + return ` + width: 200px; + margin: 0 ${getSpace(SpaceSize.XS)}px ${getSpace(SpaceSize.XXS)}px; + font-size: ${CopySize.M}px; + `; + case EditableTitleType.Primary: + default: + return ` + width: 100%; + margin: 0; + font-size: ${CopySize.S}px; + `; + } +}; + +const StyledEditableTitle = styled.input` + box-sizing: border-box; + display: inline-block; + border: 0; + padding: 0; + overflow: hidden; + text-align: center; + + ${categorizedEditableTitleStyles} :focus { + outline: none; + } +`; + +class EditableInput extends React.Component { + public componentDidMount(): void { + const node = ReactDOM.findDOMNode(this); + if (!node) { + return; + } + const element = node as HTMLInputElement; + + if (this.props.autoSelect) { + element.setSelectionRange(0, this.props.value.length); + } + } + + public render(): JSX.Element { + const { props } = this; + return ( + + ); + } +} + +export const EditableTitle: React.SFC = (props): JSX.Element => ( + + {props.nameState === EditableTitleState.Editing ? ( + + ) : ( + + {props.name} + + )} + +); diff --git a/src/components/editable-title/pattern.json b/src/components/editable-title/pattern.json new file mode 100644 index 000000000..a0580bdab --- /dev/null +++ b/src/components/editable-title/pattern.json @@ -0,0 +1,7 @@ +{ + "name": "editable-tile", + "displayName": "Editable Tile", + "flag": "alpha", + "version": "1.0.0", + "tags": ["atom"] + } diff --git a/src/components/index.ts b/src/components/index.ts index f741cce15..33d910051 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -5,6 +5,7 @@ export * from './chrome'; export * from './colors'; export * from './copy'; export * from './create-select'; +export * from './editable-title'; export * from './element'; export * from './element-slot'; export * from './fonts'; diff --git a/src/components/page-tile/demo.tsx b/src/components/page-tile/demo.tsx index 2fdb91856..2ef958c96 100644 --- a/src/components/page-tile/demo.tsx +++ b/src/components/page-tile/demo.tsx @@ -5,7 +5,7 @@ import DemoContainer from '../demo-container'; import { Headline } from '../headline'; import { Layout } from '../layout'; import { Space, SpaceSize } from '../space'; -import { EditState, PageTile } from '.'; +import { PageTile } from '.'; const handleChange = (e: React.ChangeEvent): string => e.target.value; @@ -26,44 +26,20 @@ export default (): JSX.Element => ( focused={false} highlighted={false} onChange={handleChange} - name="Editable" - nameState={EditState.Editable} - /> - - - - - - diff --git a/src/components/page-tile/index.tsx b/src/components/page-tile/index.tsx index 92db3aa35..5c4c1bb19 100644 --- a/src/components/page-tile/index.tsx +++ b/src/components/page-tile/index.tsx @@ -1,38 +1,18 @@ import { Color } from '../colors'; import * as React from 'react'; -import * as ReactDOM from 'react-dom'; import { getSpace, SpaceSize } from '../space'; import styled from 'styled-components'; -export enum EditState { - Editable = 'Editable', - Editing = 'Editing' -} - export interface PageTileProps { focused: boolean; highlighted: boolean; id?: string; - name: string; - nameState: EditState; - onBlur?: React.FocusEventHandler; - onChange?: React.ChangeEventHandler; - onClick?: React.MouseEventHandler; - onFocus?: React.FocusEventHandler; - onKeyDown?: React.KeyboardEventHandler; -} - -interface EditableTitleProps { - autoFocus: boolean; - autoSelect: boolean; - highlighted: boolean; onBlur?: React.FocusEventHandler; onChange?: React.ChangeEventHandler; onClick?: React.MouseEventHandler; onDoubleClick?: React.MouseEventHandler; onFocus?: React.FocusEventHandler; onKeyDown?: React.KeyboardEventHandler; - value: string; } interface StyledPageTileProps { @@ -40,11 +20,6 @@ interface StyledPageTileProps { focused: boolean; } -interface StyledPageTitleProps { - children: React.ReactNode; - editable: boolean; -} - const BORDER_COLOR = (props: StyledPageTileProps) => (props.focused ? Color.Blue20 : Color.Blue60); const StyledPageTile = styled.div` @@ -74,98 +49,14 @@ const StyledPageTile = styled.div` } `; -const StyledTitle = (props: StyledPageTitleProps): JSX.Element => { - const Strong = styled.strong` - display: inline-block; - width: 100%; - margin: 0; - font-size: inherit; - font-weight: normal; - text-align: center; - cursor: ${props.editable ? 'text' : 'default'}; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - padding: 0; - color: inherit; - user-select: none; - `; - return {props.children}; -}; - -const StyledEditableTitle = styled.input` - border: 0; - display: inline-block; - width: 100%; - margin: 0; - font-size: inherit; - line-height: inherit; - font-weight: normal; - text-align: center; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - padding: 3px 0; - - &:focus { - outline: none; - } -`; - -class EditableTitle extends React.Component { - public componentDidMount(): void { - const node = ReactDOM.findDOMNode(this); - - if (!node) { - return; - } - - const element = node as HTMLInputElement; - - if (this.props.autoFocus) { - element.focus(); - } - - if (this.props.autoSelect) { - element.setSelectionRange(0, this.props.value.length); - } - } - - public render(): JSX.Element { - const { props } = this; - return ( - - ); - } -} - export const PageTile: React.StatelessComponent = (props): JSX.Element => ( - {props.nameState === EditState.Editing ? ( - - ) : ( - {props.name} - )} + {props.children} ); diff --git a/src/components/view-switch/demo.tsx b/src/components/view-switch/demo.tsx index 9e2d4a0f6..7908901a3 100644 --- a/src/components/view-switch/demo.tsx +++ b/src/components/view-switch/demo.tsx @@ -9,8 +9,9 @@ const DemoViewSwitch: React.StatelessComponent = (): JSX.Element => ( onRightClick={() => null} leftVisible={true} rightVisible={true} - title="Page Name" - /> + > + Title + ); diff --git a/src/components/view-switch/index.tsx b/src/components/view-switch/index.tsx index f34a59eed..b2890b98b 100644 --- a/src/components/view-switch/index.tsx +++ b/src/components/view-switch/index.tsx @@ -2,12 +2,30 @@ import { Color } from '../colors'; import { CopySize } from '../copy'; import { IconProps, IconSize } from '../icons'; import * as React from 'react'; +import { EditableTitleState } from '../../types'; import { ChevronLeft, ChevronRight } from 'react-feather'; import { getSpace, SpaceSize } from '../space'; import styled from 'styled-components'; export type JustifyType = 'start' | 'center' | 'end' | 'stretch'; +export interface ViewEditableTitleProps { + fontSize?: CopySize; + nameState: EditableTitleState; + title: string; + onBlur?: React.FocusEventHandler; + onChange?: React.ChangeEventHandler; + onClick?: React.MouseEventHandler; + onFocus?: React.FocusEventHandler; + onKeyDown?: React.KeyboardEventHandler; +} + +interface ViewEditableInputProps { + autofocus: boolean; + fontSize?: CopySize; + justify?: 'start' | 'center' | 'end' | 'stretch'; +} + export interface ViewSwitchProps { fontSize?: CopySize; justify?: JustifyType; @@ -15,11 +33,24 @@ export interface ViewSwitchProps { onLeftClick?: React.MouseEventHandler; onRightClick?: React.MouseEventHandler; rightVisible: boolean; +} + +export interface StyledViewButtonProps { + onClick?: React.MouseEventHandler; +} + +export interface ViewButtonProps { + onClick?: React.MouseEventHandler; + title: string; +} + +export interface ViewTitleProps { + fontSize?: CopySize; + justify?: JustifyType; title: string; } interface StyledIconProps extends IconProps { - rotate?: boolean; visible: boolean; } @@ -81,11 +112,43 @@ const StyledRightIcon = styled(ChevronRight)` &:hover { background: ${Color.Grey90}; } - &:active { - background: ${Color.Grey80}; - } `; +const StyledInput = styled.input` + width: 130px; + padding: 0 ${getSpace(SpaceSize.XS)}px ${getSpace(SpaceSize.XXS)}px; + border: 0; + margin: 0; + background-color: ${Color.White}; + text-align: center; + font-size: ${(props: ViewEditableInputProps) => + props.fontSize ? `${props.fontSize}px` : `${CopySize.S}px`}; +`; + +export const ViewEditableTitle: React.SFC = (props): JSX.Element => ( + + {props.nameState === EditableTitleState.Editing ? ( + + ) : ( + {props.title} + )} + +); + +export const ViewTitle: React.SFC = (props): JSX.Element => ( + + {props.title} + +); + export const ViewSwitch: React.SFC = (props): JSX.Element => ( = (props): JSX.Element => ( size={IconSize.XS} visible={props.leftVisible} /> - {props.title} + {props.children} 0 ? toPreviousPage : AlvaUtil.noop; const next = index < pages.length ? toNextPage : AlvaUtil.noop; - return ( { @@ -65,8 +69,14 @@ export const ChromeContainer = MobxReact.inject('store')( rightVisible={index < pages.length - 1} onLeftClick={previous} onRightClick={next} - title={page ? page.getName() : ''} - /> + > + + { diff --git a/src/container/editable-title/editable-title-container.tsx b/src/container/editable-title/editable-title-container.tsx new file mode 100644 index 000000000..85cba88f6 --- /dev/null +++ b/src/container/editable-title/editable-title-container.tsx @@ -0,0 +1,129 @@ +import * as Mobx from 'mobx'; +import * as MobxReact from 'mobx-react'; +import * as React from 'react'; + +import { Page } from '../../model'; +import { ViewStore } from '../../store'; +import * as Types from '../../types'; +import { EditableTitle } from '../../components'; + +export interface EditableTitleContainerProps { + focused: boolean; + page: Page; + secondary: Types.EditableTitleType; + value: string; +} + +@MobxReact.inject('store') +@MobxReact.observer +export class EditableTitleContainer extends React.Component { + @Mobx.observable + private editNameState: Types.EditableTitleState = Types.EditableTitleState.Editable; + + @Mobx.action + protected handleBlur(): void { + const { store } = this.props as EditableTitleContainerProps & { store: ViewStore }; + + if (!this.props.page.getName()) { + this.props.page.setName( + this.props.page.getName({ unedited: true }), + Types.EditableTitleState.Editing + ); + this.editNameState = Types.EditableTitleState.Editable; + return; + } + + const name = this.props.page.getName(); + const editedName = this.props.page.getEditedName(); + + this.editNameState = Types.EditableTitleState.Editable; + this.props.page.setName( + this.props.page.getName({ unedited: true }), + Types.EditableTitleState.Editing + ); + + if (editedName !== name) { + store.commit(); + } + } + + protected handleChange(e: React.ChangeEvent): void { + this.props.page.setName(e.target.value, this.editNameState); + } + + protected handleClick(e: React.MouseEvent): void { + if (this.editNameState === Types.EditableTitleState.Editable) { + const target = e.target as HTMLElement; + if (target) { + target.focus(); + } + + this.editNameState = Types.EditableTitleState.Editing; + } + } + + protected handleFocus(): void { + this.editNameState = Types.EditableTitleState.Editing; + this.props.page.setNameState(Types.EditableTitleState.Editing); + } + + protected handleKeyDown(e: KeyboardEvent): void { + const { store } = this.props as EditableTitleContainerProps & { store: ViewStore }; + + switch (e.key) { + case 'Escape': + this.editNameState = Types.EditableTitleState.Editable; + this.props.page.setName( + this.props.page.getName({ unedited: true }), + Types.EditableTitleState.Editing + ); + return; + case 'Enter': + if (this.editNameState === Types.EditableTitleState.Editing) { + if (!this.props.page.getName()) { + this.props.page.setName( + this.props.page.getName({ unedited: true }), + this.editNameState + ); + this.editNameState = Types.EditableTitleState.Editable; + return; + } + + this.editNameState = Types.EditableTitleState.Editable; + this.props.page.setName(this.props.page.getEditedName(), this.editNameState); + store.commit(); + return; + } + if ( + e.target === document.body && + this.props.focused && + this.editNameState === Types.EditableTitleState.Editable + ) { + this.editNameState = Types.EditableTitleState.Editing; + return; + } + } + } + + public render(): JSX.Element { + const { props } = this; + return ( + this.handleBlur()} + onChange={e => this.handleChange(e)} + onClick={e => this.handleClick(e)} + onFocus={e => this.handleFocus()} + onKeyDown={e => { + e.stopPropagation(); + this.handleKeyDown(e.nativeEvent); + }} + name={props.page.getName()} + nameState={this.editNameState} + category={props.secondary} + value={props.page.getName()} + /> + ); + } +} diff --git a/src/container/page-list/page-tile-container.tsx b/src/container/page-list/page-tile-container.tsx index 5776708e1..87442947b 100644 --- a/src/container/page-list/page-tile-container.tsx +++ b/src/container/page-list/page-tile-container.tsx @@ -4,6 +4,7 @@ import { Page } from '../../model'; import * as React from 'react'; import { ViewStore } from '../../store'; import * as Types from '../../types'; +import { EditableTitleContainer } from '../editable-title/editable-title-container'; export interface PageTileContainerProps { focused: boolean; @@ -14,39 +15,6 @@ export interface PageTileContainerProps { @MobxReact.inject('store') @MobxReact.observer export class PageTileContainer extends React.Component { - private onKeyDown: (e: KeyboardEvent) => void; - - public componentDidMount(): void { - this.onKeyDown = e => this.handleKeyDown(e); - document.addEventListener('keydown', this.onKeyDown); - } - - public componentWillUnmount(): void { - if (this.onKeyDown) { - document.removeEventListener('keydown', this.onKeyDown); - } - } - - protected handleBlur(): void { - const { store } = this.props as PageTileContainerProps & { store: ViewStore }; - - if (!this.props.page.getName()) { - this.props.page.setName(this.props.page.getName({ unedited: true })); - this.props.page.setNameState(Types.EditState.Editable); - return; - } - - const name = this.props.page.getName(); - const editedName = this.props.page.getEditedName(); - - this.props.page.setNameState(Types.EditState.Editable); - this.props.page.setName(this.props.page.getEditedName()); - - if (editedName !== name) { - store.commit(); - } - } - protected handleChange(e: React.ChangeEvent): void { this.props.page.setName(e.target.value); } @@ -54,48 +22,18 @@ export class PageTileContainer extends React.Component { protected handleClick(e: React.MouseEvent): void { const { store } = this.props as PageTileContainerProps & { store: ViewStore }; - const target = e.target as HTMLElement; - - store.setActivePage(this.props.page); - - if (this.props.highlighted && target.matches('[data-title]')) { - this.props.page.setNameState(Types.EditState.Editing); + if (!this.props.focused) { + store.setActivePage(this.props.page); } } protected handleFocus(): void { - this.props.page.setNameState(Types.EditState.Editing); + this.props.page.setNameState(Types.EditableTitleState.Editing); } - protected handleKeyDown(e: KeyboardEvent): void { - const { store } = this.props as PageTileContainerProps & { store: ViewStore }; - - switch (e.key) { - case 'Escape': - this.props.page.setNameState(Types.EditState.Editable); - this.props.page.setName(this.props.page.getName({ unedited: true })); - return; - case 'Enter': - if (this.props.page.getNameState() === Types.EditState.Editing) { - if (!this.props.page.getName()) { - this.props.page.setName(this.props.page.getName({ unedited: true })); - this.props.page.setNameState(Types.EditState.Editable); - return; - } - - this.props.page.setNameState(Types.EditState.Editable); - this.props.page.setName(this.props.page.getEditedName()); - store.commit(); - return; - } - if ( - e.target === document.body && - this.props.highlighted && - this.props.page.getNameState() === Types.EditState.Editable - ) { - this.props.page.setNameState(Types.EditState.Editing); - return; - } + protected handleDoubleClick(e: React.MouseEvent): void { + if (this.props.page.getNameState() === Types.EditableTitleState.Editing) { + return; } } @@ -106,17 +44,16 @@ export class PageTileContainer extends React.Component { focused={props.focused} highlighted={props.highlighted} id={props.page.getId()} - onBlur={e => this.handleBlur()} - onChange={e => this.handleChange(e)} onClick={e => this.handleClick(e)} onFocus={e => this.handleFocus()} - onKeyDown={e => { - e.stopPropagation(); - this.handleKeyDown(e.nativeEvent); - }} - nameState={props.page.getNameState()} - name={props.page.getName()} - /> + > + + ); } } diff --git a/src/electron/renderer.tsx b/src/electron/renderer.tsx index 827f9becb..f3d48797f 100644 --- a/src/electron/renderer.tsx +++ b/src/electron/renderer.tsx @@ -122,7 +122,7 @@ Sender.receive(message => { } store.setActivePage(page); - page.setNameState(Types.EditState.Editing); + page.setNameState(Types.EditableTitleState.Editing); break; } diff --git a/src/model/page/page.ts b/src/model/page/page.ts index 795d49b12..59c2434ac 100644 --- a/src/model/page/page.ts +++ b/src/model/page/page.ts @@ -48,7 +48,7 @@ export class Page { /** * Wether the name may be edited */ - @Mobx.observable public nameState: Types.EditState = Types.EditState.Editable; + @Mobx.observable public nameState: Types.EditableTitleState = Types.EditableTitleState.Editable; private patternLibrary: PatternLibrary; @@ -202,10 +202,9 @@ export class Page { * @return The human-friendly name of the page. */ public getName(options?: { unedited: boolean }): string { - if ((!options || !options.unedited) && this.nameState === Types.EditState.Editing) { + if ((!options || !options.unedited) && this.nameState === Types.EditableTitleState.Editing) { return this.editedName; } - return this.name; } @@ -213,7 +212,7 @@ export class Page { * Get the editable state of the page name * @param state */ - public getNameState(): Types.EditState { + public getNameState(): Types.EditableTitleState { return this.nameState; } @@ -236,21 +235,19 @@ export class Page { } @Mobx.action - public setName(name: string): void { - if (this.nameState === Types.EditState.Editing) { + public setName(name: string, editNameState?: Types.EditableTitleState): void { + if (editNameState === Types.EditableTitleState.Editing) { this.editedName = name; return; } - this.name = name; } @Mobx.action - public setNameState(state: Types.EditState): void { - if (state === Types.EditState.Editing) { + public setNameState(state: Types.EditableTitleState): void { + if (state === Types.EditableTitleState.Editing) { this.editedName = this.name; } - this.nameState = state; } diff --git a/src/types/types.ts b/src/types/types.ts index 1ca418fba..0ea6714be 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -136,11 +136,16 @@ export enum AlvaView { export type SerializedAlvaView = 'PageDetail' | 'SplashScreen'; -export enum EditState { +export enum EditableTitleState { Editable = 'Editable', Editing = 'Editing' } +export enum EditableTitleType { + Primary, + Secondary +} + export enum SlotType { Children = 'children', Property = 'property'