mirror of
https://github.com/wassname/talk.git
synced 2026-06-29 06:50:44 +08:00
Removing dynamic containers from CommentStream
This commit is contained in:
@@ -75,6 +75,7 @@ class CommentStream extends Component {
|
||||
// Set up messaging between embedded Iframe an parent component
|
||||
// Using recommended Pym init code which violates .eslint standards
|
||||
new Pym.Child({ polling: 500 })
|
||||
this.props.getItemsQuery('assetTest')
|
||||
}
|
||||
|
||||
render () {
|
||||
@@ -95,65 +96,87 @@ class CommentStream extends Component {
|
||||
|
||||
// TODO: Replace teststream id with id from params
|
||||
|
||||
return <RootContainer
|
||||
items={this.props.items}
|
||||
rootId='assetTest'
|
||||
type='asset'
|
||||
getItemsQuery={this.props.getItemsQuery}>
|
||||
<Container name="commentBox">
|
||||
<Count data="{item_id,comment(type:'comment'){item_id,child(type:'comment'){item_id}}}"/>
|
||||
<CommentBox
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
appendItemRelated={this.props.appendItemRelated}
|
||||
updateItem={this.props.updateItem}
|
||||
data="{item_id}"/>
|
||||
</Container>
|
||||
<MapContainer mapOver="comment" itemType="comment">
|
||||
<Container name="comment">
|
||||
<hr aria-hidden={true}/>
|
||||
<PubDate data="{created_at}"/>
|
||||
<Content data="{content}"/>
|
||||
<Container name="commentActions">
|
||||
<Flag
|
||||
addNotification={this.props.addNotification}
|
||||
data="{item_id,flag}"
|
||||
postAction={this.props.postAction}
|
||||
currentUser={this.props.auth.user}/>
|
||||
<ReplyButton
|
||||
updateItem={this.props.updateItem}
|
||||
data="{item_id}"/>
|
||||
</Container>
|
||||
<ReplyBox
|
||||
|
||||
|
||||
const rootItemId = 'assetTest'
|
||||
const rootItem = this.props.items[rootItemId]
|
||||
console.log(this.props.items);
|
||||
return <div>
|
||||
{
|
||||
rootItem ?
|
||||
<div>
|
||||
<div id="commentBox">
|
||||
<Count
|
||||
item_id={rootItemId}
|
||||
items={this.props.items}/>
|
||||
<CommentBox
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
appendItemRelated={this.props.appendItemRelated}
|
||||
updateItem={this.props.updateItem}
|
||||
data="{item_id,showReply}"/>
|
||||
<MapContainer mapOver="child" itemType="comment">
|
||||
<Container name="reply">
|
||||
item_id={rootItemId}/>
|
||||
</div>
|
||||
{
|
||||
rootItem.related.comment.map((commentId) => {
|
||||
const comment = this.props.items[commentId]
|
||||
return <div className="comment">
|
||||
<hr aria-hidden={true}/>
|
||||
<PubDate data="{created_at}"/>
|
||||
<Content data="{content}"/>
|
||||
<Container name="replyActions">
|
||||
<PubDate created_at={comment.created_at}/>
|
||||
<Content content={comment.data.content}/>
|
||||
<div className="commentActions">
|
||||
<Flag
|
||||
addNotificiation={this.props.addNotification}
|
||||
data="{item_id,flag}"
|
||||
addNotification={this.props.addNotification}
|
||||
item_id={commentId}
|
||||
flag={comment.flag}
|
||||
postAction={this.props.postAction}
|
||||
currentUser={this.props.auth.user}/>
|
||||
<ReplyButton
|
||||
updateItem={this.props.updateItem}
|
||||
data="{parent_id}"/>
|
||||
</Container>
|
||||
</Container>
|
||||
</MapContainer>
|
||||
</Container>
|
||||
</MapContainer>
|
||||
<Notification
|
||||
notifLength={this.props.config.notifLength}
|
||||
clearNotification={this.props.clearNotification}
|
||||
notification={this.props.notification}/>
|
||||
</RootContainer>
|
||||
item_id={commentId}/>
|
||||
</div>
|
||||
<ReplyBox
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
appendItemRelated={this.props.appendItemRelated}
|
||||
updateItem={this.props.updateItem}
|
||||
item_id={commentId}
|
||||
showReply={comment.showReply}/>
|
||||
{
|
||||
comment.related &&
|
||||
comment.related.child &&
|
||||
comment.related.child.map((replyId) => {
|
||||
let reply = this.props.items[replyId]
|
||||
return <div className="reply">
|
||||
<hr aria-hidden={true}/>
|
||||
<PubDate created_at={reply.created_at}/>
|
||||
<Content content={reply.data.content}/>
|
||||
<div className="replyActions">
|
||||
<Flag
|
||||
addNotificiation={this.props.addNotification}
|
||||
item_id={replyId}
|
||||
flag={reply.flag}
|
||||
postAction={this.props.postAction}
|
||||
currentUser={this.props.auth.user}/>
|
||||
<ReplyButton
|
||||
updateItem={this.props.updateItem}
|
||||
parent_id={reply.parent_id}/>
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
})
|
||||
}
|
||||
<Notification
|
||||
notifLength={this.props.config.notifLength}
|
||||
clearNotification={this.props.clearNotification}
|
||||
notification={this.props.notification}/>
|
||||
</div>
|
||||
:'Loading'
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
import {expect} from 'chai'
|
||||
import React from 'react'
|
||||
import DynamicContainer from '../../dynamic-containers/DynamicContainer'
|
||||
import {shallow, mount} from 'enzyme'
|
||||
|
||||
describe('DynamicContainer', () => {
|
||||
let props
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
item_id: '1',
|
||||
items: {
|
||||
'1': {
|
||||
item_id: '1',
|
||||
type: 'comment',
|
||||
data: {
|
||||
content: 'stuff'
|
||||
},
|
||||
related: {
|
||||
author: '2',
|
||||
likes: ['4', '5']
|
||||
}
|
||||
|
||||
},
|
||||
'2': {
|
||||
item_id: '2',
|
||||
type: 'user',
|
||||
data: {
|
||||
name: 'Janice'
|
||||
},
|
||||
related: {
|
||||
likes: ['4', '5'],
|
||||
employer: '3'
|
||||
}
|
||||
},
|
||||
'3': {
|
||||
item_id: '3',
|
||||
type: 'employer',
|
||||
data: {
|
||||
name: 'Coral'
|
||||
}
|
||||
},
|
||||
'4': {
|
||||
item_id: '4',
|
||||
type: 'like',
|
||||
data: {
|
||||
name: 'Regina'
|
||||
}
|
||||
},
|
||||
'5': {
|
||||
item_id: '5',
|
||||
type: 'like',
|
||||
data: {
|
||||
name: 'Fatima'
|
||||
}
|
||||
}
|
||||
},
|
||||
name: 'test'
|
||||
}
|
||||
})
|
||||
describe('mapPropsFromItems', () => {
|
||||
it('should retrieve objects based on a simple graphQL query', () => {
|
||||
let query = '(type: \'comment\'){content}'
|
||||
let output = new DynamicContainer(props).getPropsFromItems(query, 1)
|
||||
expect(output).to.deep.equal({
|
||||
content: 'stuff'
|
||||
})
|
||||
})
|
||||
it('should traverse the graph and return an appropriately formatted set of properties', () => {
|
||||
let query = '(type: \'comment\'){content,author(type: \'user\'){name}}'
|
||||
let output = new DynamicContainer(props).getPropsFromItems(query, 1)
|
||||
expect(output).to.deep.equal({
|
||||
content: 'stuff',
|
||||
author: {
|
||||
name: 'Janice'
|
||||
}
|
||||
})
|
||||
})
|
||||
it('should traverse a deeply nested query', () => {
|
||||
let query = '(type: \'comment\'){content,author(type:"user"){employer(type:"employer"){name}}}'
|
||||
let output = new DynamicContainer(props).getPropsFromItems(query, 1)
|
||||
expect(output).to.deep.equal({
|
||||
content: 'stuff',
|
||||
author: {
|
||||
employer: {
|
||||
name: 'Coral'
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
it('should traverse a one to many relationship', () => {
|
||||
const query = '(type: \'comment\'){author(type: \'user\'){likes(type: \'like\'){name}},content}'
|
||||
const output = new DynamicContainer(props).getPropsFromItems(query, 1)
|
||||
expect(output).to.deep.equal({
|
||||
content: 'stuff',
|
||||
author: {
|
||||
likes: [
|
||||
{
|
||||
name: 'Regina'
|
||||
},
|
||||
{
|
||||
name: 'Fatima'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
})
|
||||
it('should traverse complex one to many relationships', () => {
|
||||
const query = '(type: "comment"){author(type: "user"){employer(type: "employer"){name},likes(type: "likes"){name},name},content,likes(type: "likes"){item_id}}'
|
||||
const output = new DynamicContainer(props).getPropsFromItems(query, 1)
|
||||
expect(output).to.deep.equal({
|
||||
author: {
|
||||
employer: {
|
||||
name: 'Coral'
|
||||
},
|
||||
likes: [
|
||||
{
|
||||
name: 'Regina'
|
||||
},
|
||||
{
|
||||
name: 'Fatima'
|
||||
}
|
||||
],
|
||||
name: 'Janice'
|
||||
},
|
||||
content: 'stuff',
|
||||
likes: [
|
||||
{
|
||||
item_id: '4'
|
||||
},
|
||||
{
|
||||
item_id: '5'
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
it('should traverse complex relationships efficiently', () => {
|
||||
let query = '(type: \'comment\'){content}'
|
||||
const start = new Date().getTime()
|
||||
for (var i = 0; i < 1000; i++) {
|
||||
new DynamicContainer(props).getPropsFromItems(query, 1)
|
||||
}
|
||||
const end = new Date().getTime()
|
||||
expect(end-start).to.be.below(100)
|
||||
})
|
||||
|
||||
it('should not require a type declaration at the beginning of a query', () => {
|
||||
let query = '{content}'
|
||||
const output = new DynamicContainer(props).getPropsFromItems(query, 1)
|
||||
expect(output).to.deep.equal({
|
||||
content: 'stuff'
|
||||
})
|
||||
})
|
||||
|
||||
it('should return an undefined object if a relationship traversal is undefined', () => {
|
||||
let query = '{unicorns(type:"notExist"){rainbows}}'
|
||||
const output = new DynamicContainer(props).getPropsFromItems(query, 1)
|
||||
expect(output).to.deep.equal({
|
||||
unicorns: undefined
|
||||
})
|
||||
})
|
||||
|
||||
it('should return an undefined object if a parameter is undefined', () => {
|
||||
let query = '{does_not_exist}'
|
||||
const output = new DynamicContainer(props).getPropsFromItems(query, 1)
|
||||
expect(output).to.deep.equal({
|
||||
does_not_exist: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('render', () => {
|
||||
it('should render a set of child components with the appropriate data', () => {
|
||||
const render = shallow(<DynamicContainer {...props}>
|
||||
<div data='{content}'/>
|
||||
<div data='{author(type:"user"){name}}'/>
|
||||
</DynamicContainer>)
|
||||
expect(render.node.props.children[0].props).to.have.property('content')
|
||||
.and.to.equal('stuff')
|
||||
expect(render.node.props.children[1].props).to.have.property('author')
|
||||
.and.to.deep.equal({name: 'Janice'})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,54 +0,0 @@
|
||||
import React from 'react'
|
||||
import {shallow} from 'enzyme'
|
||||
import {expect} from 'chai'
|
||||
import MapContainer from '../../dynamic-containers/MapContainer'
|
||||
|
||||
describe('<MapContainer/>', () => {
|
||||
let items
|
||||
beforeEach (() => {
|
||||
items = {
|
||||
a: {
|
||||
item_id: 'a',
|
||||
type: 'stream',
|
||||
data: {
|
||||
url: "http://a.site"
|
||||
},
|
||||
related: {
|
||||
comment: ['b', 'c']
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
it('should map children and pass them the appropriate item ids', () => {
|
||||
const map = shallow(<MapContainer
|
||||
item_id={'a'}
|
||||
mapOver="comment"
|
||||
items={items}>
|
||||
<div/>
|
||||
<div/>
|
||||
</MapContainer>)
|
||||
expect(map.node.props.className).to.equal('mapcomment')
|
||||
expect(map.node.props.children.length).to.equal(2)
|
||||
expect(map.node.props.children[0]).to.have.property('key')
|
||||
.and.to.equal('b')
|
||||
expect(map.node.props.children[0].props.children[0].props).to.have.property('item_id')
|
||||
.and.to.equal('b')
|
||||
expect(map.node.props.children[1]).to.have.property('key')
|
||||
.and.to.equal('c')
|
||||
expect(map.node.props.children[1].props.children[0].props).to.have.property('item_id')
|
||||
.and.to.equal('c')
|
||||
})
|
||||
it('should pass its items and config objects on to its children', () => {
|
||||
const map = shallow(<MapContainer
|
||||
item_id={'a'}
|
||||
mapOver="comment"
|
||||
items={items}>
|
||||
<div/>
|
||||
<div/>
|
||||
</MapContainer>)
|
||||
expect(map.node.props.children[0].props.children[0].props).to.have.property('items')
|
||||
.and.to.deep.equal(items)
|
||||
expect(map.node.props.children[1].props.children[0].props).to.have.property('items')
|
||||
.and.to.deep.equal(items)
|
||||
})
|
||||
})
|
||||
@@ -1,50 +0,0 @@
|
||||
import React from 'react'
|
||||
import {shallow, mount} from 'enzyme'
|
||||
import {expect} from 'chai'
|
||||
|
||||
import RootContainer from '../../dynamic-containers/RootContainer'
|
||||
|
||||
describe('<RootContainer/>', () => {
|
||||
let items
|
||||
beforeEach(() => {
|
||||
items = {
|
||||
'a': {
|
||||
type: 'stream',
|
||||
data: {comments: ['b', 'c']}
|
||||
}
|
||||
}
|
||||
})
|
||||
describe('render', () => {
|
||||
it('should render child containers with the appropriate id', () => {
|
||||
const render = shallow(<RootContainer
|
||||
rootId='a'
|
||||
type='stream'
|
||||
items={items}
|
||||
query='all'
|
||||
getItemsQuery={() => {}}>
|
||||
<div/>
|
||||
<div/>
|
||||
</RootContainer>)
|
||||
expect(render.hasClass('rootContainer')).to.be.true
|
||||
expect(render.props().children[0].props).to.have.property('item_id')
|
||||
.and.to.equal('a')
|
||||
expect(render.props().children[1].props).to.have.property('item_id')
|
||||
.and.to.equal('a')
|
||||
})
|
||||
it('should render child containers with the appropriate items', () => {
|
||||
const render = shallow(<RootContainer
|
||||
rootId='a'
|
||||
type='stream'
|
||||
items={items}
|
||||
query='all'
|
||||
getItemsQuery={() => {}}>
|
||||
<div/>
|
||||
<div/>
|
||||
</RootContainer>)
|
||||
expect(render.props().children[0].props).to.have.property('items')
|
||||
.and.to.deep.equal(items)
|
||||
expect(render.props().children[1].props).to.have.property('items')
|
||||
.and.to.deep.equal(items)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,156 +0,0 @@
|
||||
import React, {Component, PropTypes, Children, cloneElement} from 'react'
|
||||
|
||||
class DynamicContainer extends Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.getPropsFromItems = this.getPropsFromItems.bind(this)
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
name: PropTypes.string,
|
||||
items: PropTypes.object,
|
||||
item_id: PropTypes.string
|
||||
}
|
||||
|
||||
traverseEdges (edges, index, query) {
|
||||
let bracketCount = 0
|
||||
let subqueryLength = 0
|
||||
let subquery = query.slice(index).reduce((subquery, char, i) => {
|
||||
if (bracketCount === 0 && i !== 0) {
|
||||
return subquery
|
||||
}
|
||||
subqueryLength++
|
||||
switch (char) {
|
||||
case '{':
|
||||
bracketCount++
|
||||
break
|
||||
case '}':
|
||||
bracketCount--
|
||||
break
|
||||
}
|
||||
const result = subquery += char
|
||||
return result
|
||||
}, '')
|
||||
query.splice(index, subqueryLength-1)
|
||||
return edges ? edges.reduce((array, edge) => {
|
||||
array.push(this.getPropsFromItems(subquery, edge))
|
||||
return array
|
||||
}, []) : undefined
|
||||
}
|
||||
|
||||
getPropsFromItems (query, id) {
|
||||
let idStack = [id.toString()]
|
||||
let relationshipStack = []
|
||||
let result = {}
|
||||
|
||||
query.split('').reduce((string, char, i, q) => {
|
||||
const id = idStack[idStack.length - 1]
|
||||
if (!this.props.items[id]) {
|
||||
return ''
|
||||
}
|
||||
let object = relationshipStack.reduce((object, relationship) => {
|
||||
return object[relationship]
|
||||
}, result)
|
||||
if (/[^{},]/i.test(char)) {
|
||||
return string + char
|
||||
}
|
||||
// Ignore spaces
|
||||
if (/\s/.test(char)) {
|
||||
return string
|
||||
}
|
||||
switch (char) {
|
||||
case '{':
|
||||
if (!string) {
|
||||
return string
|
||||
}
|
||||
const rgx = /(.*)\(type:\s?('|")(.+)('|")\)|^{/.exec(string)
|
||||
if (!rgx) {
|
||||
console.warn('Invalid graphQL: ' + string + ' in ' + query)
|
||||
console.warn('Expecting format {relationship(type:"itemType"{prop1, prop2}')
|
||||
return ''
|
||||
}
|
||||
|
||||
const edge = rgx[1]
|
||||
const type = rgx[3]
|
||||
let typetest
|
||||
if (edge && this.props.items[id].related) {
|
||||
idStack.push(this.props.items[id].related[edge])
|
||||
relationshipStack.push(edge)
|
||||
let val = this.props.items[id].related[edge]
|
||||
if (!val || val.constructor === Array) {
|
||||
object[edge] = this.traverseEdges(val, i, q)
|
||||
idStack.pop()
|
||||
relationshipStack.pop()
|
||||
} else {
|
||||
object[edge] = {}
|
||||
}
|
||||
typetest = this.props.items[val]
|
||||
} else {
|
||||
typetest = this.props.items[id]
|
||||
}
|
||||
if (typetest && typetest.type !== type) {
|
||||
console.warn('Received unexpected type when getting props, expected ' + edge + ' of type ' + type + ' but found ' + typetest.type + '.')
|
||||
}
|
||||
break
|
||||
case '}':
|
||||
idStack.pop()
|
||||
relationshipStack.pop()
|
||||
if (!string) {
|
||||
return string
|
||||
}
|
||||
if (string === 'item_id' ||
|
||||
string === 'type' ||
|
||||
string === 'created_at' ||
|
||||
string === 'updated_at') {
|
||||
object[string] = this.props.items[id][string]
|
||||
} else {
|
||||
object[string] = this.props.items[id].data[string]
|
||||
}
|
||||
break
|
||||
case ',':
|
||||
if (!string) {
|
||||
return string
|
||||
}
|
||||
if (string === 'item_id' ||
|
||||
string === 'type' ||
|
||||
string === 'created_at' ||
|
||||
string === 'updated_at') {
|
||||
object[string] = this.props.items[id][string]
|
||||
} else {
|
||||
object[string] = this.props.items[id].data[string]
|
||||
}
|
||||
break
|
||||
}
|
||||
return ''
|
||||
}, '')
|
||||
return result
|
||||
}
|
||||
|
||||
render () {
|
||||
return <div className={this.props.name}>
|
||||
{
|
||||
Children.map(this.props.children, (child) => {
|
||||
if (child.type.name === 'DynamicContainer' || child.type.name === 'MapContainer') {
|
||||
return cloneElement(child, {
|
||||
item_id: this.props.item_id,
|
||||
items: this.props.items
|
||||
})
|
||||
}
|
||||
if (!child.props.data) {
|
||||
return child
|
||||
}
|
||||
const props = this.getPropsFromItems(child.props.data, this.props.item_id)
|
||||
return cloneElement(
|
||||
child,
|
||||
{
|
||||
data: undefined,
|
||||
...props
|
||||
})
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default DynamicContainer
|
||||
@@ -1,42 +0,0 @@
|
||||
import React, {Children, cloneElement} from 'react'
|
||||
|
||||
/*
|
||||
* Maps a set of children onto an array of item ids
|
||||
* e.g. Displaying a stream of comments
|
||||
*
|
||||
* @props
|
||||
* id- The id of the item with the property to be mapped.
|
||||
* mapOver- The property to be mapped. Should be an array of ids.
|
||||
* items- All items in the redux store.
|
||||
*/
|
||||
|
||||
const MapContainer = ({items, item_id, mapOver, children}) => {
|
||||
if (!items[item_id] || !items[item_id].related) {
|
||||
return null
|
||||
}
|
||||
const itemArray = items[item_id].related[mapOver]
|
||||
if (!itemArray) {
|
||||
return null
|
||||
}
|
||||
return <div className={'map' + mapOver}>
|
||||
{
|
||||
itemArray.map((item) => {
|
||||
return <div key={item}>
|
||||
{
|
||||
Children.map(children, (ChildComponent) => {
|
||||
let elem = cloneElement(
|
||||
ChildComponent,
|
||||
{
|
||||
item_id: item,
|
||||
items: items
|
||||
})
|
||||
return elem
|
||||
})
|
||||
}
|
||||
</div>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default MapContainer
|
||||
@@ -1,171 +0,0 @@
|
||||
# Dynamic React Containers
|
||||
|
||||
Dynamic inject React components in the spot that they're needed with the data that they need. In combination with Coral's Shelf API, they also handle all configuration of and communication with the backend. With Dynamic Containers you can write a component, say what data it needs, say where you want it to show up and the rest is handled magically. All of this is accomplished by placing your components in containers and passing them a `data` variable:
|
||||
|
||||
**app.js**
|
||||
```
|
||||
import React, {Component} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {RootContainer, Container} from 'dynamic-react-components'
|
||||
import {Title} from 'components'
|
||||
|
||||
class App extends Component {
|
||||
|
||||
render() {
|
||||
return <RootContainer rootId={this.props.params.post} type="blogpost">
|
||||
<Container name="content">
|
||||
<Title data='{title}'/>
|
||||
</Container>
|
||||
</RootContainer>
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(<App/>, document.getElementById('app'));
|
||||
```
|
||||
|
||||
This allows you to use simple react components which get exactly the data they expect.
|
||||
|
||||
**Title.js**
|
||||
```
|
||||
import React from 'react';
|
||||
|
||||
const Title = (props) => {
|
||||
return <h1>{props.title}</h1>
|
||||
};
|
||||
|
||||
export default Title;
|
||||
```
|
||||
|
||||
## What's going on here??
|
||||
|
||||
Data in this application is stored in a graph of items. The root container defines a root item, a starting place in that graph. Each container looks at the graphql in the "data" props of its children, then walks the graph and delivers that child the data that it needs. This is a little more complex than simply passing props, but has some big advantages.
|
||||
|
||||
1) It's easy to pass arbitrary data to an arbitrary component in your application, no more worrying about context or needing to pass props down a hierarchy.
|
||||
|
||||
2) Flux store and dispatch is handled for you. You get a few CRUD functions for items that handle the majority of use cases, and can defined custom actions for any other cases that pop up.
|
||||
|
||||
3) No need to configure a server. By adding up the graphQL statements in your config files the server knows exactly the data structure your application needs and how to optimize that data structure for the kinds of traversal you'll be doing.
|
||||
|
||||
Let's review the concepts describes in these files:
|
||||
|
||||
**app.js**
|
||||
- *RootContainer*: Wraps all other containers and provides an id of a root item.
|
||||
- *Container*: A div where an array of components can be injected.
|
||||
- *data*: A graphQL string describing the data used by this application.
|
||||
|
||||
## Traversing the Graph
|
||||
|
||||
A blog that just shows a title isn't very interesting, let's add some more information:
|
||||
|
||||
**app.js**
|
||||
```
|
||||
import React, {Component} from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import {RootContainer, Container} from 'dynamic-react-components'
|
||||
import {Author, Title, Content} from 'components'
|
||||
|
||||
class App extends Component {
|
||||
|
||||
render() {
|
||||
return <RootContainer rootId={this.props.params.post} type="blogpost">
|
||||
<Container name="content">
|
||||
<Author data='{author(type:"user"){name}}'/>
|
||||
<Title data='{title}'/>
|
||||
<Content data='{content}'/>
|
||||
</Container>
|
||||
</RootContainer>
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(<App/>, document.getElementById('app'))
|
||||
```
|
||||
|
||||
Now we've added an author and content to our blog post. Note that for the Author component we used graphQL to traverse the graph from `blogpost` to `author` in order to get the author's name. When making these traversals it's important to include a "type", this allows the back end to optimize using these query statements.
|
||||
|
||||
If three seperate components seems like overkill for this task, we can combine them like so:
|
||||
|
||||
**app.js**
|
||||
```
|
||||
import React, {Component} from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import {RootContainer, Container} from 'dynamic-react-components'
|
||||
import {Blogpost} from 'components'
|
||||
|
||||
class App extends Component {
|
||||
|
||||
render() {
|
||||
return <RootContainer rootId={this.props.params.post} type="blogpost">
|
||||
<Container name="content">
|
||||
<Blogpost data='{title,content,author(type:"user"){name}}'/>
|
||||
</Container>
|
||||
</RootContainer>
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(<App/>, document.getElementById('app'))
|
||||
```
|
||||
|
||||
Dynamic components lets you pull data from an arbitrary set of items in the graph into a single component for display.
|
||||
|
||||
## GraphQL
|
||||
|
||||
Currently, dynamic components only support basic graphQL and the `type` argument. Alias, mutations, fragments, and variables are not currently supported. When traversing a graph, as in `{author(type:"user"){name}}`, the "type" argument is required. This allows for validation and provides needed information to the back end.
|
||||
|
||||
## Iterating over arrays
|
||||
|
||||
What if I want to display multiple blog posts? Enter the **MapContainer** component:
|
||||
|
||||
**app.js**
|
||||
```
|
||||
import React, {Component} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {RootContainer, Container} from 'dynamic-react-components'
|
||||
import {Blogpost} from 'components'
|
||||
|
||||
class App extends Component {
|
||||
|
||||
render() {
|
||||
return <RootContainer rootId='homepage' type="contentStream">
|
||||
<MapContainer mapOver='posts' type="blogpost"/>
|
||||
<Container name="content">
|
||||
<Blogpost data='{title,content,author(type:"user"){name}}'/>
|
||||
</Container>
|
||||
</MapContainer>
|
||||
</RootContainer>
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(<App/>, document.getElementById('app'));
|
||||
```
|
||||
|
||||
Now our RootContainer is passed a different kind of item, a contentStream with an array of blogpost ids that looks something like this:
|
||||
|
||||
```
|
||||
{
|
||||
type: 'contentStream',
|
||||
posts: ['id1','id2']
|
||||
}
|
||||
```
|
||||
|
||||
MapContainer iterates over these ids, passing its children an id of type blogpost (*not* type contentstream). The resulting page would look something like this:
|
||||
|
||||
```
|
||||
<div id="app">
|
||||
<div class="rootContainer">
|
||||
<div class='contentContainer'>
|
||||
<div class='mapPostsContainer'>
|
||||
<div>
|
||||
<h1>Title 1</h1>
|
||||
<div class="author">Author 1</div>
|
||||
<div class="content">Content 1</div>
|
||||
</div>
|
||||
<div>
|
||||
<h1>Title 2</h1>
|
||||
<div class="author">Author 2</div>
|
||||
<div class="content">Content 2</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
@@ -1,43 +0,0 @@
|
||||
import React, {Component, PropTypes, Children, cloneElement} from 'react'
|
||||
|
||||
/*
|
||||
* Renders a set of dynamic components bases on a root id
|
||||
*
|
||||
*/
|
||||
|
||||
class RootContainer extends Component {
|
||||
|
||||
static propTypes = {
|
||||
rootId: PropTypes.string.isRequired,
|
||||
items: PropTypes.object.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
getItemsQuery: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const {getItemsQuery, rootId} = this.props
|
||||
getItemsQuery(rootId)
|
||||
}
|
||||
|
||||
render () {
|
||||
const {items, rootId, type, children} = this.props
|
||||
if (items[rootId] && items[rootId].type !== type) {
|
||||
console.warn('Id passed to RootContainer gets an object of an unexpected type. Expected ' + type + ' but got ' + items[rootId].type)
|
||||
}
|
||||
return <div className='rootContainer'>
|
||||
{
|
||||
items[rootId] &&
|
||||
Children.map(children, (ChildComponent) => {
|
||||
return cloneElement(
|
||||
ChildComponent,
|
||||
{
|
||||
item_id: rootId,
|
||||
items: items
|
||||
})
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default RootContainer
|
||||
@@ -2,14 +2,16 @@ import React from 'react'
|
||||
|
||||
const name = 'coral-plugin-comment-count'
|
||||
|
||||
const CommentCount = ({comment}) => {
|
||||
const CommentCount = ({items, item_id}) => {
|
||||
let count = 0
|
||||
if (comment) {
|
||||
count += comment.length
|
||||
for (var i=0; i < comment.length; i++) {
|
||||
if (comment[i].child) {
|
||||
count += comment[i].child.length
|
||||
}
|
||||
if (items[item_id]) {
|
||||
count += items[item_id].related.comment.length
|
||||
}
|
||||
const itemKeys = Object.keys(items)
|
||||
for (var i=0; i < itemKeys.length; i++) {
|
||||
const item = items[itemKeys[i]]
|
||||
if (item.type === 'comment' && item.related && item.related.child) {
|
||||
count += item.related.child.length
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user