Skip to content

Instantly share code, notes, and snippets.

@steveluscher
Last active December 2, 2015 16:26
Show Gist options
  • Save steveluscher/3abe6d6f598d70423f76 to your computer and use it in GitHub Desktop.
Save steveluscher/3abe6d6f598d70423f76 to your computer and use it in GitHub Desktop.
[WIP] New Relay Tutorial App
class Comment extends React.Component {
render() {
var {comment} = this.props;
var {author} = comment;
var savePending = this.props.relay.hasOptimisticUpdate(this.props.comment);
return (
<div style={{opacity: savePending ? 0.4 : null}}>
<img src={author.avatar} width={16} /> <strong>{author.name}</strong> {comment.text}
{savePending &&
<img src="data:image/gif;base64,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" style={{marginLeft: 4, verticalAlign: 'text-top'}} />
}
</div>
);
}
}
Comment = Relay.createContainer(Comment, {
fragments: {
comment: () => Relay.QL`
fragment on Comment {
author { avatar, name }
text
}
`,
},
});
class Story extends React.Component {
_augmentNumCommentsToShow = (delta) => {
this.props.relay.setVariables({
numCommentsToShow: this.props.relay.variables.numCommentsToShow + delta,
});
}
_handleKeyDown = (e) => {
if (e.keyCode === 13 && e.target.value != '') { // enter key
Relay.Store.update(
new AddCommentMutation({
story: this.props.story,
text: e.target.value,
viewer: this.props.viewer,
})
);
this._augmentNumCommentsToShow(1);
e.target.value = '';
}
}
_handleMoreCommentsClick = (e) => {
e.preventDefault();
this._augmentNumCommentsToShow(5);
}
render() {
var {story} = this.props;
var {author, comments} = story;
return (
<div>
<header>
<img src={author.avatar} width={32} /> <strong>{author.name}</strong>
</header>
<p>{story.text}</p>
{comments.pageInfo.hasPreviousPage &&
<a href="#" onClick={this._handleMoreCommentsClick}>
View previous comments
</a>
}
<ul>
{comments && comments.edges.map(commentEdge =>
<li key={commentEdge.node.id}>
<Comment comment={commentEdge.node} />
</li>
)}
<li>
<input
onKeyDown={this._handleKeyDown}
placeholder="Leave a comment&hellip;"
type="text"
/>
</li>
</ul>
</div>
);
}
}
Story = Relay.createContainer(Story, {
initialVariables: {
numCommentsToShow: 3,
},
fragments: {
story: () => Relay.QL`
fragment on Story {
author { avatar, name }
comments(last: $numCommentsToShow) {
edges {
node {
id
${Comment.getFragment('comment')}
}
}
pageInfo { hasPreviousPage }
}
text
${AddCommentMutation.getFragment('story')}
}
`,
viewer: () => Relay.QL`
fragment on Viewer {
avatar
id
name
${AddCommentMutation.getFragment('viewer')}
}
`,
},
});
class StoriesApp extends React.Component {
_handleLoadMoreClick = () => {
this.props.relay.setVariables({
numStoriesToLoad: this.props.relay.variables.numStoriesToLoad + 3,
});
}
render() {
var {storyFeed} = this.props.viewer;
return (
<div>
<ul>
{storyFeed.edges.map(edge =>
<li key={edge.node.id}>
<Story
story={edge.node}
viewer={this.props.viewer}
/>
</li>
)}
</ul>
{storyFeed.pageInfo.hasNextPage &&
<button onClick={this._handleLoadMoreClick}>
Load more stories
</button>
}
</div>
);
}
}
StoriesApp = Relay.createContainer(StoriesApp, {
initialVariables: {
numStoriesToLoad: 3,
},
fragments: {
viewer: () => Relay.QL`
fragment on Viewer {
storyFeed(first: $numStoriesToLoad) {
edges {
node {
id
${Story.getFragment('story')},
}
}
pageInfo { hasNextPage }
}
${Story.getFragment('viewer')}
}
`,
}
});
class AddCommentMutation extends Relay.Mutation {
static fragments = {
story: () => Relay.QL`
fragment on Story {
id
}
`,
viewer: () => Relay.QL`
fragment on Viewer {
id
}
`,
};
getCollisionKey() {
return `story-${this.props.story.id}`;
}
getMutation() {
return Relay.QL`mutation{addComment}`;
}
getFatQuery() {
return Relay.QL`
fragment on AddCommentPayload {
commentEdge
story { comments }
}
`;
}
getConfigs() {
return [{
type: 'RANGE_ADD',
parentName: 'story',
parentID: this.props.story.id,
connectionName: 'comments',
edgeName: 'commentEdge',
rangeBehaviors: {
'': 'append',
},
}];
}
getVariables() {
return {
storyId: this.props.story.id,
text: this.props.text,
};
}
getOptimisticResponse() {
return {
commentEdge: {
node: {
author: {
avatar: this.props.viewer.avatar,
id: this.props.viewer.id,
name: this.props.viewer.name,
},
story: {
id: this.props.story.id,
},
text: this.props.text,
},
},
};
}
}
class StoriesRoute extends Relay.Route {
static params = {};
static queries = {
viewer: (Component) => Relay.QL`
query ViewerQuery {
viewer {
${Component.getFragment('viewer')},
}
}
`,
};
static routeName = 'Stories';
}
ReactDOM.render(
<Relay.RootContainer
Component={StoriesApp}
route={new StoriesRoute()}
/>,
mountNode
);
import {
GraphQLBoolean,
GraphQLEnumType,
GraphQLFloat,
GraphQLID,
GraphQLInputObjectType,
GraphQLInt,
GraphQLInterfaceType,
GraphQLList,
GraphQLNonNull,
GraphQLObjectType,
GraphQLSchema,
GraphQLString,
GraphQLUnionType,
} from 'graphql';
import {
connectionArgs,
connectionDefinitions,
connectionFromArray,
cursorForObjectInConnection,
fromGlobalId,
globalIdField,
mutationWithClientMutationId,
nodeDefinitions,
toGlobalId,
} from 'graphql-relay';
/**
* Set up some test data
*/
var TUTORIAL_VERSION = 1;
class Comment {
constructor(data) {
this.authorId = data.authorId;
this.id = data.id;
this.storyId = data.storyId;
this.text = data.text;
}
}
class Person {
constructor(data) {
this.avatar = data.avatar;
this.id = data.id;
this.name = data.name;
}
}
class Story {
constructor(data) {
this.authorId = data.authorId;
this.id = data.id;
this.text = data.text;
}
}
class Viewer {
constructor(data) {
this.avatar = data.avatar;
this.id = data.id;
this.name = data.name;
}
}
var COMMENTS;
try {
COMMENTS = JSON.parse(localStorage.getItem(`relay-tutorial-${TUTORIAL_VERSION}-comments`));
} catch(e) {}
if (COMMENTS == null) {
COMMENTS = [
new Comment({id: '0', authorId: '2', storyId: '0', text: 'Yeah!'}),
new Comment({id: '1', authorId: '3', storyId: '1', text: 'OK!'}),
];
}
var PEOPLE;
try {
PEOPLE = JSON.parse(localStorage.getItem(`relay-tutorial-${TUTORIAL_VERSION}-people`));
} catch(e) {}
if (PEOPLE == null) {
PEOPLE = [
new Person({id: '0', name: 'You', avatar: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABAAgMAAADXB5lNAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAMUExURcTM3/////v8+uDk6c+84MYAAACySURBVDjLpZLBFcIgEEQDPg4UYAn0kRI8ODEHD5RgCTaRftKE/ShidAee2aDc+G9nZ9mh634/9sx3g8gA6OnugQOBGzBwB7DGJSA1IYGjAFMCIFMF7DKY2VX62gyuDcCoFZWtPmkFzBfb/h+wYdLnkgcODjgJsC9z0UGVrU8grn0HHdh2SQU2vmVcqyg35vNOYxHtJ1y3gFgULF3CG2RnA3EuUvASOQKPvoHByIo0Szu4A87wlHkPruK3AAAAAElFTkSuQmCC'}),
new Person({id: '1', name: 'Steve', avatar: 'data:image/png;base64,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'}),
new Person({id: '2', name: 'Yuzhi', avatar: 'data:image/png;base64,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'}),
new Person({id: '3', name: 'Joe', avatar: 'data:image/png;base64,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'}),
new Person({id: '4', name: 'Tim', avatar: 'data:image/png;base64,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'}),
new Person({id: '5', name: 'Jan', avatar: 'data:image/png;base64,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'}),
];
}
var STORIES;
try {
STORIES = JSON.parse(localStorage.getItem(`relay-tutorial-${TUTORIAL_VERSION}-stories`));
} catch(e) {}
if (STORIES == null) {
STORIES = [
new Story({id: '0', text: 'Everybody ready to publish a new version?', authorId: '1'}),
new Story({id: '1', text: 'Anyone want to grab lunch?', authorId: '2'}),
new Story({id: '2', text: 'I have a new idea; anyone want to grab a whiteboard and sketch it out?', authorId: '3'}),
new Story({id: '3', text: '#728131, #711151, and #817129 are fixed and landed.', authorId: '4'}),
new Story({id: '4', text: 'I\'m working on something that should increase developer efficiency. Stay tuned!', authorId: '5'}),
];
}
var VIEWER = new Viewer(PEOPLE[0]);
/**
* Let Relay map between:
* - global IDs and the object they represent
* - objects and the GraphQL type associated with them
*/
var {nodeInterface, nodeField} = nodeDefinitions(
(globalId) => {
var {type, id} = fromGlobalId(globalId);
if (type === 'Comment') {
return COMMENTS.find(obj => obj.id === id);
} else if (type === 'Person') {
return PEOPLE.find(obj => obj.id === id);
} else if (type === 'Story') {
return STORIES.find(obj => obj.id === id);
} else if (type === 'Viewer') {
return VIEWER;
}
return null;
},
(obj) => {
if (obj instanceof Comment) {
return CommentType;
} else if (obj instanceof Person) {
return PersonType;
} else if (obj instanceof Story) {
return StoryType;
} else if (obj instanceof Viewer) {
return ViewerType;
}
return null;
}
);
/**
* Define an interface that all person-like objects will conform to
*/
var PersonableInterface = new GraphQLInterfaceType({
name: 'Personable',
fields: () => ({
avatar: {
type: GraphQLString,
description: 'The URL of a person\'s avatar image',
},
comments: {
type: CommentConnectionType,
description: 'Comments made on stories by this person',
args: connectionArgs,
resolve: (obj, args) => connectionFromArray(
COMMENTS.filter(comment => comment.authorId === obj.id),
args
),
},
name: { type: GraphQLString },
stories: {
type: StoryConnectionType,
description: 'Stories written by this person',
args: connectionArgs,
resolve: (obj, args) => connectionFromArray(
STORIES.filter(story => story.authorId === obj.id),
args
),
},
}),
resolveType(obj) {
return obj instanceof Person ? PersonType :
obj instanceof Viewer ? ViewerType :
null;
},
});
/**
* Configure each type of object: Comments, People, Stories, and the Viewer
*/
var CommentType = new GraphQLObjectType({
name: 'Comment',
description: 'A comment on a story',
fields: () => ({
author: {
type: PersonType,
description: 'The author of this comment',
resolve: (obj) => PEOPLE[obj.authorId],
},
id: globalIdField('Comment'),
story: {
type: StoryType,
description: 'The story this comment is attached to',
resolve: () => STORIES[obj.storyId],
},
text: {
type: GraphQLString,
},
}),
interfaces: [nodeInterface],
});
var PersonType = new GraphQLObjectType({
name: 'Person',
description: 'A person who writes stories and comments',
fields: () => ({
avatar: {
type: GraphQLString,
description: 'The URL of a person\'s avatar image',
},
comments: {
type: CommentConnectionType,
description: 'Comments made on stories by this person',
args: connectionArgs,
resolve: (obj, args) => connectionFromArray(
COMMENTS.filter(comment => comment.authorId === obj.id),
args
),
},
id: globalIdField('Person'),
name: { type: GraphQLString },
stories: {
type: StoryConnectionType,
description: 'Stories written by this person',
args: connectionArgs,
resolve: (obj, args) => connectionFromArray(
STORIES.filter(story => story.authorId === obj.id),
args
),
},
}),
interfaces: [nodeInterface, PersonableInterface],
});
var StoryType = new GraphQLObjectType({
name: 'Story',
description: 'A story written by a person',
fields: () => ({
author: {
type: PersonType,
description: 'The author of this story',
resolve: (obj) => PEOPLE[obj.authorId],
},
comments: {
type: CommentConnectionType,
description: 'Comments people have made on this story',
args: connectionArgs,
resolve: (obj, args) => connectionFromArray(
COMMENTS.filter(comment => comment.storyId === obj.id),
args
),
},
id: globalIdField('Story'),
text: {
type: GraphQLString,
},
}),
interfaces: [nodeInterface],
});
var ViewerType = new GraphQLObjectType({
name: 'Viewer',
description: 'The acting person (eg. the logged in visitor)',
fields: () => ({
avatar: {
type: GraphQLString,
description: 'The URL of the viewer\'s avatar image',
},
comments: {
type: CommentConnectionType,
description: 'Comments on stories, made by the viewer',
args: connectionArgs,
resolve: (obj, args) => connectionFromArray(
COMMENTS.filter(c => c.authorId === obj.id),
args
),
},
id: globalIdField('Viewer'),
name: { type: GraphQLString },
friends: {
type: PersonConnectionType,
description: 'Friends of the viewer',
args: connectionArgs,
resolve: (obj, args) => connectionFromArray(
PEOPLE.filter(p => p.id !== obj.id),
args
),
},
stories: {
type: StoryConnectionType,
description: 'Stories written by the viewer',
args: connectionArgs,
resolve: (obj, args) => connectionFromArray(
STORIES.filter(story => story.authorId === obj.id),
args
),
},
storyFeed: {
type: StoryConnectionType,
description: 'Stories visible to the viewer',
args: connectionArgs,
resolve: (obj, args) => connectionFromArray(STORIES, args),
},
}),
interfaces: [nodeInterface, PersonableInterface],
});
/**
* Set up the connections between types
*/
var {
connectionType: CommentConnectionType,
edgeType: CommentEdgeType,
} = connectionDefinitions({
name: 'Comment',
nodeType: CommentType,
});
var {
connectionType: PersonConnectionType,
edgeType: PersonEdgeType,
} = connectionDefinitions({
name: 'Person',
nodeType: PersonType,
});
var {
connectionType: StoryConnectionType,
edgeType: StoryEdgeType,
} = connectionDefinitions({
name: 'Story',
nodeType: StoryType,
});
/**
* Configure a mutation to allow people to make comments
*/
var AddCommentMutation = mutationWithClientMutationId({
name: 'AddComment',
inputFields: {
storyId: { type: new GraphQLNonNull(GraphQLID) },
text: { type: new GraphQLNonNull(GraphQLString) },
},
outputFields: {
commentEdge: {
type: CommentEdgeType,
resolve: ({localCommentId}) => {
var comment = COMMENTS.find((c) => c.id === localCommentId);
return {
cursor: cursorForObjectInConnection(COMMENTS, comment),
node: comment,
};
},
},
story: {
type: StoryType,
resolve: ({storyId}) => STORIES.find(s => s.id === storyId),
},
viewer: {
type: ViewerType,
resolve: () => VIEWER,
},
},
mutateAndGetPayload: async ({storyId, text}) => {
var localCommentId = COMMENTS.length;
COMMENTS.push({
authorId: VIEWER.id,
id: localCommentId,
storyId: fromGlobalId(storyId, 'Story').id,
text,
});
localStorage.setItem(`relay-tutorial-${TUTORIAL_VERSION}-comments`, JSON.stringify(COMMENTS));
await new Promise(r => setTimeout(r, 500 + Math.random() * 1000));
return {localCommentId, storyId};
}
});
/**
* Finally, configure the root Query and Mutation type
*/
var QueryType = new GraphQLObjectType({
name: 'Query',
fields: () => ({
node: nodeField,
viewer: {
type: ViewerType,
resolve: () => VIEWER,
},
}),
});
var MutationType = new GraphQLObjectType({
name: 'Mutation',
fields: {
addComment: AddCommentMutation,
},
});
export default new GraphQLSchema({
query: QueryType,
mutation: MutationType,
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment