- Here is a poem builder app with the same styling as the last app. Going to need to clean up
index.css
, but for now, this is what we have:
- Needed to change the implementation slightly, but the look is still basically the same. Changed
<h1>
's which arecontentEditable
to<input>
's, and then changed the styling of the<input>
s to look like headings, removing the border and such. The one catch is that now the user has no idea there's an<input>
there, but I'm thinking with some focus or adding some color contrast, I don't need that ugly shadowy generic input look:
- Note to self: I'm committing at the time I take each of these screenshots, so the code at the time the photo was uploaded can be referenced in the future to figure out what contributed to the styling/other UI stuff at that point in time.
- Currently, state consists of:
haiku
(an array of 3 lines, initialized tonull
),lineCount
, which is set to3
and not used.syllableCounts
, which is an array of the syllable requirement for each of the lines.
- Instead of this, I'll pass
lineCount
andsyllableCounts
to theGame
component fromindex.js
when I mount the component, so that potentially I can have an even higher level view later on which allows users to switch between poem types. - I elevated some of the state of the
Game
component one level up to theindex.js
component(?). Or, I kept the state in theGame
component, but that state is initialized based on props passed along from a higher-level component, which has not yet been written. - Further, I renamed the
Game
component as thePoem
component, because I think that higher-level component above the nowPoem
component could aptly be called theGame
component. I also changedhaiku
in thePoem
's state topoem
, for more flexibility. Now, things are better equipped to handle different poem types, down the road. - Everything still looks the same:
- When a user updates one of the lines, the state in the
Poem
component needs to be updated. Currently, aLine
isn't a separate component, but rather a part of thePoem
component:
class Poem extends React.Component {
constructor(props) {
super(props);
this.state = {
lineCount: props.lineCount,
poem: Array(props.lineCount).fill(null),
syllableCounts: props.syllableCounts,
};
};
handleLineUpdate(lineNumber, line) {
console.log(`line #${lineNumber}: update the state with new line: ${line}`);
}
render(){
const mapping = this.state.poem.map((line, index) => <input key={index} contentEditable="true" onKeyUp={(index, line) => this.handleLineUpdate(index, line)}>{line}</input>);
return (
<div>
{mapping}
</div>
);
};
};
- I used the
map
function to create an array of inputs, prepopulated with the contents of eachline
in thepoem
, and also attached anonKeyUp
event handler to invokethis.handleLineUpdate(index,line)
, so that I can update the state whenever a line is updated. It's not yet clear to me why I would need to go another level down and add aLine
component, but I suppose I could add aLines
component, which takes thepoem
as props and returns the array of<input>
's, because later on I'll be adding more thePoem
component and don't want it to be too cluttered. But for now, let's implement the callback.
- I'm trying to have my
Line
component'sprops
updated when the line is changed. If I include a call tothis.forceUpdate()
after invokingthis.setState({ poem: currentLines });
, then I see the changes in theLine
component. Otherwise, the function components (the mapping ofLine
s) aren't re-rerendered. In fact, the entirerender
method isn't called again, although I do see an update in thePoem
's state. Very weird. - Mystery solved! I was using the wrong syntax for
this.setState
. I was doingthis.setState = {poem: currentLines}
, instead ofthis.setState({poem: currentLines})
.
Poem
component is given the propslineCount=3
, andsyllableCounts=[5,7,5]
, and maintains all of the state. This includespoem
, which is given an initial length oflineCount
.- The
Poem
's render method returns aLines
component. ALines
component takes thepoem
array andhandleLineUpdate
function from thePoem
component and maps the lines of thepoem
toLine
components. EachLine
component will be created with anonChange
event handler which will invoke thehandleLineUpdate
function from the parentPoem
component. The callingLine
component will passhandleLineUpdate
anindex
(the line number) and the newline
, and thehandleLineUpdate
method will handle updating state, which will trigger thePoem
to re-render. - Here is the latest, refactored and apparently working:
- Adding another
app
to thebuilder/
directory:word-lookup
. This is going to handle the word API. It needs to be run by a server as well, thus the separate app. - Went back through my previous implementation of builder and found some helpful work. Includes:
// wordAPI.js
var express = require('express');
var fetch = require('node-fetch');
var util = require('util');
var router = express.Router();
var Word = require('../models/word');
/*
To use this API from within React, create a function like this:
fetchWordData(word) {
var url = "http://localhost:9000/wordAPI/"+word;
fetch(url)
.then(res => res.json())
.then(res => {
this.setState({
currentWord: res.word,
currentWordDefinition: res.definition,
currentWordSyllables: res.syllables
});
})
.catch(err => err);
}
*/
router.get('/:word', function(request, response, next) {
// check if the word is stored already in the word history cache - if it is, don't bother looking it up
// create a word object for the word and assign it to the word history cache - for now, there is
// just one user, but in the future each user will have their own history cache and there
// will also be a central pool of recently looked up words.
// TODO: check if the word is a contraction, if it is look up the two parts, not the contraction
// check if word exists in DB
Word.findOne({'word': request.params.word}, function(err, results) {
if (err) { return next(err); }
console.log(results);
if (results) {
// Word already exists, return the word that already exists.
return response.send(results);
} else {
// https://www.datamuse.com/api/
var url = util.format('http://api.datamuse.com/words?sp=%s&md=ds', request.params.word);
fetch(url)
.then(res => res.json())
.then(res => {
console.log(res);
if (res==null || res.length==0){
var word = new Word(
{
word: request.params.word
}
)
} else {
var word = new Word(
{
word: res[0].word ? res[0].word : request.params.word,
definition: res[0].defs ? res[0].defs[0].toString().replace('n\t', ''): '',
syllables: res[0].numSyllables ? res[0].numSyllables : 0,
}
);
}
word.save(function(err) {
if (err) { return next(err); }
return response.send(word);
});
})
.catch(err => next(err));
}
});
});
router.get('/', function(req, res, next) {
res.send("Invalid search: please provide a word");
});
module.exports = router;
- It was the Datamuse API! Crucial piece of information there. I also have some good notes in here on how to implement mongoose/mongodb, but one thing at a time.
- In the last builder implementation, I was using express to implement the routing, but given that I'm using
create-react-app
, I'm going to do what they say is popular: React Router.
-
Followed this guide.
-
I'm no longer convinced I need a separate app up and running, but I don't think I really need a router for this. I actually don't think I need any views, links, or React Router at all... I just need to make a promise. Might have been overthinking it before, better not to get into the weeds over bad idea, trying to implement an API, however shoddy that may be, because all I really need to do is make requests to datamuse, cache data as I want somewhere down the line. Not that the there's a db yet to store that in, but w.e.!
- Refactored the app so that the
Game
component is at the top level and holds the state and thePoem
component is a stateless functional component. This will allow me to add more components downstream from theGame
component that aren't a part of thePoem
. I think it will keep things simpler, more modular. - Added syllable counting to each line. The state now maintains
syllableCounts
andsyllableLimits
, which are both arrays. ThesyllableCounts
are the current syllable counts for each line. ThesyllableLimits
are the required syllable counts for each line (e.g.[5,7,5]
for haikus). Added anh4
element to the end of eachLine
to display the currentsyllableCount
/syllableLimit
for the current line:
- The syllable counting isn't real yet, but now there's a frontend framework in place for displaying the syllable counts. The backend side (calculating the current syllable count for each line) is tbd.
- Added a
currentWord
variable to the state inGame
. There are multiple cases where thecurrentWord
will change: A. As the user types, the current word being typed is obviously the current word. B. As the user clicks around, the current word that they are focused on becomes the current word, overriding whatever the most recently typed word was.
- I can attach an event handler to the
onChange
/onKeyDown
event for eachLine
, and that event handler will need to determine what was just typed and alert theGame
component what the latestcurrentWord
is. - There are going to be a couple of cases. The user will press
space
,return
, or,
, ortab
when they are done typing a word, so this is the point to capture the current word, not on every singleonKeyDown
event, but rather only on the relevant ones. - I'll want to get the word just to the left of the
space
/return
/,
/tab
, which is not necessarily the last word in the line. - Whenever a
key up
event occurs on any of theLine
s, I figure out what the last typed word was by usinge.target.value.slice(0,e.target.selectionEnd)
to get all the text leading up to the last word, , cutting off any trailing text. Then I grab the last word from that string, and update thecurrentWord
in theGame
's state:
- The next major event which will affect the
currentWord
selection is any select events. I could attach this code to anonclick
event, and usee.target.selectionStart
ande.target.selectionEnd
to figure out what's going on. - Ended up using a SO solution I found in a response to this SO question the last time I built the app and reached this point. Does the trick. Now, whenever the user clicks on a word, that word becomes the current word.
- For now, more advanced switching of the current word can wait, this is pretty good.
- We'll want to display to user the current word, as well as its definition and syllable count.
- For now, here is the current word being displayed (definitions and individual syllable counts of words next):
- Now is the time to add a database. I'll use mongodb. Created an account and building a cluster called
BuilderCluster
. - Created a database user, login credentials
admin:admin
- Stuck on this... skipping for now
- After the user selects a word, their caret position is lost. Need to use range/selection and make sure it's restored.
- Useful link: https://stackoverflow.com/questions/30855467/unable-to-create-range
- Finally handled the issue by changing the line
input
s tospan
s with the text as their children. This way, range.setStart was able to operate on a text element, which is what was needed. Made the focus look extremely bad, so either I should change the border of the span or remove the focus entirely and figure out how to use focus cleverly, only when absolutely needed, to show the user where to type. - Could I just do a blinking cursor instead of full focus? :)
- Another deja vu issue has reared its ugly head: focusing. Now that we can build the poem, the user needs to see where the poem should go. And the focuses around the
span
s look ugly. I had to replace the lineinput
elements withspan
s because that way they could have children, which I can manage to deal with figuring out the current word and restoring the current selection. - I may need to do ref forwarding to maintain a ref to the first
span
(the first line of the poem), so that oncomponentDidMount
I can focus on that text element.
https://reactjs.org/docs/forwarding-refs.html#forwarding-refs-to-dom-components - Another thought is to remove focus entirely and have just a blinking cursor.
- Refs provide a way to access DOM nodes or React elements created in the
render
method. - In the typical React dataflow,
props
are the way that parent components interact with their children. To modify a child, you re-render it with new props. However, there are cases where you need to imperatively modify a child outside of the typical dataflow. - React provides an escape hatch to modify a child. The child can be a React component or a DOM element.
- Refs can be created using
React.createRef()
and then attached to React elements via theref
attribute. - Refs are commonly assigned to an instance property when a component is constructed so they can be referenced throughout the component:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
- A component can pass a ref down to an element in its
render
method. A reference to the node can be accessed atref.current
:
const node = this.myRef.current;
- This code uses a
ref
to store a reference to a DOM node:
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
// this ref will store the textInput DOM element
this.textInput = React.createRef();
this.focusTextInput = this.focusTextInput.bind(this);
}
focusTextInput() {
// Focus the text input using the raw DOM API
this.textInput.current.focus();
}
render() {
// associate the <input> ref with the `textInput` ref that was created in the constructor
return (
<div>
<input
type="text"
ref={this.textInput} />
/>
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}
- We can extend the
CustomTextInput
(above) to simulate it being clicked immediately after mounting by using a ref to get access to the custom input and calling itsfocusTextInput
method manually:
class AutoFocusTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
componentDidMount() {
this.textInput.current.focusTextInput();
}
render() {
return (
<CustomTextInput ref={this.textInput} />
)
}
}
- You can't use the
ref
attribute on function components because they don't have instances:
function FunctionComponent() {
return <input />;
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
render() {
// This won't work
return <FunctionComponent ref={this.textInput}/>;
}
}
- You can use the
ref
attribute inside a function component as long as you refer to a DOM element or a class component:
function CustomTextInput(props) {
// textInput must be declared here so the ref can refer to it
const textInput = useRef(null);
function handleClick() {
textInput.current.focus();
}
return (
<div>
<input
type="text"
ref={textInput}
/>
<input
type="button"
value="Focus on text input"
onClick={handleClick}
/>
</div>
);
}
- If you want to take a
ref
to a function component, you can:- Take a ref to a DOM element or class component, instead of taking a ref to the function component itself (which isn't possible because the function component has no instance, while a DOM element or class component do).
- Use
forwardRef
- Convert the component to a class
- I was able to attach a ref to each
Line
function component by attaching theref
to aspan
element, rather than to the function component itself, which is not possible because there is no instance to attach to. - Now, when the page loads, the cursor is blinking on the first line.
- Needed to use a newer React feature, hooks, specifically
useEffect
, which makes it possible to run lifecycle-like methods every time a component updates OR once when the component initially mounts. Using it in the latter context is akin tocomponentDidMount
. So, when eachLine
mounts, it checks if it's the first line, and if it is it focuses on itself. No extra data flow for this one. The parent components don't know anything about it. Dangerous potentially, but for now gets the job done exactly. - Needed to use a newer React feature, hooks, specifically
useEffect
, which makes it possible to run lifecycle-like methods every time a component updates OR once when the component initially mounts. Using it in the latter context is akin tocomponentDidMount
. So, when eachLine
mounts, it checks if it's the first line, and if it is it focuses on itself. No extra data flow for this one. The parent components don't know anything about it. Dangerous potentially, but for now gets the job done exactly.
- Found an awesome (0 votes) answer to my issue with using mongodb within React. React is client side, and the mongoose code (setting up a database connection, creating models) can't be run from the client side without a library modification. Instead, use an express server to run the mongoose connection code.
- I don't want to have to deploy 2 apps, so if I could get the React app and Express running on the same server, that would be ideal. Found an article describing how to do that:
npm install express --save
- Create a
server.js
file:
const express = require('express');
const bodyParser = require('body-parser');
const path = require('path');
const app = express();
app.use(express.static(path.join(__dirname, 'build')));
app.get('/ping', function(req, res) {
return res.send('pong');
});
app.get('/', function(req, res) {
res.sendFile(path.join(__dirname, 'build', 'index.html'));
});
app.listen(process.env.PORT || 8080);
- Update
package.json
:
"proxy": "http://localhost:8080"
- "If you didn't do this we would have to create slow production builds every time (rather than the faster for development npm run start method). This is because npm start uses port 3000, which is not the same port that the express APIs are running on (8080)."
- I didn't add this proxy line.
-
Start the express server:
node server.js
-
Start the react app:
npm start
- You can develop on
localhost:3000
usingnpm run start
and your APIs will work as expected, despite requests coming from port 3000. - To deploy, run the production build
npm run build
and serve the app fromlocalhost:8080
, which isnode server.js
in this example.
- Added some elements to the
CurrentWord
component: the definition and the syllable count, which are only displayed if they're available:
- As the user types, the syllable count of the current line should be updated. Also, the poem should have an additional
valid
state variable, which is initiallyfalse
and is set totrue
when the poem meets all of its requirements (in the case of a haiku, this is the syllable counts of the 3 line). - Added a
valid
component to theGame
's state, and avalidatePoem()
function which is invoked each time there is anonChange
event for one of theLine
components. The result ofvalidatePoem()
is used to update the state. - In order to give the user feedback as to whether or not the poem is correct, I will add styling that is dependent on state (specifically, dependent on the valid of
valid
. Found something helpful in the React docs:
render() {
let className = 'menu';
if (this.props.isActive) {
className += ' menu-active';
}
return <span className={className}>Menu</span>
}
- I discovered in implementing the poem validation that the
handleLineUpdate
method passed down from the top-levelGame
component to each of theLine
components isn't being invoked. At some point, when I changed something, it stopped working? Because it was working before... It could be when I removed theinput
elements...? - I moved all of the code from
handleLineUpdate
to theonKeyUp
event handler, and now all of the state updating changes are happening there and in theonClick
event handler. No real need for the extra event.
- Now, the current issue is that as soon as a line is validated, it isn't updated properly in the state. The poem has
valid
set totrue
, but the line that was just changed disappears. - Fixed the issue by moving all of the code from the
handleLineUpdate
function to thehandleKeyUp
function, because both events are similar enough. The only issue is that after I type, the state is now updated, but the cursor is moved back to the beginning of the line. Need to restore the cursor position at the time of typing.
- I added a
validatePoem
function, which will update a state variable calledvalid
representing whether or not the poem is valid. I also passed a prop down from theGame
poem to each of theLine
components indicating whether that individual line is valid. Depending on whether aLine
is valid or invalid, the color of the underlyingspan
will be modified:
- The current syllable counter isn't actually functional/dynamically responsive, so we're going to change that now. Before, I didn't have the syllable data, but now that I do, I can add that in.
- In order to check the syllable count for a line, I think the easiest approach is to use the backend server, because that's where the
Word
model is stored, not on the frontend. - Figuring out how to handle POST requests with Express, found something helpful
- Installed Nodemon so that I don't have to restart the Express server every time I make an edit.
- To start the server now:
nodemon server.js
- Every time a component re-rerenders, we'll lose the cursor position. So, to avoid that, we should keep track of the current line and cursor position.
- It's officially time to re-read this code and understand how it works (original source)[http://jsfiddle.net/Vap7C/80/] via (this SO post)[https://stackoverflow.com/questions/7563169/detect-which-word-has-been-clicked-on-within-a-text]:
handleClick = (e) => {
var word = '';
let selection;
if (window.getSelection && (selection = window.getSelection()).modify) {
var sel = window.getSelection();
var range = sel.getRangeAt(0);
var originalCursorPosition = range.startOffset;
if (sel.isCollapsed) {
sel.modify('move', 'forward', 'character');
sel.modify('move', 'backward', 'word');
sel.modify('extend', 'forward', 'word');
word = sel.toString();
sel.modify('move', 'forward', 'character'); // clear selection
} else {
word = sel.toString();
}
var text = range.startContainer;
var newRange = document.createRange();
newRange.setStart(text, originalCursorPosition);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
this.setState({currentWord: word});
this.fetchWordData(word);
}
- This interface represents a window containing a DOM document.
- The
document
property points to the DOM document loaded in that window. - A global variable,
window
, representing the window in which the script is running, is exposed to JavaScript code.
- There must be some way to pass the cursor position to an input element in React...?
- Found a nice succinct version of my original solution to saving and restoring cursor position here:
- Needed to change the
span
holding the text to atextarea
, which React doesn't like currently, because it hasinnerText
(children). I'm going to offset those to avalue
props instead... - Changing from a
span
broke the keydown method... - But using
defaultValue
on thetextarea
element to set the value of the text seems to work...? - Just need to fix the styling of these
textarea
s. The styling is quite fragile as it is, too dependent on pixel sizes and only looks good with certain window sizes:
- Removed the state. Here is how the sample poem looks, freshly typed:
- Great source for autoresizing an input element. For now, using this to display the definition of the current word.
- This will overwrite what is stored in the local database.
- Provide a reset button as well. You'll want it later :)
- Found this great resource for javascript event keycodes
- Implemented a lot of new ideas, also spent a good amount of time cleaning up the code. More to come, but this is a good place to pause:
- Trying to get elements to lay inline...
- https://css-tricks.com/when-do-you-use-inline-block/
- Left this issue alone for the time being, more substantive things to do first, before worrying about the alignment of things.
- Firing poem validation as the haiku is input, displaying a success message:
- Added a little outline around the haiku, here is a screen recording highlighting some of the bugs:
- I've changed the structure of the data so that poems are stored in a
history
array. Next step is to have not onlypoem
data for each event in history, but also an individualsyllableCounts
andsyllableLimits
list which is also then swapped out inherently when the state variablecurrentPoem
is changed.
- Time to get some images and source control of this crazy run I've been on for the past few days. Lots of things to clean up, but much progress. My new favorite haiku is:
do do do do do do do do do do do do do do do do do
- The next major move, before I forget it, is going to be handling state a little differently. Have more separation between what components access what state, for the sake of not re-rendering so much. Something needs to be done to keep a compromise between speed and reasonable coding. Not sure that what I'm doing is wrong, but it doesn't feel elegant. More cleaning up to be done.
- Also read about
useState
, which can be used to add state to previously (in my mind) stateless function components. Docs here - Also tried to be more diligent about immutably manipulating data. Learned a lot about
splice
vs.slice
(splice
augments the array,slice
creates a copy), the usefulness of the spread operator (...
), and used a lot of good array methods in a functional programming style approach (filter
,map
,reduce
,find
,findIndex
). Pretty cool stuff. - Here's a gif of the current flow and layout, and some more issues I'm about to investigate:
- Did something cool with the placeholders, to make them really obviously placeholders and also add some interactivity. Will look for more hover on/off opportunities to reveal to the user how to play the game, hints/suggestions, etc.
Rearranging the Components amongst the Game and Poem components for a better hierarchy, hopefully less rendering
- for now just chaos
Just fyi, lots is going down but too productive to describe it. But the styling is still shit since removing the bare amount of CSS that was there:
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
currentPoemIndex: 0,
currentWord: null,
criteria: {
lineCount: 3,
syllableLimits: [5,7,5],
placeholders: ["Line 1 uses 5 syllables", "Line 2 uses 7 syllables", "Line 3 uses 5 syllables"],
exampleHaiku: ["haikus are easy", "but sometimes they don't make sense", "refrigerator"],
exampleHaikuOriginal:["haikus are easy", "but sometimes they don't make sense", "refrigerator"],
},
history: null,
map: new Map(),
poemIsEmpty: true,
};
};
[...]
}
- Here is the only constructor, the source of all of the props downstream, the single source of truth.
- Keep changing up what is passed to the
Game
component itself as props, but for now sticking with only haikus, so the onlycriteria
object is for haikus, simpler that way for now anyway. Thecriteria
holds thesyllableLimits
and thehistory
holds the individualsyllableCounts
, although I'm feeling like doing those on the fly as needed anyway... I'm going to implement that change quickly. - As usual that tangent actually fixed the problem that lead me to start this walkthrough :). Was having issues validating the haiku for some reason, but the situation seems much improved now.
- Moving around state and props and functions very hastily introduced a lot of bugs. Systematic development is the idea here. Bursts of inspiration and progress are great but you have to clean up after your mess sometimes. Be careful about that. Many projects aren't the kind you can muck up and chuck away, etc.
- Code review to start things off. But first, a gif:
- One addition to the constructor,
syllableCounts
. Still deciding the best way to manage tallying the syllable counts to find the best flow for the user, especially given the potential for latency searching for words vs. typing speed. Once values are cached it gets much faster, obviously. Glad that I spent some time introducing the caching schema (further below):
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
currentPoemIndex: 0,
currentWord: null,
criteria: {
lineCount: 3,
syllableLimits: [5,7,5],
placeholders: ["Line 1 uses 5 syllables", "Line 2 uses 7 syllables", "Line 3 uses 5 syllables"],
exampleHaiku: ["haikus are easy", "but sometimes they don't make sense", "refrigerator"],
exampleHaikuOriginal:["haikus are easy", "but sometimes they don't make sense", "refrigerator"],
},
history: null,
map: new Map(),
poemIsEmpty: true,
syllableCounts: [0, 0, 0],
};
};
- The interplay of these functions is a little odd, but it seems to work.
lookupWord
is an extendable function. You pass it a piece oftext
, a functionnext
, and any arguments that go along withnext
in the varargs,...nextArgs
. Should look into the JS specifics of these more, but things seem to work similarly to Java and logically in general.
Side note, falling in love with ES6 syntax. And JSX. Woah. Goodbye Django! Wouldn't mind some flask, I guess... But nah this is what's most intriguing to me right now, so let's see it through. React JS. Redux next. React native. Whatever else.
- Back to the functions. I'll include everything that's relevant together for the
lookupWord
sequence:
class Game extends React.Component {
[...]
lookupWord = (text, next, ...nextArgs) => {
if (!text && !next) { console.log(`lookupWord --> word: ${text}`); return null; }
var url = `http://localhost:8080/wordAPI/${text}`;
fetch(url)
.then(res => res.json())
.then(res => {
const word = {
text: text,
definition: res.definition,
syllables: res.syllables,
edited: res.edited,
activeEdit: res.activeEdit
}
next(word, nextArgs);
})
.catch(err => err);
};
// checks in map cache for word. cache hit -->
// returns the word object, cache miss ->
// returns null and calls lookupWord before
// updating the map cache
addWordToMap = (word, next) => {
const map = new Map(this.state.map);
if (map.get(word) === undefined) {
// lookup word
this.lookupWord(word, (wordObject) => {
this.setState({map: map.set(word, wordObject)});
next(wordObject);
});
} else {
next(map.get(word)); // use cached word
}
};
addWordToMap
is the only function currently usinglookupWord
, but that could change soon.lookupWord
is a handy function that will fetch word data from the API and pass that data along to a callback you give it,next
, with optional varargs. I figure all of the code having to do with fetching on the client side should be inlookupWord
. Very useful for passing code to update state along to some external library. Should adapt the function later to be more general. Very common paradigm in JS, I'm finding.- Another great feature of
addWordToMap
is that it introduces local caching! Adding the support for storing words was quite simple compared to what I'm tackling now with managing saving current drafts implicitly, mostly because of the way I set up the app previously, which was more focused on getting the UI to a point where I don't want to kill myself operating it. Especially the text-related stuff. But that will come later. Anyway, for managing the words I used a Map, it seemed to be the most straightforward implementation. The Map, calledmap
, is maintained inGame
's enormous state. - I might think about revamping the naming or adding more objects to decrease the need to name complexity in some cases.
Clean Code
had some stuff straight off the bat about that, I should read it later.
javascript
class Game extends React.Component {
cancelDefinitionUpdate = () => {
if (this.state.currentWord.activeEdit) {
this.updateCurrentWord(null, false, true);
}
this.setState({displayDefinitionUpdate: false});
};
cancelSyllableUpdate = () => {
if (this.state.currentWord.activeEdit) {
this.updateCurrentWord(null, true, false);
}
this.setState({displaySyllableUpdate: false});
};
- The root of the issue here is that I'm relying on React and the virtual DOM to render everything for me on time, as long as I update the state of the most upstream element,
Game
. So in order to control something like displaying/hiding a set of buttons for editing the current word's syllable count & definition, which is happening at the level of theCurrentWord
component, I need to make sure that theGame
is managing the state, unless I can get thePoem
orCurrentWord
component more involved. Maybe add hooks? Not sure about their use cases yet. But the issue will always be that if the page re-renders, the virtual DOM is re-rendering all the components downstream of the top-level stateful component to make them aware of some change to state. State can change at any time! Better to implement this in a stateless way, if possible. - Thought of a way to combine the functions:
cancelWarning = (displayWarning, resetSyllable=true, resetDefinition=true) => {
if (this.state.currentWord.activeEdit) {
this.updateCurrentWord(null, resetSyllables, resetDefinition);
}
this.setState({displayWarning: false});
};
- These are also closely related:
continueDefinitionUpdate = () => {
const newDefinition = this.state.currentWord.definition;
const map = new Map(this.state.map);
const currentWord = map.get(this.state.currentWord.text);
map.delete(currentWord.text);
const newWord = {...currentWord, definition: newDefinition, original:{...currentWord}, edited:true};
map.set(currentWord.text, newWord);
let valid = false;
const history = this.state.history.map((poem, i) => {
if (i === this.state.currentPoemIndex){
valid = this.state.validatePoem();
return {...poem, valid: valid}
} else { return poem };
});
this.setState({
map: map,
displayDefinitionUpdate: false,
currentWord: newWord,
history: history,
valid:valid
});
};
continueSyllableUpdate = () => {
if (!this.state.currentWord.activeEdit) { return; }
const newSyllableCount = this.state.currentWord.activeEdit.edit.syllables;
const map = this.state.map;
const currentWord = map.get(this.state.currentWord.text);
const newWord = {...currentWord, syllables: newSyllableCount, original:{...currentWord}, edited: true, activeEdit: false};
map.delete(currentWord.text);
map.set(currentWord.text, newWord);
const history = this.state.history.map((poem, i) => {
if (i === this.state.currentPoemIndex){
const valid = this.state.validatePoem();
return {...poem, valid: valid}
} else { return poem };
});
this.setState({
map: map,
displaySyllableUpdate: false,
currentWord: newWord,
history: history
});
};
continueUpdate = (displayWarning, updateSyllables=true, updateDefinition=true) => {
if (!this.state.currentWord.activeEdit) { return; }
const currentWord = {...this.state.currentWord};
const newSyllableCount = (updateSyllables ? this.state.currentWord.activeEdit.edit.syllables : currentWord.syllables);
const newDefinition = (updateDefinition ? this.state.currentWord.activeEdit.edit.definition : currentWord.definition);
const newWord = {...currentWord, syllables: newSyllableCount, definition: newDefinition, original:{...currentWord}, edited: true, activeEdit: false};
map.delete(currentWord.text);
map.set(currentWord.text, newWord);
const history = this.state.history.map((poem, i) => {
if (i === this.state.currentPoemIndex){
const valid = this.state.validatePoem();
return {...poem, valid: valid}
} else { return poem };
});
if (updateSyllables) {
this.cancelUpdate(displaySyllableUpdate, true, false);
}
if (updateDefinition) {
this.cancelUpdate(displayDefinitionUpdate, false, true);
}
this.setState({
map: map,
displaySyllableUpdate: false,
currentWord: newWord,
history: history,
valid: valid,
});
}
- Removed all of the contents of
index.css
, dumped them in a similarly named file,index2.css
. Whipped out the HTML&CSS picture "textbook" here with examples of fixed-width and liquid layouts. %s sound more reliable to me, and there isn't so much small text, or real images, so the stretch effect won't be an issue. There seem to be other cases to use a fixed-width layout. - Here is a quick gif of the site, still buggy, and now minus the bare styling it had. Totally exposed:
-
Flex Box and Grid layouts are essential for CSS, even if I move beyond pure css later on. Have to have covered these basics at some point. Today's the day! Tackling that scary concept of layouts!
-
Notes for flexbox and grid are here.
- There's already a component for that: https://github.com/Andarist/react-textarea-autosize
- See Redux notes.
- Reviewed the basics of redux, but still need to learn thunks before I can bring over the full functionality of the previous implementation without redux.
- The CurrentWord feature has almost been added in, but it won't switch between words, and that's because currently it relies on the result of a fetch to display anything. It shouldn't do this. It should display the current word immediately, and then any more data as it becomes available:
- Added first async logic to the redux implementation using thunks to trigger the fetching of poems. Also took a lot of great logic from the redux tutorial to handle the 4 different states of the promise made while fetching from the wordAPI within
extraReducers
using thefetchPoems
reducer. I'm going to write similar reducers and extraReducers for the other data I want to fetch from the API (users, words), and soon enough, I'll have syllable counts running. Plus reactions! Also taken from the awesome amazing redux tutorial. I'll change to my own emoji's later, but for now, here is the LOADER which spins whenever poems data is being fetched:
- Add users
- Add public/private status for poems
- Add "reactions" (https://redux.js.org/tutorials/essentials/part-4-using-data#post-reaction-buttons) to public poems
This project was bootstrapped with Create React App.
In the project directory, you can run:
Runs the app in the development mode.
Open http://localhost:3000 to view it in the browser.
The page will reload if you make edits.
You will also see any lint errors in the console.
Launches the test runner in the interactive watch mode.
See the section about running tests for more information.
Builds the app for production to the build
folder.
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.
Your app is ready to be deployed!
See the section about deployment for more information.
Note: this is a one-way operation. Once you eject
, you can’t go back!
If you aren’t satisfied with the build tool and configuration choices, you can eject
at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except eject
will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
You don’t have to ever use eject
. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
You can learn more in the Create React App documentation.
To learn React, check out the React documentation.
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify